Rust in Everything
基于 tower 0.5.3

P2C 负载均衡

主线位置:几乎全在就绪主线上。Balance::poll_ready 读服务发现、推进待就绪的服务、挑出下一个请求的去处;call 只按挑好的下标转发,返回内层的 future,请求主线上没有自己的包装(只把错误转成 BoxError)。

flowchart TB
  pr["poll_ready"] --> disc[读 Discover 的增删]
  disc --> promote[推进 pending]
  promote --> cached{ready_index 有值?}
  cached -->|有| recheck[复查这个下标]
  recheck -->|仍就绪| ok["Ready(Ok)"]
  recheck -->|不就绪或失败| pick
  cached -->|没有| pick[P2C 选一个下标]
  pick -->|ready 为空| pend[Pending]
  pick -->|选到| recheck
  callreq["call(req)"] --> take[取走 ready_index]
  take --> fwd[调用,服务回 pending]

Balance 自己不管服务从哪来,也不管负载怎么算,它把三样东西拼在一起:

  • 服务集合的增删来自 Discover;
  • 每个服务的负载来自 Load;
  • 哪些服务已经就绪,由 ReadyCache 维护。

它自己只做一件事:从就绪的服务里随机取两个,把请求交给负载小的那个。这就是 Power of Two Random Choices(P2C)。

💡TIP
打个比方:超市结账只看两条队

超市有很多收银台,开着的才能排(ReadyCache 里的 ready 集合),每条队有多长写在牌子上(Load::load())。你不去把所有队都看一遍,只随手看两条(sample_floyd2 取两个下标),排短的那条(aload <= bload)。走过去之前再看一眼那台还开着没有(check_ready_index),关了就重新挑。

比方到这里为止:真实的 Balance 里,你一旦在某台结完账,这台收银台会被暂时撤出「开着」的名单(call_ready_index 把服务放回 pending),要重新确认就绪才回来,回来时排到名单末尾。

结构:四个字段

Balance:

pub struct Balance<D, Req>
where
    D: Discover,
    D::Key: Hash,
{
    discover: D,

    services: ReadyCache<D::Key, D::Service, Req>,
    ready_index: Option<usize>,

    rng: Box<dyn Rng + Send + Sync>,

    _req: PhantomData<Req>,
}
  • discover:服务发现流。实现 Service 要求 D: Unpin,因为 poll_ready 只拿到 &mut self,要 Pin::new(&mut self.discover)。结构体文档建议不满足时先 Box::pin(issue #319)。
  • services:ReadyCache,键是 Discover 给的 key。
  • ready_index:上一次 poll_ready 选中的服务在 ready 集合里的下标。存的是下标,不是 key,这一点后面会出问题。
  • rng:随机数源,装箱成 trait 对象,所以 Balance 的类型里不带 RNG 的类型。

new 用 HasherRng::default(),from_rng 接受任何 Rng + Send + Sync + 'static,用来做可复现的测试。

Balance 没有实现 Clone。多个任务共用一个负载均衡器,通常在外面套一层 Buffer。

poll_ready:先更新,再挑选

poll_ready:

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        // `ready_index` may have already been set by a prior invocation. These
        // updates cannot disturb the order of existing ready services.
        let _ = self.update_pending_from_discover(cx)?;
        self.promote_pending_to_ready(cx);

        loop {
            // If a service has already been selected, ensure that it is ready.
            // This ensures that the underlying service is ready immediately
            // before a request is dispatched to it (i.e. in the same task
            // invocation). If, e.g., a failure detector has changed the state
            // of the service, it may be evicted from the ready set so that
            // another service can be selected.
            if let Some(index) = self.ready_index.take() {
                match self.services.check_ready_index(cx, index) {
                    Ok(true) => {
                        // The service remains ready.
                        self.ready_index = Some(index);
                        return Poll::Ready(Ok(()));
                    }
                    Ok(false) => {
                        // The service is no longer ready. Try to find a new one.
                        trace!("ready service became unavailable");
                    }
                    Err(Failed(_, error)) => {
                        // The ready endpoint failed, so log the error and try
                        // to find a new one.
                        debug!(%error, "endpoint failed");
                    }
                }
            }

            // Select a new service by comparing two at random and using the
            // lesser-loaded service.
            self.ready_index = self.p2c_ready_index();
            if self.ready_index.is_none() {
                debug_assert_eq!(self.services.ready_len(), 0);
                // We have previously registered interest in updates from
                // discover and pending services.
                return Poll::Pending;
            }
        }
    }

