2456 字
约 8 分钟
17
S17-A26 · 配置分发:推送还是拉取

一个团队用"服务端推送"下发限流规则:配置中心主动推给每个实例。某次网络分区,三个实例没收到推送,继续用旧规则——而监控上"配置版本"字段只统计了收到的实例,于是面板显示"100% 已更新"。

推送与拉取的差别不在"谁先动",而在谁能证明"已经生效"。这个差别在故障复盘里会变成一句致命的话:"我们推了,但不知道谁收到了"。而拉取模式下同样的问题会表现为"实例版本落后",一眼可查。
推送是"我发出去了",拉取是"你取到了"——后者天然带证据,前者需要额外的确认机制。


1. 两种模式的成本结构

维度 推送 拉取
生效延迟 低(近实时) 取决于轮询间隔
一致性证据 需回执 天然有(版本号)
服务端压力 与实例数成正比 与轮询频率成正比
网络异常下的行为 静默失败 自动重试

第三项是关键:推送要维护 N 条长连接或 N 个回调地址,实例数上千时压力显著;拉取则是"每个实例每 N 秒问一次",压力平稳但延迟可控性差。

第四项是推送的致命点:推送失败通常不报错,服务端只知道"发过了",不知道"收到了"。第二项还有一个"连接数"的隐性成本:长连接在网关与 NAT 上都有超时,需要心跳维持;实例数上千时,心跳本身就是一份不小的流量。

2. 推送必须配的三件事

配套 作用
回执(实例上报当前版本) 证明生效
版本号(单调递增) 判断新旧
兜底轮询(低频) 修补漏推

第三项是最实用的折中:以推送为主、低频轮询兜底(如每 60 秒拉一次版本号)。它在正常情况下保持低延迟,在网络异常时自动修复。

回执还有一层价值:它能回答"谁没收到"。只统计总数无法定位具体实例,而定位到实例才能定向重推或重启。
第二项的必要性是"乱序":推送可能先发新版再发旧版,没有版本号就无法判断。所以配置项必须带单调递增的版本或时间戳。

// 配置项必须带版本:判新旧、防乱序、做回执
config:
  key: rate_limit.order
  version: 20261110-03          # 单调递增
  payload: { limit: 1800, window: 60 }
  checksum: "sha256:9f2c..."    # 防传输损坏
// 实例回执:{ instance: "app-3", version: "20261110-03" }

3. 拉取的延迟与抖动

参数 影响
轮询间隔 生效延迟上限
抖动(jitter) 避免同时请求
长轮询 兼顾低延迟与低压
版本比对 减少无效传输

第一项是硬约束:间隔 30 秒意味着最坏 30 秒后才生效。第四项(版本比对)是拉取模式的"省流量"关键:请求只带版本号,服务端返回 304 或无变更标记,传输量从"整份配置"降到"一个数字"。
对限流、开关这类配置,30 秒可能已经太晚。

第三项是最优折中:长轮询(服务端 hold 住请求直到有变更) 把延迟做到接近推送,同时保持"客户端主动拉"的一致性优势。

第二项是必须的:上千实例同一秒轮询会造成周期性尖峰,加随机抖动能把峰值摊平。

// 轮询参数的组合
间隔 30s + 抖动 ±5s        → 生效延迟 ≤ 35s,峰值被摊平
长轮询(hold 25s)+ 间隔 30s → 生效延迟 ≤ 秒级,请求数下降 90%
纯推送 + 60s 兜底轮询       → 延迟最低,一致性靠回执补齐

配置分发还有一个"首次启动"的特殊情形:新实例在启动时既没有历史版本,也不该等推送。所以启动路径必须能同步拉一次全量配置,否则新实例会"空着"跑起来直到下一次推送。

// 启动与运行期两条路径
启动:同步拉取全量(超时 3s,失败则启动失败,不带着默认值跑)
运行:推送为主 + 低频轮询兜底
// 反例:启动等推送 → 新实例可能长时间用默认值

