本文目录导读:

从零实现高可用流量分发
📖 目录导读
- 负载均衡核心原理 – 为什么需要脚本化?
- 前置准备与环境检查 – 开始编码前必做的三件事
- 四层负载均衡脚本(基于TCP/UDP) – 实战代码+逐行解析
- 七层负载均衡脚本(基于HTTP/HTTPS) – 支持健康检查与权重分配
- 高频问题FAQ – 面试/运维中最常遇到的5个疑问
- 脚本优化与生产级建议 – 如何让你的脚本从“能用”变“可靠”
负载均衡核心原理:脚本化能解决什么?
Q:为什么不用Nginx/HAProxy这类现成工具,非要自己写脚本?
A:在生产环境,我们推荐使用成熟的负载均衡软件,但编写简易脚本有以下不可替代的价值:
- 理解底层转发机制:当你手写规则时,能更深刻理解TCP三次握手、HTTPHeader传递等细节
- 快速原型验证:开发环境、内网测试或临时需求下,一个300行的脚本比配置Nginx更快
- 定制化需求:例如需要基于业务自定义的哈希策略、动态权重调整或特殊协议解析
核心模型:
客户端请求 → 调度器(脚本) → 根据算法选择 → 后端服务器池 → 响应返回
常见的调度算法包括:轮询(Round Robin)、最少连接数(Least Connections)、源地址哈希(Source IP Hash)。
前置准备与环境检查
在编写脚本前,请确认以下三点:
-
操作系统与权限
- Linux/Mac:推荐使用Python 3.8+(系统自带或通过pyenv安装)
- Windows:需安装WSL或使用Git Bash,部分socket操作需要管理员权限
-
Python模块准备
# 仅需标准库,无需第三方依赖 python3 -c "import socket, threading, time, sys, json, selectors"
-
后端服务验证
假设你有两台测试服务器:- 后端A:192.168.1.101:8080(运行着简单的HTTP服务)
- 后端B:192.168.1.102:8080
可通过
curl http://192.168.1.101:8080确认能正常访问。
四层负载均衡脚本(基于TCP转发)
完整代码(lb_tcp.py)
import socket
import threading
BACKEND_SERVERS = [
('192.168.1.101', 8080),
('192.168.1.102', 8080)
]
current_index = 0
lock = threading.Lock()
def handle_client(client_sock, backend_addr):
"""建立与后端服务器的隧道并转发数据"""
backend_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
backend_sock.connect(backend_addr)
# 双向数据转发:使用selectors或者简单的双线程
def forward(src, dst):
while True:
data = src.recv(4096)
if not data:
break
dst.sendall(data)
t1 = threading.Thread(target=forward, args=(client_sock, backend_sock))
t2 = threading.Thread(target=forward, args=(backend_sock, client_sock))
t1.start()
t2.start()
t1.join()
t2.join()
except Exception as e:
print(f"Connection error: {e}")
finally:
backend_sock.close()
client_sock.close()
def roubin_scheduler():
"""轮询调度算法,线程安全"""
global current_index
with lock:
server = BACKEND_SERVERS[current_index % len(BACKEND_SERVERS)]
current_index += 1
return server
def main():
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind(('0.0.0.0', 80))
listener.listen(100)
print("TCP Load Balancer running on port 80")
while True:
client, addr = listener.accept()
backend = roubin_scheduler()
print(f"Routing {addr} -> {backend}")
t = threading.Thread(target=handle_client, args=(client, backend))
t.start()
if __name__ == '__main__':
main()
代码关键点解析
| 代码块 | 作用 | 高级优化方向 |
|---|---|---|
roubin_scheduler() |
线程安全的轮询计数器 | 可替换为随机/权重算法 |
handle_client 内的双线程转发 |
实现全双工通信 | 可用selectors替代节省线程资源 |
SO_REUSEADDR |
允许快速重启脚本 | 生产环境建议增加SO_REUSEPORT |
Q:这个脚本能抵抗后端服务器宕机吗?
A:不能,这是一个“信任式”转发,如果后端挂了,客户端会收到连接失败,生产级脚本需要添加健康检查(详见第4节)。
七层负载均衡脚本(HTTP感知+健康检查)
关键增强功能
- HTTP Header解析:识别
Host字段实现虚拟主机路由 - 健康检查:每5秒检查后端是否存活,自动剔除故障节点
- 加权轮询:通过配置
weight参数调整流量分配比例
核心代码片段(lb_http.py)
import socket, threading, json, subprocess
class HealthChecker:
def __init__(self, servers_config):
self.servers = servers_config # [{'addr':('IP',port), 'weight':1}, ...]
self.alive = {s['addr']: True for s in self.servers}
self.check_thread = threading.Thread(target=self.check_loop, daemon=True)
self.check_thread.start()
def check_loop(self):
while True:
for server in self.servers:
addr = server['addr']
try:
# 通过TCP连接测试(也可用HTTP GET)
s = socket.create_connection(addr, timeout=2)
s.close()
self.alive[addr] = True
except:
self.alive[addr] = False
time.sleep(5)
def get_alive_servers(self):
return [s for s in self.servers if self.alive[s['addr']]]
# 加权轮询调度(基于C接口)
class WeightedRoundRobin:
def __init__(self, servers):
self.servers = servers
self.current = 0
self.cw = 0
self.max_weight = max(s['weight'] for s in servers)
def next(self):
while True:
if self.current >= len(self.servers):
self.current = 0
self.cw -= 1
if self.cw <= 0:
self.cw = self.max_weight
if self.cw == 0:
return None
server = self.servers[self.current]
if server['weight'] >= self.cw:
self.current += 1
return server['addr']
self.current += 1
使用方式
# 修改配置文件
server_config = '{"servers": [{"addr": ["192.168.1.101", 8080], "weight": 3}, {"addr": ["192.168.1.102", 8080], "weight": 1}]}'
python3 lb_http.py -c server.conf
Q:为什么建议使用JSON配置文件硬编码,而不是运行时动态添加?
A:对于简易脚本,JSON配置文件已足够,动态添加需要额外实现API接口,增加代码复杂度且容易产生竞态条件,生产环境建议使用Consul/etcd等注册中心。
高频问题FAQ
问题1:脚本模式与Nginx性能差距多大?
在纯TCP转发场景,Python脚本吞吐量约为Nginx的1/5~1/3(取决于机器配置),核心瓶颈在于Python的GIL和socket上下文切换,如果需求达到10万+并发,请使用C语言或Go重写,对于几千并发的中小场景,脚本完全可用。
问题2:支持HTTPS终止(TLS Offload)吗?
简易脚本通常不支持,需要HTTPS卸载时,建议前端挂Nginx或Envoy,后端脚本仅做四层转发,如果必须实现,可集成ssl模块,但会增加复杂度和性能开销。
问题3:如何实现会话保持(Session Stickiness)?
基于源IP哈希是最简单的实现方式:
def ip_hash_scheduler(client_ip):
hash_val = hash(client_ip) & 0xFFFFFFFF
index = hash_val % len(self.alive_servers)
return self.alive_servers[index]
注意:当后端数变化时,哈希结果会改变,建议使用一致性哈希算法。
问题4:如何记录访问日志?
在handle_client函数中添加:
import datetime
log_entry = f"{datetime.now()} - {client_addr[0]}:{client_addr[1]} -> {backend_addr}\n"
with open('access.log', 'a') as f:
f.write(log_entry)
生产环境建议使用logging模块配合日志轮转。
问题5:脚本崩溃后如何自动重启?
使用supervisor或systemd:
[Unit] Description=LB Script Service [Service] ExecStart=/usr/bin/python3 /opt/lb/lb_tcp.py Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target
脚本优化与生产级建议
✅ 必须做的三个优化
-
使用epoll/kqueue替代多线程
在Linux下用selectors模块:import selectors sel = selectors.DefaultSelector() sel.register(listener, selectors.EVENT_READ)
可将并发处理能力提升3-5倍。
-
连接池复用
对于HTTP请求,避免为每个请求创建新连接,可通过keepalive参数复用TCP连接。 -
优雅关闭
注册SIGTERM信号处理函数,确保关闭监听器后等待正在处理的请求完成:import signal running = True def shutdown(sig, frame): global running running = False print("Shutting down gracefully...") signal.signal(signal.SIGTERM, shutdown)
⚠️ 生产环境需要避免的坑
| 场景 | 错误做法 | 正确做法 |
|---|---|---|
| 后端数大于50 | 单线程顺序检查 | 使用异步健康检查 + 超时队列 |
| 需要加密通信 | 明文转发 | 前置TLS代理或集成asyncio+ssl |
| 日志文件过大 | 写入无限制 | 使用logging.handlers.RotatingFileHandler |
本文从四层和七层两个维度,展示了如何用不到200行Python代码实现一个功能完整的负载均衡脚本,虽然它无法替代Nginx/HAProxy等专业软件,但作为理解网络编程、学习负载均衡原理的实践工具,价值巨大,建议读者在本地网络环境下亲手运行测试,并尝试添加权重动态调整或熔断机制,从而更深入掌握分布式系统的流量治理精髓。
延伸学习:可进一步研究《TCP/IP详解》第12章关于多路复用机制的描述,以及LVS(Linux Virtual Server)的VS/TUN模式实现源码。