From dc765854a15b8d06490ef9235edf16796f795e12 Mon Sep 17 00:00:00 2001 From: xfy911 Date: Tue, 9 Jun 2026 06:07:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(proxy):=20=E5=B9=B3=E6=BB=91=E5=8A=A0?= =?UTF-8?q?=E6=9D=83=E8=BD=AE=E8=AF=A2=E8=B4=9F=E8=BD=BD=E5=9D=87=E8=A1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 集成测试全部通过 - 编译零警告 --- cocoon.h | 1 + cocoon.json | 6 +- config.c | 3 + proxy.c | 113 +++++++++++++++++++++++++++++++------- proxy.h | 5 +- server.c | 2 +- tests/integration_test.sh | 75 +++++++++++++++++++++++++ 7 files changed, 181 insertions(+), 24 deletions(-) diff --git a/cocoon.h b/cocoon.h index 1421f5d..c3922b9 100644 --- a/cocoon.h +++ b/cocoon.h @@ -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; diff --git a/cocoon.json b/cocoon.json index 5564418..eb79f4d 100644 --- a/cocoon.json +++ b/cocoon.json @@ -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} ] } diff --git a/config.c b/config.c index 1eb7822..e73b259 100644 --- a/config.c +++ b/config.c @@ -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); diff --git a/proxy.c b/proxy.c index f854480..126e6bf 100644 --- a/proxy.c +++ b/proxy.c @@ -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); diff --git a/proxy.h b/proxy.h index 56a2014..a9ab432 100644 --- a/proxy.h +++ b/proxy.h @@ -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 - 查找匹配路径的代理规则 diff --git a/server.c b/server.c index f238b4e..71b3c1c 100644 --- a/server.c +++ b/server.c @@ -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); } /* 加载插件 */ diff --git a/tests/integration_test.sh b/tests/integration_test.sh index 83c555b..c6aab1e 100755 --- a/tests/integration_test.sh +++ b/tests/integration_test.sh @@ -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 反向代理测试 ==="