16 KiB
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 对应的基础完整快照,并在
每份快照有效期的一半前重新构建、下发版本递增的完整快照;这样 ownership、Routing 和
凭据变化会在同一长连接内收敛,而无需等待有效期到达后重新注册。Controller 内已提交的
Upstream 启停、Routing 切换和配置发布还会向全部本地 Worker 流广播一次合并后的立即刷新;
多 Controller 副本仍由该定时机制完成跨进程收敛。
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/SOCKS5 BASIC、EGRESS、TARGET 探测进程。TARGET Profile 按
(routing_name, target_url) 独立调度和归并,BASIC/EGRESS/TARGET 共享每个 Upstream 的
in-flight 上限。
100,000 QPS 仍是未验证的设计目标。
WatchSnapshots 建立时校验当前 session;每次签发快照引用时也把 session_id
交给 Redis 原子校验。重复 Register 会同时清除旧 Runtime 和已签发引用,因此迟到的
旧 Stream 既不能覆盖新 session 的引用,也不会向旧连接发送未获授权的快照。
2. Worker 会话
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。
version 在同一 Worker 流内严格连续递增,即使 ownership_epoch 因分配、Drain
或所有权恢复而前进也不重置。Gateway 只接受比当前 version 恰好大一的完整快照,
并拒绝倒退的 epoch;因此 epoch 变化不会让仍在长连接内的刷新快照被误判为缺口。
usable_until = expires_at - allocationSafetyMargin。Worker 必须以
usable_until 作为最后可分配时刻;达到该时间后即使尚未到 expires_at,
也不得再为新请求选择该 Proxy。
Gateway 接收完整快照时必须拒绝缺失、格式错误或已到期的 valid_until,并将其
保存在本地不可变视图。该整体期限到达后,调度直接按无候选处理,不再使用旧视图
发起新的上游连接,也不查询 Redis 或 PostgreSQL 补偿。
Controller 只会下发尚未到期的完整快照,并在每份快照有效期的一半前发送下一版完整
快照。刷新构建失败、流结束或可恢复控制面错误时,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 时:
- 新 Snapshot 标记或移除该 Proxy,使 Worker 停止新预留。
- Worker 上报
draining=true以及 Active/Reserved。 - 两个计数都归零后 Controller 清除所有权。
- 无所有权 Proxy 才能进入 Distribution 的 Redis 原子提取操作。
首次 BeginDrain 会原子创建按 proxy_id + worker_id + assignment_epoch 栅栏的待绑定
Drain Ticket,并推进全局 ownership epoch,促使下一份完整 Snapshot 撤销该 Proxy。Ticket
按 Worker 有界读取,供 Controller 在确认完整 Snapshot 确实不含该 Proxy 后绑定快照屏障。
Worker Handler 先登记该 Snapshot 引用,再将 Ticket 绑定到 session、version、epoch 与
checksum。后续完整 Runtime 替换在同一 Redis Lua 事务中检查:Ticket 屏障是否属于当前
session、该 session 是否已 ACK 不早于屏障的快照、以及本次完整稀疏报告中该 Proxy 的
Active/Reserved 是否均为零(缺失项按零)。三者同时成立才清除 owner、Ticket 与
Worker Drain 索引,并在记录仍为 AVAILABLE 时恢复可分配索引;因此不存在先读 Runtime
再释放所有权的竞争窗口。AcknowledgeDrain 保留为显式完成原语,用于不经 Runtime
上报的受控维护流程。
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:通过任务级 HTTP/HTTPS URL 检查出口可达性。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、
检查层级、仅属于 TARGET 的目标 Profile 与领取者一致,Reducer 成功后才确认任务;
相同领取者对已确认任务的同一事实可重放,由活动池摘要幂等处理。EGRESS 的探测 URL
只存在于下发任务,Checker 回传时不携带 URL 或 Routing Profile,因此它只归并 Proxy
全局健康。生产 Controller 装配 Redis 共享 broker,按启用 Upstream 的有效检查策略
调度 BASIC/EGRESS 任务,并按启用 Routing 的 check.targets 调度 TARGET 任务;未装配
broker 的 fixture 服务仍会以 Unavailable 拒绝任务流,而不下发无租约任务。三类任务
使用独立 due-index 和引用,且共享 Upstream in-flight 上限。
每次 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/SOCKS5 Proxy 验证到 Proxy 的
请求/认证握手;EGRESS 与 TARGET 通过 Proxy 请求任务指定的 HTTP/HTTPS 目标并将
非成功状态作为事实。EGRESS 对成功响应解析纯文本或常见 JSON IP 字段;EGRESS 与 TARGET
均已进入生产调度。
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:实例可用区。-auto-identity/PROXY_POOL_AUTO_IDENTITY=true:仅 mTLS 模式可用;从客户端 证书中唯一的.../worker/<worker-id>SPIFFE URI 派生 Worker ID,并在未显式配置时 使用同一值作为 Instance ID。
当 controlPlane.tls.mode=mtls 时,Gateway 使用 controlPlane.gatewayTLS 中独立的
客户端证书、私钥和 Controller CA 发起 TLS 1.3 连接;证书必须符合 Controller 的
SPIFFE Worker 身份校验。客户端 X.509-SVID 叶证书必须且只能包含一个 URI SAN,且该 URI
必须是对应角色的身份。Gateway 自动解析和 Controller 授权共用严格 URI 规则:身份 URI
不得包含用户信息、端口、查询、片段或转义路径。
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能力集合。-auto-identity/PROXY_POOL_AUTO_IDENTITY=true:仅 mTLS 模式可用;从客户端 证书中唯一的.../checker/<checker-id>SPIFFE URI 派生 Checker ID 和缺省 Instance ID。
当 controlPlane.tls.mode=mtls 时,Checker 使用 controlPlane.checkerTLS 的独立
客户端证书、私钥和 Controller CA 建立 TLS 1.3 连接;证书必须符合 Controller 的
SPIFFE Checker 身份校验。disabled 仅接受回环控制面地址。