顺序是固定的四步。

第一步,读服务发现。 update_pending_from_discover 把 Discover 里攒下的变化一次读完:

        loop {
            match ready!(Pin::new(&mut self.discover).poll_discover(cx))
                .transpose()
                .map_err(|e| error::Discover(e.into()))?
            {
                None => return Poll::Ready(None),
                Some(Change::Remove(key)) => {
                    trace!("remove");
                    self.services.evict(&key);
                }
                Some(Change::Insert(key, svc)) => {
                    trace!("insert");
                    // If this service already existed in the set, it will be
                    // replaced as the new one becomes ready.
                    self.services.push(key, svc);
                }
            }
        }

Insert 进 pending 集合,Remove 调 evict。Discover 返回 Pending 时 ready! 退出,同时登记了唤醒。三种结果在 poll_ready 里的处理:

update_pending_from_discover 返回poll_ready 怎么办
Pending(暂时没有新变化)忽略,继续往下
Ready(None)(发现流结束了)忽略,继续用已有的服务
Ready(Some(Err(_)))? 把错误包成 error::Discover 返回,显示为 load balancer discovery error: ...

let _ = ...? 这一行就是这张表:? 只拦错误,剩下的 Poll<Option<()>> 直接丢掉。

第二步,推进 pending。 promote_pending_to_ready 反复调 poll_pending,就绪的挪进 ready 集合。某个服务的 poll_ready 报错时,ReadyCache 已经把它删掉,Balance 只记一条 debug! 日志就继续。单个后端的错误不会传给调用方,调用方能看到的错误只有服务发现的错误。

第三步,复查已选的下标。 如果上一次 poll_ready 已经选好了服务、但调用方还没 call,这次先用 check_ready_index 再问一次那个服务的 poll_ready。注释给的理由:让「确认就绪」和「发请求」发生在同一次任务执行里,中间状态变了(比如连接断了)就换一个。复查不通过的服务被挪回 pending(Pending)或删掉(报错),然后落到第四步。

第四步,P2C 选一个。 p2c_ready_index 按 ready 集合的大小分三种情况:

    fn p2c_ready_index(&mut self) -> Option<usize> {
        match self.services.ready_len() {
            0 => None,
            1 => Some(0),
            len => {
                // Get two distinct random indexes (in a random order) and
                // compare the loads of the service at each index.
                let [aidx, bidx] = sample_floyd2(&mut self.rng, len as u64);
                debug_assert_ne!(aidx, bidx, "random indices must be distinct");

                let aload = self.ready_index_load(aidx as usize);
                let bload = self.ready_index_load(bidx as usize);
                let chosen = if aload <= bload { aidx } else { bidx };
                // ...
                Some(chosen as usize)
            }
        }
    }
就绪服务数结果
0None,poll_ready 返回 Pending
1直接选下标 0,不看负载
2 个及以上随机取两个不同的下标,负载小的胜出;相等时选 aidx

选出来的下标不直接返回,而是回到循环开头再走一遍第三步,所以刚选中的服务也会被 poll_ready 一次才算数。这一步失败就再选,直到选中一个就绪的,或者 ready 集合空了。

Pending 之前不需要再登记什么:第一步里 Discover 返回 Pending 时登记过,第二步里 poll_pending 返回 Pending 时每个待就绪的服务都登记过。有新服务加入或某个服务就绪,任务都会被唤醒。

📝NOTE
没有蠢问题

问: 所有后端都没了(发现流是空的,或者全都 poll_ready 报错被删了),ready().await 会返回错误吗?

答: 不会,它一直 Pending。本页实验里一个空的 ServiceList 等 1 秒还是没有返回(输出见文末)。如果发现流也已经结束,任务连被唤醒的机会都没有。想要「没有后端就失败」,Timeout 帮不上忙,它只从 call 开始计时;可以在 ready() 外面套 tokio::time::timeout,或者在 Balance 外面套 LoadShed,让它在不就绪时直接返回 Overloaded。

随机取两个:sample_floyd2 与 HasherRng

sample_floyd2 是 Floyd 组合抽样在「取 2 个」时的特化版:

