Files
lijiaoqiao/gateway/internal/router/circuit.go
Your Name 472d9ad4c1 P3-B: Router 熔断器实现 - 健康检查/状态机/半开试探
Gateway:
- ProviderHealth 新增熔断器字段 (CircuitState, ConsecutiveFailures, LastStateChange, OpenReason)
- CircuitBreakerConfig 熔断器配置 (FailureRateThreshold=50%, ConsecutiveFailureThreshold=5, HalfOpenSuccessThreshold=3, OpenTimeout=30s)
- circuit.go: 熔断器状态机 (Closed→Open→HalfOpen→Closed)
- healthcheck.go: 后台健康检查循环 (ProviderHealthCheckInterval 探测 + 自动半开转换)
- RecordResult 集成熔断器状态转换
- isProviderAvailable: CircuitOpen=false, CircuitHalfOpen=true (允许试探)
- GetCircuitState/SetCircuitConfig 管理接口
- metrics.go: 新增 circuit_state_changes_total 指标
- bootstrap.go: BuildServer 返回 ServerBundle(含 Router 和 ShutdownFunc)
- main.go: 适配 ServerBundle,graceful shutdown 停止健康检查器
- bootstrap_test.go: 适配 ServerBundle

17 个新测试,50 个 router 测试全部通过
2026-04-21 17:46:02 +08:00

127 lines
3.2 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 router
import (
"time"
)
// transitionCircuit 尝试进行熔断器状态转换
// 返回是否发生了状态转换
func (r *Router) transitionCircuit(providerName string, success bool, cfg CircuitBreakerConfig) bool {
r.mu.Lock()
defer r.mu.Unlock()
health, ok := r.health[providerName]
if !ok {
return false
}
now := time.Now()
prevState := health.CircuitState
switch health.CircuitState {
case CircuitClosed:
if !success {
health.ConsecutiveFailures++
health.ConsecutiveSuccesses = 0
// 检查是否应该打开熔断器
if health.FailureRate > cfg.FailureRateThreshold ||
health.ConsecutiveFailures >= cfg.ConsecutiveFailureLimit {
health.CircuitState = CircuitOpen
health.OpenReason = "failure_rate_or_consecutive_failures"
health.LastStateChange = now
health.Available = false
}
} else {
health.ConsecutiveSuccesses++
health.ConsecutiveFailures = 0
// 成功后重置连续失败计数,让 FailureRate 慢慢下降
}
case CircuitOpen:
// 处于打开状态,检查是否超时可以进入半开
if now.Sub(health.LastStateChange) >= cfg.OpenTimeout {
health.CircuitState = CircuitHalfOpen
health.LastStateChange = now
health.ConsecutiveFailures = 0
health.ConsecutiveSuccesses = 0
// 半开状态下 Available 仍为 false但允许少量试探请求
}
case CircuitHalfOpen:
if success {
health.ConsecutiveSuccesses++
if health.ConsecutiveSuccesses >= cfg.HalfOpenSuccessThreshold {
// 成功达到阈值,关闭熔断器
health.CircuitState = CircuitClosed
health.LastStateChange = now
health.Available = true
health.ConsecutiveFailures = 0
health.FailureRate = 0 // 重置失败率
}
} else {
health.ConsecutiveFailures++
health.ConsecutiveSuccesses = 0
// 失败,回到打开状态
health.CircuitState = CircuitOpen
health.LastStateChange = now
health.OpenReason = "half_open_probe_failed"
}
}
return prevState != health.CircuitState
}
// CheckAndTransitionToHalfOpen 检查 Open 状态的 provider 是否可以进入半开
// 由后台健康检查循环调用
func (r *Router) CheckAndTransitionToHalfOpen(providerName string, providerHealthy bool) bool {
r.mu.Lock()
defer r.mu.Unlock()
health, ok := r.health[providerName]
if !ok {
return false
}
if health.CircuitState != CircuitOpen {
return false
}
// Provider 自身健康,转换到 HalfOpen 允许试探流量
if providerHealthy {
health.CircuitState = CircuitHalfOpen
health.LastStateChange = time.Now()
health.ConsecutiveFailures = 0
health.ConsecutiveSuccesses = 0
return true
}
return false
}
// GetCircuitState 返回 provider 的熔断器状态
func (r *Router) GetCircuitState(providerName string) CircuitState {
r.mu.RLock()
defer r.mu.RUnlock()
health, ok := r.health[providerName]
if !ok {
return CircuitClosed
}
return health.CircuitState
}
// SetCircuitConfig 设置熔断器配置
func (r *Router) SetCircuitConfig(cfg CircuitBreakerConfig) {
r.mu.Lock()
defer r.mu.Unlock()
r.circuitConfig = cfg
}
// GetCircuitConfig 返回熔断器配置
func (r *Router) GetCircuitConfig() CircuitBreakerConfig {
r.mu.RLock()
defer r.mu.RUnlock()
return r.circuitConfig
}