Principles and Implementation of Reverse Proxy and Load Balancing

Principles and Implementation of Reverse Proxy and Load Balancing

Problem Description
Reverse proxy and load balancing are core components in modern backend architectures. A reverse proxy acts as a gateway for servers, receiving client requests and forwarding them to backend services; load balancing is responsible for distributing traffic reasonably across multiple server instances. Understanding their working principles is crucial for designing highly available systems.

Core Principles

  1. Reverse Proxy: Sits between the client and backend servers, hiding real server information and providing a unified entry point.
  2. Load Balancing: Distributes requests to multiple servers using specific algorithms to improve system throughput and reliability.

Implementation Details

Step 1: Basic Reverse Proxy Implementation

# Simple reverse proxy example
import http.server
import socketserver
import requests

class ReverseProxyHandler(http.server.BaseHTTPRequestHandler):
    backend_servers = ['http://localhost:8001', 'http://localhost:8002']
    
    def do_GET(self):
        # Select backend server (simple round-robin)
        backend = self.backend_servers.pop(0)
        self.backend_servers.append(backend)
        
        # Forward request
        resp = requests.get(f"{backend}{self.path}")
        
        # Return response to client
        self.send_response(resp.status_code)
        for key, value in resp.headers.items():
            self.send_header(key, value)
        self.end_headers()
        self.wfile.write(resp.content)

Step 2: Evolution of Load Balancing Algorithms

  1. Round Robin:
class RoundRobinBalancer:
    def __init__(self, servers):
        self.servers = servers
        self.index = 0
    
    def get_server(self):
        server = self.servers[self.index]
        self.index = (self.index + 1) % len(self.servers)
        return server
  1. Weighted Round Robin:
class WeightedRoundRobinBalancer:
    def __init__(self, server_weights):
        self.servers = []
        for server, weight in server_weights.items():
            self.servers.extend([server] * weight)
        self.index = 0
    
    def get_server(self):
        server = self.servers[self.index]
        self.index = (self.index + 1) % len(self.servers)
        return server
  1. Least Connections:
class LeastConnectionsBalancer:
    def __init__(self, servers):
        self.server_connections = {server: 0 for server in servers}
    
    def get_server(self):
        return min(self.server_connections.items(), key=lambda x: x[1])[0]
    
    def release_connection(self, server):
        if server in self.server_connections:
            self.server_connections[server] -= 1

Step 3: Health Check Mechanism

import threading
import time

class HealthChecker:
    def __init__(self, servers, check_interval=30):
        self.servers = servers
        self.healthy_servers = set(servers)
        self.check_interval = check_interval
    
    def check_server(self, server):
        try:
            resp = requests.get(f"{server}/health", timeout=5)
            return resp.status_code == 200
        except:
            return False
    
    def start_checking(self):
        def check_loop():
            while True:
                for server in self.servers:
                    is_healthy = self.check_server(server)
                    if is_healthy and server not in self.healthy_servers:
                        self.healthy_servers.add(server)
                    elif not is_healthy and server in self.healthy_servers:
                        self.healthy_servers.remove(server)
                time.sleep(self.check_interval)
        
        thread = threading.Thread(target=check_loop)
        thread.daemon = True
        thread.start()

Step 4: Session Affinity (Sticky Sessions)

class SessionAwareBalancer:
    def __init__(self, servers):
        self.servers = servers
        self.session_map = {}  # session_id -> server
    
    def get_server(self, session_id=None):
        if session_id and session_id in self.session_map:
            return self.session_map[session_id]
        
        server = self.servers[len(self.session_map) % len(self.servers)]
        if session_id:
            self.session_map[session_id] = server
        return server

Step 5: Complete Implementation Example

class AdvancedLoadBalancer:
    def __init__(self, servers):
        self.servers = servers
        self.balancer = LeastConnectionsBalancer(servers)
        self.health_checker = HealthChecker(servers)
        self.session_balancer = SessionAwareBalancer(servers)
        self.health_checker.start_checking()
    
    def handle_request(self, request):
        # Check session stickiness
        session_id = request.cookies.get('session_id')
        if session_id:
            server = self.session_balancer.get_server(session_id)
        else:
            server = self.balancer.get_server()
        
        # Forward request
        self.balancer.server_connections[server] += 1
        try:
            return self.forward_request(server, request)
        finally:
            self.balancer.release_connection(server)

Key Technical Points

  1. Connection Pool Management: Reuse backend connections to reduce overhead.
  2. Timeout Control: Set connection and read timeouts to avoid blocking.
  3. Failover: Automatically detect failed requests and retry with other servers.
  4. Dynamic Configuration: Support adding/removing backend servers at runtime.

Practical Application Scenarios

  • Nginx: Event-driven reverse proxy implementation.
  • HAProxy: Professional TCP/HTTP load balancer.
  • Cloud Services: Managed services like AWS ALB, GCP Load Balancer, etc.

This architecture significantly improves system availability and performance by distributing request pressure and enhancing fault tolerance.