负载均衡
前面几部分的中间件都只包一个内层服务。这一部分处理「一组可以互相替代的服务」:同一个后端的多个实例,请求发给哪个都行,要挑一个合适的。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
- Discover:
Discover只是TryStream<Ok = Change<K, S>>的别名 trait,靠 blanket impl 自动实现,并且 sealed,用户只能实现Stream。ServiceList是固定列表的最简实现,动态发现用一个通道就够;Balance只在poll_ready里读它。 - Load 与三种负载度量:
Load::load()返回一个PartialOrd的值。Constant给固定值;PendingRequests数Arc的引用;PeakEwma大的延迟立即采用、小的按时间加权,负载 = 估计延迟 ×(在途 + 1),而且load()本身会让估计衰减。TrackCompletion决定请求什么时候算结束。 - ReadyCache:
pending是FuturesUnordered,ready是IndexMap。push进pending,poll_pending把就绪的挪进ready,call之后回到pending;evict用取消标志停掉待就绪的服务。所有移除都是swap_remove,缓存的下标在evict之后可能指向另一个服务。 - P2C 负载均衡:
Balance::poll_ready先读Discover,再推进pending,然后随机取两个不同的就绪服务比负载,把选中的下标存下来,call时用它。为什么随机取两个而不是取全局最小。 - Hedge:请求在途时间超过历史延迟的某个分位数,就再发一份,谁先回来用谁;它本身由几个更小的中间件拼成。
- 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 的源码里已经没有地方用到它。
💬 关联讨论
发起讨论加载中...