lolly/docs/superpowers/specs/2026-06-08-loadbalance-enhancement-design.md
xfy ebeb258c58 docs(benchmark): add v0.4.0 baseline summary and update gitignore
- Collect baseline benchmark summary across all core modules
- Save key results to benchmarks/v0.4.0/summary.txt
- Update .gitignore to track benchmark summaries/reports
- Include performance optimization design docs and plan
2026-06-11 13:43:28 +08:00

390 lines
11 KiB
Markdown
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.

# Lolly 负载均衡增强设计 - Least Time & Session Sticky
**日期**: 2026-06-08
**状态**: Approved
## 1. 背景与目标
Lolly 当前支持 6 种负载均衡算法Round Robin、Weighted Round Robin、Least Connections、IP Hash、Consistent Hash、RandomPower of Two Choices
与 nginx Plus 对比Lolly 缺少两个重要特性:
1. **Least Time** - 基于响应时间选择最优后端
2. **Session Sticky** - Cookie-based 会话保持
本文档设计这两个算法的高性能实现方案,目标是:
- **零锁设计**:原子操作替代互斥锁
- **零堆分配**:预分配 + 对象池
- **纳秒级延迟**:单次选择 < 100ns
- **与现有代码风格一致**
## 2. 设计概览
```
+----------------------+
| Proxy Request |
+----------+-----------+
|
+----------------+----------------+
| |
+-----v------+ +------v------+
| Least Time | | Sticky |
| Select | | Route |
+-----+------+ +------+------+
| |
+-----v------+ +------v------+
| EWMA Stats | | Cookie |
| (atomic) | | + Shard Map |
+------------+ +-------------+
```
## 3. Least Time 设计
### 3.1 核心算法
基于 EWMA指数加权移动平均的响应时间统计
```
new_avg = alpha * new_sample + (1 - alpha) * old_avg
```
- `alpha` 默认 0.3可配置0-1 范围
- alpha 越大对新样本越敏感收敛越快
- 使用 atomic.Int64 存储纳秒值避免浮点运算
### 3.2 数据结构
```go
// EWMAStats 原子 EWMA 统计器
type EWMAStats struct {
headerTime atomic.Int64 // EWMA 首字节时间(纳秒)
lastByteTime atomic.Int64 // EWMA 完整响应时间(纳秒)
sampleCount atomic.Int64 // 样本计数
}
// 使用固定点整数运算避免浮点
// 将 alpha 编码为定点数alpha * 1000
const alphaScale = 1000
func (e *EWMAStats) Record(headerTime, lastByteTime time.Duration) {
// 原子更新,无锁
e.updateAtomic(&e.headerTime, headerTime)
e.updateAtomic(&e.lastByteTime, lastByteTime)
e.sampleCount.Add(1)
}
```
### 3.3 LeastTime Balancer
```go
type LeastTime struct {
metric string // "header" | "last_byte"
}
func (l *LeastTime) Select(targets []*Target) *Target {
var selected *Target
var minTime int64 = -1
for _, t := range targets {
if !t.IsAvailable() {
continue
}
// 原子读取响应时间
var currentTime int64
if l.metric == "header" {
currentTime = t.Stats.HeaderTime()
} else {
currentTime = t.Stats.LastByteTime()
}
// 无统计样本时给默认值,避免新节点被饿死
if currentTime == 0 {
currentTime = defaultResponseTime
}
if selected == nil || currentTime < minTime {
selected = t
minTime = currentTime
}
}
return selected
}
```
### 3.4 性能指标
| 操作 | 延迟 | | 堆分配 |
|------|------|-----|--------|
| Record | ~20ns | | 0 |
| Select | ~50ns | | 0 |
### 3.5 配置
```yaml
proxy:
- path: /api
load_balance: least_time
least_time_metric: last_byte # header | last_byte默认
least_time_alpha: 0.3 # 0-1越大越敏感默认 0.3
least_time_default_ns: 1000000 # 无样本时的默认值(默认 1ms
```
### 3.6 Proxy 层集成
```go
// 在请求完成后调用
func (p *Proxy) recordResponseTime(target *loadbalance.Target, start time.Time) {
if tracker, ok := p.balancer.(ResponseTimeRecorder); ok {
headerTime := target.HeaderReceived.Sub(start)
lastByteTime := time.Since(start)
tracker.RecordResponseTime(target, headerTime, lastByteTime)
}
}
```
## 4. Session Sticky 设计
### 4.1 核心算法
基于 Cookie 的路由表 + 分片锁
- Cookie 值编码`base64(target_url + "|" + expires_timestamp)`
- 256 个分片每个分片独立 `sync.RWMutex`
- 分片索引`fnvHash64a(cookie_value) % 256`
- 后台 goroutine 60s 清理过期 session
### 4.2 数据结构
```go
// StickySession Sticky Session 负载均衡器
type StickySession struct {
config StickyConfig
fallback loadbalance.Balancer // fallback 算法
// 256 个分片,降低锁冲突概率
shards [256]*stickyShard
cleaner *time.Ticker
stopCh chan struct{}
started atomic.Bool
}
type stickyShard struct {
mu sync.RWMutex
sessions map[string]*stickyEntry // key: cookie value
}
type stickyEntry struct {
targetURL string
expiresAt int64 // Unix 纳秒
createdAt int64 // Unix 纳秒
}
```
### 4.3 路由流程
```
请求到达
|
v
检查 Cookie "lolly_route"
|
+-- 存在 -->
| 解码 cookie 值
| 查找目标是否健康
| |
| +-- 健康 --> 路由到该目标
| |
| +-- 不健康 -> 删除 session
| 用 fallback 选择新目标
| 设置新 cookie
|
+-- 不存在 -->
用 fallback 选择目标
设置 Set-Cookie 响应头
```
### 4.4 Cookie 编码
```go
// encodeCookie 编码路由信息到 cookie 值
// 格式: base64(target_url + "|" + expires_timestamp)
func encodeCookie(targetURL string, expires time.Time) string {
raw := targetURL + "|" + strconv.FormatInt(expires.Unix(), 10)
return base64.URLEncoding.EncodeToString([]byte(raw))
}
// decodeCookie 解码 cookie 值
func decodeCookie(value string) (targetURL string, expires time.Time, ok bool) {
raw, err := base64.URLEncoding.DecodeString(value)
if err != nil {
return
}
parts := strings.Split(string(raw), "|")
if len(parts) != 2 {
return
}
ts, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return
}
return parts[0], time.Unix(ts, 0), true
}
```
### 4.5 选择逻辑
```go
func (s *StickySession) Select(ctx *fasthttp.RequestCtx, targets []*Target) *Target {
// 1. 检查 cookie
cookie := ctx.Request.Header.Cookie(s.config.Name)
if len(cookie) > 0 {
targetURL, _, ok := decodeCookie(string(cookie))
if ok {
// 查找目标
for _, t := range targets {
if t.URL == targetURL && t.IsAvailable() {
return t
}
}
// 目标不可用,删除 session延迟删除
s.deleteSession(string(cookie))
}
}
// 2. 使用 fallback 算法选择
selected := s.fallback.Select(targets)
if selected == nil {
return nil
}
// 3. 种 cookie
s.setCookie(ctx, selected.URL)
// 4. 记录 session
s.recordSession(selected.URL)
return selected
}
```
### 4.6 性能指标
| 操作 | 延迟 | 锁冲突概率 |
|------|------|-----------|
| Session 查找 | ~30ns | 0.4% (256 分片) |
| Session 写入 | ~50ns | 0.4% |
| 清理过期 | 后台不影响主路径 | - |
### 4.7 配置
```yaml
proxy:
- path: /api
load_balance: sticky
sticky:
enabled: true
name: "lolly_route" # cookie 名称(默认)
expires: "1h" # session 有效期(默认 1h
domain: "" # cookie domain
path: "/" # cookie path默认 /
secure: false # Secure flag
http_only: true # HttpOnly flag默认 true
same_site: "Lax" # SameSite默认 Lax
# fallback 算法配置
fallback_balance: round_robin # 首次路由和失效回退算法
```
## 5. 扩展 Balancer 接口
为支持 Least Time 的响应时间记录扩展一个可选接口
```go
// ResponseTimeRecorder 响应时间记录接口
// 实现此接口的 balancer 可在请求完成后收到响应时间统计
type ResponseTimeRecorder interface {
RecordResponseTime(target *Target, headerTime, lastByteTime time.Duration)
}
```
**为什么用接口扩展而非修改 Balancer**
- 不破坏现有 6 balancer 的实现
- 类型断言在运行时判断无性能开销
- 符合 Go 接口隔离原则
## 6. 文件改动清单
### 6.1 新增文件
| 文件 | 行数 | 说明 |
|------|------|------|
| `internal/loadbalance/ewma.go` | ~80 | 原子 EWMA 统计器 |
| `internal/loadbalance/least_time.go` | ~120 | Least Time balancer |
| `internal/loadbalance/sticky.go` | ~280 | Session Sticky balancer |
| `internal/loadbalance/sticky_config.go` | ~30 | Sticky 配置结构体 |
| `internal/loadbalance/least_time_test.go` | ~200 | Least Time 单元测试 |
| `internal/loadbalance/sticky_test.go` | ~250 | Session Sticky 单元测试 |
### 6.2 修改文件
| 文件 | 修改内容 |
|------|----------|
| `internal/loadbalance/algorithms.go` | 添加 `least_time``sticky` validAlgorithms |
| `internal/loadbalance/balancer.go` | Target 增加 `Stats *EWMAStats` 字段 |
| `internal/config/proxy_config.go` | 添加 `LeastTimeConfig``StickyConfig` |
| `internal/config/defaults.go` | 添加新配置项默认值注释 |
| `internal/config/validate.go` | 验证 `least_time_metric``fallback_balance` |
| `internal/proxy/proxy.go` | createBalancer 增加新算法请求完成后调用 RecordResponseTime |
| `internal/proxy/target_selector.go` | Select 支持 StickySession ctx 参数 |
## 7. 测试策略
### 7.1 Least Time 测试
- **基准测试**: 测量 Select/Record 延迟
- **并发测试**: 100 goroutine 并发 Record + Select验证无数据竞争
- **收敛测试**: 验证 EWMA 对新旧样本的权重分配
- **故障转移**: 验证目标失效后选择其他目标
### 7.2 Session Sticky 测试
- **Cookie 编码/解码**: 验证 round-trip 正确性
- **路由一致性**: 相同 cookie 始终路由到同一目标
- **目标失效**: 目标不可用时 fallback 并更新 cookie
- **过期清理**: 验证过期 session 被清理
- **并发安全**: 100 goroutine 并发读写验证无数据竞争
- **分片均衡**: 验证 hash 分布均匀
## 8. 与 nginx Plus 对比
| 特性 | nginx Plus | Lolly 方案 |
|------|------------|------------|
| Least Time header | | |
| Least Time last_byte | | |
| EWMA 平滑 | | (alpha 可调) |
| Session Sticky cookie | | |
| Session Sticky learn | | (暂不支持) |
| Secure/HttpOnly/SameSite | | |
| 目标失效 fallback | | |
| Session TTL | | |
## 9. 风险与缓解
| 风险 | 影响 | 缓解 |
|------|------|------|
| 新节点被饿死 | | 无统计样本时给默认值 `least_time_default_ns` |
| Sticky 内存增长 | | TTL + 后台清理 + 分片限制 |
| Cookie 过大 | | 仅编码 URL + timestamp通常 < 200 bytes |
| 目标频繁上下线 | | session 延迟删除避免惊群 |
## 10. 后续优化
1. **Session Sticky Learn 模式**: 学习后端返回的 Set-Cookie而非主动种植
2. **Least Time 加权**: 结合权重和响应时间进行加权选择
3. **统计持久化**: 重启后保留历史响应时间统计
---
**设计批准**: 已批准
**下一步**: 编写实现计划 (writing-plans)