cocoon/healthcheck.c
xfy911 ca620c17f2 feat(healthcheck): 新增主动健康检查模块
- 新增 healthcheck.c / healthcheck.h 模块
- 每个后端支持独立配置探测路径(默认 /health)、间隔(默认 5s)、超时(默认 2s)
- 使用线程 + sleep 实现周期性探测
- 探测成功/失败更新后端 healthy 状态,与现有被动检测共用状态字段
- 配置 JSON 新增 healthcheck 字段(path, interval_ms, timeout_ms, enabled)
- 优雅启动/关闭:server_create 启动探测线程,server_destroy 停止并 join
- 编译零警告
- 142 单元测试 + 103 集成测试全部通过(新增 4 项健康检查测试)
2026-06-09 14:11:52 +08:00

349 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* healthcheck.c - 主动健康检查实现
*
* 为每个代理规则启动一个后台线程,周期性向所有后端发送
* HTTP GET 探测请求,根据响应状态码更新后端健康状态。
*
* 与被动健康检查共用 cocoon_proxy_backend_t 中的状态字段:
* healthy, fail_count, success_count, last_check
*
* @author xfy
*/
#include "healthcheck.h"
#include "log.h"
#include "platform.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
#include <poll.h>
/**
* healthcheck_build_request - 构建 HTTP 探测请求
*/
static void healthcheck_build_request(const cocoon_proxy_backend_t *backend,
const char *path,
char *buf, size_t buf_len) {
snprintf(buf, buf_len,
"GET %s HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"Connection: close\r\n"
"User-Agent: Cocoon-Healthcheck/1.0\r\n"
"\r\n",
path && path[0] ? path : HC_DEFAULT_PATH,
backend->target_host,
backend->target_port);
}
/**
* healthcheck_connect - 建立到后端的 TCP 连接(非阻塞)
*
* 使用 poll 等待连接完成,支持超时。
*
* @param backend 后端
* @param timeout_ms 超时毫秒
* @return socket fd失败返回 COCOON_INVALID_SOCKET
*/
static cocoon_socket_t healthcheck_connect(const cocoon_proxy_backend_t *backend,
uint32_t timeout_ms) {
struct hostent *host = gethostbyname(backend->target_host);
if (!host) {
log_debug("[hc] 无法解析主机: %s", backend->target_host);
return COCOON_INVALID_SOCKET;
}
cocoon_socket_t fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == COCOON_INVALID_SOCKET) {
return COCOON_INVALID_SOCKET;
}
/* 设为非阻塞 */
int flags = fcntl(fd, F_GETFL, 0);
if (flags >= 0) {
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(backend->target_port);
memcpy(&addr.sin_addr, host->h_addr_list[0], (size_t)host->h_length);
int rc = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
if (rc == 0) {
/* 立即连接成功 */
return fd;
}
if (rc < 0 && errno != EINPROGRESS && errno != EAGAIN) {
cocoon_socket_close(fd);
return COCOON_INVALID_SOCKET;
}
/* 等待连接完成 */
struct pollfd pfd = { .fd = fd, .events = POLLOUT };
rc = poll(&pfd, 1, (int)timeout_ms);
if (rc <= 0) {
cocoon_socket_close(fd);
return COCOON_INVALID_SOCKET;
}
int soerr = 0;
socklen_t soerr_len = sizeof(soerr);
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &soerr_len) < 0 || soerr != 0) {
cocoon_socket_close(fd);
return COCOON_INVALID_SOCKET;
}
return fd;
}
/**
* healthcheck_parse_status - 从 HTTP 响应解析状态码
*
* 只读取首行,不关心 body。
*
* @param data 响应数据
* @param len 数据长度
* @return 状态码,解析失败返回 -1
*/
static int healthcheck_parse_status(const char *data, size_t len) {
if (len < 12) return -1;
if (strncmp(data, "HTTP/1.1", 8) != 0 && strncmp(data, "HTTP/1.0", 8) != 0) return -1;
const char *p = data + 8;
while (p < data + len && (*p == ' ' || *p == '\t')) p++;
if (p + 3 > data + len) return -1;
int status = 0;
for (int i = 0; i < 3; i++) {
if (p[i] < '0' || p[i] > '9') return -1;
status = status * 10 + (p[i] - '0');
}
return status;
}
/**
* healthcheck_probe_backend - 对单个后端执行探测
*
* 发送 HTTP GET 请求解析响应状态码。2xx 视为健康,其他视为不健康。
*
* @param backend 后端
* @param path 探测路径
* @param timeout_ms 超时毫秒
* @return true 健康
*/
static bool healthcheck_probe_backend(cocoon_proxy_backend_t *backend,
const char *path,
uint32_t timeout_ms) {
/* 跳过 HTTPS 后端(暂不支持 TLS 探测) */
if (backend->target_https) {
log_debug("[hc] 跳过 HTTPS 后端探测: %s:%d",
backend->target_host, backend->target_port);
return backend->healthy; /* 保持原状态 */
}
cocoon_socket_t fd = healthcheck_connect(backend, timeout_ms);
if (fd == COCOON_INVALID_SOCKET) {
log_debug("[hc] 连接失败: %s:%d", backend->target_host, backend->target_port);
return false;
}
char request[512];
healthcheck_build_request(backend, path, request, sizeof(request));
if (send(fd, request, strlen(request), 0) < 0) {
cocoon_socket_close(fd);
return false;
}
char response[4096];
ssize_t total = 0;
struct pollfd pfd = { .fd = fd, .events = POLLIN };
int poll_rc = poll(&pfd, 1, (int)timeout_ms);
if (poll_rc > 0) {
ssize_t r = recv(fd, response, sizeof(response) - 1, 0);
if (r > 0) total = r;
}
cocoon_socket_close(fd);
if (total <= 0) {
log_debug("[hc] 无响应: %s:%d", backend->target_host, backend->target_port);
return false;
}
response[total] = '\0';
int status = healthcheck_parse_status(response, (size_t)total);
bool healthy = (status >= 200 && status < 300);
log_debug("[hc] %s:%d -> HTTP %d (%s)",
backend->target_host, backend->target_port,
status, healthy ? "healthy" : "unhealthy");
return healthy;
}
/**
* healthcheck_probe_once - 对单个后端执行一次探测(公共 API
*/
bool healthcheck_probe_once(cocoon_proxy_backend_t *backend, uint32_t timeout_ms) {
if (!backend) return false;
return healthcheck_probe_backend(backend, HC_DEFAULT_PATH, timeout_ms);
}
/**
* healthcheck_update_state - 更新后端健康状态(与被动检查共用逻辑)
*
* 连续 success_count 次成功恢复,连续 fail_count 次失败标记不健康。
*/
static void healthcheck_update_state(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("[hc] 后端恢复健康: %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("[hc] 后端标记不健康: %s:%d (连续失败 %d 次)",
backend->target_host, backend->target_port, backend->fail_count);
}
}
}
/**
* healthcheck_thread_func - 探测线程主函数
*
* 每个代理规则一个线程,周期性探测所有后端。
*/
static void *healthcheck_thread_func(void *arg) {
cocoon_healthcheck_thread_t *ctx = (cocoon_healthcheck_thread_t *)arg;
cocoon_proxy_rule_t *rule = ctx->rule;
if (!rule) return NULL;
log_info("[hc] 探测线程启动: %s (%zu 后端)", rule->path_prefix, rule->backend_count);
while (ctx->running) {
for (size_t i = 0; i < rule->backend_count; i++) {
cocoon_proxy_backend_t *backend = &rule->backends[i];
cocoon_healthcheck_config_t *hc = &backend->hc_config;
if (!hc->enabled) continue;
uint32_t interval_ms = hc->interval_ms > 0 ? hc->interval_ms : HC_DEFAULT_INTERVAL_MS;
uint32_t timeout_ms = hc->timeout_ms > 0 ? hc->timeout_ms : HC_DEFAULT_TIMEOUT_MS;
const char *path = hc->path[0] ? hc->path : HC_DEFAULT_PATH;
bool healthy = healthcheck_probe_backend(backend, path, timeout_ms);
healthcheck_update_state(backend, healthy);
(void)interval_ms; /* 可能用于后续扩展 */
}
/* 等待间隔(可被提前中断) */
uint32_t sleep_ms = HC_DEFAULT_INTERVAL_MS;
if (rule->backend_count > 0) {
cocoon_healthcheck_config_t *hc = &rule->backends[0].hc_config;
if (hc->interval_ms > 0) sleep_ms = hc->interval_ms;
}
/* 分步睡眠,检查 running 标志 */
uint32_t slept = 0;
while (ctx->running && slept < sleep_ms) {
uint32_t step = sleep_ms - slept;
if (step > 500) step = 500;
usleep(step * 1000);
slept += step;
}
}
log_info("[hc] 探测线程退出: %s", rule->path_prefix);
return NULL;
}
/**
* healthcheck_manager_init - 初始化管理器
*/
void healthcheck_manager_init(cocoon_healthcheck_manager_t *mgr) {
memset(mgr, 0, sizeof(*mgr));
}
/**
* healthcheck_start - 启动所有探测线程
*/
bool healthcheck_start(cocoon_healthcheck_manager_t *mgr,
cocoon_proxy_config_t *proxy_cfg,
uint32_t global_timeout_ms) {
(void)global_timeout_ms; /* 预留:可作为未配置后端的默认超时 */
if (!mgr || !proxy_cfg) return false;
healthcheck_manager_init(mgr);
for (size_t i = 0; i < proxy_cfg->count; i++) {
cocoon_proxy_rule_t *rule = &proxy_cfg->rules[i];
if (rule->backend_count == 0) continue;
/* 检查是否有后端启用了健康检查 */
bool any_enabled = false;
for (size_t j = 0; j < rule->backend_count; j++) {
if (rule->backends[j].hc_config.enabled) {
any_enabled = true;
break;
}
}
if (!any_enabled) continue;
if (mgr->count >= COCOON_MAX_PROXY_RULES) {
log_warn("[hc] 规则数超过上限,跳过: %s", rule->path_prefix);
continue;
}
cocoon_healthcheck_thread_t *t = &mgr->threads[mgr->count];
t->rule = rule;
t->running = true;
int rc = pthread_create(&t->thread, NULL, healthcheck_thread_func, t);
if (rc != 0) {
log_error("[hc] 探测线程创建失败: %s (%d)", rule->path_prefix, rc);
t->running = false;
t->rule = NULL;
continue;
}
mgr->count++;
}
log_info("[hc] 已启动 %zu 个探测线程", mgr->count);
return true;
}
/**
* healthcheck_stop - 停止所有探测线程并 join
*/
void healthcheck_stop(cocoon_healthcheck_manager_t *mgr) {
if (!mgr) return;
for (size_t i = 0; i < mgr->count; i++) {
cocoon_healthcheck_thread_t *t = &mgr->threads[i];
if (t->running) {
t->running = false;
pthread_join(t->thread, NULL);
t->rule = NULL;
}
}
mgr->count = 0;
}