feat(proxy): 后端被动健康检查 + 修复 SIGPIPE 崩溃

- 在 cocoon_proxy_backend_t 中增加健康状态字段(healthy/fail_count/success_count/last_check)
- 实现 proxy_relay_backend() 辅助函数,支持流式转发响应
- 代理请求时优先尝试 healthy 后端,全部失败后再 fallback 到所有后端
- 健康阈值:连续 2 次成功恢复,连续 3 次失败标记不健康
- 修复 SIGPIPE 信号导致进程在客户端提前关闭连接时崩溃的问题
- 全部 142 单元测试 + 90 集成测试通过
This commit is contained in:
xfy911 2026-06-08 21:26:33 +08:00
parent 61d9b01ed6
commit 77979b44d8
3 changed files with 202 additions and 134 deletions

1
main.c
View File

@ -290,6 +290,7 @@ int main(int argc, char *argv[]) {
/* 注册信号处理 */
cocoon_signal_setup(signal_handler);
signal(SIGUSR1, reload_handler);
signal(SIGPIPE, SIG_IGN); /* 忽略 SIGPIPE防止写入已关闭的连接时进程终止 */
/* 设置日志级别 */
log_set_level(config.log_level);

122
proxy.c
View File

@ -17,12 +17,20 @@
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <time.h>
void proxy_init(cocoon_proxy_config_t *cfg) {
memset(cfg, 0, sizeof(*cfg));
cfg->count = 0;
}
static void backend_init_health(cocoon_proxy_backend_t *backend) {
backend->healthy = true;
backend->fail_count = 0;
backend->success_count = 0;
backend->last_check = 0;
}
static void parse_backend_url(const char *target_url, cocoon_proxy_backend_t *backend) {
/* 解析目标URL */
bool https = false;
@ -103,6 +111,7 @@ 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);
backend_init_health(backend);
log_info("追加后端: %s -> %s://%s:%d%s",
rule->path_prefix,
backend->target_https ? "https" : "http",
@ -127,6 +136,7 @@ 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);
backend_init_health(backend);
rule->backend_count++;
log_info("添加代理规则: %s -> %s://%s:%d%s",
@ -156,6 +166,27 @@ cocoon_proxy_rule_t *proxy_match(cocoon_proxy_config_t *cfg, const char *path) {
/**
* proxy_connect_backend -
*/
static void proxy_update_health(cocoon_proxy_backend_t *backend, bool success) {
time_t now = time(NULL);
backend->last_check = now;
if (success) {
backend->fail_count = 0;
backend->success_count++;
if (!backend->healthy && backend->success_count >= COCOON_HEALTHY_THRESHOLD) {
backend->healthy = true;
log_info("后端恢复健康: %s:%d", backend->target_host, backend->target_port);
}
} else {
backend->success_count = 0;
backend->fail_count++;
if (backend->healthy && backend->fail_count >= COCOON_UNHEALTHY_THRESHOLD) {
backend->healthy = false;
log_warn("后端标记不健康: %s:%d (连续失败 %d 次)",
backend->target_host, backend->target_port, backend->fail_count);
}
}
}
static cocoon_socket_t proxy_connect_backend(const cocoon_proxy_backend_t *backend) {
struct hostent *host = gethostbyname(backend->target_host);
if (!host) {
@ -264,23 +295,18 @@ static int proxy_send_all_tls(proxy_tls_conn_t *conn, const char *data, size_t l
return 0;
}
bool proxy_forward(cocoon_socket_t client_fd, const http_request_t *req,
cocoon_proxy_rule_t *rule,
const struct sockaddr_storage *client_addr) {
if (!rule || rule->backend_count == 0) {
log_error("代理规则无后端");
return false;
}
/* 轮询起始索引 */
size_t start_idx = rule->current_index;
size_t tried = 0;
bool success = false;
while (tried < rule->backend_count) {
size_t idx = (start_idx + tried) % rule->backend_count;
cocoon_proxy_backend_t *backend = &rule->backends[idx];
/**
* proxy_relay_backend -
*
*
* fd
*
* @return true false
*/
static bool proxy_relay_backend(cocoon_socket_t client_fd, const http_request_t *req,
cocoon_proxy_rule_t *rule, cocoon_proxy_backend_t *backend,
const struct sockaddr_storage *client_addr,
ssize_t *total_forwarded) {
cocoon_socket_t backend_fd = COCOON_INVALID_SOCKET;
proxy_tls_conn_t *tls_conn = NULL;
bool use_https = backend->target_https;
@ -288,16 +314,14 @@ bool proxy_forward(cocoon_socket_t client_fd, const http_request_t *req,
if (use_https) {
tls_conn = proxy_tls_connect(backend->target_host, backend->target_port);
if (!tls_conn) {
log_warn("后端 %s:%d 连接失败,尝试下一个", backend->target_host, backend->target_port);
tried++;
continue;
log_warn("后端 %s:%d 连接失败", backend->target_host, backend->target_port);
return false;
}
} else {
backend_fd = proxy_connect_backend(backend);
if (backend_fd == COCOON_INVALID_SOCKET) {
log_warn("后端 %s:%d 连接失败,尝试下一个", backend->target_host, backend->target_port);
tried++;
continue;
log_warn("后端 %s:%d 连接失败", backend->target_host, backend->target_port);
return false;
}
}
@ -366,9 +390,10 @@ bool proxy_forward(cocoon_socket_t client_fd, const http_request_t *req,
}
/* 流式转发响应回客户端 */
bool success = false;
ssize_t total = 0;
if (send_ok) {
char relay_buf[8192];
ssize_t total_forwarded = 0;
bool recv_ok = true;
while (1) {
ssize_t r;
@ -389,17 +414,19 @@ bool proxy_forward(cocoon_socket_t client_fd, const http_request_t *req,
recv_ok = false;
break;
}
total_forwarded += r;
total += r;
}
if (recv_ok) {
log_debug("代理完成: %s -> 后端[%zu] %s:%d%s, 转发 %zd bytes",
req->path, idx, backend->target_host, backend->target_port,
forwarded_path, total_forwarded);
log_debug("代理完成: %s -> 后端 %s:%d%s, 转发 %zd bytes",
req->path, backend->target_host, backend->target_port,
forwarded_path, total);
success = true;
}
}
if (total_forwarded) *total_forwarded = total;
/* 清理后端连接 */
if (use_https) {
proxy_tls_close(tls_conn);
@ -407,13 +434,46 @@ bool proxy_forward(cocoon_socket_t client_fd, const http_request_t *req,
cocoon_socket_close(backend_fd);
}
if (success) {
/* 轮询指针前移 */
return success;
}
bool proxy_forward(cocoon_socket_t client_fd, const http_request_t *req,
cocoon_proxy_rule_t *rule,
const struct sockaddr_storage *client_addr) {
if (!rule || rule->backend_count == 0) {
log_error("代理规则无后端");
return false;
}
size_t start_idx = rule->current_index;
/* 第一轮:优先尝试 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;
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);
}
tried++;
/* 第二轮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];
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);
}
log_error("所有后端均不可用: %s", rule->path_prefix);

View File

@ -15,6 +15,8 @@
#include <stdbool.h>
#define COCOON_MAX_PROXY_BACKENDS 8
#define COCOON_HEALTHY_THRESHOLD 2 /* 连续成功次数恢复健康 */
#define COCOON_UNHEALTHY_THRESHOLD 3 /* 连续失败次数标记不健康 */
/**
* cocoon_proxy_backend_t -
@ -24,6 +26,11 @@ typedef struct {
char target_path[256];
uint16_t target_port;
bool target_https;
/* 健康状态 */
bool healthy; /* 当前是否健康 */
int fail_count; /* 连续失败次数 */
int success_count; /* 连续成功次数 */
time_t last_check; /* 上次检测时间 */
} cocoon_proxy_backend_t;
/**