Day 12 / 共 20 天 · 第 3 周 xDS 与上游
配置订阅机制
Day 11 讲了 xDS 协议,今天看它怎么实现:Subscription/SubscriptionCallbacks 接口、GrpcMux 多路复用(ADS 复用一条 gRPC 流)、以及"订阅→下发→应用→ACK"的热更新闭环。
📍 你在整门课的位置 · 第 3 周「xDS 与上游」(承接 D11 xDS 协议,今天看它怎么落地成代码)
D11 xDS 总览→
D12 配置订阅→
D13 集群管理→
D14 负载均衡→
D15 健康检查/发现
L01
两个核心接口
envoy/config/subscription.h 定义订阅的两面:
// 订阅者视角(subscription.h:212)—— 每个 xDS(如 CDS)持一个
class Subscription {
virtual void start(const absl::flat_hash_set<std::string>& resource_names) PURE; // :221
virtual void updateResourceInterest(...) PURE; // 改变关注的资源
};
// 回调视角(subscription.h:97)—— 配置到达时被回调
class SubscriptionCallbacks {
virtual absl::Status onConfigUpdate(const std::vector<DecodedResourceRef>& resources,
const std::string& version_info) PURE; // :110 全量
virtual absl::Status onConfigUpdate(added, removed, version) PURE; // :122 增量
virtual void onConfigUpdateFailed(reason, exception) PURE; // :133
};
读法:
Subscription = "我要订阅什么"(start/更新关注);SubscriptionCallbacks = "配置来了怎么处理"(onConfigUpdate)。每种 xDS(CDS/LDS/EDS…)都实现这两个接口。全量(SotW)和增量(Delta)两种 onConfigUpdate。L02
SubscriptionBase 模板
为复用,source/common/config/subscription_base.h:11 提供模板 SubscriptionBase<Current>,自动绑定资源类型(如 CDS 绑 Cluster)。
泛型模板省了什么?
CDS、LDS、EDS 的订阅逻辑 90% 一样,只是"处理哪种资源类型"不同(Cluster vs Listener vs Endpoint)。C++ 模板
SubscriptionBase<Cluster> 让 CDS 一行继承就自动获得"解码成 Cluster 类型"的通用逻辑,不用每个 xDS 重写一遍解码。这是泛型编程消除重复的典型——和你在 langgraph/eino 见过的"接口/实现分离"是同一目标(少写重复、易维护),只是这里用 C++ 模板实现。L03
订阅工厂分发
SubscriptionFactoryImpl::subscriptionFromConfigSource()(subscription_factory_impl.cc:27)按配置类型创建不同 Subscription:
// :64 kApiConfigSource 分支再按 ApiConfigSource 枚举细分:
// :78 AGGREGATED_GRPC / :80 AGGREGATED_DELTA_GRPC → 走 ADS(共享一条 gRPC 流)
// :87 REST → REST 轮询
// :90 GRPC / :93 DELTA_GRPC → 独立 gRPC 流
读法:工厂根据你配的"配置来源类型"(文件/REST/gRPC/ADS)选对应实现。生产几乎都用 ADS(AGGREGATED_GRPC)——所有 xDS 复用一条流(L06)。这是"工厂模式":调用方只说"我要订阅",工厂决定用哪种传输。
L04
GrpcSubscriptionImpl
// source/extensions/config_subscription/grpc/grpc_subscription_impl.h:20
class GrpcSubscriptionImpl : public Subscription, protected UntypedConfigUpdateCallbacks {
GrpcSubscriptionImpl(GrpcMuxSharedPtr grpc_mux, SubscriptionCallbacks& callbacks, ...); // :24
void start(const absl::flat_hash_set<std::string>& resource_names) override; // :31
};
读法:关键:它不直接管 gRPC 流,而是持有一个
GrpcMux(多路复用器)。start() 实际是向 GrpcMux addWatch(注册"我关注 type_url 的这些资源")。多个 xDS 订阅共享同一个 GrpcMux——这就是 ADS 的基础(L06)。🤔 痛点:LDS/CDS/RDS/EDS 各开一条连接,会乱套
接着 D11 的"连锁总部"比喻:如果总部用 4 个不同频道分别播报"监听/集群/路由/端点",门店可能先听到"新端点"、还没听到"新集群"——引用了不存在的集群,短暂报错。多条连接也更占资源、难保证顺序。
💡 本质:GrpcMux = 一根对讲机线路,复用多个"频道"
GrpcMux 就是"多路复用器":把 LDS/CDS/RDS/EDS 多类规章塞进同一条 gRPC 长连接,每类是一个 watch(频道)。收到下发就按 type_url 分发给对应订阅者。ADS 更进一步——总部在这一条线路上控制播报顺序(先集群后端点),门店永远不会遇到"引用了还没到的东西"。📝 举个例子:CDS 订阅怎么走 mux
CDS 调
addWatch("...Cluster", {"user-service"}, 回调) 注册一个 watch → GrpcMux 在同一条流上 sendDiscoveryRequest(首帧带 node)→ 收到 onDiscoveryResponse 后,按 type_url=Cluster 把配置只分发给 CDS 的回调,不会误发给 LDS。四类配置各是一个 watch,全部复用同一条 gRPC 流(ADS);GrpcMux 收发并按 type_url 分发。
L05
GrpcMux 多路复用
// 接口 envoy/config/grpc_mux.h:58
class GrpcMux {
virtual void start() PURE; // :65
virtual GrpcMuxWatchPtr addWatch(const std::string& type_url,
const absl::flat_hash_set<std::string>& resources,
SubscriptionCallbacks& callbacks, ...) PURE; // :104
};
// 实现 source/extensions/config_subscription/grpc/grpc_mux_impl.cc
// :160 start():建立 gRPC 双向流 establishNewStream()
// :169 sendDiscoveryRequest(type_url):填 resource_names,首帧带 node,sendMessage
// :357 onDiscoveryResponse():收下发→分发给 watch 回调→成功后发 ACK
// :564 onStreamEstablished():(重)连成功后重新发起所有 type_url 的 Request(断线重连恢复)
"多路复用器"是什么?
想象一根网线要同时传电话、电视、上网——多路复用(mux)就是"多种数据流共用一条物理通道"。GrpcMux 让 LDS/CDS/RDS/EDS 多种配置类型复用同一条 gRPC 连接:每种类型是一个 "watch",GrpcMux 统一管理这条流上的收发,按 type_url 把下发的配置分发给对应的订阅者。断线了,
onStreamEstablished 自动重连并重新订阅所有类型——这是生产可靠性的关键。L06
ADS:一条流的意义
ADS(Aggregated Discovery Service,聚合发现服务) = 所有 xDS 类型复用一条 gRPC 流。
为什么要"聚合成一条流"?
如果 LDS、CDS、RDS、EDS 各开一条独立连接,会有顺序问题:比如新集群(CDS)和它的端点(EDS)分别从两条流来,可能端点先到、集群还没到 → 短暂不一致甚至报错。ADS 把它们放一条流,管理服务器能控制下发顺序(先 CDS 后 EDS、先 LDS 后 RDS),保证配置更新时序一致、无中间态错误。这就是为什么生产环境几乎都用 ADS。
pausable_ack_queue.cc 的可暂停 ACK 队列就是为保证这种依赖顺序服务的。🚶 第一人称·你现在是一次配置热更新,跟着单步走查表走完闭环(以 CDS 新增一个集群为例):
| 步 | 此刻发生什么 | 关键状态 |
|---|---|---|
| 1 | 建流 establishNewStream() | gRPC 双向流就绪 |
| 2 | 发 Request:我要 Cluster 类资源 | type_url=Cluster,首帧带 node |
| 3 | 控制面推 onDiscoveryResponse | resources=[user-service],version=v7 |
| 4 | 解码成 DecodedResource,调 CDS 的 onConfigUpdate | ClusterManager 新增该集群 |
| 5 | 应用成功 → 回 ACK | Request 带 version=v7 + 相同 nonce |
| ✗ | 若第 4 步失败 → 回 NACK | 带 error_detail,保留旧配置继续跑 |
记忆口诀:建流→订阅→下发→应用→确认
五个字概括热更新闭环:「建、订、发、用、认」。全程进程不重启——这正是 D11 那句"改配置不停机"落到代码上的样子。断线了?
onStreamEstablished 自动重连并重发所有 type_url 的订阅,恢复如初。L07
热更新闭环
establishNewStream(建流)
→ sendDiscoveryRequest(订阅:我要这些资源)
→ Server 推 onDiscoveryResponse(下发配置)
→ 解码为 DecodedResource
→ 调各 xDS 的 onConfigUpdate(应用到运行时对象,如新增 Cluster)
→ 回 ACK(version_info + nonce)
# 全程无需重启进程!
读法:这就是"配置不重启热更新"的完整闭环(Day 11 的协议在这里落地为代码流程)。应用成功回 ACK、失败回 NACK 保留旧配置。后面几天看
onConfigUpdate 具体怎么"应用"——CDS 增删集群(Day 13)、端点变化(Day 15)。L08
今日小结 + 动手
🧠 今天你应该能回答
- Subscription 和 SubscriptionCallbacks 各是什么视角?
- SubscriptionBase 模板省了什么?
- 订阅工厂按什么分发?生产为什么用 ADS?
- GrpcMux 为什么是"多路复用器"?断线怎么恢复?
- 热更新闭环的完整步骤?
✋ 动手
cd /Users/bitmart/work/codes/github/higress-group/envoy
sed -n '97,135p' envoy/config/subscription.h
sed -n '58,110p' envoy/config/grpc_mux.h
grep -n 'sendDiscoveryRequest\|onDiscoveryResponse\|establishNewStream' source/extensions/config_subscription/grpc/grpc_mux_impl.cc | head
明天预告 · Day 13:Cluster Manager——集群怎么被管理?
ClusterManagerImpl、初始化状态机、线程本地集群状态(主线程算配置、Worker 无锁读副本,Envoy 高性能关键)、CDS 如何驱动增删改。