Documentation
¶
Overview ¶
Package hrw 实现了 Highest Random Weight (HRW),即 Rendezvous Hashing 一致性哈希负载均衡算法。
算法原理: HRW (Rendezvous Hashing / 最高随机权重哈希) 是一种分布式哈希算法, 前身为 "Browser-based Thundering Herd" 问题解决器,目前已广泛用于负载均衡。
核心公式:对每个候选后端 backend[i],计算:
score[i] = weight[i] * (1 / -ln(hash(clientKey + backend[i].name)))
其中 hash 映射到 (0, 1) 浮点区间,-ln(x) 将均匀分布转变为偏态分布, 使得每个 client 的后端得分具有显著区分度。最终选择 score 最高的后端。
请求处理流程(ServeHTTP):
客户端请求到来
│
┌─────────┴─────────┐
│ 提取客户端 key │
│ strategy.GetIP() │
│ 或 nginxUpstreamHashBy │
└─────────┬─────────┘
│
┌─────────┴─────────┐
│ nextServer(key) │
│ │
│ 1.过滤 healthy │
│ 2.排除 fenced │
│ 3.逐个计算 score │
│ 4.选最高分 ← HRW │
└─────────┬─────────┘
│
┌─────────┴─────────┐
│ err != nil ? │
│ Y: 503/500 │
│ N: 转发到后端 │
└────────────────────┘
一致性保证(Same-Key-Same-Backend):
假设有 3 个后端 srv-a (weight=2), srv-b (weight=1), srv-c (weight=3) client IP = 10.0.0.5 → key = "10.0.0.5" score["10.0.0.5" + "srv-a"] = 2 * 1/(-ln(hash)) ≈ 0.43 score["10.0.0.5" + "srv-b"] = 1 * 1/(-ln(hash)) ≈ 0.15 score["10.0.0.5" + "srv-c"] = 3 * 1/(-ln(hash)) ≈ 0.31 → 选中 srv-a,只要 srv-a 存活,10.0.0.5 始终路由到 srv-a
后端扩缩容影响最小:
新增 srv-d 时,只有 hash(key+"srv-d") 最高的那些 key 会被迁移,其余 key 不受影响 移出 srv-a 时,只有原本命中 srv-a 的 key 需要重新分配到其他后端
YAML 配置示例:
http:
services:
my-hrw-service:
loadBalancer:
servers:
- url: "http://backend1:8080"
weight: 2
- url: "http://backend2:8080"
weight: 1
- url: "http://backend3:8080"
weight: 3
healthCheck:
path: "/healthz"
interval: "10s"
Index ¶
- type Balancer
- func (b *Balancer) Add(name string, handler http.Handler, weight *int, fenced bool)
- func (b *Balancer) AddServer(name string, handler http.Handler, server dynamic.Server)
- func (b *Balancer) RegisterStatusUpdater(fn func(up bool)) error
- func (b *Balancer) ServeHTTP(w http.ResponseWriter, req *http.Request)
- func (b *Balancer) SetStatus(ctx context.Context, childName string, up bool)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Balancer ¶
type Balancer struct {
// contains filtered or unexported fields
}
Balancer 是基于 HRW(Rendezvous Hashing)的负载均衡器。
相比传统哈希环(如 Ketama),HRW 的优势:
- 无需维护虚拟节点
- 后端增删只影响最少量的 key 重分配
- 天然支持加权选择
- 算法简单,无环状结构维护开销
线程安全:handlersMu 保护 handlers/status/fenced 三个字段的并发读写。
SetStatus → updaters 状态传播链(与 failover/mirror 中的模式一致):
健康检查探测到后端 UP/DOWN
│
▼
SetStatus(childName, up)
│
▼
upBefore vs upAfter 是否变化?
│
├── 不变 → 跳过通知(避免无意义抖动)
│
└── 变化 → 遍历 updaters,逐个调用 fn(upAfter)
│
▼
父层(如 sticky/failover)感知状态变化
使用示例(在路由构建时创建):
lb := hrw.New(logger, true, "$http_x_user_id")
lb.AddServer("backend-1", handler1, dynamic.Server{Weight: intPtr(2)})
lb.AddServer("backend-2", handler2, dynamic.Server{Weight: intPtr(1)})
// 注册到父层以传播健康状态
lb.RegisterStatusUpdater(func(up bool) { parentLB.SetStatus(ctx, "hrw-group", up) })
func New ¶
New 创建一个新的 HRW 负载均衡器实例。
参数:
- log: zap logger,用于记录 HRW 选后端、状态变更等日志
- wantHealthCheck: 是否启用健康检查。若为 false,RegisterStatusUpdater 会拒绝注册
- nginxUpstreamHashBy: nginx 风格的自定义哈希变量,如 "$http_x_user_id"。 空字符串表示仅使用客户端 IP 作为哈希 key。
使用示例:
// 场景1:基于客户端 IP 的一致性哈希 lb := hrw.New(logger, true, "") // 场景2:基于自定义请求头的会话保持 lb := hrw.New(logger, true, "$http_x_session_id") // 此时所有携带相同 X-Session-Id 头的请求都会路由到同一后端
func (*Balancer) Add ¶
Add 向负载均衡器中添加一个后端处理器。
参数:
- name: 后端唯一标识,用于 HRW 哈希计算和 status 追踪
- handler: 实际处理请求的 http.Handler
- weight: 权重指针,nil 默认为 1,<=0 会被忽略
- fenced: 是否处于优雅退出(draining)状态
添加行为:
- 新后端默认标记为健康(加入 status map)
- 如果 fenced=true,同时加入 fenced map,nextServer 将排除此后端
- 权重 <= 0 的后端直接忽略,不会添加到 handlers 列表中
使用示例:
// 正常后端,权重 2
lb.Add("http://10.0.0.1:8080", handler1, intPtr(2), false)
// 默认权重 1
lb.Add("http://10.0.0.2:8080", handler2, nil, false)
// 优雅退出中的后端(新请求不路由,已有连接继续处理)
lb.Add("http://10.0.0.3:8080", handler3, intPtr(1), true)
// 权重 <= 0 的后端不会被添加
lb.Add("http://10.0.0.4:8080", handler4, intPtr(0), false) // 忽略!
func (*Balancer) AddServer ¶
AddServer 根据 dynamic.Server 配置添加一个后端处理器。
这是 Add 方法的便捷包装,从 dynamic.Server 结构体中提取 Weight 和 Fenced 字段。
使用示例(在路由构建时调用):
for _, srv := range config.Servers {
// srv = {URL: "http://10.0.0.1:8080", Weight: intPtr(2), Fenced: false}
lb.AddServer(srv.URL, handlerFor(srv.URL), srv)
}
func (*Balancer) RegisterStatusUpdater ¶
RegisterStatusUpdater 注册一个状态变更回调函数。
当 Balancer 整体状态("至少一个 UP" vs "全 DOWN")发生变化时, 所有注册的回调函数都会被调用,传入当前的 up 状态。
典型用途:将 HRW 组的健康状态向上传播到父层组件, 如 failover、mirror、sticky 等负载均衡包装层。
线程不安全:此方法仅在配置构建阶段调用,此时尚无并发请求。
使用示例:
// 在路由初始化时注册
lb := hrw.New(logger, true, "")
err := lb.RegisterStatusUpdater(func(up bool) {
parentFailover.SetHandlerStatus(ctx, "hrw-group", up)
})
if err != nil {
// healthCheck 未启用
log.Fatal("cannot register status updater without health check")
}
func (*Balancer) ServeHTTP ¶
func (b *Balancer) ServeHTTP(w http.ResponseWriter, req *http.Request)
ServeHTTP 处理 HTTP 请求,使用 HRW 算法将请求路由到一致性哈希选中的后端。
请求路由流程:
- 从请求中提取客户端标识 key(默认 RemoteAddr IP)
- 如果配置了 nginxUpstreamHashBy,使用自定义变量覆盖 key
- 调用 nextServer(key) 选出得分最高的健康后端
- 如果无可用后端 → 503;如果后端存在 → 直接转发
一致性保证示例:
client A (IP 10.0.0.1) 请求 /api/users:
key = "10.0.0.1"
nextServer("10.0.0.1") → srv-2 (得分最高)
client A 再次请求 /api/orders:
key = "10.0.0.1" (不变)
nextServer("10.0.0.1") → srv-2 (同一后端!)
即使后端列表发生变化(新增 srv-4),只要 srv-2 仍然健康:
nextServer("10.0.0.1") → srv-2 (不变,一致性保证)
func (*Balancer) SetStatus ¶
SetStatus 设置指定子后端的健康状态,并根据状态变化决定是否传播通知。
它是健康检查探针与负载均衡器之间的桥梁:
- 健康检查周期探测后端 /healthz
- 探测结果 → 调用 SetStatus(name, true/false)
- SetStatus 更新 status map
- 如果 Balancer 整体状态("有UP" vs "全DOWN")发生变化 → 触发所有 updaters
状态变化判断逻辑(决策树):
upBefore = (len(status) > 0) ← 当前是否有任何后端 UP?
更新 status map ← 插入或删除 childName
upAfter = (len(status) > 0) ← 更新后是否有任何后端 UP?
upBefore == upAfter ?
│
├── true (无变化) → 跳过通知
│ └── 示例:已有 srv-a UP,现在 srv-b 也变 UP
│ upBefore=true, upAfter=true → 不传播
│ └── 示例:原本全 DOWN,srv-a 变 DOWN 仍然是全 DOWN
│ upBefore=false, upAfter=false → 不传播
│
└── false (有变化) → 调用所有 updaters
├── upAfter=true: 从全 DOWN → 至少一个 UP(恢复服务)
│ 示例:唯一后端 srv-a 从 DOWN 恢复为 UP
│ → 父层 failover 感知到 UP,切回主 handler
└── upAfter=false: 从至少一个 UP → 全 DOWN(服务降级)
示例:最后一个健康后端 srv-a 变 DOWN
→ 父层 failover 感知到 DOWN,切换到 fallback handler
使用示例(健康检查回调中调用):
// health checker goroutine
resp, err := http.Get("http://backend:8080/healthz")
if err != nil || resp.StatusCode != 200 {
balancer.SetStatus(ctx, "http://backend:8080", false) // DOWN
} else {
balancer.SetStatus(ctx, "http://backend:8080", true) // UP
}