pub(crate) fn sample_floyd2<R: Rng>(rng: &mut R, length: u64) -> [u64; 2] {
    debug_assert!(2 <= length);
    let aidx = rng.next_range(0..length - 1);
    let bidx = rng.next_range(0..length);
    let aidx = if aidx == bidx { length - 1 } else { aidx };
    [aidx, bidx]
}

aidx 从 [0, length-1) 里取,bidx 从 [0, length) 里取;撞上了就把 aidx 换成 length - 1,而 aidx 原本取不到 length - 1,所以两个下标一定不同。总共两次随机数,不分配内存。一个推论在后面的复现里会用到:如果 RNG 永远返回 0,sample_floyd2(len) 永远是 [len - 1, 0]。

随机数来自 Rng trait,只有一个必须实现的方法 next_u64。next_range 是 (n % range) + start,直接取模。默认实现 HasherRng 用标准库的哈希器当随机源:

impl<H> Rng for HasherRng<H>
where
    H: BuildHasher,
{
    fn next_u64(&mut self) -> u64 {
        let mut hasher = self.hasher.build_hasher();
        hasher.write_u64(self.counter);
        self.counter = self.counter.wrapping_add(1);
        hasher.finish()
    }
}

默认的 H 是 RandomState,它的哈希密钥是随机的;每次把递增的计数器 counter 哈希一下,就得到一串看起来随机的数。负载均衡只需要「别总是挑同一对」,不需要密码学强度,这样就够了,也不用依赖 rand。

📝NOTE
冷知识:P2C 的随机抽样换过两次

早先 Balance 用 rand crate 的 SmallRng,from_rng 返回 Result<Self, rand::Error>。PR #686 写了只依赖标准库的 Rng trait 和 HasherRng,理由是 tower 的中间件「don't need the complexity and vast utilities of the rand crate」,from_rng 也从此不再返回 Result。那时取两个下标用的是 sample_inplace:先分配一个装着 0..length 全部下标的 Vec,洗前两个再截断,每次挑选都是 O(length) 的分配。

不到半年后 PR #716 把它换成了现在的 sample_floyd2。PR 说每次抽样「only saves 40ns (70ns down to 30ns - probably more if there are other threads causing allocator contention)」,但「I still think it makes sense to not allocate here」。两个改动都在 tower 0.5.0 发布。

为什么是两个,而不是全局最小

模块文档引了 Finagle 的说法(p2c/mod.rs):随机取两个、选负载小的,重复这样做,任意两台服务器之间的最大负载差的上界是 ln(ln(n))。

从代码上看还有两点:

  • 代价固定。 每次挑选读两次负载、生成两个随机数,和后端数量无关。全局最小要把每个就绪服务的 load() 都读一遍。
  • 负载最大的那个永远不会被选中(至少两个就绪、它的负载严格最大时)。它和谁比都输。反过来,负载最小的也不会独占:只有它被抽中时才会赢。负载数字过时的时候(比如所有客户端看到的都是同一份旧数据),全局最小会让请求全部涌向同一个后端,P2C 只是把请求往轻的那边偏。

下面的程序用 Constant 给三个总是就绪的服务固定负载 0、1、10。负载永远不变,正好模拟「负载数据一直是旧的」。需要 tower = { version = "=0.5.3", features = ["balance"] },以及 tokio(macros、rt、time):

use std::convert::Infallible;
use std::future::poll_fn;
use std::task::Poll;
use tower::balance::p2c::Balance;
use tower::discover::ServiceList;
use tower::load::Constant;
use tower::{Service, ServiceExt, service_fn};

type Svc = Constant<tower::util::BoxService<(), usize, Infallible>, u32>;

fn endpoint(id: usize, load: u32) -> Svc {
  let svc = service_fn(move |_: ()| async move { Ok::<_, Infallible>(id) });
  Constant::new(tower::util::BoxService::new(svc), load)
}

