proxy-pool/docs/operations/runbook.md
youfak 84ed10bd7a
Some checks are pending
ci / proto (push) Waiting to run
ci / test (ubuntu-latest) (push) Waiting to run
ci / test (windows-latest) (push) Waiting to run
ci / race (push) Waiting to run
ci / integration (push) Waiting to run
feat: reap sustained unhealthy proxies
2026-08-02 10:15:25 +08:00

334 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Proxy Pool 运维手册
## 1. 运行边界
- Gateway 是数据面,正常请求热路径不访问 PostgreSQL、Redis 或 Provider。
- Gateway 的 Proxy Outcome 仅进入进程内有界队列Controller 只维护当前 session
的最后确认序列和摘要,原始 Outcome 不写入 Redis 或 PostgreSQL。队列满时丢弃
观测样本,代理转发和 Redis session TTL 不受影响。
- Controller 编排 Fetch、生命周期、所有权、Snapshot 与 Extract多个副本只有
一个 Provider 逻辑 Leader。短效 Proxy 明细只存在于 Redis TTL 活动池和节点
内存,可由 Provider 重建。
- PostgreSQL 只持久化配置版本、Upstream/Routing 管理状态、Admin 审计与
outbox以及可选聚合指标不保存 Proxy 明细或逐次提取记录。
- Checker 执行有界健康探测,只上报 Observation最终状态由 Controller
reducer 决定。
- Extract 是一次性独占发放。Redis 在单次原子操作中校验并从可分配活动池移除
候选,同时写入带 TTL 的幂等结果;没有 Lease、续租或 Release 接口。
- `reserveForGateway` 是共享池硬约束Extract 不得把 Gateway 库存清空。
- 集群峰值 100,000 QPS 是设计目标,只有完成本文容量验收后才能作为已验证
能力对外承诺。
## 2. 本地拓扑模板
`cmd/proxy-controller` 已完成配置单次加载、PostgreSQL 迁移、Redis 活动池、
Distribution/Admin/Metrics 独立监听和有界停机装配。Provider 自动补池、分布式
配额、动态重载和 Admin 低基数统计已装配Controller 已装配 Redis BASIC/EGRESS/TARGET 任务 broker
`proxy-checker` 可执行 HTTP/HTTPS/SOCKS5 BASIC/EGRESS/TARGET 探测。`proxy-loadgen` 已提供有界 HTTP
请求场景CONNECT 长连接/Extract 压测与完整 mTLS 环境 Overlay 仍属于
`implementation-plan.md` 后续任务。
因此 Compose/Kubernetes 资产当前仍用于评审网络、资源、探针和依赖关系,不能
视为完整可运行拓扑。
### 2.1 前置条件
- Docker Engine 25+Compose v2.30+。
- 至少 8 CPU、16 GiB 内存和 20 GiB 可用磁盘。
- 本地端口 `3000`、`8080`、`8081`、`8082`、`8404`、`9091` 未占用。
### 2.2 配置凭据
`deploy/config/local.yaml` 只用于本机拓扑验证。进入运行时实施阶段后,再通过
密钥系统提供下列变量,并把 Provider 地址替换为测试 fixture
```powershell
$env:PROXY_POOL_GATEWAY_PASSWORD = "LOCAL_GATEWAY_PASSWORD"
$env:PROXY_POOL_EXTRACT_TOKEN = "LOCAL_EXTRACT_TOKEN"
$env:PROXY_POOL_ADMIN_TOKEN = "LOCAL_ADMIN_TOKEN"
$env:PROXY_POOL_CONFIG_FINGERPRINT_KEY = "LOCAL_HIGH_ENTROPY_KEY_AT_LEAST_32_BYTES"
$env:PROVIDER_A_TOKEN = "PROVIDER_A_TOKEN"
$env:PROVIDER_B_TOKEN = "PROVIDER_B_TOKEN"
```
### 2.3 静态检查
```powershell
docker compose -f deploy/docker-compose.yml config
kubectl kustomize deploy/kubernetes/base > rendered.yaml
```
本地 Compose 的 Redis 只作为可重建短效状态 fixture固定使用
`--appendonly no --save ""`,且不挂载 `/data` 或命名卷。真实 Redis 8.2 契约可
通过 `.\scripts\test-redis.ps1` 执行;脚本使用唯一命名空间并在结束时定向清理,
不执行 `FLUSHDB`。生产环境的 Redis 高可用与持久化策略必须独立评审,不能照搬
本地 fixture。
PostgreSQL 管理面集成测试通过 `.\scripts\test-postgres.ps1` 启动
`postgres:18-alpine`,仅绑定 `127.0.0.1:15432`,并将 PG18 数据根目录挂载为
tmpfs。`ApplyMigrations` 在同一物理连接上执行仓库内嵌的幂等前向迁移;测试为
每个契约创建唯一 Schema结束时只删除该 Schema 和临时 Compose 项目。该脚本
禁止指向开发或生产数据库。
`.\scripts\test-controller.ps1` 同时启动两个隔离 fixture验证 Controller
bootstrap 的迁移、启动配置提交、Redis Readiness、Admin Status、`/readyz` 与
Prometheus 输出。脚本不启动
部署模板中的 Controller 容器,也不连接开发或生产存储。
目标拓扑入口:
- Gateway`127.0.0.1:8080`
- Distribution`http://127.0.0.1:8081`
- Admin`http://127.0.0.1:8082`
- Controller Metrics`http://127.0.0.1:9090`
- HAProxy 状态:`http://127.0.0.1:8404/stats`
- Prometheus`http://127.0.0.1:9091`
- Grafana`http://127.0.0.1:3000`
`deploy/config/local.yaml` 中的 `.invalid` Provider 是故障演示占位。未替换时
Fetch 应表现为 Error 与退避,不应增加 Empty 计数,也不影响已有 Proxy。
## 3. Kubernetes 发布
本节是运行时实施完成后的发布规格,不代表当前代码已达到生产就绪。
### 3.1 准备
1. 使用托管 PostgreSQL 和 Redis分别配置 TLS、备份、监控和多可用区。
2. 复制 `secret.example.yaml` 到环境私密配置系统,由 External Secrets、SOPS
或密钥管理平台生成 `proxy-pool-secrets`,不要提交真实 Secret。
`PROXY_POOL_CONFIG_FINGERPRINT_KEY` 必须使用至少 32 字节的高熵随机值,所有
Controller 副本保持一致,且与配置中的业务 Secret 分离管理。
3. 在环境 Overlay 替换镜像、Provider 地址、允许网段、外部存储地址、资源量
和 LoadBalancer 注解。
4. 根据集群 CNI 能力收紧 NetworkPolicy 的外部网段。
5. 在预发布环境完成数据库向前兼容迁移,再发布 Controller。
### 3.2 服务端应用顺序
```bash
kubectl apply -f deploy/kubernetes/base/namespace.yaml
kubectl -n proxy-pool apply -f ENVIRONMENT_SECRET.yaml
kubectl apply -k deploy/kubernetes/base
kubectl -n proxy-pool rollout status deployment/proxy-controller --timeout=5m
kubectl -n proxy-pool rollout status deployment/proxy-gateway --timeout=10m
```
### 3.3 探针语义
- `/livez`:进程事件循环仍可运行。数据库或 Redis 短暂失败不得导致 Gateway
liveness 失败。
- `/readyz`进程可以接收新工作。Gateway 只有在持有未超过 `maxStaleAge`
的完整 Snapshot 且仍有准入能力时才 Ready。
- Controller 按能力判定就绪Distribution/Fetch 依赖 Redis 活动池Admin 持久化
写依赖 PostgreSQL 与兼容迁移。PostgreSQL 故障不得单独使 Extract 返回 503。
- 同一 Controller 同时启用 Distribution 与 Admin 时Pod `/readyz` 以 Redis
活动池为服务流量门槛PostgreSQL 故障由 Admin 接口独立返回不可用,不把仍可
完成的 Extract 从 Service 摘除。Admin-only 进程才同时检查 PostgreSQL 与 Redis。
- `/metrics`独立于业务入口NetworkPolicy 仅允许监控命名空间访问。
探针不得执行 Provider 请求或完整数据库扫描。
Checker 指标使用固定标签集:
- `proxy_pool_checker_tasks_dispatched_total{level}`:成功写入 Checker 任务流后的任务数。
- `proxy_pool_checker_observations_total{level,result}`:已解码的 Observation 被 Controller 接受或拒绝的数量。
`level` 仅为 BASIC、EGRESS、TARGET`result` 仅为 accepted、rejected。不得将
Proxy ID、IP、Checker ID、目标 URL、Client ID 或凭据加入指标标签。
Gateway 指标同样使用固定标签集:
- `proxy_pool_gateway_outcomes_total{stage,result}`:每次代理尝试在最远完成阶段的成功/失败数量。
- `proxy_pool_gateway_outcome_queue_dropped_total`Outcome 本地有界队列已满后丢弃的观测数量。
`stage` 仅为 DIAL、PROXY_HANDSHAKE、RESPONSE_HEADERS、TUNNEL`result` 仅为 success、failure。
前者在写入本地队列前计数,后者用于识别观测背压;二者均不包含 Proxy ID、路由、目标、客户端或凭据。
### 3.4 发布顺序与兼容性
1. 先做向前兼容数据库迁移。
2. 发布 Controller确认旧 Worker 仍能消费旧 Snapshot 协议版本。
3. 发布 Checker。
4. 逐批发布 Gateway每次至少保留 PDB 要求的健康副本。
5. 观察 30 分钟,再清理已经无人读取的旧字段或旧迁移。
迁移失败时停止 Controller 发布,不自动重试结果不确定的管理 mutation。恢复后
先确认 Schema 版本、最近 revision、审计和 outbox 一致,再开放 Admin 写入口。
回滚只能回到仍兼容当前 Schema 和 Snapshot 版本的镜像。涉及不可逆数据迁移时,
必须使用前向修复。
## 4. 容量规划
```text
worker_replicas =
ceil(peak_qps / (tested_worker_qps * target_utilization))
+ largest_failure_domain_replicas
```
- `peak_qps`:当前目标为 100,000。
- `tested_worker_qps`:在同 CPU、内存、网络、Go 版本、Snapshot 规模、TLS 与
上游响应模型下测出的单 Pod 持续能力。
- `target_utilization`:不高于 0.60给突发、GC 和故障转移留空间。
- `largest_failure_domain_replicas`:最大单可用区失效时丢失的副本数。
禁止用 CPU 核数直接推算 QPS也禁止把短时峰值当持续容量。Kubernetes 基线的
6 个 Gateway 副本只是初始值,必须由压测结果调整。
HPA 使用 CPU/内存作为保护性信号;生产环境建议通过 Prometheus Adapter 加入:
- 每 Pod Gateway QPS。
- 活跃连接数和建连速率。
- p99 Dispatch 延迟。
- 拒绝率和 Available Slots。
连接型工作负载缩容至少稳定 10 分钟,终止前先 NotReady再等待现有隧道排空。
## 5. 日常检查
每班次检查:
1. Gateway QPS、错误率、p95/p99 和活跃连接。
2. Snapshot age、epoch/version、重同步与 ACK 延迟。
3. Available Slots、Reserved、Active、各 Proxy 状态数量。
4. Provider Success、Empty、Duplicate-only、Error、429 与退避。
5. Checker 队列、SUSPECT 数、检查延迟和目标级失败。
6. Extract requested/returned、insufficient、冲突和幂等命中。
7. PostgreSQL 连接、管理事务失败、outbox backlog、WAL 与备份。
8. Redis 延迟、内存、TTL 淘汰、原子操作错误、主从状态和 Leader 租约抖动。
Prometheus 标签禁止包含 Proxy IP、Client ID、Session、完整 URL、request ID。
需要逐请求调查时使用受控、脱敏且采样的结构化日志。
## 6. 优雅停机
### Gateway
1. readiness 立即失败,停止新连接。
2. 停止应用新 Snapshot但保留当前不可变版本。
3. 等待 HTTP 请求和 CONNECT 隧道排空。
4. 到达 60 秒上限后关闭残余连接,保证容量 reservation 被释放。
### Controller
1. 停止接收新的 Extract/Admin 写请求。
2. 停止发起 Fetch释放 Provider Leader 租约。
3. 等待已提交 Redis 原子操作返回;未确认请求的客户端必须使用相同幂等键重试。
4. 刷新 Admin outbox、审计和 Worker ACK再关闭 PostgreSQL/Redis 连接池。
Outbox 发布器必须以稳定 consumer ID 有界领取;发布成功后原子 ACK。进程在发布
成功但 ACK 结果不确定时,不得伪造确认;等待租约到期后重领,并由下游事件消费者
按事件 ID/revision 去重。持续积压时先暂停新的管理变更,检查发布目标、租约和
最老未发布事件,不删除未发布行。
### Checker
1. 停止领取新任务。
2. 在 45 秒内完成或取消现有探测。
3. 批量上报已完成 Observation未完成任务由队列重新投递。
## 7. 故障处置
### 7.1 Snapshot 陈旧
症状:`ProxyPoolGatewaySnapshotStale`、Worker 重同步增加、Gateway Ready 下降。
1. 检查 Controller、控制流和 outbox 延迟。
2. 确认 Worker epoch 与 Controller epoch禁止手工降低 epoch。
3. 缺版本时强制完整 Snapshot不要继续应用 Delta。
4. `maxStaleAge` 内允许旧 Snapshot 服务;超限自动拒绝新流量并排空。
5. 不得通过无限增大 `maxStaleAge` 隐藏控制面故障。
### 7.2 PostgreSQL 不可用
1. Gateway 继续使用最后有效 Snapshot。
2. Redis 健康且运行配置有效时Distribution 继续执行原子 ExtractProvider
按 last-known 管理状态继续刷新 TTL 活动池;状态读取失败不得触发全进程退出。
3. 拒绝配置版本、Upstream/Routing 管理状态和其他需要 Admin 审计/outbox 的写入;
不得把 Proxy 明细临时落入 PostgreSQL。
4. 恢复后核对迁移、管理事务回滚、Admin 审计与 outbox backlog不存在 Proxy
明细或逐次提取记录恢复步骤。
5. 若权威配置指纹已变化,确认每个 Controller 的共享配置源和 Secret 版本已同步;
同时确认 `PROXY_POOL_CONFIG_FINGERPRINT_KEY` 一致。指纹不匹配的副本会停止旧
Provider匹配并预检成功后按 PostgreSQL revision 自动恢复;迟到旧 revision
不会覆盖较新本地配置。
### 7.3 Redis 不可用
1. Gateway 继续读取节点内最后有效 Snapshot直到 `maxStaleAge`;请求热路径不
回查 PostgreSQL 或 Provider。
2. Distribution 立即失败关闭并返回 503禁止本地内存提取或 PostgreSQL 兜底。
3. Controller 停止活动池写入和需要分布式互斥的工作,防止多个 Fetch Leader
Distribution 的 Redis 权威限流同时失败关闭,本地早期限流不冒充跨副本额度。
4. 恢复后确认 Leader 唯一和租约 epoch 单调,由 Provider 重新 Fetch 并构建 TTL
活动池,再恢复 Distribution。Redis 整体丢失会终止原活动池代次的排他状态和
短期幂等窗口;高可用、持久化、监控和告警必须明确并降低该风险。
### 7.4 Provider 故障
1. 超时、DNS、认证、非预期 HTTP、响应超限与模板错误全部计 Error。
2. 429 尊重 `Retry-After`,其余 Error 使用指数退避和 jitter。
3. Error 不增加 Empty合法候选为零才增加 Empty。
4. Duplicate-only 重置 Empty 并记录独立指标。
5. 达到 Empty 阈值后每条受影响 Routing 只原子切换一次。
### 7.5 Gateway 容量耗尽
1. 检查 Available Slots而不是只看 Proxy 数量。
2. 确认是否大量容量停留在 Reserved排查 Commit/Cancel 泄漏。
3. 检查 TTL safety margin、健康状态和 Worker 所有权是否导致候选被过滤。
4. 快速拒绝新请求,禁止无界等待或把压力转移到 Controller。
5. 扩容 Gateway 前确认存在可分配 Proxy 所有权切片。
### 7.6 Extract 库存不足
1. `partial` 返回实际数量;`allOrNothing` 不足时不改变 Redis 活动池。
2. 检查 TTL、health age、filter、Worker ownership 和 `reserveForGateway`
3. 不得降低 `reserveForGateway` 到导致 Gateway 容量告警的水平。
4. 回收 Worker-owned Proxy 必须先 DRAINING、等待 active/reserved 为零、清除
ownership再由 Redis 原子操作将其从可分配活动池移除并写入短期幂等结果。
5. 已提取代理没有 Release客户端归还请求只记录为无效调用不恢复库存。
### 7.7 Checker 积压
1. 优先新 Proxy 与 SUSPECT 复检。
2. 降低稳定 AVAILABLE 的普通复检频率。
3. 检查目标超时、DNS 与出口网络,再按任务延迟扩容 Checker。
4. 队列必须有上限;不得无限积压耗尽内存或 Redis。
### 7.8 持续 UNHEALTHY 回收
1. 先确认 `check.maxConsecutiveFailures`、检查目标和出口网络;首次失败进入
SUSPECT阈值达到后才进入 UNHEALTHY。
2. `check.unhealthyRemoveAfter: 0s` 仅保留异常状态;设置正值后 Controller 才会在
持续异常超过该窗口时执行有界回收。
3. 回收器只删除没有 Worker ownership 的记录。发现所有权时会延后处理,不得手动
删除 Redis 记录或索引。
4. 对已分配代理,按既有流程执行 DRAINING等待 Worker ACK 且 active/reserved
都归零;所有权清除后,下一轮回收才会删除该异常 Proxy。
## 8. 备份与恢复
- PostgreSQL每日全量、连续 WAL/PITR保护配置版本、Upstream/Routing 管理
状态、Admin 审计与 outbox至少每季度做恢复演练。
- Redis保存可由 Provider 重建的 TTL 活动池、所有权/Leader 协调和短期幂等
结果;生产环境可使用高可用与受控持久化降低窗口丢失风险,但不把它当长期
业务档案或备份源。本地 Compose 刻意关闭持久化并且不挂载数据卷。
- 配置:版本化保存校验通过的不可变 Revision 与校验和。
- Secret由密钥平台版本化日志和备份中不得出现明文。
管理面恢复顺序PostgreSQL -> Controller/Admin代理运行面恢复顺序Redis ->
Provider 重建活动池 -> Controller/Checker -> 发布新 Snapshot。验证 Worker
ownership epoch、活动池 TTL、短期幂等窗口、outbox 和配置 Revision 一致后,再
开放相应能力Gateway 在旧 Snapshot 未超过 `maxStaleAge` 时无需等待数据库恢复。
## 9. Secret 轮换
1. 创建新 Secret 版本,不覆盖旧值。
2. Provider/API 凭据支持双版本重叠时先发布新版本。
3. 更新配置 Revision确认 Controller 成功重建 Adapter。
4. 观察 Fetch Error、认证失败与 Snapshot ACK。
5. 所有副本应用后撤销旧 Secret。
Proxy 凭据轮换必须增加 `credentialVersion`,确保唯一键不会把新旧凭据错误合并。