233 lines
13 KiB
Markdown
233 lines
13 KiB
Markdown
# Control Plane gRPC API
|
||
|
||
## 1. 契约范围
|
||
|
||
Proto 源文件位于 `api/proto/controlplane/v1/controlplane.proto`,包含两项
|
||
内部服务:
|
||
|
||
- `WorkerControlPlane`:Worker 注册、Snapshot/Delta 分发、ACK、运行态与结果
|
||
批量上报。
|
||
- `CheckerControlPlane`:健康检查任务流和 Observation 批量上报。
|
||
|
||
该协议不承载 Client 的独占提取,也没有 extraction lease/release。Proxy 的
|
||
`AVAILABLE -> EXTRACTED` 只在 Controller 调用的 Redis 原子操作中完成。
|
||
|
||
## 当前实现状态
|
||
|
||
Controller 已实现并验证 `RegisterWorker`、`AcknowledgeSnapshot` 和
|
||
`ReportRuntime` 的一元 RPC。Register 创建带 Redis 服务端 TTL 的 session;ACK 只
|
||
接受 Controller 已签发的 `(version, ownership_epoch, checksum)`;Runtime 的空
|
||
`counters` 是完整稀疏替换,也续期 session。负向 ACK 会关闭该 session 的 Runtime
|
||
写入栅栏,直到收到新的正向 ACK,延迟的旧报告不能重新开启它。
|
||
|
||
生产配置使用 mTLS,并将叶子证书 SPIFFE URI 约束为
|
||
`spiffe://<trust-domain>/<environment>/worker/<worker-id>`;仅经配置校验的回环
|
||
监听允许明文 fixture 模式。单消息大小、并发流数和 gRPC keepalive 策略由
|
||
`controlPlane` 配置限定。
|
||
|
||
`WatchSnapshots` 已在 Register 后发送与当前 ownership epoch 对应的基础完整快照,
|
||
Gateway 校验后 ACK 并开始 Runtime 心跳。Controller 会从 Redis 的有界 Worker ownership
|
||
索引构建已归属 Proxy 内容,并将租约到期收紧到 Proxy 的 `usable_until`。Proxy 引用的
|
||
凭据材料按 `secret_ref + credential_version` 去重,随完整 Snapshot 经 mTLS 下发,仅保留在
|
||
Gateway 当前内存 View。完整 Snapshot
|
||
已从配置原始顺序和 Admin 当前状态合成 Gateway Routing,并与 Proxy 一起纳入 checksum;
|
||
Gateway 已将该 payload 编译并原子发布到与 Proxy 相同版本的本地 View,动态 Router 只匹配
|
||
当前未过期 View;派发器已按五种策略从该 View 选择上游,且在 Proxy 容量耗尽时只在该
|
||
View 的其余候选中回退。`wait_timeout` 随 `on_unavailable=WAIT` 下发并在 Gateway 作为有界
|
||
本地容量等待使用;`DIRECT` 仍先经过 TargetPolicy 再建立 HTTP/CONNECT 直连。`proxy-gateway`
|
||
已装配 Register/Watch/ACK/Runtime/Outcome 会话、HTTP 代理监听和 Snapshot 就绪探针;控制面中断时
|
||
保持进程运行并以有界退避重连,未取得有效 Snapshot 的 Worker 不会 Ready。`ReportOutcomes`
|
||
已实现为客户端流:Controller 校验流内固定的 Worker/Session 身份,对每个批次以
|
||
`(session_id, sequence, SHA-256)` 建立 Redis 原子栅栏,并只返回最后确认的序列。
|
||
Redis 只保留每个当前会话的一条序列和摘要;原始 Outcome、代理明细与逐请求记录均不写入
|
||
Redis 或 PostgreSQL。Gateway 只将结果写入本地有界队列,队列满时丢弃样本,不等待控制面
|
||
或存储。Checker 的有界任务领取、任务租约归属校验和 Observation 上报已经由
|
||
gRPC 契约测试覆盖;Controller 已装配 Redis 共享 due-index、生产任务 broker 与独立
|
||
Checker 的 HTTP/HTTPS BASIC 探测进程。EGRESS 和 TARGET 尚未进入生产调度。
|
||
`100,000 QPS` 仍是未验证的设计目标。
|
||
|
||
`WatchSnapshots` 建立时校验当前 session;每次签发快照引用时也把 `session_id`
|
||
交给 Redis 原子校验。重复 Register 会同时清除旧 Runtime 和已签发引用,因此迟到的
|
||
旧 Stream 既不能覆盖新 session 的引用,也不会向旧连接发送未获授权的快照。
|
||
|
||
## 2. Worker 会话
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant W as Gateway Worker
|
||
participant C as Controller
|
||
W->>C: RegisterWorker(worker, instance, zone)
|
||
C-->>W: session + ownershipEpoch + maxStaleAge
|
||
W->>C: WatchSnapshots(lastVersion, checksum)
|
||
C-->>W: full WorkerSnapshot
|
||
W->>W: validate + build immutable snapshot
|
||
W->>W: atomic swap
|
||
W->>C: AcknowledgeSnapshot(version, epoch, checksum)
|
||
loop bounded interval
|
||
W->>C: ReportRuntime(active, reserved, draining)
|
||
W->>C: ReportOutcomes(batch sequence)
|
||
end
|
||
```
|
||
|
||
`worker_id` 是逻辑节点,`instance_id` 区分进程重启,`session_id` 防止旧进程
|
||
继续上报。所有权 `epoch` 小于 Controller 当前值的数据必须拒绝。
|
||
|
||
同一 `worker_id` 重注册会替换 session,并清除旧 Runtime 与已签发 Snapshot
|
||
Reference。旧 Stream 即使在替换后仍收到上游更新,其签发操作也会以
|
||
`FailedPrecondition` 结束,不能影响新 session 的 ACK 基线。
|
||
|
||
`ReportRuntimeRequest.report_sequence` 在当前 `session_id` 内严格单调递增。
|
||
相同序号只允许内容完全相同的幂等重放;较小序号或相同序号的不同内容必须
|
||
拒绝。`observed_at` 只用于观测,不作为乱序判定依据,运行态 TTL 统一使用
|
||
Controller 侧 Redis 服务端时间。session 同时保存 Controller 已接受的
|
||
`snapshot_version/ownership_epoch`;运行态报告必须与该 ACK 上界完全一致,
|
||
Worker 自报的超前版本或 epoch 也必须拒绝。
|
||
|
||
运行态上报是完整稀疏替换:只携带 Active/Reserved 非零的 Proxy,空列表表示
|
||
当前会话全部归零。Controller 只有在 Worker session、报告 TTL、Proxy ownership
|
||
和 ownership epoch 同时有效时才使用计数;报告缺失、过期或不一致时按零可用
|
||
容量 fail-closed,不能把未知计数解释成空闲容量。
|
||
|
||
## 3. Snapshot 与 Delta
|
||
|
||
完整 Snapshot 包含:
|
||
|
||
- 单调 `version`、`ownership_epoch`、生成时间和有效期。
|
||
- 对该 Worker 可见的有序 Routing。
|
||
- 仅归该 Worker 所有的 Proxy、每个 Proxy 的容量、硬过期时间 `expires_at`
|
||
和停止新分配的 `usable_until`。
|
||
- 内容 `checksum`。
|
||
|
||
`usable_until = expires_at - allocationSafetyMargin`。Worker 必须以
|
||
`usable_until` 作为最后可分配时刻;达到该时间后即使尚未到 `expires_at`,
|
||
也不得再为新请求选择该 Proxy。
|
||
|
||
Gateway 接收完整快照时必须拒绝缺失、格式错误或已到期的 `valid_until`,并将其
|
||
保存在本地不可变视图。该整体期限到达后,调度直接按无候选处理,不再使用旧视图
|
||
发起新的上游连接,也不查询 Redis 或 PostgreSQL 补偿。
|
||
|
||
Controller 只会下发尚未到期的完整快照,并在最近一次成功下发快照的
|
||
`valid_until` 到达时结束 `WatchSnapshots` 流。Gateway 的 `SessionSupervisor` 会在流
|
||
结束或可恢复控制面错误后按带 jitter 的有界退避重建 Register/Watch 会话;参数、认证
|
||
和协议不兼容错误直接返回。新快照通过校验并原子替换前,旧视图仍按其整体有效期
|
||
fail-closed。
|
||
|
||
Delta 声明 `base_version`。Worker 只有在本地版本恰好等于 base 且 checksum
|
||
验证成功时才能应用;否则丢弃 Delta 并请求完整 Snapshot。构建在后台完成,
|
||
热路径只读取一次原子指针。
|
||
|
||
超过 `max_stale_age` 仍未取得有效快照时,Worker 停止接收新流量并排空已有
|
||
请求。控制面中断不能让 Worker 查询 PostgreSQL 或 Redis 补偿热路径。
|
||
|
||
## 4. 所有权与 Drain
|
||
|
||
同一 Proxy 同时只归一个 Worker。Controller 回收用于独占提取的 Proxy 时:
|
||
|
||
1. 新 Snapshot 标记或移除该 Proxy,使 Worker 停止新预留。
|
||
2. Worker 上报 `draining=true` 以及 Active/Reserved。
|
||
3. 两个计数都归零后 Controller 清除所有权。
|
||
4. 无所有权 Proxy 才能进入 Distribution 的 Redis 原子提取操作。
|
||
|
||
Worker 崩溃时必须等待所有权 epoch/有效期失效后再转移,避免双主。Proto 中
|
||
`ReportRuntimeResponse.revoke_proxy_ids` 是加速 Drain 的控制信号,不绕过
|
||
Snapshot 版本和 Redis 中的权威所有权状态。
|
||
|
||
## 5. Outcome 上报
|
||
|
||
Outcome 按 Worker 单调 `sequence` 批量上报。Controller 返回已接受的最大序号,
|
||
从而支持有限重试和去重。阶段区分:
|
||
|
||
- `DIAL`:连接 Proxy 地址失败。
|
||
- `PROXY_HANDSHAKE`:HTTP CONNECT 或 SOCKS 握手失败。
|
||
- `RESPONSE_HEADERS`:目标响应头前失败。
|
||
- `TUNNEL`:隧道建立后结束或失败。
|
||
|
||
Outcome 是 Observation,不直接让 Worker 修改 Redis 活动池状态,也不创建
|
||
PostgreSQL Proxy 明细。Gateway 将每次尝试写入进程内有界队列,当前默认容量为
|
||
`65536` 个事件,单批最多为 `min(512, maxRuntimeCounters)`;一次 ReportOutcomes RPC
|
||
最多合并 16 个已就绪批次。队列满时只递增本地丢弃计数,不反压 Gateway 热路径。
|
||
|
||
同一 session 内,Controller 仅接受递增 `sequence`;相同序列且摘要一致视为幂等
|
||
重放,相同序列且摘要不同返回 `AlreadyExists`,较小序列返回 `Aborted`。Gateway 在
|
||
未收到确认时保留并重发完全相同的批次。暂态传输错误在当前 session 内按有上限的
|
||
退避重试;会话栅栏错误交由 session supervisor 重建 session。重注册会保留未确认
|
||
事件,并以新 session 从序列 `1` 重新封装后上报。栅栏随 session 替换或过期清理,
|
||
因此长期在线 Worker 不会因独立 Outcome TTL 接受旧序列。
|
||
|
||
## 6. Checker 任务
|
||
|
||
Checker 注册自身最大并发与支持层级,Controller 发送有 deadline 的任务:
|
||
|
||
- `BASIC`:基础连通和协议握手。
|
||
- `EGRESS`:出口身份与匿名性。
|
||
- `TARGET`:针对 Routing/目标组的可达性。
|
||
|
||
Checker 只返回 `HealthObservation`。Controller reducer 按 Proxy、检查层级和
|
||
Routing 决定 AVAILABLE、SUSPECT 或 UNHEALTHY,并更新 Redis 活动池,避免
|
||
多个 Checker 并发写状态。
|
||
|
||
`StreamCheckTasks` 是有界 pull:请求中的 `max_in_flight` 受服务端单次领取上限
|
||
限制,且同一 `checker_id` 的未完成租约会占用该窗口,重连不会扩大并发。任务仅在
|
||
被领取时通过认证的 mTLS 流携带 endpoint、`secret_ref`、版本和任务期凭据;Checker
|
||
不访问 Redis 或 PostgreSQL。`ReportObservations` 在调用 Reducer 前校验 task、Proxy、
|
||
检查层级、目标 Profile 与领取者一致,Reducer 成功后才确认任务;相同领取者对已确认
|
||
任务的同一事实可重放,由活动池摘要幂等处理。生产 Controller 装配 Redis 共享
|
||
broker,按启用 Upstream 的有效检查策略调度 BASIC 任务;未装配 broker 的 fixture
|
||
服务仍会以 `Unavailable` 拒绝任务流,而不下发无租约任务。EGRESS 和 TARGET 的
|
||
任务索引及调度策略尚未实现。
|
||
每次 Claim 还会签发新的不可预测 `lease_token`;Observation 必须回传该值。任务被重新
|
||
领取后,旧 token 即使拥有相同 `task_id` 和 `checker_id` 也会被拒绝,避免过期实例的
|
||
迟到事实覆盖新租约结果。
|
||
|
||
`proxy-checker` 使用固定大小 worker-pool 执行每个 pull 批次,任务数不超过该请求的
|
||
`max_in_flight`;每次尝试都受 `deadline` 和 `timeout` 的较小值约束,失败可在同一
|
||
deadline 内最多执行到 `max_attempts`。BASIC 针对 HTTP/HTTPS Proxy 验证到 Proxy 的
|
||
请求/认证握手;TARGET 通过 Proxy 请求指定目标并将非成功状态作为事实。SOCKS5 与
|
||
EGRESS 的专用出口语义仍待后续探测器扩展。
|
||
|
||
## 7. 兼容与演进
|
||
|
||
- Proto 字段号一旦发布不得复用。
|
||
- 删除字段使用 `reserved` 保留名称和编号。
|
||
- 新枚举值必须让旧接收方按 UNSPECIFIED/拒绝策略处理。
|
||
- Worker 注册携带 `supported_protocol_version`,不兼容时注册失败而不是静默
|
||
降级。
|
||
- Stream 断开后使用带 jitter 的有界指数退避,禁止紧密重连。
|
||
|
||
## 8. 传输安全
|
||
|
||
集群环境使用 mTLS,证书身份绑定 Worker/Checker 类型和环境。服务端校验
|
||
消息中的逻辑 ID 与证书授权一致,设置单消息大小、流持续时间、并发 Stream
|
||
和上报批次上限。`secret_ref` 是受控引用;完整 Snapshot 的 `credentials` 在 mTLS 会话中
|
||
携带引用对应材料,Controller 和 Gateway 仅在内存处理,禁止写入 Redis/PostgreSQL、日志或指标。
|
||
Checker 任务也遵循相同边界:连接凭据只存在于 Controller 的解析过程、任务流和 Checker
|
||
的单次执行期,不进入任务日志、指标或独立任务存储。
|
||
|
||
## 9. Gateway 启动参数
|
||
|
||
Gateway 不复用 `controlPlane.listen` 作为客户端地址。`listen` 是 Controller 的
|
||
服务端绑定地址;每个 Gateway 必须显式提供以下独立参数或同名环境变量:
|
||
|
||
- `-control-plane` / `PROXY_POOL_CONTROL_PLANE_ADDRESS`:Controller 的可拨号地址。
|
||
- `-cluster-id` / `PROXY_POOL_CLUSTER_ID`:快照所属集群。
|
||
- `-worker-id` / `PROXY_POOL_WORKER_ID`:唯一逻辑 Worker。
|
||
- `-instance-id` / `PROXY_POOL_INSTANCE_ID`:唯一进程实例。
|
||
- `-zone` / `PROXY_POOL_ZONE`:实例可用区。
|
||
|
||
当 `controlPlane.tls.mode=mtls` 时,Gateway 使用 `controlPlane.gatewayTLS` 中独立的
|
||
客户端证书、私钥和 Controller CA 发起 TLS 1.3 连接;证书必须符合 Controller 的
|
||
SPIFFE Worker 身份校验。
|
||
`disabled` 仅接受回环控制面地址,供本地 fixture 使用。
|
||
|
||
## 10. Checker 启动参数
|
||
|
||
- `-control-plane` / `PROXY_POOL_CONTROL_PLANE_ADDRESS`:Controller 的可拨号地址。
|
||
- `-checker-id` / `PROXY_POOL_CHECKER_ID`:唯一逻辑 Checker。
|
||
- `-instance-id` / `PROXY_POOL_CHECKER_INSTANCE_ID`:唯一进程实例。
|
||
- `-max-in-flight` / `PROXY_POOL_CHECKER_MAX_IN_FLIGHT`:本进程任务上限。
|
||
- `-levels`:逗号分隔的 `basic,egress,target` 能力集合。
|
||
|
||
当 `controlPlane.tls.mode=mtls` 时,Checker 使用 `controlPlane.checkerTLS` 的独立
|
||
客户端证书、私钥和 Controller CA 建立 TLS 1.3 连接;证书必须符合 Controller 的
|
||
SPIFFE Checker 身份校验。`disabled` 仅接受回环控制面地址。
|