async fn poll_once(b: &mut Balance<ServiceList<Vec<Svc>>, ()>) -> &'static str {
  poll_fn(|cx| {
    Poll::Ready(match b.poll_ready(cx) {
      Poll::Ready(Ok(())) => "Ready",
      Poll::Ready(Err(_)) => "Err",
      Poll::Pending => "Pending",
    })
  })
  .await
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  // 0 services: the discover stream ends immediately.
  let mut empty = Balance::new(ServiceList::new(Vec::<Svc>::new()));
  println!("0 services: poll_ready -> {}, len = {}", poll_once(&mut empty).await, empty.len());
  let waited = tokio::time::timeout(std::time::Duration::from_secs(1), empty.ready()).await;
  println!("0 services: ready() after 1s -> {}", if waited.is_err() { "timed out" } else { "returned" });

  // 3 always-ready services with constant loads 0, 1, 10.
  let mut b = Balance::new(ServiceList::new(vec![
    endpoint(0, 0),
    endpoint(1, 1),
    endpoint(2, 10),
  ]));
  let mut hits = [0u32; 3];
  for _ in 0..9000 {
    let id = b.ready().await.unwrap().call(()).await.unwrap();
    hits[id] += 1;
  }
  println!("loads [0, 1, 10], 9000 requests -> hits {:?}", hits);
}
❗IMPORTANT
动脑筋:9000 个请求,负载为 10 的服务 2 能分到几个?服务 0 和服务 1 大约各分到多少?全局最小的话答案是 9000、0、0。P2C 呢?完整输出在文末。

call:按下标转发

call:

    fn call(&mut self, request: Req) -> Self::Future {
        let index = self.ready_index.take().expect("called before ready");
        self.services
            .call_ready_index(index, request)
            .map_err(Into::into)
    }

没 poll_ready 就 call,ready_index 是 None,panic 信息是 called before ready。

call_ready_index 用 swap_remove_index 把服务从 ready 集合里取出来,调用它,再放回 pending。所以每发一个请求,被选中的服务都要重新等一次就绪,下一次 poll_ready 的第二步再把它挪回 ready 集合,挪回时排在末尾。ready 集合的顺序在一次次请求之间不断变化,这对 P2C 没有影响(它本来就随机取),但对任何「记住一个下标」的做法都是隐患。

take() 保证一个下标最多被用一次。Future 类型是内层 future 加一个 MapErr,Balance 在请求主线上不做别的事。

0.5.3 的已知问题:删除节点后 ready_index 指向别的服务

poll_ready 开头那段注释说:「These updates cannot disturb the order of existing ready services.」这句话不对。第一步里的 Remove 调用 evict,它从 ready 集合删除用的是 swap_remove_full:把最后一个元素挪到被删的位置。ReadyCache 自己的文档其实警告过(见 ReadyCache):poll_pending 和 evict 会打乱 ready 集合的顺序,调用之后缓存的下标都应该丢掉。Balance 在 evict 之后没有丢掉 ready_index。

出事的时间窗口是:poll_ready 返回 Ready,ready_index 记下了下标 i;调用方还没 call,又调了一次 poll_ready(契约允许这么做,见 Service trait);这次 poll_ready 恰好从 Discover 读到一个 Remove。删掉的是谁,决定了后果:

被删的服务evict 之后下标 i 上是谁结果
就是选中的那个,且不在最后原来排最后的服务复查通过,请求发给它,绕过了 P2C
就是选中的那个,且排最后越界check_ready_index 返回 Ok(false),重新选,无害
别的服务,选中的排最后越界(选中的被挪走了)重新选,无害
别的服务,选中的不在最后还是选中的那个无影响

只有第一行出错。请求不会发给已经删除的后端(复查时问的是新挪来的那个服务),但这个服务没经过负载比较,可能正是负载最高的那个。

这个问题是 2026 年 3 月报告的 issue #856,原文的总结是:「This does not send traffic to a removed endpoint, but it can silently bypass the balancer's intended P2C choice during endpoint churn.」

在 0.5.3 上复现

用一个手动喂数据的 Discover,和一个永远返回 0 的 RNG(于是 sample_floyd2(len) 永远是 [len - 1, 0]),每次 poll_ready 只喂一条变化,ready 集合的顺序就是确定的。依赖 tower = { version = "=0.5.3", features = ["balance"] }、futures、tokio(macros、rt):

// Reproduces tower issue #856 on tower 0.5.3: a Discover removal after
// poll_ready picked a service makes the cached ready_index point elsewhere.
use std::cell::RefCell;
use std::collections::VecDeque;
use std::convert::Infallible;
use std::future::{Ready, ready};
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll};
use futures::Stream;
use tower::balance::p2c::Balance;
use tower::discover::Change;
use tower::load::Constant;
use tower::util::rng::Rng;
use tower::Service;

