feat(proxy): 平滑加权轮询负载均衡

- proxy.h: 后端添加 weight / current_weight 字段
- proxy.c: 实现平滑加权轮询算法(Nginx SWW),按权重比例分配请求
- proxy.c: 健康后端优先选择, unhealthy fallback 机制保留
- config.c: JSON 解析器新增 weight 字段解析
- cocoon.h: 代理配置结构添加 weight 字段
- server.c: 加载代理规则时传入 weight
- cocoon.json: 示例配置展示多后端权重 3:1
- tests: 新增加权轮询集成测试(3项),验证配置加载与多后端分布
- 142 单元测试 + 93 集成测试全部通过
- 编译零警告
This commit is contained in:
xfy911 2026-06-09 06:07:06 +08:00
parent 23574ab2ba
commit dc765854a1
7 changed files with 181 additions and 24 deletions

View File

@ -57,6 +57,7 @@ typedef struct cocoon_config {
char prefix[256];
char target[256];
uint32_t pool_size; /* 连接池大小默认4最大16 */
uint32_t weight; /* 权重默认1 */
} proxies[COCOON_MAX_PROXY_RULES];
size_t num_proxies;
} cocoon_config_t;

View File

@ -11,8 +11,8 @@
"plugins": ["plugins/hello.so"],
"access_log": "-",
"proxies": [
{"prefix": "/api", "target": "http://localhost:3000", "pool_size": 8},
{"prefix": "/api", "target": "http://localhost:3001"},
{"prefix": "/backend", "target": "https://localhost:8443", "pool_size": 4}
{"prefix": "/api", "target": "http://localhost:3000", "pool_size": 8, "weight": 3},
{"prefix": "/api", "target": "http://localhost:3001", "weight": 1},
{"prefix": "/backend", "target": "https://localhost:8443", "pool_size": 4, "weight": 1}
]
}

View File

@ -439,6 +439,9 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) {
} else if (strcmp(pk, "pool_size") == 0 && pval.type == TOKEN_NUMBER) {
char *v = token_str_dup(&pval);
if (v) { config->proxies[config->num_proxies].pool_size = (uint32_t)atoi(v); free(v); }
} else if (strcmp(pk, "weight") == 0 && pval.type == TOKEN_NUMBER) {
char *v = token_str_dup(&pval);
if (v) { config->proxies[config->num_proxies].weight = (uint32_t)atoi(v); free(v); }
}
free(pk);
token_t psep = parser_next_token(&p);

113
proxy.c
View File

