3. 线程基础与生命周期
负载均衡这东西,说白了就是「把流量分给多台机器干」。
我刚开始做高并发系统时,以为负载均衡就是加个 Nginx 完事。后来线上流量一上来,才发现不同层次的负载均衡各有各的战场。今天咱们就把 DNS、Nginx、LVS 和一致性哈希这四块掰开揉碎讲清楚。
核心观点:没有万能的负载均衡方案。DNS 解决入口分散,LVS 解决四层高吞吐,Nginx 解决七层精细化,一致性哈希解决缓存分片。四者组合使用才是生产常态。
3.1 DNS 负载均衡:最容易被忽视的入口层
DNS 负载均衡,其实就是同一个域名配多个 A 记录。用户访问时,DNS 服务器轮询或按地域返回不同 IP。
我记得有一次做跨国业务,所有流量都打到北京机房,海外用户延迟高得离谱。后来我们在 DNS 层面做了分地域解析——北美用户走 AWS 新加坡节点,欧洲用户走法兰克福节点。效果立竿见影,延迟从 300ms 降到了 80ms。
| 特性 | 说明 |
|---|---|
| 优点 | 天然分布式,无中心节点;成本极低;支持地域/运营商调度 |
| 缺点 | DNS 缓存导致生效慢(TTL 期间不可控);无法感知后端健康状态;粒度粗(只能到 IP 级别) |
| 典型场景 | 多机房入口分流、CDN 调度、容灾切换 |
避坑:我曾经遇到过 DNS 缓存雪崩——某个大版本升级时改了 DNS 记录,但大量用户设备缓存了旧 IP,导致新老集群同时被访问,数据不一致。后来我们强制要求:DNS 变更前先把 TTL 降到 60 秒,观察 24 小时再正式切换。
3.2 Nginx 反向代理:七层负载均衡的瑞士军刀
Nginx 工作在 OSI 第七层,能读懂 HTTP 协议。这意味着它可以做很多「聪明」的事——按 URL 路径路由、按 Header 分发、甚至根据 Cookie 做会话保持。
我个人习惯在生产环境用 Nginx 做「网关层」,前面挂 LVS 扛流量,后面挂业务服务。Nginx 负责:
- 路由转发:
/api/*走后端 A,/web/*走后端 B - 限流熔断: 单 IP 每秒 100 次,超了直接 503
- 健康检查: 自动摘除宕机节点
- SSL 卸载: 证书放在 Nginx,后端走 HTTP 明文
# Nginx 负载均衡配置示例
upstream backend {
# 默认轮询
server 10.0.1.1:8080 weight=5;
server 10.0.1.2:8080 weight=3;
server 10.0.1.3:8080 backup; # 备用节点
# 健康检查(需 nginx_upstream_check_module)
check interval=3000 rise=2 fall=5 timeout=1000;
}
server {
listen 80;
location /api/ {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
经验之谈:Nginx 的 worker_processes 建议设为 CPU 核心数。我见过有人设成 64,结果上下文切换开销比处理请求还大。另外,keepalive 一定要开,能减少大量 TCP 握手开销。
3.3 LVS 四层负载均衡:高并发场景的基石
LVS(Linux Virtual Server)工作在第四层(传输层),只处理 TCP/UDP 包的转发。它不解析 HTTP 内容,所以性能极高——单机轻松跑满 10Gbps 线速。
LVS 有三种工作模式,我简单对比一下:
| 模式 | 工作原理 | 性能 | 典型场景 |
|---|---|---|---|
| DR(直接路由) | 修改目标 MAC 地址,RS 直接回包给客户端 | ★★★★★ | 最常用,要求 RS 和 LVS 在同一二层网络 |
| NAT(网络地址转换) | 修改目标 IP 和端口,RS 回包经过 LVS | ★★★☆☆ | LVS 容易成为瓶颈,适合小规模集群 |
| TUN(IP 隧道) | IP 包再封装一层,RS 解封装后直接回包 | ★★★★☆ | 跨机房部署,RS 可以不在同一网络 |
我在项目中用得最多的是 DR 模式。配置起来也不复杂:
# LVS DR 模式配置示例(ipvsadm)
# 在 LVS 节点上配置 VIP
ip addr add 192.168.1.100/32 dev lo:0
# 添加虚拟服务
ipvsadm -A -t 192.168.1.100:80 -s rr
# 添加真实服务器(RS)
ipvsadm -a -t 192.168.1.100:80 -r 10.0.1.1:80 -g
ipvsadm -a -t 192.168.1.100:80 -r 10.0.1.2:80 -g
ipvsadm -a -t 192.168.1.100:80 -r 10.0.1.3:80 -g
注意:DR 模式下,RS 必须配置抑制 ARP 响应,否则 RS 会跟 LVS 抢 VIP。我曾经因为忘了配 arp_ignore 和 arp_announce,导致整个集群网络风暴,排查了整整一个下午。
3.4 一致性哈希:缓存分片与数据路由的利器
前面讲的 DNS、Nginx、LVS 都是「无状态」的负载均衡——请求分给谁都可以。但缓存场景不一样:你希望同一个 key 每次都落到同一台缓存节点上,否则缓存命中率会暴跌。
普通哈希取模的问题很明显:节点数一变,几乎所有 key 都要重新映射。这就是所谓的「缓存雪崩」——大量请求穿透到数据库。
一致性哈希解决了这个问题。它把哈希值空间组织成一个环(0 ~ 2^32-1),节点和 key 都哈希到环上。key 顺时针找到的第一个节点就是目标节点。
我举个例子:
- 有 3 个缓存节点:A、B、C
- 哈希环上有 100 个 key
- 现在加一个节点 D
- 普通哈希:100 个 key 全部重新映射
- 一致性哈希:只有 D 节点顺时针方向到上一个节点之间的 key 需要迁移,大约 25 个
关键优化:虚拟节点。如果物理节点少,哈希环上分布不均匀会导致「倾斜」——某个节点负载特别高。解决办法是每个物理节点创建 100~200 个虚拟节点,均匀分布在环上。我在 Redis Cluster 和 Memcached 集群中都这么干过,效果很好。
# 一致性哈希的 Python 伪代码(示意)
import hashlib
class ConsistentHash:
def __init__(self, nodes=None, virtual_nodes=150):
self.virtual_nodes = virtual_nodes
self.ring = {}
self.sorted_keys = []
if nodes:
for node in nodes:
self.add_node(node)
def _hash(self, key):
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_node(self, node):
for i in range(self.virtual_nodes):
vnode_key = f"{node}#{i}"
hash_val = self._hash(vnode_key)
self.ring[hash_val] = node
self.sorted_keys.append(hash_val)
self.sorted_keys.sort()
def get_node(self, key):
if not self.ring:
return None
hash_val = self._hash(key)
# 二分查找第一个 >= hash_val 的节点
for k in self.sorted_keys:
if k >= hash_val:
return self.ring[k]
# 如果超出环尾,回到环头
return self.ring[self.sorted_keys[0]]
实际应用:一致性哈希不只在缓存层有用。我在做数据库分库分表时也用过——按用户 ID 哈希路由到不同的分库,加机器时只需要迁移少量数据。另外,Nginx 的 ngx_http_upstream_consistent_hash 模块可以直接用,省得自己实现。
3.5 四种策略如何组合?
你想想看,一个真正的高并发系统,通常不会只用一种负载均衡策略。我画了个典型的组合链路:
- DNS 层: 按地域解析到不同机房的 LVS 集群 VIP
- LVS 层: 四层转发,把流量分到机房的 Nginx 集群
- Nginx 层: 七层路由,按 URL 路径分到不同的业务服务
- 业务服务层: 内部再用一致性哈希路由到缓存或数据库分片
每一层解决不同的问题。DNS 解决「用户该去哪个机房」,LVS 解决「机房内流量怎么分给 Nginx」,Nginx 解决「请求该去哪个服务」,一致性哈希解决「数据该存到哪个节点」。
嗯,这套组合拳打下来,单机房万级 QPS、跨机房百万级 QPS 都不是问题。我参与过的几个日活千万级的项目,底层基本都是这个架构。