lolly/internal/loadbalance/balancer.go
xfy fa95b2a76e feat(loadbalance): implement Least Time balancer
- Add atomic EWMA Stats field to Target
- Implement LeastTime balancer with header_time and last_byte metrics
- Support Select and SelectExcluding with zero-lock design
- Add ResponseTimeRecorder interface for proxy integration
2026-06-08 17:21:20 +08:00

638 lines
17 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 loadbalance 提供负载均衡算法实现。
//
// 该文件包含负载均衡相关的核心定义,包括:
// - Target 目标结构体定义
// - Balancer 接口定义
// - ValidAlgorithms 有效算法列表
//
// 主要用途:
//
// 用于定义负载均衡的标准接口和目标结构,支持多种负载均衡算法。
//
// 注意事项:
// - 所有实现必须并发安全
// - 目标的健康状态使用 atomic.Bool
//
// 作者xfy
package loadbalance
import (
"net"
"net/url"
"sync"
"sync/atomic"
"time"
"rua.plus/lolly/internal/hash"
)
type filterContext struct {
available []*Target
backups []*Target
excludeSet map[string]bool
}
var filterContextPool = sync.Pool{
New: func() any {
return &filterContext{
available: make([]*Target, 0, 64),
backups: make([]*Target, 0, 64),
excludeSet: make(map[string]bool, 8),
}
},
}
func acquireFilterContext() *filterContext {
fc, _ := filterContextPool.Get().(*filterContext)
return fc
}
func releaseFilterContext(fc *filterContext) {
fc.available = fc.available[:0]
fc.backups = fc.backups[:0]
for k := range fc.excludeSet {
delete(fc.excludeSet, k)
}
filterContextPool.Put(fc)
}
func fnvHash64a(key string) uint64 {
return hash.FNV64a(key)
}
// Target 表示 HTTP 代理L7 层)的负载均衡后端服务器目标。
//
// HTTP Target 特性(区别于 Stream Target
// - URL 解析:支持完整 URL如 http://backend:8080包含协议、路径、查询参数
// - DNS 动态解析resolvedIPs 和 lastResolved 字段支持 DNS TTL 缓存和动态重解析
// - Failover 支持:配合 Balancer.SelectExcluding 实现失败节点排除重试
// - 一致性哈希VirtualHashes 支持一致性哈希算法的虚拟节点
//
// 语义差异说明:
// - HTTP 代理工作在应用层L7需要处理 URL 和 DNS 解析
// - Stream 代理工作在传输层L4只需简单 host:port无需 DNS 缓存
// - 因此 HTTP Target 和 Stream Target 必须保持独立定义,不可合并
//
// 所有字段都设计为使用原子操作进行并发访问(如适用)。
type Target struct {
resolvedIPs atomic.Pointer[[]string]
URL string
hostname string
VirtualHashes []uint64
Weight int
Connections int64
lastResolved atomic.Int64
hostnameOnce sync.Once
Healthy atomic.Bool
// MaxConns 最大并发连接数0 表示不限制
MaxConns int64
// MaxFails 最大失败次数0 表示不检测
MaxFails int64
// FailTimeout 失败冷却时间
FailTimeout time.Duration
// Backup 备份服务器标记
Backup bool
// Down 永久不可用标记
Down bool
// ProxyURI 代理传递的 URI 路径
ProxyURI string
failCount atomic.Int64
failedUntil atomic.Int64
// 慢启动相关字段
// EffectiveWeight 当前有效权重(慢启动期间动态变化)
EffectiveWeight atomic.Int64
// SlowStart 慢启动时间(配置)
SlowStart time.Duration `yaml:"slow_start"`
// Stats 响应时间统计(用于 least_time 算法)
Stats *EWMAStats
}
// Balancer 是 HTTP 代理L7 层)负载均衡算法的接口。
//
// HTTP Balancer 特性(区别于 Stream Balancer
// - Select(): 标准选择方法,按算法策略选择健康目标
// - SelectExcluding(): 故障转移支持,排除失败节点后选择替代目标
//
// 语义差异说明:
// - HTTP 代理需要 failover 重试能力next_upstream 配置),因此需要 SelectExcluding
// - Stream 代理工作在传输层L4无重试机制仅需要 Select 方法
// - 因此 HTTP Balancer 和 Stream Balancer 接口签名不同,不可合并
//
// 实现必须是并发安全的。
type Balancer interface {
// Select 根据算法策略从提供的列表中选择一个目标。
// 如果没有健康目标可用,返回 nil。
Select(targets []*Target) *Target
// SelectExcluding 根据算法策略选择一个目标,排除指定的目标列表。
// 用于故障转移场景,避免选择已失败的目标。
// 如果除了排除列表外没有可用目标,返回 nil。
SelectExcluding(targets []*Target, excluded []*Target) *Target
}
// RoundRobin 实现简单的轮询负载均衡。
//
// 它按顺序将请求均匀分配到所有健康目标上,适合后端服务器性能相近的场景。
//
// 并发安全counter 使用 atomic 操作,支持多 goroutine 并发调用。
type RoundRobin struct {
// counter 请求计数器,原子递增,用于确定轮询位置
counter atomic.Uint64
}
// NewRoundRobin 创建一个新的轮询负载均衡器。
//
// 该函数初始化一个无状态的 RoundRobin 实例,内部 counter 从零开始。
// 适合后端服务器性能相近、无需权重区分的场景。
//
// 返回值:
// - *RoundRobin: 初始化的轮询均衡器实例
func NewRoundRobin() *RoundRobin {
return &RoundRobin{}
}
// Select 选择轮询顺序中的下一个目标。
// 只考虑健康目标。如果没有健康目标则返回 nil。
func (r *RoundRobin) Select(targets []*Target) *Target {
fc := acquireFilterContext()
defer releaseFilterContext(fc)
healthy := filterInto(fc, targets)
if len(healthy) == 0 {
return nil
}
idx := r.counter.Add(1) - 1
return healthy[idx%uint64(len(healthy))]
}
// WeightedRoundRobin 实现权重轮询负载均衡。
//
// 权重越高的目标接收成比例更多的请求,适合后端服务器性能差异较大的场景。
//
// 并发安全counter 使用 atomic 操作,支持多 goroutine 并发调用。
type WeightedRoundRobin struct {
// counter 请求计数器,原子递增,用于确定权重分布位置
counter atomic.Uint64
}
// NewWeightedRoundRobin 创建一个新的权重轮询负载均衡器。
//
// 该函数初始化一个 WeightedRoundRobin 实例,内部 counter 从零开始。
// 权重越高的目标接收成比例更多的请求,适合后端服务器性能差异较大的场景。
//
// 返回值:
// - *WeightedRoundRobin: 初始化的权重轮询均衡器实例
func NewWeightedRoundRobin() *WeightedRoundRobin {
return &WeightedRoundRobin{}
}
// Select 基于权重分布选择目标。
// 只考虑健康目标。如果没有健康目标则返回 nil。
func (w *WeightedRoundRobin) Select(targets []*Target) *Target {
fc := acquireFilterContext()
defer releaseFilterContext(fc)
healthy := filterInto(fc, targets)
if len(healthy) == 0 {
return nil
}
totalWeight := 0
for _, t := range healthy {
if t.Weight <= 0 {
totalWeight++
} else {
totalWeight += t.Weight
}
}
if totalWeight == 0 {
return nil
}
idx := w.counter.Add(1) - 1
pos := int(idx % uint64(totalWeight))
currentWeight := 0
for _, t := range healthy {
weight := t.Weight
if weight <= 0 {
weight = 1
}
currentWeight += weight
if pos < currentWeight {
return t
}
}
return healthy[len(healthy)-1]
}
// LeastConnections 实现最少连接负载均衡。
//
// 它选择活跃连接数最少的目标,适合请求处理时间差异较大的场景。
//
// 该算法无状态设计,不维护内部计数器,直接读取目标连接数。
type LeastConnections struct{}
// NewLeastConnections 创建一个新的最少连接负载均衡器。
//
// 返回值:
// - 初始化的 LeastConnections 实例
func NewLeastConnections() *LeastConnections {
return &LeastConnections{}
}
// Select 选择连接数最少的目标。
// 只考虑健康目标。如果没有健康目标则返回 nil。
func (l *LeastConnections) Select(targets []*Target) *Target {
var selected *Target
var minConns int64 = -1
for _, t := range targets {
if !t.Healthy.Load() {
continue
}
// 原子地读取连接计数
conns := atomic.LoadInt64(&t.Connections)
if selected == nil || conns < minConns {
selected = t
minConns = conns
}
}
return selected
}
// IPHash 实现基于 IP 哈希的负载均衡。
//
// 它将来自同一客户端 IP 的请求始终路由到同一目标,实现会话保持。
// 适合需要会话粘性的场景,如无状态后端的有状态请求处理。
type IPHash struct{}
// NewIPHash 创建一个新的 IP 哈希负载均衡器。
//
// 返回值:
// - 初始化的 IPHash 实例
func NewIPHash() *IPHash {
return &IPHash{}
}
// Select 基于客户端 IP 的哈希值选择目标。
// 只考虑健康目标。如果没有健康目标则返回 nil。
// clientIP 参数应该是客户端的 IP 地址字符串。
func (i *IPHash) Select(targets []*Target) *Target {
return i.SelectByIP(targets, "")
}
// SelectByIP 基于提供的 IP 地址的哈希值选择目标。
// 只考虑健康目标。如果没有健康目标则返回 nil。
func (i *IPHash) SelectByIP(targets []*Target, clientIP string) *Target {
fc := acquireFilterContext()
defer releaseFilterContext(fc)
healthy := filterInto(fc, targets)
if len(healthy) == 0 {
return nil
}
hash := fnvHash64a(clientIP)
idx := hash % uint64(len(healthy))
return healthy[idx]
}
// GetEffectiveWeight 获取目标的有效权重。
//
// 如果未配置慢启动或不在慢启动状态,返回配置权重。
// 慢启动期间,权重从 1 逐渐增加到配置权重。
func (t *Target) GetEffectiveWeight() int {
ew := t.EffectiveWeight.Load()
if ew == 0 {
return t.Weight // 未配置慢启动时返回配置权重
}
return int(ew)
}
// IsAvailable 检查目标是否可用。
// 目标不可用的条件(优先级从高到低):
// - Healthy 为 false硬性不可用由健康检查器设置
// - Down 为 true配置标记永久不可用
// - 超过 MaxConns 限制
// - 失败冷却期内failCount >= MaxFails 且未超过 FailTimeout
func (t *Target) IsAvailable() bool {
if !t.Healthy.Load() || t.Down {
return false
}
if t.MaxConns > 0 && atomic.LoadInt64(&t.Connections) >= t.MaxConns {
return false
}
if t.MaxFails > 0 {
failCount := t.failCount.Load()
if failCount >= t.MaxFails {
failedUntil := t.failedUntil.Load()
if time.Now().UnixNano() < failedUntil {
return false
}
if failedUntil > 0 {
t.failCount.Store(0)
t.failedUntil.Store(0)
}
}
}
return true
}
// RecordFailure 记录一次失败。
// 使用互斥锁保护 failCount/failedUntil 的协调更新。
// 返回当前失败计数。
func (t *Target) RecordFailure() int64 {
if t.MaxFails <= 0 {
return 0
}
count := t.failCount.Add(1)
if count >= t.MaxFails {
timeout := t.FailTimeout
if timeout <= 0 {
timeout = 10 * time.Second
}
t.failedUntil.Store(time.Now().Add(timeout).UnixNano())
}
return count
}
// RecordSuccess 记录一次成功,重置软失败状态。
// 仅重置 failCount 和 failedUntil不修改 Healthy健康检查器权威
func (t *Target) RecordSuccess() {
if t.MaxFails <= 0 {
return
}
t.failCount.Store(0)
t.failedUntil.Store(0)
}
// IsBackup 返回目标是否为备份服务器。
func (t *Target) IsBackup() bool {
return t.Backup
}
func filterInto(fc *filterContext, targets []*Target) []*Target {
for _, t := range targets {
if !t.IsAvailable() {
continue
}
if t.IsBackup() {
fc.backups = append(fc.backups, t)
} else {
fc.available = append(fc.available, t)
}
}
if len(fc.available) > 0 {
return fc.available
}
return fc.backups
}
// IncrementConnections 原子地增加目标的连接计数。
// 当新连接建立时应调用此函数。
func IncrementConnections(t *Target) {
atomic.AddInt64(&t.Connections, 1)
}
// DecrementConnections 原子地减少目标的连接计数。
// 当连接关闭时应调用此函数。
func DecrementConnections(t *Target) {
atomic.AddInt64(&t.Connections, -1)
}
func filterIntoExcluding(fc *filterContext, targets []*Target, excluded []*Target) []*Target {
for _, t := range excluded {
if t != nil {
fc.excludeSet[t.URL] = true
}
}
for _, t := range targets {
if !t.IsAvailable() || fc.excludeSet[t.URL] {
continue
}
if t.IsBackup() {
fc.backups = append(fc.backups, t)
} else {
fc.available = append(fc.available, t)
}
}
if len(fc.available) > 0 {
return fc.available
}
return fc.backups
}
// SelectExcluding 根据轮询策略选择一个目标,排除指定的目标列表。
// 只考虑健康且不在排除列表中的目标。
func (r *RoundRobin) SelectExcluding(targets []*Target, excluded []*Target) *Target {
fc := acquireFilterContext()
defer releaseFilterContext(fc)
available := filterIntoExcluding(fc, targets, excluded)
if len(available) == 0 {
return nil
}
idx := r.counter.Add(1) - 1
return available[idx%uint64(len(available))]
}
// SelectExcluding 根据权重分布选择目标,排除指定的目标列表。
// 只考虑健康且不在排除列表中的目标。
func (w *WeightedRoundRobin) SelectExcluding(targets []*Target, excluded []*Target) *Target {
fc := acquireFilterContext()
defer releaseFilterContext(fc)
available := filterIntoExcluding(fc, targets, excluded)
if len(available) == 0 {
return nil
}
totalWeight := 0
for _, t := range available {
if t.Weight <= 0 {
totalWeight++
} else {
totalWeight += t.Weight
}
}
if totalWeight == 0 {
return nil
}
idx := w.counter.Add(1) - 1
pos := int(idx % uint64(totalWeight))
currentWeight := 0
for _, t := range available {
weight := t.Weight
if weight <= 0 {
weight = 1
}
currentWeight += weight
if pos < currentWeight {
return t
}
}
return available[len(available)-1]
}
// SelectExcluding 选择连接数最少的目标,排除指定的目标列表。
// 优先选择非备份目标,仅当无可用非备份目标时选择备份目标。
func (l *LeastConnections) SelectExcluding(targets []*Target, excluded []*Target) *Target {
fc := acquireFilterContext()
defer releaseFilterContext(fc)
for _, t := range excluded {
if t != nil {
fc.excludeSet[t.URL] = true
}
}
var selected *Target
var selectedBackup *Target
var minConns int64 = -1
var minBackupConns int64 = -1
for _, t := range targets {
if !t.IsAvailable() || fc.excludeSet[t.URL] {
continue
}
conns := atomic.LoadInt64(&t.Connections)
if t.IsBackup() {
if selectedBackup == nil || conns < minBackupConns {
selectedBackup = t
minBackupConns = conns
}
} else {
if selected == nil || conns < minConns {
selected = t
minConns = conns
}
}
}
if selected != nil {
return selected
}
return selectedBackup
}
// SelectExcluding 基于客户端 IP 的哈希值选择目标,排除指定的目标列表。
// 只考虑健康且不在排除列表中的目标。
func (i *IPHash) SelectExcluding(targets []*Target, excluded []*Target) *Target {
return i.SelectExcludingByIP(targets, excluded, "")
}
// SelectExcludingByIP 基于提供的 IP 地址的哈希值选择目标,排除指定的目标列表。
// 只考虑健康且不在排除列表中的目标。
func (i *IPHash) SelectExcludingByIP(targets []*Target, excluded []*Target, clientIP string) *Target {
fc := acquireFilterContext()
defer releaseFilterContext(fc)
available := filterIntoExcluding(fc, targets, excluded)
if len(available) == 0 {
return nil
}
hash := fnvHash64a(clientIP)
idx := hash % uint64(len(available))
return available[idx]
}
// Hostname 返回目标主机名(从 URL 提取)。
// 使用 sync.Once 确保线程安全,只初始化一次。
func (t *Target) Hostname() string {
t.hostnameOnce.Do(t.initHostname)
return t.hostname
}
// ResolvedIPs 返回解析后的 IP 列表。
// 如果未解析过,返回 nil。
func (t *Target) ResolvedIPs() []string {
ips := t.resolvedIPs.Load()
if ips == nil {
return nil
}
return *ips
}
// SetResolvedIPs 设置解析后的 IP 列表,并更新最后解析时间。
func (t *Target) SetResolvedIPs(ips []string) {
// 创建副本避免外部修改
ipsCopy := make([]string, len(ips))
copy(ipsCopy, ips)
t.resolvedIPs.Store(&ipsCopy)
t.lastResolved.Store(time.Now().UnixNano())
}
// NeedsResolve 检查是否需要重新解析。
// 如果 hostname 是 IP 地址,返回 false。
// 如果从未解析过或超过 TTL返回 true。
func (t *Target) NeedsResolve(ttl time.Duration) bool {
host := t.Hostname()
// IP 类型的 URL 不需要解析
if net.ParseIP(host) != nil {
return false
}
last := t.lastResolved.Load()
if last == 0 {
return true // 首次解析
}
return time.Since(time.Unix(0, last)) > ttl
}
// initHostname 从 URL 中提取并缓存主机名。
// 必须在 Target 创建后调用一次。
func (t *Target) initHostname() {
u, err := url.Parse(t.URL)
if err != nil {
// 解析失败,使用整个 URL 作为 hostname
t.hostname = t.URL
return
}
// 提取主机名(去掉端口)
host := u.Host
if h, _, err := net.SplitHostPort(host); err == nil {
t.hostname = h
} else {
t.hostname = host
}
}
// NewTargetFromConfig 从配置创建 Target推荐入口
// 自动初始化 hostname 和 Healthy 状态,设置上游参数。
func NewTargetFromConfig(url string, weight int, maxConns int64, maxFails int64, failTimeout time.Duration, backup bool, down bool, proxyURI string) *Target {
t := &Target{
URL: url,
Weight: weight,
MaxConns: maxConns,
MaxFails: maxFails,
FailTimeout: failTimeout,
Backup: backup,
Down: down,
ProxyURI: proxyURI,
Stats: NewEWMAStats(),
}
t.initHostname()
if !down {
t.Healthy.Store(true)
}
return t
}
// LastResolved 返回最后解析时间。
func (t *Target) LastResolved() time.Time {
nano := t.lastResolved.Load()
if nano == 0 {
return time.Time{}
}
return time.Unix(0, nano)
}