nginx-源码带读-05-反向代理与负载均衡
NGINX 源码带读 第5篇:反向代理与负载均衡
本篇目标
理解NGINX的upstream反向代理机制和负载均衡算法,深入分析upstream执行流程、连接管理和健康检查机制。
前置知识
- HTTP反向代理概念
- 阅读过第1-4篇
1. upstream机制概述
1.1 什么是upstream
upstream是NGINX的反向代理核心。当请求到达NGINX后,NGINX将请求转发到后端服务器(upstream server),并将响应返回给客户端。
1.2 配置示例
upstream backend {
server 192.168.1.1:8080 weight=3; # 权重3
server 192.168.1.2:8080; # 权重1
server 192.168.1.3:8080 backup; # 备用服务器
server 192.168.1.4:8080 down; # 标记为不可用
keepalive 32; # 保持32个空闲连接
}
server {
location /api {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
1.3 upstream服务器参数
| 参数 | 说明 | 默认值 |
|---|---|---|
weight |
权重 | 1 |
max_fails |
最大失败次数 | 1 |
fail_timeout |
失败超时时间 | 10s |
backup |
备用服务器 | 否 |
down |
标记为不可用 | 否 |
max_conns |
最大连接数 | 0(不限制) |
2. ngx_http_upstream 模块
2.1 核心结构
// src/http/ngx_http_upstream.h
typedef struct ngx_http_upstream_s {
// 后端连接
ngx_peer_connection_t peer;
// 请求
ngx_http_request_t *request;
// 响应缓冲区
ngx_buf_t *buffer;
size_t buffer_size;
// 状态机回调
ngx_int_t (*create_request)(ngx_http_request_t *r);
ngx_int_t (*reinit_request)(ngx_http_request_t *r);
void (*process_header)(ngx_http_request_t *r);
ngx_int_t (*input_filter)(void *data, ssize_t bytes);
void (*finalize_request)(ngx_http_request_t *r, ngx_int_t rc);
// 响应头
ngx_int_t (*output_filter)(ngx_http_request_t *r, ngx_chain_t *chain);
// 状态
ngx_int_t state;
ngx_int_t status;
// 缓存
ngx_http_cache_t *cache;
// 超时
ngx_msec_t connect_timeout;
ngx_msec_t send_timeout;
ngx_msec_t read_timeout;
// 标志
unsigned buffered:1;
unsigned header_sent:1;
} ngx_http_upstream_t;
2.2 upstream执行流程
proxy_pass 指令
-> ngx_http_upstream() 设置upstream
-> ngx_http_upstream_create() 创建upstream结构
-> ngx_http_upstream_init() 初始化
-> ngx_http_upstream_init_request() 初始化请求
-> ngx_http_upstream_cache_check() 检查缓存
-> u->create_request(r) 调用proxy模块创建请求
-> ngx_http_upstream_resolve() 解析域名
-> ngx_http_upstream_connect() 连接后端
-> ngx_event_connect_peer() 建立连接
-> ngx_http_upstream_send_request() 发送请求
-> ngx_http_upstream_process_header() 处理响应头
-> ngx_http_upstream_send_response() 发送响应
-> ngx_http_upstream_finalize() 完成
2.3 ngx_http_upstream_init()
// src/http/ngx_http_upstream.c
void ngx_http_upstream_init(ngx_http_request_t *r) {
ngx_http_upstream_t *u;
u = r->upstream;
// 清除客户端读超时
ngx_del_timer(c->read);
// 设置写事件回调
u->write_event_handler = ngx_http_upstream_handler;
u->read_event_handler = ngx_http_upstream_handler;
// 初始化请求
ngx_http_upstream_init_request(r);
}
2.4 ngx_http_upstream_connect()
static void ngx_http_upstream_connect(ngx_http_request_t *r,
ngx_http_upstream_t *u) {
ngx_connection_t *c;
// 创建到后端的连接
rc = ngx_event_connect_peer(&u->peer);
if (rc == NGX_BUSY) {
// 后端连接数满
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"no live upstreams");
ngx_http_upstream_finalize(r, u, 502);
return;
}
if (rc == NGX_ERROR || rc == NGX_DECLINED) {
// 连接失败
ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_ERROR);
return;
}
// 设置连接
c = u->peer.connection;
c->single_connection = 1;
// 设置事件回调
c->read->handler = ngx_http_upstream_handler;
c->write->handler = ngx_http_upstream_handler;
// 设置状态机
u->write_event_handler = ngx_http_upstream_send_request;
u->read_event_handler = ngx_http_upstream_process_header;
// 发送请求
ngx_http_upstream_send_request(r, u);
}
3. 负载均衡算法
3.1 Round-Robin(轮询)——默认
// src/http/ngx_http_upstream_round_robin.c
static ngx_int_t ngx_http_upstream_init_round_robin_peer(
ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us) {
ngx_http_upstream_round_robin_peer_t *rrp;
rrp = &r->upstream->peer;
rrp->peers = us->peer.data;
rrp->current = rrp->peers->peer; // 从第一个开始
// 设置回调
rrp->peer.get = ngx_http_upstream_get_round_robin_peer;
rrp->peer.free = ngx_http_upstream_free_round_robin_peer;
return NGX_OK;
}
static ngx_peer_t *ngx_http_upstream_get_round_robin_peer(
ngx_peer_connection_t *pc, void *data) {
ngx_http_upstream_round_robin_peer_t *rrp = data;
ngx_peer_t *peer;
peer = rrp->current; // 获取当前peer
rrp->current = peer->next; // 移到下一个
// 循环回来
if (rrp->current == NULL) {
rrp->current = rrp->peers->peer;
}
return peer;
}
3.2 Weighted Round-Robin(加权轮询)
加权轮询通过增加权重来分配更多请求到高性能服务器:
配置:
server 192.168.1.1:8080 weight=3; # 权重3
server 192.168.1.2:8080 weight=1; # 权重1
结果:
请求1 -> server 1
请求2 -> server 1
请求3 -> server 1
请求4 -> server 2
请求5 -> server 1
...
3.3 IP Hash
// 根据客户端IP的哈希值选择后端
static ngx_int_t ngx_http_upstream_init_ip_hash_peer(
ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us) {
ngx_http_upstream_ip_hash_peer_t *iphp;
iphp = &r->upstream->peer.iphp;
// 计算IP哈希值
ngx_inetSockaddr(r->connection->sockaddr, &iphp->hash);
return NGX_OK;
}
static ngx_peer_t *ngx_http_upstream_get_ip_hash_peer(
ngx_peer_connection_t *pc, void *data) {
ngx_http_upstream_ip_hash_peer_t *iphp = data;
// 使用哈希值选择后端
index = iphp->hash % iphp->peers->number;
peer = &iphp->peers->peer[index];
return peer;
}
IP Hash的缺点:
- 负载不均衡(某些IP可能有更多请求)
- 代理后IP可能变化(需要配置
proxy_set_header X-Real-IP) - 后端故障时需要重新分配
3.4 Least Connections(最少连接)
选择当前活跃连接数最少的后端服务器:
static ngx_peer_t *ngx_http_upstream_get_least_conn_peer(
ngx_peer_connection_t *pc, void *data) {
ngx_http_upstream_least_conn_peer_t *lcp = data;
ngx_peer_t *peer, *best;
ngx_uint_t i;
best = NULL;
best_count = NGX_MAX_INT_T_VALUE;
for (i = 0; i < lcp->peers->number; i++) {
peer = &lcp->peers->peer[i];
// 检查服务器是否可用
if (peer->max_fails && peer->fails >= peer->max_fails) {
continue;
}
// 选择连接数最少的服务器
if (peer->current_conn < best_count) {
best_count = peer->current_conn;
best = peer;
}
}
return best;
}
3.5 一致性哈希(Consistent Hash)
NGINX支持一致性哈希,用于缓存场景:
upstream backend {
hash $request_uri consistent;
server 192.168.1.1:8080;
server 192.168.1.2:8080;
}
一致性哈希的优势:
- 服务器数量变化时,只有少量key重新分配
- 缓存命中率高
4. 健康检查
4.1 被动检查
当upstream连接失败时,标记该server为不可用:
// src/http/ngx_http_upstream.c
static void ngx_http_upstream_next(ngx_http_request_t *r,
ngx_http_upstream_t *u, ngx_uint_t ft_type) {
// 增加失败计数
peer->fails++;
// 检查是否达到最大失败次数
if (peer->fails >= peer->max_fails) {
// 标记为不可用
peer->max_fails_time = ngx_time();
// 等待fail_timeout后自动恢复
peer->fails_timeout = ngx_time() + peer->fail_timeout;
}
// 尝试下一个后端
ngx_http_upstream_connect(r, u);
}
4.2 健康检查配置
upstream backend {
server 192.168.1.1:8080 max_fails=3 fail_timeout=30s;
server 192.168.1.2:8080 max_fails=3 fail_timeout=30s;
# 被动健康检查
# 连续失败3次后标记为不可用
# 30秒后自动恢复
}
4.3 主动检查(商业版)
NGINX Plus支持主动健康检查,定期向后端发送探测请求:
upstream backend {
zone backend 64k;
server 192.168.1.1:8080;
# 主动健康检查
health_check interval=10 fails=3 passes=2 uri=/health;
}
server {
location / {
proxy_pass http://backend;
}
}
5. 连接池
5.1 后端连接池
NGINX维护到每个upstream server的连接池,复用TCP连接:
upstream backend {
server 192.168.1.1:8080;
keepalive 32; # 保持32个空闲连接
}
server {
location /api {
proxy_pass http://backend;
proxy_http_version 1.1; # 必须使用HTTP/1.1
proxy_set_header Connection ""; # 复用连接
}
}
5.2 keepalive连接管理
// src/http/ngx_http_upstream_keepalive.c
static void ngx_http_upstream_keepalive_handler(ngx_event_t *ev) {
ngx_connection_t *c;
ngx_http_upstream_keepalive_cache_t *cache;
c = ev->data;
// 检查连接是否超时
if (ev->timedout) {
// 关闭超时连接
ngx_http_upstream_keepalive_close(c);
return;
}
// 将连接放回缓存
cache = ngx_http_upstream_get_keepalive_cache(c);
ngx_http_upstream_keepalive_cache(c, cache);
}
5.3 连接池大小计算
最佳连接数 = 后端服务器数 * 单服务器处理能力 * 平均响应时间
示例:
后端服务器数 = 3
单服务器处理能力 = 100 req/s
平均响应时间 = 0.1s
最佳连接数 = 3 * 100 * 0.1 = 30
建议值:每个后端服务器16-64个空闲连接
5.4 连接池配置建议
upstream backend {
server 192.168.1.1:8080;
server 192.168.1.2:8080;
keepalive 64; # 每个worker保持64个空闲连接
}
server {
location /api {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_connect_timeout 5s; # 连接超时
proxy_send_timeout 60s; # 发送超时
proxy_read_timeout 60s; # 读取超时
}
}
6. 缓冲机制
6.1 缓冲模式
NGINX支持两种缓冲模式:
缓冲模式(默认):
客户端 -> NGINX(缓冲) -> 后端服务器
|
v
NGINX(缓冲响应) -> 客户端
非缓冲模式:
客户端 -> NGINX(透传) -> 后端服务器
|
v
NGINX(透传响应) -> 客户端
6.2 缓冲配置
location /api {
proxy_pass http://backend;
# 缓冲配置
proxy_buffering on; # 启用缓冲
proxy_buffer_size 4k; # 响应头缓冲区大小
proxy_buffers 8 4k; # 响应体缓冲区数量和大小
proxy_busy_buffers_size 8k; # 忙碌时缓冲区大小
proxy_max_temp_file_size 1024m; # 临时文件最大大小
}
7. upstream状态机
7.1 upstream状态
// src/http/ngx_http_upstream.h
typedef enum {
NGX_HTTP_UPSTREAM_INIT = 0, // 初始化
NGX_HTTP_UPSTREAM_CONNECTING, // 连接中
NGX_HTTP_UPSTREAM_CONNECT, // 已连接
NGX_HTTP_UPSTREAM_SEND_REQUEST, // 发送请求中
NGX_HTTP_UPSTREAM_HEADER, // 接收响应头中
NGX_HTTP_UPSTREAM_BODY, // 接收响应体中
NGX_HTTP_UPSTREAM_DONE, // 完成
NGX_HTTP_UPSTREAM_INVALID_HEADER // 无效响应头
} ngx_http_upstream_state_e;
7.2 状态转换
INIT -> CONNECTING -> CONNECT -> SEND_REQUEST -> HEADER -> BODY -> DONE
| | | | | |
v v v v v v
错误 超时 连接失败 发送失败 接收失败 接收完成
| | | | | |
v v v v v v
FINALIZE (next/upstream/重试)
7.3 upstream_next机制
当后端连接失败时,NGINX可以自动尝试下一个后端:
upstream backend {
server 192.168.1.1:8080;
server 192.168.1.2:8080;
# 重试策略
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 3; # 最多重试3次
proxy_next_upstream_timeout 10s; # 总超时时间
}
8. 本篇小结
| 概念 | 要点 |
|---|---|
| upstream | 反向代理核心模块 |
| 负载均衡 | round-robin(默认)/ ip-hash / least-conn / consistent-hash |
| 健康检查 | 被动:基于失败计数;主动:NGINX Plus |
| 连接池 | keepalive复用后端连接 |
| 缓冲 | 缓冲/非缓冲模式 |
| 状态机 | init->connect->send->recv->done |
| 重试 | proxy_next_upstream自动重试 |
思考题
- 什么场景下IP Hash比Round-Robin更合适?
- 如何在upstream中实现会话粘滞(session sticky)?
- upstream连接池大小如何影响性能?
思考题解答
1. 什么场景下IP Hash比Round-Robin更合适?
IP Hash适用场景:
-
会话保持(Session Sticky):
# 用户登录后需要保持会话 upstream backend { ip_hash; server 192.168.1.1:8080; server 192.168.1.2:8080; }同一IP的请求始终访问同一后端,会话数据不会丢失。
-
有状态应用:
- 购物车(本地存储在后端)
- 用户偏好设置
- 游戏状态
-
缓存一致性:
- 后端有本地缓存时,同一IP的请求访问同一后端,缓存命中率更高
Round-Robin适用场景:
- 无状态应用(REST API)
- 后端完全对等
- 负载均衡优先级高于会话保持
IP Hash的缺点:
- 负载不均衡(某些IP可能有更多请求)
- 后端故障时需要重新分配
- 代理后IP可能变化(需要配置
proxy_set_header X-Real-IP)
2. 如何实现会话粘滞?
方法1:ip_hash(最简单):
upstream backend {
ip_hash;
server 192.168.1.1:8080;
server 192.168.1.2:8080;
}
方法2:cookie粘滞(更灵活):
upstream backend {
hash $cookie_sessionid consistent;
server 192.168.1.1:8080;
server 192.168.1.2:8080;
}
方法3:sticky模块(NGINX Plus):
upstream backend {
sticky cookie srv_id expires=1h;
server 192.168.1.1:8080;
server 192.168.1.2:8080;
}
方法4:Lua自定义逻辑:
location /api {
set_by_lua_block $backend {
local session = ngx.var.cookie_session
if session then
local hash = ngx.crc32_long(session)
local servers = {"192.168.1.1:8080", "192.168.1.2:8080"}
return servers[hash % #servers + 1]
end
}
proxy_pass http://$backend;
}
推荐方案:简单场景用ip_hash,需要精确控制用cookie粘滞或Lua。
3. upstream连接池大小如何影响性能?
连接池过小的后果:
- 连接等待:请求需要等待空闲连接,增加延迟
- 连接建立开销:频繁创建和关闭TCP连接
- 后端压力:大量短连接可能耗尽后端的连接数限制
连接池过大的后果:
- 内存浪费:每个空闲连接占用内存
- 后端压力:后端需要维护大量空闲连接
- 文件描述符浪费:系统文件描述符有限
配置建议:
upstream backend {
server 192.168.1.1:8080;
keepalive 32; # 保持32个空闲连接
}
server {
location /api {
proxy_pass http://backend;
proxy_http_version 1.1; # 必须使用HTTP/1.1
proxy_set_header Connection ""; # 复用连接
}
}
连接池大小计算:
最佳连接数 = 后端服务器数 * 单服务器处理能力 * 平均响应时间
监控指标:
upstream_keepalive:当前空闲连接数upstream_connect_time:连接建立时间upstream_wait_time:等待连接时间
建议:从小值开始(如16),根据监控数据逐步调整。通常每个后端服务器保持16-64个空闲连接是合理的。

浙公网安备 33010602011771号