hrw

package
v0.0.0-...-a7bc12e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2026 License: MIT Imports: 10 Imported by: 0

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

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

func New(log *zap.Logger, wantHealthCheck bool, nginxUpstreamHashBy string) *Balancer

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

func (b *Balancer) Add(name string, handler http.Handler, weight *int, fenced bool)

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

func (b *Balancer) AddServer(name string, handler http.Handler, server dynamic.Server)

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

func (b *Balancer) RegisterStatusUpdater(fn func(up bool)) error

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 算法将请求路由到一致性哈希选中的后端。

请求路由流程:

  1. 从请求中提取客户端标识 key(默认 RemoteAddr IP)
  2. 如果配置了 nginxUpstreamHashBy,使用自定义变量覆盖 key
  3. 调用 nextServer(key) 选出得分最高的健康后端
  4. 如果无可用后端 → 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

func (b *Balancer) SetStatus(ctx context.Context, childName string, up bool)

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
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL