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:
parent
23574ab2ba
commit
dc765854a1
1
cocoon.h
1
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;
|
||||
|
||||
@ -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}
|
||||
]
|
||||
}
|
||||
|
||||
3
config.c
3
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);
|
||||
|
||||
113
proxy.c
113
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);
|
||||
|
||||
5
proxy.h
5
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 - 查找匹配路径的代理规则
|
||||
|
||||
2
server.c
2
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);
|
||||
}
|
||||
|
||||
/* 加载插件 */
|
||||
|
||||
@ -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 反向代理测试 ==="
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user