结论先行:传播延迟的度量锚点不是「缓存 TTL」,而是「撤销操作提交时刻 → 验证方首次拒绝时刻」的差值。三类机制的 P99 传播延迟由三个独立因子决定:信号传播介质(push vs pull)、验证方实例生命周期(冷启动/滚动发布)、本地缓存刷新策略。预算不是三类之和,而是 max(三类 P99) + 安全余量——因为三类是并行的,安全侧只需证明「最坏那一类」在窗口内。
一、可复现的测量方法
1. 定义「撤销生效时刻」
不要用「第一台拒绝」或「最后一台拒绝」——前者只证明信号到了,后者被冷启动/滚动发布等一次性事件主导,不可控、不可 SLO。推荐:
R_prop = t_reject − t_revoke,对每个验证方实例独立测量,取 P99(99 分位实例) 作为 SLI。允许 1% 的实例(比如正在滚动发布的那个)略超,但 99% 的实例在预算内——合规能接受这个口径。
2. 构造探针(每类各一条,可自动化)
JWT 黑名单探针:
# 撤销一个测试 JWT(JTI=test-rev-probe-<ts>),记录 t_revoke = 撤销 API 返回时刻
# 循环发这个 JWT 到每个网关实例(直连或 header 路由注入)
# 记录 t_reject = 实例返回 401/403 的时刻
# R_prop = t_reject − t_revoke
关键:要覆盖正在滚动发布的实例(冷启动、还没拉黑名单),这才是长尾来源。你实测的 95s 大概率就是这类。做法:在 canary 发布窗口内触发撤销探针,专门量「发布中实例」的传播延迟。
CRL/OCSP 探针:
# 1. openssl ca -revoke -in cert.pem # 记录 t_revoke
# 2. 用真实验证方(Envoy mTLS)发请求:curl --cert client.pem https://...
# 记录 t_reject = TLS handshake failure 时刻
# 注意:openssl verify -crl_check 走的是本地 CRL 文件,不是真实验证方,不能当探针
CRL 传播延迟 = crlNextUpdate 窗口 + 验证方本地缓存 TTL + 冷启动缓存未加载。你实测 3 分 40 秒的长尾,大概率是「新起验证方 + 冷 CRL 缓存 + crlNextUpdate=1h」。
DB 密码探针:
-- 记录 t_revoke = ALTER USER 提交时刻
-- 新建连接尝试旧密码 → 记录 t_reject(ALTER USER 后通常 < 1s 即被拒)
-- 对连接池中已有连接发 SELECT 1 → 记录 t_still_alive(最后一次成功时刻)
-- R_prop_db = max(t_reject, t_still_alive) − t_revoke
DB 密码的传播延迟分两段:(a) 新连接被拒(ALTER USER 后立刻生效),(b) 存量连接池里的旧连接仍活着(取决于 maxLifetime 或主动 KILL)。你引用的 1913 号讨论已经覆盖了 (b) 的排空判定。
3. 测量频率与基线
- 日常:每小时跑一次探针(低峰期),记录 P50/P99/Max 到 Prometheus
- 发布窗口:每次滚动发布时额外跑一次(覆盖冷启动实例)
- 基线:取 30 天 P99 的中位数作为 baseline,告警阈值 = baseline × 2
二、预算模型
核心公式
安全窗口 = T_detect(泄露检测)+ T_revoke(撤销操作)+ R_prop(传播延迟)+ T_buffer(安全余量)
你的 KPI 是「暴露面压到分钟级」,假设安全窗口 ≤ 5 分钟:
| 阶段 | 典型值 | 可压缩性 | |---|---|---| | T_detect | 30s ~ 2min | 取决于 SIEM/审计日志,难压缩 | | T_revoke | < 5s(API 调用) | 已够快 | | R_prop | 这就是你要量的 | 可压缩(换机制) | | T_buffer | 30s | 安全余量 |
所以 R_prop 的预算 ≈ 5min − T_detect − T_revoke − T_buffer ≈ 2~3 分钟。
三类的机制替换(把 R_prop 压到 ≤ 60s)
| 类型 | 当前机制 | 当前 R_prop P99 | 目标机制 | 预期 R_prop P99 |
|---|---|---|---|---|
| JWT 黑名单 | 各实例本地缓存 + 60s 轮询 | ~60-95s | 推送:撤销时通过 Redis Pub/Sub 或 K8s Event 推送到所有实例,实例收到后立即失效本地缓存 | < 5s(网络 RTT) |
| CRL | crlNextUpdate=1h + 本地缓存 | ~3-4 min | OCSP stapling:CA 在 TLS 握手时直接返回 revocation status,验证方无需拉 CRL 文件;或 crlNextUpdate 缩到 5min + OCSP 兜底 | < 30s |
| DB 密码 | ALTER USER + 连接池自然排空 | 取决于 maxLifetime | ALTER USER + 主动 KILL(KILL <id> 所有旧密码连接)+ 连接池 maxLifetime ≤ 60s | < 60s |
三类是并行的,安全侧只需证明「最坏那一类」在预算内。R_prop 的预算 = max(三类 P99) + buffer,不是三者之和。
三、可审计 SLI(直接进合规报告)
# Prometheus 指标
revocation_propagation_lag_seconds:
type: histogram
buckets: [5, 15, 30, 60, 120, 300, 600]
labels:
- credential_type: jwt_blacklist | crl | db_password
- instance: <pod-name>
- phase: normal | rolling_update | cold_start
# SLI 定义
sli: revocation_propagation_lag_p99
definition: |
P99 of (t_reject - t_revoke) across all verification instances,
per credential_type, 30-day rolling window.
target:
jwt_blacklist: ≤ 60s
crl: ≤ 120s (with OCSP stapling)
db_password: ≤ 60s
baseline: 30-day median of P99
alert: > 2 × baseline for 5 consecutive minutes
# 审计口径
report_frequency: weekly
scope: all 60+ microservices, 3 credential types
exclusion: instances in active rolling update (tagged, ≤ 2 instances)
这个 SLI 能直接进合规报告,因为:有明确定义(不是「大概」)、有基线和告警阈值(可审计)、有排除规则(滚动发布中的实例单独标注,不隐藏)、每类凭据独立度量(不混在一起模糊处理)。
四、一个反直觉的补充
你实测发现「长尾由实例生命周期决定,而不是缓存 TTL 决定」——这个观察非常准确,但根因比这更深一层:
传播延迟的长尾不是「缓存没刷新」,而是「实例还没活」。
冷启动实例在拉黑名单之前,验证逻辑是「没有黑名单 = 全部放行」。这不是缓存问题,是初始状态问题。解法不是缩短缓存 TTL(60s → 10s 也没用,因为实例前 90s 可能根本没拉),而是:
- 启动门禁:实例启动时同步拉一次黑名单,拉不到就不进 Ready(和你 2109 号提问的 readiness 门禁思路一致)
- 默认拒绝(fail-closed):冷启动实例在首次黑名单加载完成前,对需要验证的请求返回 503 而不是 200 放行。这是安全侧最想要的行为
- 发布窗口标记:审计报告里把「滚动发布中的实例」单独标注,不混进 P99——因为这类实例的 R_prop 是结构性的(生命周期决定),不是运维能消除的
打個比方:你量门禁卡注销传播延迟,量的不是「闸机刷新列表要多久」,而是「新装的闸机在第一次同步列表之前,是默认开门还是默认关门」。