Rust in Everything
基于 tower 0.5.3

Discover

主线位置:就绪主线的起点。Balance 在自己的 poll_ready 里先把 Discover 读到 Pending 为止,节点的增删就发生在「问能不能接请求」的时候;请求主线上没有它。

flowchart TB
  impl["实现方:写一个 Stream"] --> item["Item = Result Change E"]
  item --> blanket[blanket impl]
  blanket --> disc["自动成为 Discover"]
  disc --> bound["使用方:只写在约束里"]
  bound --> lb["Balance 在 poll_ready 里读"]
  lb -->|Insert| push["ReadyCache::push"]
  lb -->|Remove| evict["ReadyCache::evict"]

负载均衡器要回答的第一个问题是「现在有哪些后端」。后端会上线、下线、换地址,所以这不是一个列表,而是一串变化。tower 把这串变化表示成一个流,Discover 就是这个流的名字。

一个 trait 别名

Discover:

pub trait Discover: Sealed<Change<(), ()>> {
    /// A unique identifier for each active service.
    ///
    /// An identifier can be re-used once a [`Change::Remove`] has been yielded for its service.
    type Key: Eq;

    /// The type of [`Service`] yielded by this [`Discover`].
    ///
    /// [`Service`]: crate::Service
    type Service;

    /// Error produced during discovery
    type Error;

