双十一当天上线了一个改价逻辑。评审过了、灰度也过了、监控没报警。但当天流量是平日十几倍,改价逻辑在极端并发下把一次缓存穿透放大成数据库热点,二十分钟里订单创建失败率上升。复盘第一句话是:这个改动为什么放在了今天。
发布从来不是纯技术节奏。业务有自己的时间轴:大促、结算、监管申报、财报。技术侧的"随时可发"与业务侧的"某些时刻不能出错"之间需要一张显式映射——不是流程文件,是能进流水线的判据。
1. 四类约束与禁发强度
| 约束类型 | 时间尺度 | 强度 | 判定依据 | 违反后果 |
|---|---|---|---|---|
| 大促 | 天级,日期已知 | 强 | 流量峰值倍数 | 故障被放大,回滚也慢 |
| 结算期 | 小时级,跨日常态 | 强 | 对账窗口与日切点 | 对账不平,人工核账 |
| 监管节点 | 月/季级,日期可查 | 中到强 | 申报/审计截止日 | 证据链缺口,补不上 |
| 财报窗口 | 季度级 | 中 | 财务封账 | 披露口径不一致 |
强度取决于窗口内出事能不能回滚:大促让回滚本身变危险,结算是半成品数据回滚不干净,监管是证据带时间戳,财报口径可事后修正。
统一判据:事故在该窗口内的恢复时间,是否超过窗口本身的长度。 超过就是强禁发。
//─── 冻结期:发布停,其余照旧
freeze_window:
from: 2026-11-08T18:00+08:00
to: 2026-11-12T06:00+08:00
allowed: [config_hot_reload, read_only_index_build, observability_tuning]
needs_approval: [code_deploy, schema_change, cache_key_change]
"禁发"不等于"停工"。冻结期该做的是:演练、观测强化(不动业务代码)、债务清理、文档与预案、验证过的回滚脚本。最容易犯的错是把冻结期当"提前发版"的机会:既然下周发不了,这周多发一点——恰好把风险集中到冻结前。
2. 冻结长度、那次发布,与例外通道
| 冻结长度 | 风险等级 | 典型场景 | 代价 |
|---|---|---|---|
| 不冻结 | 低 | 内部工具、无季节性 B 端系统 | 大促仍可能撤版本 |
| 24 小时 | 中 | 大促日、单日结算 | 一次发布窗口顺延 |
| 3–5 天 | 中高 | 年度大促、月末结算 | 一两个迭代被打乱 |
| 2 周以上 | 高 | 监管申报、静默期 | 里程碑要重排 |
| 无上限 | 极高 | 关键监管事件 | 团队进维护态 |
长度不是越短越好。冻结 24 小时但每天发三次的团队,把风险搬到冻结前那一次发布;冻结两周但没灰度能力的团队,把风险搬到冻结后第一次发布。判据:长度应与冻结前后的发布密度匹配。
冻结前最后一次发布最危险——它是唯一一次带着"下一个可用窗口在两周后"前提的发布,也是唯一一次会把积压改动一起带上去的发布。
//─── 冻结前检查:把"能等"和"不能等"分开
$ git log --oneline --no-merges --since="2 weeks ago" | wc -l // 积压量
$ ./scripts/check-freeze-diff.sh v1.4.1 HEAD // 有无 schema / cache 面
推荐组合是"只发关键修复 + 准备回滚脚本"。回滚脚本必须在冻结前验证过一次——没验证过的脚本只是个文件。冻结前一天要有答案的是:必须发版时走哪条通道、谁批、多久。
| 通道设计 | 审批时间 | 被滥用风险 | 适用 |
|---|---|---|---|
| 单人批 | 分钟级 | 高,很快成默认 | 小团队、低风险 |
| 双人批(业务+技术) | 半小时到几小时 | 中 | 多数场景 |
| 委员会批 | 一天以上 | 低 | 强监管 |
//─── 例外通道:成本写清,否则它就会变成默认通道
emergency_release:
trigger: [生产故障, 监管要求, 安全事件]
approval: [业务负责人, 技术负责人] // 缺一不可
max_duration: 24h // 只覆盖这一次发布
required: [rollback_plan_verified, affected_scope_listed, oncall_notified]
//─── 例外留痕:事后能回答"破了多少次规矩"
$ rg -n 'emergency_release' audit/change-*.log | wc -l
判据是事后成本:每走一次例外记一次账(时间、原因、影响)。一个月走五次,说明冻结长度与业务节奏不匹配——问题不在通道,在日历。
3. 结算与监管:两类"错不起"的窗口
结算与普通业务最大的区别:错误不表现为报错,而表现为数字不对,要到对账时才发现,那时窗口已关。
| 约束 | 表现 | 发布层面的含义 |
|---|---|---|
| 幂等 | 重试不能重复入账 | 入账改动在冻结外单发 |
| 对账 | 与科目或第三方逐笔核对 | 发版避开对账运行时段 |
| 跨日切点 | 前后分属不同账期 | 切点附近禁改账期判定 |
| 汇率/费率 | 带生效时间语义 | 按生效时间回溯,高风险 |
| 冲正 | 错账需反向分录 | 能力须冻结前验证过 |
-- 跨日切点判定集中一处,变更走独立窗口;改动前确认对账任务不在运行
SELECT biz_date, COUNT(*) FROM settle_record
WHERE created_at >= '${cutover_start}' AND created_at < '${cutover_end}'
GROUP BY biz_date;
费率与汇率这类"带生效时间的变更"最容易被当配置随手发,它们实际是行为变更:同一笔订单在生效点前后走不同逻辑,历史数据已按旧逻辑入账,所以要走代码发布流程。
监管节点的技术含义常被低估:申报、审计、备案要求的往往不是"系统正常",而是能证明当时的系统状态。
| 监管要求 | 技术含义 | 冻结期要做的事 |
|---|---|---|
| 数据可追溯 | 变更留痕完整 | 确认日志不轮转丢失 |
| 口径一致 | 申报期口径不变 | 禁改口径映射 |
| 证据可提取 | 可导出指定时段记录 | 提前演练导出 |
| 系统稳定 | 变更受控 | 冻结本身即证据 |
冻结期的存在本身会被当作受控证据,这与"降低故障率"是两个目的,都要写进日历。跨时区的冲突很直观:总部冻结了,某个区域正在月底结算;或区域大促落在总部结算期内。
| 冲突形态 | 判定方式 | 处理 |
|---|---|---|
| 多区域结算日重叠 | 各区域日历取并集 | 过密时按区域冻结 |
| 时区导致切点错位 | 按 UTC 与本地分别算 | 切点不得跨冻结期 |
| 区域大促落在总部结算期 | 日历对齐表 | 提前一期排期 |
4. 日历的责任人与进流水线的方式
"我们不知道下个月有大促"是流程问题,不是技术问题。日历必须有责任人与固定更新节奏。
| 项 | 建议做法 | 理由 |
|---|---|---|
| 责任人 | 业务一人 + 技术一人共同维护 | 单人维护必然过期 |
| 更新节奏 | 每月固定 + 临时插入 | 大促常提前一季度定 |
| 数据来源 | 市场排期、财务封账、合规申报 | 缺一张就有盲区 |
| 发布形式 | 进仓库的配置,进流水线校验 | 没人看的 wiki 页等于没有 |
| 校验 | 流水线检查日期是否在冻结窗内 | 靠人记必然出错 |
//─── 日历进仓库,流水线读它:唯一能防"忘了"的做法
calendars:
- name: annual-promo
windows: ["2026-11-08T18:00+08:00/2026-11-12T06:00+08:00"]
owner: marketing
rules: [no_deploy, no_schema_change]
- name: settlement-daily
windows: ["daily 23:30-01:30 Asia/Shanghai"]
owner: finance
rules: [no_deploy, no_cutover_change]
//─── 门禁:命中冻结窗就拦住,例外走审批
$ python3 scripts/check_calendar.py --now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
&& echo "OK: 可发布" || echo "BLOCKED: 命中冻结窗,需走例外通道"
冻结期的观测强化必须提前做:平时不看的指标要在冻结前打开。
| 观测项 | 平时 | 冻结期 | 为什么 |
|---|---|---|---|
| 链路耗时与错误率 | 有告警 | 阈值收紧 | 没有发布,波动即异常 |
| 容量水位 | 看趋势 | 逐小时看 | 冻结期无法扩容 |
| 对账/结算任务 | 失败告警 | 运行后人工确认 | 数字错不报错 |
| 依赖方状态 | 不常看 | 主动订阅 | 别人发版也会打到你 |
| 业务指标 | 运营看 | 技术也看 | 业务异常常先于技术异常 |
5. 度量与边界
| 指标 | 计算方式 | 读数含义 |
|---|---|---|
| 冻结期长度 | 全年冻结天数 ÷ 365 | 与发布容量冲突的量 |
| 例外发布次数 | 冻结期内走例外次数 | 季度超两次说明日历不合理 |
| 冻结期事故数 | 冻结期内生产事故数 | 验证冻结是否起作用 |
| 日历准确率 | 提前 30 天已知 ÷ 实际发生 | 流程成熟度 |
| 解冻首日风险 | 解冻后 24 小时事故数 | 顺延变更的账单 |
"日历准确率"最值得单独看:一半冻结窗靠临时通知,说明日历只是形式,起作用的是临时叫停。
| 场景 | 建议 | 理由 |
|---|---|---|
| 业务无季节性 | 一句"避开值班交接" | 正式日历成本高于收益 |
| 与交易无直接关系 | 不冻结 | 事故影响范围有限 |
| 发布低于月一次 | 压到一句通知 | 天然避开多数风险窗 |
| 有灰度与秒级回滚 | 冻结期可缩短 | 恢复短,禁发强度可降 |
| 强监管、要提交证据 | 日历正式化留痕 | 冻结本身就是证据 |
正式日历的成本不在配置,而在每月有人对齐三张业务表并维护例外账。没人做时日历两三个月就过期,而过期的日历比没有更危险:流水线放行,警惕已关闭。
6. 排错:五种"发完才发现撞了"
| 现象 | 根因 | 处理 |
|---|---|---|
| 大促当天故障率上升 | 改动没问题,被流量放大 | 补"大促前 N 天禁发" |
| 对账不平 | 发版落在对账任务运行期 | 按任务运行时段定窗口 |
| 审计记录导不出来 | 日志轮转变通删掉窗口内记录 | 冻结期延长日志保留期 |
| 解冻首日连续故障 | 顺延变更集中上线 | 首日限制发布条数 |
| 团队说不知道有冻结 | 只在群里通知 | 日历进仓库 + 门禁 |
第一行是开头场景的复盘结论:那天的改动换个日子发可能完全没问题——这就是业务日历的全部理由。
7. 自测
- 判断某类业务节点该不该强禁发,用哪句话做判据?
- 冻结期长度应该和什么指标匹配,而不是拍一个"两周"?
- 冻结前最后一次发布为什么风险最高,推荐怎么发?
- 例外发布达到什么量级时,说明问题不在通道而在日历?
- 费率与汇率这类"带生效时间的改动"为什么不能当配置发?
- 什么时候可以不要正式日历机制,为什么?