@ -25,11 +25,13 @@ void proxy_init(cocoon_proxy_config_t *cfg) {
cfg->count = 0;
}
static void backend_init_health(cocoon_proxy_backend_t *backend) {
static void backend_init(cocoon_proxy_backend_t *backend, uint32_t weight) {
backend->healthy = true;
backend->fail_count = 0;
backend->success_count = 0;
backend->last_check = 0;
backend->weight = weight > 0 ? weight : 1;
backend->current_weight = 0;
}
void proxy_pool_init(cocoon_proxy_backend_t *backend, size_t max_pool_size) {
@ -247,7 +249,7 @@ static void parse_backend_url(const char *target_url, cocoon_proxy_backend_t *ba
proxy_pool_init(backend, pool_size);
}
bool proxy_add_rule(cocoon_proxy_config_t *cfg, const char *prefix, const char *target_url, size_t pool_size) {
bool proxy_add_rule(cocoon_proxy_config_t *cfg, const char *prefix, const char *target_url, size_t pool_size, uint32_t weight) {
if (!prefix || !target_url || prefix[0] == '\0' || target_url[0] == '\0') {
return false;
}
@ -269,13 +271,14 @@ bool proxy_add_rule(cocoon_proxy_config_t *cfg, const char *prefix, const char *
}
cocoon_proxy_backend_t *backend = &rule->backends[rule->backend_count];
parse_backend_url(target_url, backend, pool_size);
backend_init_health(backend);
log_info("追加后端: %s -> %s://%s:%d%s",
backend_init(backend, weight);
log_info("追加后端: %s -> %s://%s:%d%s (weight=%u)",
rule->path_prefix,
backend->target_https ? "https" : "http",
backend->target_host,
backend->target_port,
backend->target_path);
backend->target_path,
backend->weight);
rule->backend_count++;
return true;
}
@ -294,15 +297,16 @@ bool proxy_add_rule(cocoon_proxy_config_t *cfg, const char *prefix, const char *
cocoon_proxy_backend_t *backend = &rule->backends[rule->backend_count];
parse_backend_url(target_url, backend, pool_size);
backend_init_health(backend);
backend_init(backend, weight);
rule->backend_count++;
log_info("添加代理规则: %s -> %s://%s:%d%s",
log_info("添加代理规则: %s -> %s://%s:%d%s (weight=%u)",
rule->path_prefix,
backend->target_https ? "https" : "http",
backend->target_host,
backend->target_port,
backend->target_path);
backend->target_path,
backend->weight);
cfg->count++;
return true;
@ -595,6 +599,73 @@ static bool proxy_relay_backend(cocoon_socket_t client_fd, const http_request_t
return success;
}
/**
* select_backend_sww -
*
* Nginx
* 1. current_weight += weight
* 2. current_weight
* 3. current_weight -= total_weight
* 4.
*
*
*
* @param rule
* @return NULL
*/
static cocoon_proxy_backend_t *select_backend_sww(cocoon_proxy_rule_t *rule) {
if (!rule || rule->backend_count == 0) return NULL;
cocoon_proxy_backend_t *best = NULL;
int32_t total_weight = 0;
for (size_t i = 0; i < rule->backend_count; i++) {
cocoon_proxy_backend_t *b = &rule->backends[i];
if (!b->healthy) continue;
b->current_weight += (int32_t)b->weight;
total_weight += (int32_t)b->weight;
if (!best || b->current_weight > best->current_weight) {
best = b;
}
}
if (best) {
best->current_weight -= total_weight;
}
return best;
}
/**
* select_backend_sww_all -
*
* fallback
*/
static cocoon_proxy_backend_t *select_backend_sww_all(cocoon_proxy_rule_t *rule) {
if (!rule || rule->backend_count == 0) return NULL;
cocoon_proxy_backend_t *best = NULL;
int32_t total_weight = 0;
for (size_t i = 0; i < rule->backend_count; i++) {
cocoon_proxy_backend_t *b = &rule->backends[i];
b->current_weight += (int32_t)b->weight;
total_weight += (int32_t)b->weight;
if (!best || b->current_weight > best->current_weight) {
best = b;
}
}
if (best) {
best->current_weight -= total_weight;
}
return best;
}
bool proxy_forward(cocoon_socket_t client_fd, const http_request_t *req,
cocoon_proxy_rule_t *rule,
const struct sockaddr_storage *client_addr) {
@ -603,32 +674,36 @@ bool proxy_forward(cocoon_socket_t client_fd, const http_request_t *req,
return false;
}
size_t start_idx = rule->current_index;
/* 第一轮:优先尝试 healthy 后端(平滑加权轮询) */
cocoon_proxy_backend_t *backend = select_backend_sww(rule);
if (backend) {
ssize_t total = 0;
if (proxy_relay_backend(client_fd, req, rule, backend, client_addr, &total)) {
proxy_update_health(backend, true);
return req->keep_alive;
}
proxy_update_health(backend, false);
}
/* 第一轮:优先尝试 healthy 后端 */
/* 如果唯一选中的后端失败了,尝试其他 healthy 后端 */
for (size_t t = 0; t < rule->backend_count; t++) {
size_t idx = (start_idx + t) % rule->backend_count;
cocoon_proxy_backend_t *backend = &rule->backends[idx];
if (!backend->healthy) continue;
backend = select_backend_sww(rule);
if (!backend) break;
ssize_t total = 0;
if (proxy_relay_backend(client_fd, req, rule, backend, client_addr, &total)) {
proxy_update_health(backend, true);
rule->current_index = (idx + 1) % rule->backend_count;
return req->keep_alive;
}
proxy_update_health(backend, false);
}
/* 第二轮fallback 到所有后端(包括 unhealthy */
for (size_t t = 0; t < rule->backend_count; t++) {
size_t idx = (start_idx + t) % rule->backend_count;
cocoon_proxy_backend_t *backend = &rule->backends[idx];
backend = select_backend_sww_all(rule);
if (backend) {
ssize_t total = 0;
if (proxy_relay_backend(client_fd, req, rule, backend, client_addr, &total)) {
proxy_update_health(backend, true);
rule->current_index = (idx + 1) % rule->backend_count;
return req->keep_alive;
}
proxy_update_health(backend, false);

View File

@ -60,6 +60,9 @@ typedef struct {
time_t last_check; /* 上次检测时间 */
/* 连接池 */
cocoon_conn_pool_t pool; /**< 后端连接池 */
/* 权重 */
uint32_t weight; /**< 配置权重默认1 */
int32_t current_weight; /**< 当前动态权重(平滑轮询算法用) */
} cocoon_proxy_backend_t;
/**
@ -93,7 +96,7 @@ void proxy_init(cocoon_proxy_config_t *cfg);
* @param target_url URL "http://localhost:3000"
* @return true
*/
bool proxy_add_rule(cocoon_proxy_config_t *cfg, const char *prefix, const char *target_url, size_t pool_size);
bool proxy_add_rule(cocoon_proxy_config_t *cfg, const char *prefix, const char *target_url, size_t pool_size, uint32_t weight);
/**
* proxy_match -

View File

@ -1265,7 +1265,7 @@ server_context_t *server_create(const cocoon_config_t *config) {
/* 初始化反向代理配置 */
proxy_init(&ctx->proxy_config);
for (size_t i = 0; i < ctx->config.num_proxies; i++) {
proxy_add_rule(&ctx->proxy_config, ctx->config.proxies[i].prefix, ctx->config.proxies[i].target, ctx->config.proxies[i].pool_size);
proxy_add_rule(&ctx->proxy_config, ctx->config.proxies[i].prefix, ctx->config.proxies[i].target, ctx->config.proxies[i].pool_size, ctx->config.proxies[i].weight);
}
/* 加载插件 */

View File

@ -1017,6 +1017,81 @@ kill -9 $BACKEND_PID 2>/dev/null || true
kill_server
sleep 1
# === 加权轮询反向代理测试 ===
echo ""
echo "=== 加权轮询反向代理测试 ==="
# 创建两个后端目录,放不同内容
mkdir -p "$TMPDIR/backend_a" "$TMPDIR/backend_b"
echo "Backend-A" > "$TMPDIR/backend_a/index.html"
echo "Backend-B" > "$TMPDIR/backend_b/index.html"
# 启动两个后端服务器
python3 -m http.server 9002 --directory "$TMPDIR/backend_a" > "$TMPDIR/backend_a.log" 2>&1 &
BACKEND_A_PID=$!
python3 -m http.server 9003 --directory "$TMPDIR/backend_b" > "$TMPDIR/backend_b.log" 2>&1 &
BACKEND_B_PID=$!
sleep 1
# 创建加权轮询配置(权重 3:1
WEIGHTED_CONFIG="$TMPDIR/weighted_proxy_config.json"
cat > "$WEIGHTED_CONFIG" << EOF
{
"root_dir": "./tests/fixtures",
"port": 9999,
"log_level": "debug",
"proxies": [
{"prefix": "/backend", "target": "http://localhost:9002", "weight": 3, "pool_size": 2},
{"prefix": "/backend", "target": "http://localhost:9003", "weight": 1, "pool_size": 2}
]
}
EOF
$SERVER -c "$WEIGHTED_CONFIG" > "$TMPDIR/server_weighted.log" 2>&1 &
for i in {1..30}; do
if nc -z localhost 9999 2>/dev/null; then break; fi
sleep 0.1
done
# 发送4个请求验证加权轮询工作至少能收到响应
weighted_status=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOST/backend/index.html")
if [[ "$weighted_status" == "200" ]]; then
echo " ✓ 加权轮询代理 — HTTP 200"
pass
else
echo " ✗ 加权轮询代理 — 期望 200, 实际 $weighted_status"
fail
fi
# 检查日志中是否记录了两个后端及其权重
if grep -q "weight=3" "$TMPDIR/server_weighted.log" && grep -q "weight=1" "$TMPDIR/server_weighted.log"; then
echo " ✓ 加权轮询配置 — 权重 3:1 已加载"
pass
else
echo " ✗ 加权轮询配置 — 未找到权重日志"
fail
fi
# 发送多个请求,验证平滑轮询分布(至少两个后端都收到请求)
for i in {1..4}; do
curl -s -o /dev/null "http://$HOST/backend/index.html"
done
# 检查两个后端日志中都有请求
if grep -q "GET /index.html" "$TMPDIR/backend_a.log" && grep -q "GET /index.html" "$TMPDIR/backend_b.log"; then
echo " ✓ 加权轮询分布 — 两个后端均收到请求"
pass
else
echo " ✗ 加权轮询分布 — 后端请求分布异常"
fail
fi
# 清理加权轮询后端服务器
kill -9 $BACKEND_A_PID 2>/dev/null || true
kill -9 $BACKEND_B_PID 2>/dev/null || true
kill_server
sleep 1
# === HTTPS 反向代理测试 ===
echo ""
echo "=== HTTPS 反向代理测试 ==="