    /// Yields the next discovery change set.
    fn poll_discover(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Option<Result<Change<Self::Key, Self::Service>, Self::Error>>>;
}

poll_discover 的签名和 TryStream::try_poll_next 一模一样:Pending 表示暂时没有变化,Ready(Some(Ok(change))) 是一条变化,Ready(Some(Err(e))) 是发现过程本身出错,Ready(None) 是流结束。紧跟着的是一个 blanket impl:

impl<K, S, E, D: ?Sized> Discover for D
where
    D: TryStream<Ok = Change<K, S>, Error = E>,
    K: Eq,
{
    type Key = K;
    type Service = S;
    type Error = E;

    fn poll_discover(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Option<Result<D::Ok, D::Error>>> {
        TryStream::try_poll_next(self, cx)
    }
}

所以「是 Discover」和「是一个 Item = Result<Change<K, S>, E> 的 Stream」是同一件事(TryStream 本身对所有 Item = Result<T, E> 的 Stream 都有 blanket impl)。Discover 存在的意义只是在约束里少写几个类型参数:D: Discover 之后可以直接用 D::Key、D::Service、D::Error。

D: ?Sized 让 dyn Stream<...> 本身也算 Discover;Pin<Box<dyn Stream<...>>> 是 Stream,自然也算,而且它是 Unpin,后面会看到 Balance 需要这一点。

Change 只有两种:

#[derive(Debug, Clone)]
pub enum Change<K, V> {
    /// A new service identified by key `K` was identified.
    Insert(K, V),
    /// The service identified by key `K` disappeared.
    Remove(K),
}

模块文档给 key 定的规矩是:key 在当前活着的服务里唯一;Remove(k) 发出之后,k 可以再用。对已经存在的 key 再发一次 Insert,Balance 当作「换一个新实例」处理,旧实例要等新实例就绪后才被替换,细节在 ReadyCache。

sealed:只能用,不能实现

Discover 的父 trait Sealed 定义在 lib.rs 的私有模块里:

#[allow(unreachable_pub)]
#[cfg(any(feature = "balance", feature = "discover", feature = "make"))]
mod sealed {
    pub trait Sealed<T> {}
}

Sealed 是 pub 的,但所在的模块 sealed 不是,crate 外面写不出它的路径,也就没法为自己的类型实现它。实现 Sealed<Change<(), ()>> 的只有 第 76 行 那个同样以 TryStream<Ok = Change<K, S>> 为条件的 blanket impl。结果是:想成为 Discover,唯一的路是实现 Stream。

直接给自己的类型写 impl Discover 会怎样:

tower = { version = "=0.5.3", features = ["discover"] }
use std::{
  pin::Pin,
  task::{Context, Poll},
};
use tower::discover::{Change, Discover};

struct MyDiscover;

impl Discover for MyDiscover {
  type Key = u32;
  type Service = ();
  type Error = ();

  fn poll_discover(
    self: Pin<&mut Self>,
    _cx: &mut Context<'_>,
  ) -> Poll<Option<Result<Change<u32, ()>, ()>>> {
    Poll::Ready(None)
  }
}

fn main() {}

rustc 1.99.0 的报错(删掉了末尾「完整类型名已写入某个文件」的两行 note):

error[E0277]: the trait bound `MyDiscover: tower::sealed::Sealed<Change<(), ()>>` is not satisfied
  --> src/bin/sealed.rs:9:19
   |
 9 | impl Discover for MyDiscover {
   |                   ^^^^^^^^^^ unsatisfied trait bound
   |
help: the trait `futures_core::stream::Stream` is not implemented for `MyDiscover`
  --> src/bin/sealed.rs:7:1
   |
 7 | struct MyDiscover;
   | ^^^^^^^^^^^^^^^^^
   = help: the following other types implement trait `futures_core::stream::Stream`:
             &mut S
             AssertUnwindSafe<S>
             Box<S>
             CallAll<Svc, S>
             CallAllUnordered<Svc, S>
             Constant<D, M>
             PeakEwmaDiscover<D, C>
             PendingRequestsDiscover<D, C>
           and 84 others
   = note: required for `MyDiscover` to implement `futures_core::stream::TryStream`
   = note: required for `MyDiscover` to implement `tower::sealed::Sealed<Change<(), ()>>`
note: required by a bound in `tower::discover::Discover`
  --> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-0.5.3/src/discover/mod.rs:55:21
   |
55 | pub trait Discover: Sealed<Change<(), ()>> {
   |                     ^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Discover`
   = note: `Discover` is a "sealed trait", because to implement it you also need to implement `tower::sealed::Sealed`, which is not accessible; this is usually done to force you to use one of the provided types that already implement it
   = help: the following types implement the trait:
             D
             S
             M

For more information about this error, try `rustc --explain E0277`.
error: could not compile `exp-tower-bala` (bin "sealed") due to 1 previous error

报错的推导链正好就是上面两个 blanket impl:要 Sealed 就要 TryStream,要 TryStream 就要 Stream。help 里列出来的 Constant、PeakEwmaDiscover、PendingRequestsDiscover 是 Load 一页的三个包装,它们实现的也是 Stream。

为什么 Sealed 带一个类型参数 T?同一个 Sealed 还封住了 MakeService(Sealed<(Target, Request)>)和 MakeConnection(Sealed<(Target,)>),它们各自的 blanket impl 条件是「实现了 Service」。如果 Sealed 不带参数,「所有 TryStream 实现 Sealed」和「所有 Service 实现 Sealed」是同一个 trait 的两个 blanket impl,编译器无法排除一个类型两者都是,会报冲突。下面是一个去掉 tower 细节的最小复现,用 rustc --edition 2024 nogeneric.rs 编译:

trait Stream { type Item; }
trait Service<R> { type Response; }
trait Sealed {}
impl<D: Stream> Sealed for D {}
impl<S: Service<u8>> Sealed for S {}
fn main() {}
error[E0119]: conflicting implementations of trait `Sealed`
 --> nogeneric.rs:5:1
  |
4 | impl<D: Stream> Sealed for D {}
  | ---------------------------- first implementation here
5 | impl<S: Service<u8>> Sealed for S {}
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0119`.

把 Sealed 改成 Sealed<T>,两个 impl 分别写 Sealed<((), ())> 和 Sealed<(u8,)>,就是两个不同的 trait,能编译。tower 用 Change<(), ()>、(Target, Request)、(Target,) 当标签,作用相同。

📝NOTE
冷知识:0.3 的 Discover 是要自己实现的,而且流不能结束

PR #443(首次发布于 tower 0.4.0)之前,Discover 是一个普通 trait:使用者自己实现 poll_discover,返回 Poll<Result<Change<..>, ..>>,没有 Option,发现流没有「结束」这个状态;Key 要求 Hash + Eq;另有一个 discover::stream 模块里的 ServiceStream,把 Stream 适配成 Discover。PR 的说明是:「Discover was really just a TryStream<Item = Change>, so this change makes that much clearer. Specifically, users are intended to use Discover only in bounds, whereas implementors should implement Stream with the appropriate Item type.」于是有了上面的 blanket impl 和 Sealed,返回值多了一层 Option,Key 放宽到 Eq,ServiceStream 也就不需要了(「This obviates the need for the discover::stream module」)。lib.rs 里的 mod sealed 也是这个 PR 加的。

Key 只要 Eq,Balance 却要更多

Discover::Key 只要求 Eq,但 Balance 的定义要 D::Key: Hash,实现 Service 时还要 Clone,并且要求 D: Unpin。这些都不是 Discover 的要求,而是下游的:Balance 把服务放进 ReadyCache,ReadyCache 用 IndexMap 按 key 存,要 Hash;同一个 key 要同时放进「待就绪」和「取消句柄」两张表,要 Clone。Unpin 是因为 Balance 在 &mut self 的 poll_ready 里用 Pin::new(&mut self.discover) 去读它,Balance 的文档建议不满足时用 Box::pin 包一层。

ServiceList:固定列表

ServiceList 是最简单的实现,它是一个 Stream,不是直接实现 Discover:

    pub struct ServiceList<T>
    where
        T: IntoIterator,
    {
        inner: Enumerate<T::IntoIter>,
    }

poll_next 每次从迭代器取一个,配上 enumerate 的序号当 key,产出 Change::Insert(i, service);取完了返回 Ready(None)。它从不返回 Pending,也从不产出 Remove。Item 的错误类型是 Infallible。

new 带一个类型参数 Request 和约束 U: Service<Request>,但 Request 不出现在 ServiceList<T> 里。这个约束只是在构造时检查「列表里放的是服务」,大多数情况下编译器能推出 Request,不用写 turbofish。

动态发现:一个通道就够

既然任何 Stream 都行,最直接的动态发现就是一条通道:别的任务(DNS 轮询、注册中心的订阅)往发送端塞 Change,接收端交给 Balance。下面的程序先逐条读 ServiceList 和一个 futures 的 mpsc 接收端,再把另一个接收端交给 Balance,看节点数什么时候变。Constant 给每个服务一个固定负载,满足 Balance 对 Load 的要求,见 Load。

tower = { version = "=0.5.3", features = ["balance"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
futures = "0.3"
use std::{
  convert::Infallible,
  future::poll_fn,
  pin::Pin,
  task::{Context, Poll, Waker},
};
use futures::channel::mpsc;
use tower::{
  balance::p2c::Balance,
  discover::{Change, Discover, ServiceList},
  load::Constant,
  service_fn, Service,
};

fn show<D>(name: &str, d: Pin<&mut D>)
where
  D: Discover,
  D::Key: std::fmt::Debug,
{
  let mut cx = Context::from_waker(Waker::noop());
  let s = match d.poll_discover(&mut cx) {
    Poll::Pending => "Pending".to_string(),
    Poll::Ready(None) => "Ready(None)".to_string(),
    Poll::Ready(Some(Ok(Change::Insert(k, _)))) => format!("Insert({k:?})"),
    Poll::Ready(Some(Ok(Change::Remove(k)))) => format!("Remove({k:?})"),
    Poll::Ready(Some(Err(_))) => "Err".to_string(),
  };
  println!("{name}: {s}");
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  // 1. ServiceList: a fixed list, keys are positions
  let svc = service_fn(|x: u32| async move { Ok::<_, Infallible>(x) });
  let mut list = ServiceList::new(vec![svc, svc, svc]);
  for _ in 0..5 {
    show("list", Pin::new(&mut list));
  }

  // 2. any TryStream of Change is a Discover: an mpsc receiver
  let (tx, mut rx) = mpsc::unbounded::<Result<Change<&str, _>, Infallible>>();
  tx.unbounded_send(Ok(Change::Insert("a", svc))).unwrap();
  tx.unbounded_send(Ok(Change::Insert("b", svc))).unwrap();
  show("chan", Pin::new(&mut rx));
  show("chan", Pin::new(&mut rx));
  show("chan", Pin::new(&mut rx));
  tx.unbounded_send(Ok(Change::Remove("a"))).unwrap();
  show("chan", Pin::new(&mut rx));
  drop(tx);
  show("chan", Pin::new(&mut rx));

  // 3. Balance consumes a Discover inside poll_ready
  let (tx, rx) = mpsc::unbounded::<Result<Change<&str, _>, Infallible>>();
  let mut lb = Balance::new(rx);
  for k in ["a", "b", "c"] {
    tx.unbounded_send(Ok(Change::Insert(k, Constant::new(svc, 0u8)))).unwrap();
  }
  println!("lb before poll_ready: len = {}", lb.len());
  poll_fn(|cx| lb.poll_ready(cx)).await.unwrap();
  println!("lb after poll_ready:  len = {}", lb.len());
  tx.unbounded_send(Ok(Change::Remove("b"))).unwrap();
  println!("lb Remove(b) sent:    len = {}", lb.len());
  poll_fn(|cx| lb.poll_ready(cx)).await.unwrap();
  println!("lb after poll_ready:  len = {}", lb.len());
  println!("lb.call(7) = {:?}", lb.call(7).await);
}

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

list: Insert(0)
list: Insert(1)
list: Insert(2)
list: Ready(None)
list: Ready(None)
chan: Insert("a")
chan: Insert("b")
chan: Pending
chan: Remove("a")
chan: Ready(None)
lb before poll_ready: len = 0
lb after poll_ready:  len = 3
lb Remove(b) sent:    len = 3
lb after poll_ready:  len = 2
lb.call(7) = Ok(7)
  • ServiceList 的 key 是位置 0、1、2,取完以后一直是 Ready(None)(vec::IntoIter 取完后继续返回 None)。
  • 通道里没有消息、发送端还在时是 Pending;发送端全部 drop 后是 Ready(None)。这就是「暂时没有变化」和「发现结束」的区别。
  • Balance 不会自己去读通道。三条 Insert 发出去之后 len 还是 0,第一次 poll_ready 才把它们读进来;Remove(b) 也要等下一次 poll_ready 才生效。读 Discover 是 Balance::poll_ready 的第一步,它在 update_pending_from_discover 里一直读到 Pending,Insert 调 ReadyCache::push,Remove 调 ReadyCache::evict。

poll_ready 里读 Discover 的那一行是 let _ = self.update_pending_from_discover(cx)?;:发现流结束(None)被忽略,Balance 继续用手上已有的服务;发现流报错则经 ? 变成 poll_ready 的错误,按 Service trait 的规则,这个 Balance 就报废了。选节点的部分见 P2C 负载均衡。

📝NOTE
没有蠢问题

问: 发现流里只有 Insert 和 Remove,那后端「变慢了」「健康检查失败了」怎么告诉 Balance?

答: 不经过 Discover。Discover 只管集合里有谁。慢不慢由每个服务自己的 Load 报告;能不能接请求由每个服务自己的 poll_ready 报告,ReadyCache 在选中它之后还会再问一次,返回 Pending 就挪回待就绪集合,返回错误就把它丢掉。真要把一个节点踢掉,才发 Remove。

小结

  • Discover 是 TryStream<Ok = Change<K, S>> 的别名 trait,靠 blanket impl 自动实现;Change 只有 Insert(K, S) 和 Remove(K)。
  • sealed:Sealed 在私有模块里,用户写不了 impl Discover,只能实现 Stream;Sealed<T> 的类型参数让 Discover、MakeService、MakeConnection 的 blanket impl 互不冲突。
  • Key 只要 Eq;Balance 另外要 Hash + Clone 和 D: Unpin,来自 ReadyCache 和 poll_ready 的需要。
  • ServiceList 一次性产出 Insert(0..n) 然后结束;动态发现用一个通道的接收端就行。
  • Balance 只在 poll_ready 里读 Discover,变化在下一次 poll_ready 才生效;流结束被忽略,流出错让 Balance 报错。

💬 关联讨论

发起讨论
加载中...