- 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
11 KiB
11 KiB
Lolly 负载均衡增强设计 - Least Time & Session Sticky
日期: 2026-06-08 状态: Approved
1. 背景与目标
Lolly 当前支持 6 种负载均衡算法:Round Robin、Weighted Round Robin、Least Connections、IP Hash、Consistent Hash、Random(Power of Two Choices)。
与 nginx Plus 对比,Lolly 缺少两个重要特性:
- Least Time - 基于响应时间选择最优后端
- 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 数据结构
// 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
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 配置
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 层集成
// 在请求完成后调用
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 数据结构
// 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 编码
// 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 选择逻辑
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 配置
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 的响应时间记录,扩展一个可选接口:
// 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. 后续优化
- Session Sticky Learn 模式: 学习后端返回的 Set-Cookie,而非主动种植
- Least Time 加权: 结合权重和响应时间进行加权选择
- 统计持久化: 重启后保留历史响应时间统计
设计批准: ✅ 已批准 下一步: 编写实现计划 (writing-plans)