4. 一致性证据怎么收

手段 强度
实例主动上报版本 强(可对账)
服务端记录回执 中(可能丢)
心跳里带版本 中高(持续)
监控面板聚合 弱(口径易错)

第一项与第三项的区别在"主动性":上报是实例定期说"我是什么版本",回执是服务端记录"我推给了谁"。前者在实例重启后依然有效,后者会丢历史。
第三项最划算:心跳本来就有,顺手带上版本号。于是"哪些实例还是旧版本"成为一个随时可查的查询,而不需要额外的回执通道。

第四项是最容易骗人的:面板只统计"有上报的实例",缺席的实例不会被算作"未更新"。所以面板必须显示"总数 / 已更新 / 未知"三个数。

// 配置生效面板应当有的三个数
total_instances: 24
updated: 21              # 上报版本 == 最新
unknown: 3               # 最近 5 分钟无上报 → 视为未知,不是"已更新"
// 反例:只显示 "24/24 已更新",而其中 3 个是"没有数据"

5. 灰度下发与回滚

阶段 做法
1 单实例 先推一台,观察
2 小比例 10% 实例
3 全量 其余实例
4 回滚 推旧版本(带更高版本号)

第四项有个"版本号陷阱":回滚要发一个"更新的版本号",而不是重新发旧版本号灰度还要有"自动停止"条件:单实例应用新配置后,若错误率或延迟超阈值即停止推进。这个条件应当由发布系统判断,而不是靠人盯面板。
,否则实例会认为"这个版本我已经有了"而忽略。

所以配置版本应当单调递增,回滚 = 发布一个"内容等于旧版"的新版本。

// 回滚的正确做法
v03: limit=1800     ← 当前
v04: limit=900      ← 出问题
v05: limit=1800     ← 回滚(内容同 v03,版本号更大)
// 反例:重发 v03 → 实例比较版本后判定"无变化" → 不生效

6. 排错:五种"配置没生效"

现象 成因 处理方向
部分实例用旧配置 推送漏达 加兜底轮询
面板显示全量更新 只统计已上报 显示未知数
回滚不生效 重发了旧版本号 发更高版本号
生效延迟大 轮询间隔太长 长轮询或缩短间隔
同时有新旧值 无版本号无法比较 加单调版本

第二项(面板口径)还有一个常见错误:把"推送成功"计入"已更新"。推送成功只表示服务端发出了请求,与实例是否应用无关。
第五项在多来源配置里最常见:本地文件、配置中心、环境变量三处都有同一个键,最终生效哪一个取决于优先级——而这需要在启动日志里打印来源。

7. 边界:什么情况推送或拉取都够

场景 为什么 做法
几乎不变的配置 延迟无意义 启动时拉一次
单实例部署 无分发问题 本地文件即可
允许分钟级生效 拉取足够 30–60 秒轮询
已有平台统一管理 平台负责 跟随平台

判断口径一句话:配置错了以后,多久能被发现并修正。最后一条与"配置粒度"有关的边界:一次下发的粒度决定了故障半径。把 200 个键打包成一份配置,改一个键就要全量下发,任何一处格式错误都会让整份配置失效——所以按"变更频率"拆包比按"业务模块"拆包更安全。
若容忍窗口只有几秒(限流、熔断),要推送 + 回执;若容忍几分钟(业务参数),拉取足够。


8. 自测

  1. 推送与拉取的核心差别是什么?为什么"我发出去了"不等于"它生效了"?
  2. 推送必须配的三件事分别解决什么问题?为什么版本号能防乱序?
  3. 长轮询如何兼顾低延迟与低压力?抖动为什么是必须的?
  4. 为什么"心跳里带版本"是最划算的一致性证据?面板为什么要显示"未知"?
  5. 回滚为什么必须发更高的版本号?重发旧版本号会发生什么?
  6. 判断"选推送还是拉取"的那个问题是什么?
S17-A26 · 配置分发:推送还是拉取
https://www.clxhxhhr.top/posts/6487/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。