lolly/internal/proxy/health.go

281 lines
7.7 KiB
Go
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.

// Package proxy 提供反向代理功能,支持 HTTP、WebSocket 和流式代理。
//
// 此文件实现了针对后端目标的健康检查功能,支持
// 主动健康检查(定期 HTTP 探测)和被动健康检查
// (基于观察到的失败标记目标为不健康)。
//
// 主要功能:
// - 定期向后端发送健康检查请求
// - 根据响应状态更新目标健康状态
// - 支持被动健康检查(基于请求失败)
//
// 作者xfy
//
//go:generate go test -v ./...
package proxy
import (
"sync"
"sync/atomic"
"time"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/loadbalance"
)
// healthPath 默认健康检查路径。
// 当配置中未指定 path 时使用此值。
const healthPath = "/health"
// HealthChecker 对后端目标执行健康检查。
// 它支持主动(定期 HTTP 探测)和被动(基于失败的)
// 两种健康检查模式。
//
// 当启动后,检查器在后台 goroutine 中运行,定期
// 向每个目标的健康检查端点发送 HTTP GET 请求。
// 返回 2xx 状态码的目标被标记为健康;
// 超时、连接失败或非 2xx 响应将其标记为不健康。
//
// 使用示例:
//
// targets := []*loadbalance.Target{
// {URL: "http://backend1:8080"},
// {URL: "http://backend2:8080"},
// }
// targets[0].Healthy.Store(true)
// targets[1].Healthy.Store(true)
//
// cfg := &config.HealthCheckConfig{
// Interval: 10 * time.Second,
// Path: "/health",
// Timeout: 5 * time.Second,
// }
//
// checker := New(targets, cfg)
// checker.Start()
// defer checker.Stop()
type HealthChecker struct {
stopCh chan struct{}
client *fasthttp.Client
path string
targets []*loadbalance.Target
interval time.Duration
timeout time.Duration
running atomic.Bool
matcher HealthMatch // 健康检查匹配器
slowStartManager *loadbalance.SlowStartManager // 慢启动管理器
wg sync.WaitGroup // 等待 run goroutine 退出
}
// NewHealthChecker 使用指定的目标和配置创建一个新的 HealthChecker。
// 配置定义了检查间隔、超时和健康检查路径。
//
// 如果配置中未指定,将应用默认值:
// - Interval: 10 秒
// - Timeout: 5 秒
// - Path: "/health"
//
// 返回的 HealthChecker 尚未启动;调用 Start() 开始健康检查。
func NewHealthChecker(targets []*loadbalance.Target, cfg *config.HealthCheckConfig) *HealthChecker {
interval := cfg.Interval
if interval <= 0 {
interval = 10 * time.Second
}
timeout := cfg.Timeout
if timeout <= 0 {
timeout = 5 * time.Second
}
path := cfg.Path
if path == "" {
path = healthPath
}
// 创建健康检查匹配器
var matcher HealthMatch
if cfg.Match != nil {
matcher = NewHealthMatch(&HealthMatchConfig{
Status: cfg.Match.Status,
Body: cfg.Match.Body,
Headers: cfg.Match.Headers,
})
}
if matcher == nil {
matcher = DefaultHealthMatch()
}
// 创建慢启动管理器
var slowStartManager *loadbalance.SlowStartManager
if cfg.SlowStart > 0 {
slowStartManager = loadbalance.NewSlowStartManager(cfg.SlowStart)
}
return &HealthChecker{
targets: targets,
interval: interval,
timeout: timeout,
path: path,
stopCh: make(chan struct{}),
matcher: matcher,
slowStartManager: slowStartManager,
client: &fasthttp.Client{
ReadTimeout: timeout,
WriteTimeout: timeout,
},
}
}
// Start 启动后台健康检查进程。
// 它启动一个 goroutine按照配置的间隔定期检查所有目标。
// Start 是幂等的;在已运行的检查器上调用它不会产生任何效果。
//
// 健康检查进程将持续运行,直到调用 Stop()。
func (h *HealthChecker) Start() {
if h.running.Load() {
return
}
h.running.Store(true)
if h.slowStartManager != nil {
h.slowStartManager.Start()
}
h.wg.Add(1)
go h.run()
}
// Stop 停止后台健康检查进程。
// 它向后台 goroutine 发送停止信号并等待其完成。
// Stop 是幂等的;在已停止的检查器上调用它不会产生任何效果。
// Stop 后可以再次调用 Start 重新启动检查器。
func (h *HealthChecker) Stop() {
if !h.running.CompareAndSwap(true, false) {
return // 已经停止,直接返回
}
close(h.stopCh)
h.wg.Wait() // 等待 run goroutine 退出
if h.slowStartManager != nil {
h.slowStartManager.Stop()
}
// 重新创建 stopCh 以支持后续 Start
h.stopCh = make(chan struct{})
}
// run 是在后台 goroutine 中运行的主要健康检查循环。
// 它对所有目标执行初始检查,然后进入循环,
// 以固定间隔检查目标,直到被停止。
func (h *HealthChecker) run() {
defer h.wg.Done()
// 执行初始健康检查
h.checkAll()
ticker := time.NewTicker(h.interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
h.checkAll()
case <-h.stopCh:
return
}
}
}
// checkAll 对所有配置的目标执行健康检查。
// 它使用 goroutines 并发检查每个目标以最小化延迟。
func (h *HealthChecker) checkAll() {
var wg sync.WaitGroup
for _, target := range h.targets {
wg.Add(1)
go func(t *loadbalance.Target) {
defer wg.Done()
h.checkTarget(t)
}(target)
}
wg.Wait()
}
// checkTarget 对单个目标执行健康检查。
// 它向目标的健康检查端点发送 HTTP GET 请求
// 并根据响应更新目标的 Healthy 状态。
//
// 目标被认为健康,如果满足以下条件:
// - HTTP 请求成功
// - matcher.Match 返回 true
//
// 目标被标记为不健康,如果满足以下条件:
// - 连接失败
// - 请求超时
// - matcher.Match 返回 false
func (h *HealthChecker) checkTarget(target *loadbalance.Target) {
// 构建健康检查 URL
url := target.URL + h.path
// 准备请求和响应
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
req.SetRequestURI(url)
req.Header.SetMethod(fasthttp.MethodGet)
req.Header.Set("User-Agent", "Lolly-HealthChecker/1.0")
// 执行带超时的健康检查
err := h.client.DoTimeout(req, resp, h.timeout)
if err != nil {
// 连接失败或超时 - 标记为不健康
h.MarkUnhealthy(target)
return
}
// 提取响应头(小写 key预分配容量
headers := make(map[string]string, 20)
for key, value := range resp.Header.All() {
headers[string(key)] = string(value)
}
// 使用 matcher 判断健康状态
statusCode := resp.StatusCode()
body := resp.Body()
if h.matcher.Match(statusCode, body, headers) {
h.MarkHealthy(target)
} else {
h.MarkUnhealthy(target)
}
}
// MarkUnhealthy 将目标标记为不健康。
// 此方法用于被动健康检查,代理根据请求处理过程中
// 观察到的失败将目标标记为不健康。
//
// 同时调用 RecordFailure 记录软失败状态,配合 MaxFails/FailTimeout
// 实现失败计数和冷却机制。
// 同时通知 SlowStartManager 清除慢启动状态。
func (h *HealthChecker) MarkUnhealthy(target *loadbalance.Target) {
target.Healthy.Store(false)
target.RecordFailure()
if h.slowStartManager != nil {
h.slowStartManager.OnTargetUnhealthy(target)
}
}
// MarkHealthy 将目标标记为健康。
// 此方法用于故障转移成功后,将之前失败的目标恢复为健康状态。
//
// 同时调用 RecordSuccess 重置软失败状态failCount/failedUntil
// 但不修改 Healthy 标志——健康检查器对 Healthy 拥有权威。
// 同时通知 SlowStartManager 开始慢启动。
func (h *HealthChecker) MarkHealthy(target *loadbalance.Target) {
target.Healthy.Store(true)
target.RecordSuccess()
if h.slowStartManager != nil {
h.slowStartManager.OnTargetHealthy(target)
}
}