// Always ready; answers with its own id.
struct IdSvc(char);
impl Service<()> for IdSvc {
  type Response = char;
  type Error = Infallible;
  type Future = Ready<Result<char, Infallible>>;
  fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Infallible>> {
    Poll::Ready(Ok(()))
  }
  fn call(&mut self, _: ()) -> Self::Future {
    ready(Ok(self.0))
  }
}

type Endpoint = Constant<IdSvc, u32>;
type Queue = Rc<RefCell<VecDeque<Change<char, Endpoint>>>>;

// A Discover we feed by hand, one change per poll_ready.
struct Disco(Queue);
impl Stream for Disco {
  type Item = Result<Change<char, Endpoint>, Infallible>;
  fn poll_next(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
    match self.0.borrow_mut().pop_front() {
      Some(c) => Poll::Ready(Some(Ok(c))),
      None => Poll::Pending,
    }
  }
}

// Every draw is 0, so sample_floyd2(len) always returns [len - 1, 0].
struct ZeroRng;
impl Rng for ZeroRng {
  fn next_u64(&mut self) -> u64 {
    0
  }
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let q: Queue = Rc::new(RefCell::new(VecDeque::new()));
  let mut bal = Balance::from_rng(Disco(q.clone()), ZeroRng);
  let poll = |bal: &mut Balance<Disco, ()>| {
    let waker = futures::task::noop_waker();
    let mut cx = Context::from_waker(&waker);
    match bal.poll_ready(&mut cx) {
      Poll::Ready(Ok(())) => "Ready",
      Poll::Ready(Err(_)) => "Err",
      Poll::Pending => "Pending",
    }
  };

  for (id, load) in [('a', 0), ('b', 1), ('c', 10)] {
    q.borrow_mut().push_back(Change::Insert(id, Constant::new(IdSvc(id), load)));
    println!("insert {id} (load {load:>2}) -> poll_ready {}", poll(&mut bal));
  }
  // `a` was picked by the first poll_ready and is cached as ready_index = 0.
  q.borrow_mut().push_back(Change::Remove('a'));
  println!("remove a          -> poll_ready {}", poll(&mut bal));
  let got = bal.call(()).await.unwrap();
  println!("request went to {got}");

  // What a fresh P2C over the same ready set picks: poll again, call again.
  println!("next poll_ready {}", poll(&mut bal));
  let next = bal.call(()).await.unwrap();
  println!("next request went to {next}");
}

M2 Max、rustc 1.99.0、--release,tower 0.5.3,跑 3 次输出相同:

insert a (load  0) -> poll_ready Ready
insert b (load  1) -> poll_ready Ready
insert c (load 10) -> poll_ready Ready
remove a          -> poll_ready Ready
request went to c
next poll_ready Ready
next request went to b
  • 第一次 poll_ready 时 ready 集合只有 a,选下标 0。后两次各插入一个服务,ready 集合变成 [a, b, c],下标 0 复查通过,ready_index 一直是 0。
  • Remove('a'):swap_remove 把 c 挪到下标 0,集合变成 [c, b]。复查下标 0,问的是 c,它总是就绪,于是 Ready。请求发给了负载为 10 的 c。
  • 对照:下一次是一次新的 P2C。c 被调用后回到 pending 又被挪回来,集合是 [b, c],ZeroRng 让 sample_floyd2(2) 返回 [1, 0],比较 c(10)和 b(1),选 b。在同样的服务集合上,P2C 自己会选 b。

修复:df06d70,不在 0.5.3 里

修复是 PR #874,2026-06-22 合并进 master,提交 df06d70。它在 tag tower-0.5.3 之后,0.5.3 不包含这个修复;写这一页时还没有包含它的 tower 版本发布。service.rs 的改动只有两处(测试部分略):

                 Some(Change::Remove(key)) => {
                     trace!("remove");
                     self.services.evict(&key);
+                    // `evict` removes from the ready set with `swap_remove`,
+                    // which can move a different endpoint into the slot a
+                    // previously-selected `ready_index` points at. Discard the
+                    // cached selection so `poll_ready` re-runs P2C over the
+                    // current ready set instead of dispatching to the
+                    // swapped-in endpoint.
+                    self.ready_index = None;
                 }
     fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
