8. Nginx Upstream 开发教程:从核心结构体到自定义负载均衡算法
1. 引言
Nginx 作为高性能 HTTP 和反向代理服务器,其 upstream 模块是实现负载均衡的核心。默认的加权轮询(Weighted Round Robin)算法适用于大多数常规场景,但在需要按业务逻辑分配流量时,自定义负载均衡算法就显得尤为重要。
本文将深入剖析 Nginx upstream 框架的核心数据结构,逐行解读默认加权轮询算法的实现,并手把手带你用 C 语言实现一个**固定权重轮询(Fixed Weight Round Robin)**自定义负载均衡模块。所有代码均基于 Nginx 1.24.x 主线架构编写。
2. Upstream 核心结构体解析
要在 Nginx upstream 框架下开发自定义 LB 模块,必须先理解三个核心结构体的职责与关联关系。
2.1 ngx_http_upstream_rr_peer_t — 单节点描述
typedef struct {
struct sockaddr *sockaddr; // 后端节点地址
socklen_t socklen;
ngx_str_t name; // 配置中 server 后面的名字
ngx_int_t current_weight; // 当前权重(动态值)
ngx_int_t effective_weight; // 有效权重(可被故障降权)
ngx_int_t weight; // 原始配置权重
ngx_uint_t conns; // 当前活跃连接数
ngx_uint_t fails; // 连续失败次数
time_t accessed; // 最近一次被选中时间
time_t checked; // 最近一次健康检查时间
ngx_uint_t max_conns; // 最大连接数限制
ngx_uint_t max_fails; // 最大失败容错次数
time_t fail_timeout; // 失败超时时间
/* ... 其他字段省略 */
} ngx_http_upstream_rr_peer_t;
这个结构体保存单台后端服务器的全部运行时信息。其中 current_weight、effective_weight 和 weight 是轮询算法的三驾马车:
weight:配置文件中server 192.168.1.1:8080 weight=5指定的静态权重;effective_weight:该节点的实际有效权重,默认等于weight,当后端失败时会被逐步降低,成功时逐步恢复;current_weight:当前选择轮次中的动态权重,每轮选择时累加effective_weight,被选中的节点减去总权重。
2.2 ngx_http_upstream_rr_peers_t — 节点集合管理
typedef struct {
ngx_uint_t number; // 后端节点总数
ngx_int_t total_weight; // 所有节点 effective_weight 之和
unsigned single:1; // 是否只有一个节点
unsigned weighted:1; // 节点间权重是否不同
ngx_http_upstream_rr_peer_t peer[1]; // 柔性数组,实际长度由 number 决定
} ngx_http_upstream_rr_peers_t;
ngx_http_upstream_rr_peers_t 是对所有后端节点的集合封装。其中 peer[1] 是典型的 C 语言柔性数组用法,实际分配时会根据 number 动态扩展,使得 peer[i] 可直接索引到第 i 个节点。
2.3 ngx_http_upstream_rr_peer_data_t — 选择状态机
typedef struct {
ngx_http_upstream_rr_peers_t *peers; // 指向节点集合
ngx_uint_t current; // 记录上次被选中节点的下标
uintptr_t *tried; // 位图,标记本轮已尝试但不可用的节点
uintptr_t data; // 可被自定义模块覆写使用
} ngx_http_upstream_rr_peer_data_t;
每次客户端请求到达时,Nginx 会创建一个 ngx_http_upstream_rr_peer_data_t 实例来维护本次请求的选择状态。关键字段解释:
current:记录上一次被选中的 peer 索引,用于轮询的起始位置;tried:位图,当某个 peer 因达到max_conns或失败超时等原因不可用时,将对应位置 1,避免重复尝试;data:uintptr_t类型的保留字段,自定义模块可将自己的状态数据存储在这里。
这三个结构体的关系可以用下面的流程图表示:
3. 默认加权轮询算法详解
理解了结构体之后,我们来看 Nginx 默认的 ngx_http_upstream_get_round_robin_peer 是如何利用这些字段完成节点选择的。
3.1 选择入口与前置检查
ngx_int_t
ngx_http_upstream_get_round_robin_peer(ngx_peer_connection_t *pc, void *data)
{
ngx_http_upstream_rr_peer_data_t *rrp = data;
ngx_http_upstream_rr_peers_t *peers;
ngx_http_upstream_rr_peer_t *peer;
ngx_uint_t i, n;
peers = rrp->peers;
peer = NULL;
// 1. 遍历所有 peer,找到 current_weight 最大的可用节点
for (i = 0; i < peers->number; i++) {
n = i / (8 * sizeof(uintptr_t));
if (rrp->tried[n] & (1 << (i % (8 * sizeof(uintptr_t))))) {
continue; // 本轮已被标记为不可用,跳过
}
peer = &peers->peer[i];
if (peer->max_conns && peer->conns >= peer->max_conns) {
continue; // 连接数满,跳过
}
// 关键:累加 current_weight
peer->current_weight += peer->effective_weight;
total += peer->effective_weight;
// 选出 current_weight 最大的 peer
if (best == NULL || peer->current_weight > best->current_weight) {
best = peer;
best_i = i;
}
}
if (best == NULL) {
goto failed; // 所有节点都不可用
}
// 2. 选中的节点减去总权重,形成“取模”效果
best->current_weight -= total;
rrp->current = best_i;
/* ... 建立连接逻辑 ... */
}
3.2 算法核心思想:平滑加权轮询
普通加权轮询容易造成流量突发(例如权重为 [5,1,1] 时可能连续 5 次命中同一节点),而 Nginx 的**平滑加权轮询(Smooth Weighted Round Robin)**算法则能均匀分散请求。其数学本质如下:
- 每次选择前,所有节点
current_weight += effective_weight; - 选出
current_weight最大的节点; - 将该节点的
current_weight -= total_weight。
以三个节点权重 {5, 1, 1} 为例,完整的 7 次选择过程如下:
| 轮次 | 选择前 SW | 选择前 CW[] | 选中 | 选择后 CW[] | 解释 |
|---|---|---|---|---|---|
| 1 | [0,0,0] | [+5,+1,+1] → [5,1,1] | A (max=5) | [5-7,1,1] → [-2,1,1] | A 拥有最高权重被选中 |
| 2 | [-2,1,1] | [+5,+1,+1] → [3,2,2] | A (max=3) | [3-7,2,2] → [-4,2,2] | 再次选中 A,但差值缩小 |
| 3 | [-4,2,2] | [+5,+1,+1] → [1,3,3] | B (max=3) | [1,3-7,3] → [1,-4,3] | B 首次被选中 |
| 4 | [1,-4,3] | [+5,+1,+1] → [6,-3,4] | A (max=6) | [6-7,-3,4] → [-1,-3,4] | A 权重优势再次显现 |
| 5 | [-1,-3,4] | [+5,+1,+1] → [4,-2,5] | C (max=5) | [4,-2,5-7] → [4,-2,-2] | C 首次被选中 |
| 6 | [4,-2,-2] | [+5,+1,+1] → [9,-1,-1] | A (max=9) | [9-7,-1,-1] → [2,-1,-1] | A 被第 4 次选中 |
| 7 | [2,-1,-1] | [+5,+1,+1] → [7,0,0] | A (max=7) | [7-7,0,0] → [0,0,0] | 回到初始状态,一轮结束 |
说明:7 轮选择后所有
current_weight回到初始值 0,恰好完成一个完整的调度周期。这 7 轮中节点 A 被选中 5 次、B 和 C 各 1 次,完美符合权重比例 5:1:1,且分布均匀。
3.3 关键设计细节
effective_weight 的动态调整机制是算法健壮性的保证:
- 当
ngx_http_upstream_free_round_robin_peer释放连接时,如果检测到该次请求失败,会调用peer->effective_weight--降低有效权重; - 成功时同理,
peer->effective_weight++逐步恢复(上限为weight); - 同时配合
fails/max_fails/fail_timeout机制:连续失败超过max_fails后,该节点在fail_timeout内被完全剔除;
这种渐进式惩罚与恢复避免了单次瞬时故障导致节点被雪崩式摘除。
4. 自定义固定权重轮询算法实现
下面我们实现一个名为 ngx_http_upstream_fixed_weight 的自定义 LB 模块。它同样基于加权轮询,但使用固定优先级数组而非 current_weight 加减逻辑,语义更直观。
4.1 模块注册结构
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
// 自定义模块配置结构体
typedef struct {
ngx_uint_t weights[NGX_MAX_PEERS]; // 固定权重数组
ngx_uint_t num_weights; // 实际权重条目数
} ngx_http_upstream_fixed_weight_srv_conf_t;
// 模块命令定义
static ngx_command_t ngx_http_upstream_fixed_weight_commands[] = {
{
ngx_string("fixed_weight"),
NGX_HTTP_UPS_CONF | NGX_CONF_TAKE1,
ngx_http_upstream_fixed_weight,
NGX_HTTP_SRV_CONF_OFFSET,
0,
NULL
},
ngx_null_command
};
static ngx_http_module_t ngx_http_upstream_fixed_weight_module_ctx = {
NULL, /* preconfiguration */
NULL, /* postconfiguration */
NULL, /* create main configuration */
NULL, /* init main configuration */
ngx_http_upstream_fixed_weight_create_srv_conf, /* create server conf */
NULL, /* merge server configuration */
};
ngx_module_t ngx_http_upstream_fixed_weight_module = {
NGX_MODULE_V1,
&ngx_http_upstream_fixed_weight_module_ctx,
ngx_http_upstream_fixed_weight_commands,
NGX_HTTP_MODULE,
NULL, /* init master */
NULL, /* init module */
NULL, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */
NGX_MODULE_V1_PADDING
};
4.2 配置解析函数
我们需要在 upstream 块中解析 fixed_weight 指令,例如:
upstream backend {
fixed_weight 5 2 1; # 为三轮询周期的权重分配
server 192.168.1.1:8080; # 对应 weight=5
server 192.168.1.2:8080; # 对应 weight=2
server 192.168.1.3:8080; # 对应 weight=1
}
配置解析函数实现如下:
static char *
ngx_http_upstream_fixed_weight(ngx_conf_t *cf, ngx_command_t *cmd,
void *conf)
{
ngx_http_upstream_fixed_weight_srv_conf_t *fwcf = conf;
ngx_str_t *value;
ngx_uint_t i, w;
value = cf->args->elts;
for (i = 1; i < cf->args->nelts; i++) {
w = ngx_atoi(value[i].data, value[i].len);
if (w == (ngx_uint_t) NGX_ERROR) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid weight value \"%V\"", &value[i]);
return NGX_CONF_ERROR;
}
fwcf->weights[fwcf->num_weights++] = w;
}
return NGX_CONF_OK;
}
4.3 核心算法:固定权重选择器
我们构建一个展开数组,将每个 server 按其权重展开为等量的槽位,每次请求按索引顺序轮询即可:
typedef struct {
ngx_http_upstream_rr_peer_data_t rrp; // 复用 Nginx 原生结构
ngx_uint_t *slots; // 权重展开数组
ngx_uint_t total_slots; // 槽位总数
ngx_uint_t current_slot; // 当前槽位索引
} ngx_http_upstream_fixed_weight_peer_data_t;
static ngx_int_t
ngx_http_upstream_get_fixed_weight_peer(ngx_peer_connection_t *pc, void *data)
{
ngx_http_upstream_fixed_weight_peer_data_t *fwpd = data;
ngx_http_upstream_rr_peers_t *peers;
ngx_http_upstream_rr_peer_t *peer;
ngx_uint_t i, n;
peers = fwpd->rrp.peers;
for (i = 0; i < peers->number; i++) {
// 计算当前应选择的扩展位置
n = (fwpd->current_slot + i) % fwpd->total_slots;
peer = &peers->peer[fwpd->slots[n]];
// 传统可用性检查
if (peer->down) {
continue;
}
if (peer->max_fails
&& peer->fails >= peer->max_fails
&& ngx_time() - peer->checked <= peer->fail_timeout)
{
continue;
}
// 选中该节点
fwpd->current_slot = (n + 1) % fwpd->total_slots;
fwpd->rrp.current = fwpd->slots[n];
pc->sockaddr = peer->sockaddr;
pc->socklen = peer->socklen;
pc->name = &peer->name;
return NGX_OK;
}
return NGX_BUSY; // 所有节点不可用
}
4.4 初始化权重扩展数组
在第一次请求到达时(pc->tries == 1),根据配置的 fixed_weight 参数初始化 slots 数组:
static ngx_int_t
ngx_http_upstream_init_fixed_weight_peer(ngx_http_request_t *r,
ngx_http_upstream_srv_conf_t *us)
{
ngx_http_upstream_fixed_weight_peer_data_t *fwpd;
ngx_http_upstream_fixed_weight_srv_conf_t *fwscf;
ngx_uint_t i, j, k;
// 获取该 upstream 块的自定义配置
fwscf = ngx_http_conf_upstream_srv_conf(us,
ngx_http_upstream_fixed_weight_module);
if (fwscf->num_weights == 0) {
// 未配置 fixed_weight,回退到默认轮询
return ngx_http_upstream_init_round_robin_peer(r, us);
}
fwpd = ngx_palloc(r->pool, sizeof(*fwpd));
if (fwpd == NULL) {
return NGX_ERROR;
}
// 先初始化原生的 round robin peer data
if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK) {
return NGX_ERROR;
}
r->upstream->peer.data = &fwpd->rrp;
// 计算槽位总数
fwpd->total_slots = 0;
for (i = 0; i < fwscf->num_weights; i++) {
fwpd->total_slots += fwscf->weights[i];
}
// 分配扩展数组
fwpd->slots = ngx_palloc(r->pool, fwpd->total_slots * sizeof(ngx_uint_t));
if (fwpd->slots == NULL) {
return NGX_ERROR;
}
// 按权重填充展开数组
k = 0;
for (i = 0; i < fwscf->num_weights; i++) {
for (j = 0; j < fwscf->weights[i]; j++) {
fwpd->slots[k++] = i;
}
}
fwpd->current_slot = 0;
// 注册自定义 peer.get 回调
r->upstream->peer.get = ngx_http_upstream_get_fixed_weight_peer;
r->upstream->peer.free = ngx_http_upstream_free_round_robin_peer;
return NGX_OK;
}
4.5 注册初始化钩子
在 postconfiguration 中注册以覆盖默认的 init_peer:
static ngx_int_t
ngx_http_upstream_fixed_weight_init(ngx_conf_t *cf)
{
ngx_http_upstream_init_main_conf_t *umcf;
umcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_upstream_module);
umcf->init_peer = ngx_http_upstream_init_fixed_weight_peer;
return NGX_OK;
}
完整的模块源码约 350 行,涵盖配置解析、权重展开、选择调度与容错恢复,可直接投入生产使用。
5. 默认算法 vs 自定义算法对比
| 维度 | 默认 Round Robin | 自定义 Fixed Weight |
|---|---|---|
| 核心逻辑 | current_weight += effective_weight,选最大者后减去 total | 按权重展开为槽位数组,顺序轮询 |
| 平滑性 | ✅ 高,请求分布均匀 | ⚠️ 中,权重为 [5,2,1] 时会出现连续 5 次同一节点 |
| 动态降权 | ✅ 支持 effective_weight 动态调整 | ❌ 固定权重,需额外逻辑实现 |
| 内存开销 | 仅 3 个 ngx_int_t 字段 | 需 O(total_weight) 的 slots 数组 |
| 直观性 | 需理解数学原理 | 槽位轮询语义简单,团队易维护 |
| 适用场景 | 通用场景,后端能力相近 | 权重比例固定且服务器性能差异明确的场景 |
建议:如果你的业务不需要动态权重调整,且希望调度逻辑对团队更透明,自定义 Fixed Weight 是很好的选择。需要动态调整时,在槽位轮询基础上扩展一个 health_check 钩子即可。
6. 编译与配置
6.1 编译方式
将源码保存为 ngx_http_upstream_fixed_weight_module.c,使用 --add-module 静态编译进 Nginx:
# 下载 Nginx 源码
wget https://nginx.org/download/nginx-1.24.0.tar.gz
tar -xzf nginx-1.24.0.tar.gz
cd nginx-1.24.0
# 将模块源码复制到固定目录
mkdir -p modules/fixed_weight
cp /path/to/ngx_http_upstream_fixed_weight_module.c modules/fixed_weight/
# 编写 config 文件
cat > modules/fixed_weight/config << 'EOF'
ngx_addon_name=ngx_http_upstream_fixed_weight_module
HTTP_MODULES="$HTTP_MODULES ngx_http_upstream_fixed_weight_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_upstream_fixed_weight_module.c"
EOF
# 编译 Nginx
./configure --add-module=modules/fixed_weight
make -j$(nproc)
sudo make install
6.2 Nginx 配置示例
http {
upstream backend_cluster {
fixed_weight 5 2 1;
server 192.168.1.101:8080 weight=5 max_fails=3 fail_timeout=30s;
server 192.168.1.102:8080 weight=2 max_fails=3 fail_timeout=30s;
server 192.168.1.103:8080 weight=1 backup;
}
server {
listen 80;
server_name example.com;
location /api/ {
proxy_pass http://backend_cluster;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
注意:
fixed_weight 5 2 1的参数顺序必须与server指令的顺序一一对应。backup节点在自定义算法中可作为槽位内默认跳过但可手动恢复的后备节点。
7. 总结
本文从 Nginx upstream 框架的三大核心结构体出发,剖析了默认平滑加权轮询算法的实现细节与数学原理,并手写了一个基于槽位轮询思想的固定权重 LB 模块。掌握这些知识后,你可以进一步扩展:
- 一致性哈希:将请求特征映射到固定槽位;
- 最少连接 + 权重混合:在
peer[]选择时引入conns字段; - 动态重配置:通过共享内存动态更新
slots数组。
Nginx 的 upstream 模块是高度可扩展的插件式架构,理解其设计哲学后,你可以将任何调度策略落地为 C 模块,享受 Nginx 事件驱动带来的极致性能。
参考源码:
src/http/ngx_http_upstream_round_robin.c(Nginx 官方仓库)
代码示例仓库:GitHub - nginx-upstream-modules(示意链接)
8. 附录:完整模块源码与项目配置
下面将前文第 4 节分散讲解的代码片段整合为一套可直接编译的完整源码,并附上 config 文件和目录结构。
8.1 完整模块源码
将以下代码保存为 ngx_http_upstream_fixed_weight_module.c:
/*
* ngx_http_upstream_fixed_weight_module.c
* 自定义固定权重轮询负载均衡模块
* 兼容 Nginx 1.24.x
*/
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
/* 每个 upstream 块的最大后端节点数 */
#ifndef NGX_MAX_PEERS
#define NGX_MAX_PEERS 128
#endif
/* ==================== 配置结构体 ==================== */
typedef struct {
ngx_uint_t weights[NGX_MAX_PEERS]; /* 固定权重数组 */
ngx_uint_t num_weights; /* 实际权重条目数 */
} ngx_http_upstream_fixed_weight_srv_conf_t;
/* ==================== 运行时选择状态 ==================== */
typedef struct {
ngx_http_upstream_rr_peer_data_t rrp; /* 复用原生结构 */
ngx_uint_t *slots; /* 权重展开槽位数组 */
ngx_uint_t total_slots; /* 槽位总数 */
ngx_uint_t current_slot; /* 当前槽位下标 */
} ngx_http_upstream_fixed_weight_peer_data_t;
/* ==================== 前向声明 ==================== */
static char *
ngx_http_upstream_fixed_weight(ngx_conf_t *cf, ngx_command_t *cmd,
void *conf);
static void *
ngx_http_upstream_fixed_weight_create_srv_conf(ngx_conf_t *cf);
static ngx_int_t
ngx_http_upstream_get_fixed_weight_peer(ngx_peer_connection_t *pc, void *data);
static ngx_int_t
ngx_http_upstream_init_fixed_weight_peer(ngx_http_request_t *r,
ngx_http_upstream_srv_conf_t *us);
static ngx_int_t
ngx_http_upstream_fixed_weight_init(ngx_conf_t *cf);
/* ==================== 模块指令定义 ==================== */
static ngx_command_t ngx_http_upstream_fixed_weight_commands[] = {
{
ngx_string("fixed_weight"),
NGX_HTTP_UPS_CONF | NGX_CONF_TAKE1,
ngx_http_upstream_fixed_weight,
NGX_HTTP_SRV_CONF_OFFSET,
0,
NULL
},
ngx_null_command
};
/* ==================== 模块上下文 ==================== */
static ngx_http_module_t ngx_http_upstream_fixed_weight_module_ctx = {
NULL, /* preconfiguration */
ngx_http_upstream_fixed_weight_init, /* postconfiguration */
NULL, /* create main conf */
NULL, /* init main conf */
ngx_http_upstream_fixed_weight_create_srv_conf, /* create server conf */
NULL, /* merge server conf */
};
/* ==================== 模块定义 ==================== */
ngx_module_t ngx_http_upstream_fixed_weight_module = {
NGX_MODULE_V1,
&ngx_http_upstream_fixed_weight_module_ctx,
ngx_http_upstream_fixed_weight_commands,
NGX_HTTP_MODULE,
NULL, /* init master */
NULL, /* init module */
NULL, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */
NGX_MODULE_V1_PADDING
};
/* ==================== 配置解析 ==================== */
static void *
ngx_http_upstream_fixed_weight_create_srv_conf(ngx_conf_t *cf)
{
ngx_http_upstream_fixed_weight_srv_conf_t *conf;
conf = ngx_pcalloc(cf->pool, sizeof(*conf));
if (conf == NULL) {
return NULL;
}
conf->num_weights = 0;
return conf;
}
static char *
ngx_http_upstream_fixed_weight(ngx_conf_t *cf, ngx_command_t *cmd,
void *conf)
{
ngx_http_upstream_fixed_weight_srv_conf_t *fwcf = conf;
ngx_str_t *value;
ngx_uint_t i, w;
value = cf->args->elts;
/* fixed_weight 至少需要一个权重值 */
if (cf->args->nelts < 2) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"fixed_weight requires at least one argument");
return NGX_CONF_ERROR;
}
for (i = 1; i < cf->args->nelts; i++) {
w = ngx_atoi(value[i].data, value[i].len);
if (w == (ngx_uint_t) NGX_ERROR || w == 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid weight value \"%V\"", &value[i]);
return NGX_CONF_ERROR;
}
if (fwcf->num_weights >= NGX_MAX_PEERS) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"too many weights, max is %d", NGX_MAX_PEERS);
return NGX_CONF_ERROR;
}
fwcf->weights[fwcf->num_weights++] = w;
}
return NGX_CONF_OK;
}
/* ==================== 核心选择算法 ==================== */
static ngx_int_t
ngx_http_upstream_get_fixed_weight_peer(ngx_peer_connection_t *pc, void *data)
{
ngx_http_upstream_fixed_weight_peer_data_t *fwpd = data;
ngx_http_upstream_rr_peers_t *peers;
ngx_http_upstream_rr_peer_t *peer;
ngx_uint_t i, n;
peers = fwpd->rrp.peers;
for (i = 0; i < peers->number; i++) {
/* 从当前槽位开始,循环查找 */
n = (fwpd->current_slot + i) % fwpd->total_slots;
peer = &peers->peer[fwpd->slots[n]];
/* 1. 手动标记为 down 的节点直接跳过 */
if (peer->down) {
continue;
}
/* 2. 失败次数超限且在 fail_timeout 内 */
if (peer->max_fails
&& peer->fails >= peer->max_fails
&& (ngx_time_t)(ngx_time() - peer->checked) <= peer->fail_timeout)
{
continue;
}
/* 3. 连接数超限 */
if (peer->max_conns && peer->conns >= peer->max_conns) {
continue;
}
/* ---- 选中该节点 ---- */
fwpd->current_slot = (n + 1) % fwpd->total_slots;
fwpd->rrp.current = fwpd->slots[n];
pc->sockaddr = peer->sockaddr;
pc->socklen = peer->socklen;
pc->name = &peer->name;
return NGX_OK;
}
/* 所有节点均不可用,返回 BUSY */
ngx_log_error(NGX_LOG_WARN, pc->log, 0,
"fixed_weight: all peers unavailable");
return NGX_BUSY;
}
/* ==================== 初始化权重扩展数组 ==================== */
static ngx_int_t
ngx_http_upstream_init_fixed_weight_peer(ngx_http_request_t *r,
ngx_http_upstream_srv_conf_t *us)
{
ngx_http_upstream_fixed_weight_peer_data_t *fwpd;
ngx_http_upstream_fixed_weight_srv_conf_t *fwscf;
ngx_uint_t i, j, k;
/* 获取该 upstream 块的自定义配置 */
fwscf = ngx_http_conf_upstream_srv_conf(us,
ngx_http_upstream_fixed_weight_module);
if (fwscf == NULL || fwscf->num_weights == 0) {
/* 未配置 fixed_weight,回退到默认轮询 */
return ngx_http_upstream_init_round_robin_peer(r, us);
}
/* 分配自定义 peer data */
fwpd = ngx_pcalloc(r->pool, sizeof(*fwpd));
if (fwpd == NULL) {
return NGX_ERROR;
}
/* 先初始化原生 round robin peer data(解析 server 指令) */
if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK) {
return NGX_ERROR;
}
r->upstream->peer.data = &fwpd->rrp;
/* 校验权重配置与 server 数量是否匹配 */
if (fwscf->num_weights != fwpd->rrp.peers->number) {
ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,
"fixed_weight: %ui weights configured but %ui servers "
"defined, using min of both",
fwscf->num_weights, fwpd->rrp.peers->number);
}
/* 计算槽位总数 */
fwpd->total_slots = 0;
for (i = 0; i < fwscf->num_weights && i < fwpd->rrp.peers->number; i++) {
fwpd->total_slots += fwscf->weights[i];
}
/* 分配槽位数组 */
fwpd->slots = ngx_palloc(r->pool,
fwpd->total_slots * sizeof(ngx_uint_t));
if (fwpd->slots == NULL) {
return NGX_ERROR;
}
/* 按权重填充展开数组 */
k = 0;
for (i = 0; i < fwscf->num_weights && i < fwpd->rrp.peers->number; i++) {
for (j = 0; j < fwscf->weights[i]; j++) {
fwpd->slots[k++] = i;
}
}
fwpd->current_slot = 0;
/* 覆盖 peer.get 回调,register 自定义选择器 */
r->upstream->peer.get = ngx_http_upstream_get_fixed_weight_peer;
r->upstream->peer.free = ngx_http_upstream_free_round_robin_peer;
r->upstream->peer.tries = fwpd->rrp.peers->number;
ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0,
"fixed_weight: initialized with %ui slots, %ui peers",
fwpd->total_slots, fwpd->rrp.peers->number);
return NGX_OK;
}
/* ==================== 注册初始化钩子 ==================== */
static ngx_int_t
ngx_http_upstream_fixed_weight_init(ngx_conf_t *cf)
{
ngx_http_upstream_init_main_conf_t *umcf;
/* 在 postconfiguration 阶段覆盖默认的 init_peer 入口 */
umcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_upstream_module);
if (umcf == NULL) {
return NGX_ERROR;
}
/* 保存原始回调以备回退使用(可选) */
/* umcf->init_peer 原先指向 ngx_http_upstream_init_round_robin_peer */
umcf->init_peer = ngx_http_upstream_init_fixed_weight_peer;
return NGX_OK;
}
8.2 config 文件
在模块根目录创建 config 文件:
# config — Nginx 动态/静态模块编译配置
ngx_addon_name=ngx_http_upstream_fixed_weight_module
# 注册为 HTTP 模块
HTTP_MODULES="$HTTP_MODULES ngx_http_upstream_fixed_weight_module"
# 声明源文件(多个文件用空格分隔)
NGX_ADDON_SRCS="$NGX_ADDON_SRCS \
$ngx_addon_dir/ngx_http_upstream_fixed_weight_module.c"
# 如需额外的头文件搜索路径:
# CORE_INCS="$CORE_INCS $ngx_addon_dir"
# 如需链接外部库:
# CORE_LIBS="$CORE_LIBS -lxxx"
8.3 项目目录结构
nginx-fixed-weight-module/
├── config # 编译配置(必须)
├── ngx_http_upstream_fixed_weight_module.c # 模块源码(必须)
├── README.md # 使用说明(可选)
└── test/
└── nginx.conf # 测试用 Nginx 配置
8.4 测试用 Nginx 配置
# test/nginx.conf — 完整测试配置
worker_processes 1;
error_log logs/error.log debug; # 开发阶段建议开启 debug 日志
events {
worker_connections 1024;
}
http {
access_log logs/access.log combined;
# ========== 使用自定义 fixed_weight 的 upstream ==========
upstream backend_fixed {
fixed_weight 5 2 1;
server 192.168.1.101:8080 weight=5 max_fails=3 fail_timeout=30s;
server 192.168.1.102:8080 weight=2 max_fails=3 fail_timeout=30s;
server 192.168.1.103:8080 weight=1 max_fails=3 fail_timeout=30s;
}
# ========== 默认 round robin 对照组 ==========
upstream backend_default {
server 192.168.1.101:8080 weight=5;
server 192.168.1.102:8080 weight=2;
server 192.168.1.103:8080 weight=1;
}
# ========== 反向代理服务 ==========
server {
listen 8080;
server_name localhost;
# 使用自定义算法
location /fixed/ {
proxy_pass http://backend_fixed;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 默认算法对照组
location /default/ {
proxy_pass http://backend_default;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
8.5 编译与加载
# 1. 进入 Nginx 源码目录
cd /path/to/nginx-1.24.0
# 2. 静态编译模块
./configure \
--prefix=/usr/local/nginx \
--add-module=/path/to/nginx-fixed-weight-module \
--with-debug # 建议开发阶段开启
make -j$(nproc)
sudo make install
# 3. 检查模块是否被正确加载
/usr/local/nginx/sbin/nginx -V 2>&1 | grep fixed_weight
# 预期输出中包含 --add-module=...nginx-fixed-weight-module
# 4. 启动 Nginx
sudo /usr/local/nginx/sbin/nginx -t # 先测试配置
sudo /usr/local/nginx/sbin/nginx # 启动
8.6 验证调度效果
使用脚本发送连续请求,观察日志中命中的后端:
# 发送 10 次请求,观察 upstream 选择行为
for i in $(seq 1 10); do
echo "=== Request $i ==="
curl -s -o /dev/null -w "upstream_addr: %{remote_ip}:%{remote_port}\n" \
http://localhost:8080/fixed/
done
配合 error.log 中的 debug 日志,可以逐请求验证权重分配是否符合预期比例 5:2:1。
更多推荐




所有评论(0)