AIGC标识 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自动重试

思考题

  1. 什么场景下IP Hash比Round-Robin更合适?
  2. 如何在upstream中实现会话粘滞(session sticky)?
  3. upstream连接池大小如何影响性能?

思考题解答

1. 什么场景下IP Hash比Round-Robin更合适?

IP Hash适用场景

  1. 会话保持(Session Sticky)

    # 用户登录后需要保持会话
    upstream backend {
        ip_hash;
        server 192.168.1.1:8080;
        server 192.168.1.2:8080;
    }
    

    同一IP的请求始终访问同一后端,会话数据不会丢失。

  2. 有状态应用

    • 购物车(本地存储在后端)
    • 用户偏好设置
    • 游戏状态
  3. 缓存一致性

    • 后端有本地缓存时,同一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个空闲连接是合理的。

posted @ 2026-09-04 17:26  IcarusLee  阅读(2)  评论(0)    收藏  举报