-        // `ready_index` may have already been set by a prior invocation. These
-        // updates cannot disturb the order of existing ready services.
+        // `ready_index` may have already been set by a prior invocation. If a
+        // discovered removal evicts a service, `update_pending_from_discover`
+        // clears it, since eviction can reorder the ready set (via
+        // `swap_remove`) and leave the cached index pointing at a different
+        // endpoint.

PR 解释了为什么只清这一处就够:poll_pending 往 ready 集合里只做 insert(新 key 追加到末尾,已有的 key 原地更新),已有元素的下标不变;Insert 只进 pending;check_ready_index 和 call_ready_index 也会 swap_remove,但它们发生时 ready_index 已经被 take() 掉了。issue 里还提了一个更稳的改法:缓存 key 而不是下标。PR 没有这么做,认为改动更大,修这个 bug 用不着。

同一个程序把依赖换成本地 master(df06d70)上的 tower,输出变成:

insert a (load  0) -> poll_ready Ready
insert b (load  1) -> poll_ready Ready
insert c (load 10) -> poll_ready Ready
remove a          -> poll_ready Ready
request went to b
next poll_ready Ready
next request went to b

Remove('a') 之后 ready_index 被清掉,poll_ready 在 [c, b] 上重新做 P2C,选了 b。

在 0.5.3 上,想避开这个问题,就不要在 poll_ready 返回 Ready 之后、call 之前再去 poll_ready:svc.ready().await?.call(req) 这种「就绪后立刻调用」的写法不会碰到它。

造负载均衡器的工厂:MakeBalance

p2c 模块还导出 MakeBalance 和 MakeBalanceLayer。它们包的内层服务接收一个「目标描述」,返回一个 Discover;MakeBalance 把这个 Discover 包进 Balance::new 返回。按目标造服务正是 MakeService 的模式,见 Steer 与 MakeService。balance feature 依赖 make 就是因为它。

动脑筋答案

M2 Max、rustc 1.99.0、--release,跑 3 次:

0 services: poll_ready -> Pending, len = 0
0 services: ready() after 1s -> timed out
loads [0, 1, 10], 9000 requests -> hits [5969, 3031, 0]
0 services: poll_ready -> Pending, len = 0
0 services: ready() after 1s -> timed out
loads [0, 1, 10], 9000 requests -> hits [5984, 3016, 0]
0 services: poll_ready -> Pending, len = 0
0 services: ready() after 1s -> timed out
loads [0, 1, 10], 9000 requests -> hits [5968, 3032, 0]

HasherRng 的密钥每次运行不同,具体数字每次都变,但比例稳定:

  • 服务 2:0 个。 三个服务里任取两个,有三种组合:{0, 1}、{0, 2}、{1, 2}。服务 2 出现在两种里,每次都输。
  • 服务 0:约 2/3。 它出现在 {0, 1}、{0, 2} 里,每次都赢,9000 × 2/3 = 6000。
  • 服务 1:约 1/3。 只在 {1, 2} 里赢,约 3000。
  • 前两行:空的 Balance 第一次 poll_ready 是 Pending,ready() 等 1 秒也没有返回。

一般地,n 个负载各不相同的服务里,负载第 r 小的被选中的概率是 2(n−r) / (n(n−1))。真实的 Load 实现里,被选中的服务负载会上升,下一次就不那么容易赢了。

小结

  • Balance = Discover + ReadyCache + ready_index + Box<dyn Rng>,不实现 Clone。
  • poll_ready 四步:读 Discover(Insert 进 pending、Remove 调 evict)、推进 pending、复查已选下标、P2C 选新的;选中的也要先复查一次。
  • 就绪服务 0 个时 Pending(没有错误,可能永远等下去),1 个时直接选,2 个以上用 sample_floyd2 取两个不同下标,负载小的胜,相等选第一个。
  • 单个后端报错只会被删除并记日志,调用方只看得到服务发现的错误。
  • call 按下标转发,服务回到 pending,ready_index 用一次就清空。
  • 0.5.3 的 bug(#856):evict 用 swap_remove,Remove 之后缓存的 ready_index 可能指向原来排最后的服务。修复 df06d70(#874)在 tag 之后,不在 0.5.3 里。

💬 关联讨论

发起讨论
加载中...