收敛
收敛步骤,即“所有 worker 都已注册,所有回调都会触发”,由通用的 Coordinator<Req, Resp, Callback> 模板实现,TopologyCoordinator 和 BarrierCoordinator 都继承自该模板。该模板拥有一个小型状态机、一个待处理回调向量、一个 absl::Notification,以及一个周期性 alarm;在 rendezvous 进行期间,它会发出可读的状态信息。
状态机
| 状态 | 含义 |
|---|---|
0 | 初始状态:尚未收到请求,也未调度 alarm |
1 | 就绪状态:至少收到一个请求,周期性状态报告已启用 |
2 | 已完成:IsComplete() 返回 true;cached_response_ 保存成功响应;每个待处理回调都已被调用;后续请求会使用缓存 |
3 | 错误:已设置致命条件(例如 slice_id 越界);cached_response_ 保存粘性错误;所有过去和未来的回调都返回同一个错误 |
状态字节位于每个 Coordinator 实例的 +0x58。状态转换如下:
state=0
│ first AddRequest
▼
state=1 ──ScheduleStatusReport──┐
│ │
│ AddRequest │
│ │
│ IsComplete()=true │
▼ │
state=2 ◄──────────────────────┘ CancelAlarm
│
│ any AddRequest → serve cached_response_
▼
state=2 (stable)
state=0 or 1 + fatal request (out-of-range slice_id, etc.)
│
▼
state=3 → every callback serves the sticky StatusOr
```text
## 待处理回调向量
`Coordinator<>` 的 `+0x88` 字段是一个 `std::vector<AnyInvocable<void(StatusOr<Resp> const&)>>`(每个条目 32 字节:slow-path helper 和 inline push 都以 `0x20` 为步长;条目保存 `absl::AnyInvocable` 的 invoker 指针、manager 指针,以及类型擦除的状态缓冲区)。worker 会异步到达;当 coordinator 仍处于状态 0/1 时,每个 handler 都会把自己的 `cb` 推入此向量,然后立即返回,不通过 gRPC 发送响应。由于 handler 没有调用其 `finish` 回调,gRPC 框架会在服务端保持请求打开。
一旦 `IsComplete()` 返回 true(或状态转换到 3),coordinator 就会遍历该向量,并用同一个 `StatusOr<Resp>` 调用每个回调。每次调用都会触发 gRPC 框架序列化响应,并完成对应的客户端 RPC。
该向量按倍增方式增长(标准 libc++ 行为);`0x1ccb4ac0` 处的 `emplace_back_slow_path` 符号是容量不足时的 helper。
## Notification
字段 `+0xa0` 是一个 32 字节的 `absl::Notification`。`Coordinator<>::AddRequest` 中的完成广播(例如 Barrier 实例中的 `0x1ccb4638`)会在所有待处理回调都已服务后调用 `Notification::Notify()`。
Notification 供**同步的进程内调用方**使用,这些调用方直接与 coordinator 通信,而不经过 gRPC。具体来说:当 `CommunicationBackend::DiscoverTopologyAndAddressBindings` 在 coordinator 进程自身上被调用时,runtime 会走一条快速路径,同步调用 `topology_coordinator_->AddRequest(...)`(无 gRPC),然后等待响应。任何其他需要阻塞直到 rendezvous 完成的进程内消费者,也可以等待同一个 Notification。
对于 gRPC 客户端,Notification 并不相关;`callbacks_` 中的回调扇入才是信号机制。
## `ScheduleStatusReport`
当 coordinator 从状态 0 转换到 1 时,它会通过 fiber executor 启用一个周期性 alarm。在 Barrier `AddRequest`(`0x1ccb42a0`)中,状态字节在 `0x1ccb4654` 被设置(`movb $0x1,0x58(%r14)`),executor 在 `0x1ccb4677` 被取得(`call thread::DefaultFiberExecutor`),deadline 在 `0x1ccb4696` 被计算(`call absl::Duration::operator+=`),alarm 在 `0x1ccb46c9` 被启用(`call thread::AddCancellableAt`):
```cpp
if (state_ == 0) { // 0x1ccb4654
state_ = 1;
thread::Executor* exec = thread::DefaultFiberExecutor(); // 0x1ccb4677
absl::Time deadline = absl::Now();
deadline += /* ~1s */; // operator+= @0x1ccb4696
thread::AddCancellableAt(exec, deadline, // 0x1ccb46c9
[this] { this->ScheduleStatusReport(); },
&alarm_handle_ /* this+0xb0 */);
}启用步骤受 IsComplete() 控制(即它之前的 (*vtable+40)(this) 调用):只有 rendezvous 尚未完成时才会调度 alarm。
ScheduleStatusReport lambda 的主体(Barrier 实例化中的 Coordinator<...>::ScheduleStatusReport()::{lambda()#1}::operator(),位于 0x1ccb4ea0)首先通过 vtable slot +0x38((*vtable+56)(this))调用派生类的 ReportStatus(),然后重新检查 IsComplete()(vtable slot +0x28,(*vtable+40)(this));如果 rendezvous 仍未完成,它会通过同一条 thread::AddCancellableAt 路径重新启用自身,并把 alarm handle 重新指向 this+0xb0。
重新启用的间隔是固定常量:每次重新启用都使用初次启用时在 absl::Duration::operator+= 中看到的同一个增量(约 1 秒);没有指数退避。循环会在 IsComplete() 第一次返回 true 时终止,此时不会再调度新的 alarm。
TopologyCoordinator::ReportStatus
在 0x213b7ba0(位于 .text.unlikely)反编译。主体如下:
void TopologyCoordinator::ReportStatus() const {
if (this->IsComplete()) {
LOG(INFO) << "MegaScale Topology Discovery completed."; // 0xa0a3869
return;
}
MissingHostsList missing = GetMissingHosts();
LOG(INFO) << "MegaScale Topology Discovery in progress. "
"Missing hosts (num_slices=" << missing.num_slices
<< ", num_hosts=" << missing.num_hosts
<< "): " << missing.text;
}
```text
`0x1cf51340` 处的 `GetMissingHosts()` 会遍历 `slice_state_`,并针对 `[0, num_slices_)` 中的每个 slice id `s` 返回 host id 列表:这些 host 要么从未出现过(map 中缺失该 slice),要么尚未达到 quorum。该列表通过 `absl::StrCat` 格式化。
## `BarrierCoordinator::ReportStatus`
在 `0x213b7ce0` 反编译。形状相同,但使用 `GetSeenHosts()` 生成参与者列表:
```cpp
void BarrierCoordinator::ReportStatus() const {
if (this->IsComplete()) {
LOG(INFO) << "MegaScale Barrier completed."; // 0xa0a3986
return;
}
size_t seen = *(uint64_t*)(this+0xe0) >> 17; // SwissMap size: field>>17
int num_workers = this->num_workers_; // +0xd0 (32-bit)
std::string seen_hosts = GetSeenHosts(); // 0x1cf55280
LOG(INFO) << "MegaScale Barrier in progress. Seen " << seen
<< " of " << num_workers
<< " expected participants. Seen hosts: " << seen_hosts;
}seen 计数不是方法调用:在 0x213b7d5b,代码执行 mov 0xe0(%rbx),%rax,随后在 0x213b7d62 执行 shr $0x11,%rax,也就是将 +0xe0 处的原始 SwissMap header 字段右移 17 位;这与 IsComplete 使用的 size>>17 习惯用法相同。num_workers_ 在 0x213b7d8d 作为 32 位值加载(mov 0xd0(%rbx),%eax)。这两个值分别通过 LogMessage::operator<<<unsigned long> 和 LogMessage::operator<<<int> 流式输出。
为什么收敛是单次的
state=2 之后不会重新启用。coordinator 的缓存响应只设置一次,并服务所有后续注册。含义是:如果某个 worker 在 bootstrap 后重启,其 GetMultiSliceTopology 重新注册会立即用缓存响应解析(也就是交付给其前任的同一个响应),不会强制 coordinator 等待任何其他 worker。
这正是静默 worker 重启能够与 rendezvous 兼容的原因:只要重启后的 worker 重新呈现同一个 (slice_id, host_id)、等价的 topology_args,并且同一个 incarnation_id,ProcessRequest(它在 AddRequest 中的 state-2 缓存服务路径之前运行)就会接受该请求,缓存也保持有效。任何一项发生漂移的重启,尤其是新的 incarnation_id,都会被 ProcessRequest 以 INVALID_ARGUMENT 拒绝,而不是服务缓存(见 故障处理)。worker 侧的 LogUniqueIds "Created communicator." 行是本地 (slice_id, host_id, incarnation_id) 三元组发生变化的取证记录。
交叉引用
- Bootstrap › 概览 — 该收敛步骤所结束的完整 rendezvous 生命周期
- Worker 注册 — 其重复到达驱动状态机的请求
- 故障处理 — 到达的 worker 在身份三元组上发生漂移时会发生什么
- 跨主机 Barrier — 具有相同收敛 / 状态 alarm 机制的同级
Coordinator<>特化 - ICI › 拓扑发现 — rendezvous 所联合的、提供
TpuTopologyArgsProto的 slice 内层级