Files
goproxy/internal/healthcheck/healthchecker.go
2025-03-13 15:56:33 +08:00

249 lines
4.8 KiB
Go

package healthcheck
import (
"context"
"net"
"net/http"
"net/url"
"sync"
"time"
)
// HealthChecker 健康检查器
type HealthChecker struct {
// 配置
config *Config
// 健康检查状态
statusMap sync.Map
// 状态变更回调
statusChangeCallback func(string, bool)
// 是否运行中
running bool
// 上下文
ctx context.Context
// 取消函数
cancel context.CancelFunc
// 互斥锁
mu sync.Mutex
}
// Config 健康检查配置
type Config struct {
// 检查间隔
Interval time.Duration
// 检查超时
Timeout time.Duration
// 检查路径
Path string
// 检查方法
Method string
// 检查状态码
SuccessStatus int
// 最大失败次数
MaxFails int
// 最小成功次数
MinSuccess int
}
// NewHealthChecker 创建健康检查器
func NewHealthChecker(config *Config) *HealthChecker {
if config.Path == "" {
config.Path = "/"
}
if config.Method == "" {
config.Method = http.MethodGet
}
if config.SuccessStatus == 0 {
config.SuccessStatus = http.StatusOK
}
if config.MaxFails == 0 {
config.MaxFails = 3
}
if config.MinSuccess == 0 {
config.MinSuccess = 2
}
ctx, cancel := context.WithCancel(context.Background())
return &HealthChecker{
config: config,
ctx: ctx,
cancel: cancel,
running: false,
}
}
// Start 启动健康检查
func (hc *HealthChecker) Start() {
hc.mu.Lock()
defer hc.mu.Unlock()
if hc.running {
return
}
hc.running = true
go hc.run()
}
// Stop 停止健康检查
func (hc *HealthChecker) Stop() {
hc.mu.Lock()
defer hc.mu.Unlock()
if !hc.running {
return
}
hc.cancel()
hc.running = false
}
// AddTarget 添加监控目标
func (hc *HealthChecker) AddTarget(target string) error {
u, err := url.Parse(target)
if err != nil {
return err
}
// 初始化为健康状态
hc.statusMap.Store(u.String(), &backendStatus{
URL: u,
Healthy: true,
FailCount: 0,
SuccessCount: 0,
})
return nil
}
// RemoveTarget 移除监控目标
func (hc *HealthChecker) RemoveTarget(target string) error {
u, err := url.Parse(target)
if err != nil {
return err
}
hc.statusMap.Delete(u.String())
return nil
}
// IsHealthy 检查目标是否健康
func (hc *HealthChecker) IsHealthy(target string) bool {
u, err := url.Parse(target)
if err != nil {
return false
}
value, ok := hc.statusMap.Load(u.String())
if !ok {
return false
}
status := value.(*backendStatus)
return status.Healthy
}
// SetStatusChangeCallback 设置状态变更回调
func (hc *HealthChecker) SetStatusChangeCallback(callback func(string, bool)) {
hc.statusChangeCallback = callback
}
// backendStatus 后端健康状态
type backendStatus struct {
URL *url.URL
Healthy bool
FailCount int
SuccessCount int
}
// run 运行健康检查
func (hc *HealthChecker) run() {
ticker := time.NewTicker(hc.config.Interval)
defer ticker.Stop()
for {
select {
case <-hc.ctx.Done():
return
case <-ticker.C:
hc.checkAll()
}
}
}
// checkAll 检查所有后端
func (hc *HealthChecker) checkAll() {
hc.statusMap.Range(func(key, value interface{}) bool {
go hc.check(key.(string), value.(*backendStatus))
return true
})
}
// check 检查单个后端
func (hc *HealthChecker) check(key string, status *backendStatus) {
// 创建检查请求
u := *status.URL
u.Path = hc.config.Path
req, err := http.NewRequest(hc.config.Method, u.String(), nil)
if err != nil {
hc.updateStatus(key, status, false)
return
}
// 设置超时的客户端
client := &http.Client{
Timeout: hc.config.Timeout,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: hc.config.Timeout,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: hc.config.Timeout,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
},
}
// 发送请求
resp, err := client.Do(req)
if err != nil {
hc.updateStatus(key, status, false)
return
}
defer resp.Body.Close()
// 检查响应状态
if resp.StatusCode == hc.config.SuccessStatus {
hc.updateStatus(key, status, true)
} else {
hc.updateStatus(key, status, false)
}
}
// updateStatus 更新后端状态
func (hc *HealthChecker) updateStatus(key string, status *backendStatus, success bool) {
if success {
status.SuccessCount++
status.FailCount = 0
if !status.Healthy && status.SuccessCount >= hc.config.MinSuccess {
status.Healthy = true
if hc.statusChangeCallback != nil {
hc.statusChangeCallback(key, true)
}
}
} else {
status.FailCount++
status.SuccessCount = 0
if status.Healthy && status.FailCount >= hc.config.MaxFails {
status.Healthy = false
if hc.statusChangeCallback != nil {
hc.statusChangeCallback(key, false)
}
}
}
hc.statusMap.Store(key, status)
}