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 别名
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,) 当标签,作用相同。
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 负载均衡。
问: 发现流里只有 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报错。