Rust in Everything
基于 tower 0.5.3

负载均衡

前面几部分的中间件都只包一个内层服务。这一部分处理「一组可以互相替代的服务」:同一个后端的多个实例,请求发给哪个都行,要挑一个合适的。tower 把这件事拆成三个独立的问题,各自一个模块:

  • 现在有哪些后端? discover:一个产出 Insert / Remove 的流。
  • 哪个比较闲? load:每个后端一个能比较大小的负载值。
  • 哪些现在能接请求? ready_cache:同时驱动所有后端的 poll_ready,把就绪的记下来。

balance::p2c::Balance 把三者接起来,它本身也是一个 Service,所以对调用方来说,一组后端和一个后端没有区别。整个选择过程都发生在 Balance::poll_ready 里,也就是就绪主线上;call 只是把请求交给选好的那一个。

flowchart TB
  disc["Discover:Change 流"] -->|Insert| push["ReadyCache::push"]
  disc -->|Remove| evict["ReadyCache::evict"]
  push --> pend[pending 集合]
  pend -->|poll_pending| rdy[ready 集合]
  rdy --> pick["P2C:随机取两个"]
  ld["Load::load()"] --> pick
  pick -->|负载小的| idx[记下 ready_index]
  idx --> callidx["call_ready_index"]
  callidx -->|用过回去等就绪| pend
  1. Discover:Discover 只是 TryStream<Ok = Change<K, S>> 的别名 trait,靠 blanket impl 自动实现,并且 sealed,用户只能实现 Stream。ServiceList 是固定列表的最简实现,动态发现用一个通道就够;Balance 只在 poll_ready 里读它。
  2. Load 与三种负载度量:Load::load() 返回一个 PartialOrd 的值。Constant 给固定值;PendingRequests 数 Arc 的引用;PeakEwma 大的延迟立即采用、小的按时间加权,负载 = 估计延迟 ×(在途 + 1),而且 load() 本身会让估计衰减。TrackCompletion 决定请求什么时候算结束。
  3. ReadyCache:pending 是 FuturesUnordered,ready 是 IndexMap。push 进 pending,poll_pending 把就绪的挪进 ready,call 之后回到 pending;evict 用取消标志停掉待就绪的服务。所有移除都是 swap_remove,缓存的下标在 evict 之后可能指向另一个服务。
  4. P2C 负载均衡:Balance::poll_ready 先读 Discover,再推进 pending,然后随机取两个不同的就绪服务比负载,把选中的下标存下来,call 时用它。为什么随机取两个而不是取全局最小。
  5. Hedge:请求在途时间超过历史延迟的某个分位数,就再发一份,谁先回来用谁;它本身由几个更小的中间件拼成。
  6. Steer 与 MakeService:Steer 按请求内容挑服务,poll_ready 要等所有服务都就绪;MakeService 按目标造服务,hyper 每个连接造一个服务就是这个模式。

前三页是 P2C 的零件,可以单独使用:ReadyCache 不依赖 Discover 和 Load,Load 也可以脱离 Balance 自己比较两个服务(load 模块文档里的例子就是这样)。对应的 feature 是 discover、load、ready-cache,balance 打开它们和 make、util。balance 还会拉进一个 slab 依赖,那是 0.5.0 删掉的 balance::Pool(PR #658)留下的,0.5.3 的源码里已经没有地方用到它。

💬 关联讨论

发起讨论
加载中...