59.1 DORA 四指标
一句话
DORA 四指标是把“交付快”和“交付稳”拆成两组、四个能从流水线里采出来的数——吞吐看部署频率与变更前置时间,稳定看变更失败率与恢复时长;单看任何一个都会被优化成假象,而官方口径在 2024 年已经扩成五个(多了返工率)。
为什么是这四个
2014 年 DORA 想把“IT 性能”量化,试了一堆变量;2015 年的研究把它收敛成两个统计上独立的因子:
| 因子 | 指标 | 回答的问题 | 单位 |
|---|---|---|---|
| 吞吐 | 部署频率 | 多久能上一次线 | 次/周 |
| 吞吐 | 变更前置时间 | 一个提交到生产可用要多久 | 小时 |
| 稳定性 | 变更失败率(CFR) | 多少次上线需要救火 | % |
| 稳定性 | 失败部署恢复时长(FDRT) | 救火要多久 | 分钟 |
| 稳定性(2024 新增) | 部署返工率 | 多少上线是计划外返工 | % |
🔑 两个因子独立,是这四个指标能成立的前提:“快”与“稳”在数据上不是一根轴的两端,所以不能用一个综合分把它们压成一个数。压成一个数,等于放弃了“哪一维在恶化”的诊断能力。
口径:四个指标各自的起止点
| 指标 | 起点 | 终点 | 从哪采 | 常见错法 |
|---|---|---|---|---|
| 部署频率 | — | 每次成功部署到生产 | 部署系统 / CD 流水线(发布方式见 55.1 发布策略) | 同一 sha 重复部署算多次;把测试环境发布算进来 |
| 变更前置时间 | 提交进版本库 | 该提交在生产可用 | git log + 部署记录 | 起点写成“需求提出”(那是需求前置时间,见 59.2 交付周期与吞吐) |
| 变更失败率 | — | 部署后需立即介入(回滚/热修) | 部署记录 + 回滚标记 | 分母只算计划内,或把热修也算成“正常发布” |
| 恢复时长 | 失败部署被发现 | 服务恢复可用 | 事故记录 / 值班日志(流程见 56.5 事故响应) | 把机房断电、上游故障也算进来 |
| 返工率 | — | 计划外部署 | 部署记录的 planned 字段 | 把热修标成计划内,返工率恒为 0 |
三条口径细节,差一个就会让数字前后不可比:
- 分母要写死。CFR = 失败部署 / 全部部署;若只算计划内,本节样例数据是 10%,含计划外是 8.7%——差 1.3 个百分点,跨季度对比就会失真。
- 前置时间算“批内第一个提交”。一次部署含 3 个提交时,从最早的提交算起,否则批量越大数字越好看。
- 恢复时长的终点是“服务恢复”,不是“根因查清”。根因分析可能拖一周,那属于复盘,不进这个数。
四指标已经变成五指标:口径在演进
把“DORA 四指标”当固定不变的教科书,是常见误解。官方口径改过两次,每次都有明确的统计理由:
| 时间 | 变化 | 为什么改 |
|---|---|---|
| 2014–2015 | 从一堆变量收敛为四指标、两因子 | 统计分析显示吞吐与稳定是两个独立因子;顺带证伪了“快就不稳” |
| 2023 | MTTR → 失败部署恢复时长 | 旧的 MTTR 把机房断电、上游故障也算进去,测的不再是“交付能力”;新口径只算由变更引起、需立即介入的失败 |
| 2024 | 新增 部署返工率 | CFR 只是“返工”的代理指标;返工率直接测“计划外工作占比”,补上“通过了但随后返工”的盲区 |
💡 出处:DORA 指标指南 (opens new window)、DORA 指标演进史 (opens new window)。报告本身也在改分档,别把某一年的分档当永久分数线。
行业里流传的分档只能当量级判断用:
| 指标 | 领先(约) | 中位(约) | 落后(约) |
|---|---|---|---|
| 部署频率 | 每天多次 | 每周 1 次 ~ 每月 1 次 | 少于每月 1 次 |
| 变更前置时间 | 小于 1 天 | 1 天 ~ 1 周 | 大于 1 个月 |
| 变更失败率 | 约 5% | 约 15% | 大于 30% |
| 恢复时长 | 小于 1 小时 | 小于 1 天 | 大于 1 周 |
用这张表的正确方式是回答“我在哪个数量级”,不是回答“我离 5% 还差几个点”。
速度与稳定性不是跷跷板
DORA 反复验证的结论是:高绩效团队四个指标同时好,低绩效团队同时差。机制不神秘:
| 机制 | 对吞吐的作用 | 对稳定的作用 |
|---|---|---|
| 小批量 | 前置时间短、部署频率高 | 爆炸半径小,失败代价低 |
| 自动化测试与流水线 | 等待少、反馈快 | 失败在合并前被发现 |
| 可回滚设计(开关、灰度) | 敢发,频率高 | 恢复快(FDRT 低) |
| 主干开发、短分支 | 合并冲突少 | 集成问题早暴露 |
反过来,“慢工出细活”在数据上不成立:批量越大,一次失败要回滚的东西越多,恢复越慢,失败率也不会因为“多检查几天”而下降——多出来的往往只是等待(推导见 40.5 变更成本)。
从 git 与部署记录里把四个数算出来
采集器的输入只有两样:git log 和部署流水线写出的 deploy.jsonl。后者每行一次部署:
{"id": "D000", "ts": "2026-09-02T09:52:04+08:00", "sha": "17f2a96b...", "status": "ok", "planned": true, "recovered_at": null}
{"id": "D007", "ts": "2026-09-09T15:10:00+08:00", "sha": "0e3c9a11...", "status": "failed", "planned": true, "recovered_at": "2026-09-09T15:35:00+08:00"}
{"id": "D007H", "ts": "2026-09-09T17:10:00+08:00", "sha": "0e3c9a11...", "status": "ok", "planned": false, "recovered_at": null}
#!/usr/bin/env python3
"""dora_collect.py —— 从 git 与部署流水线数据算 DORA 指标(无外部依赖)。
用法:python3 dora_collect.py <repo> <deploy.jsonl>
"""
import json
import subprocess
import sys
from datetime import datetime, timezone
from statistics import median
def ts(s):
return datetime.fromisoformat(s).astimezone(timezone.utc)
def pct(xs, p):
xs = sorted(xs)
k = (len(xs) - 1) * p
lo, hi = int(k), min(int(k) + 1, len(xs) - 1)
return xs[lo] + (xs[hi] - xs[lo]) * (k - lo)
def main(repo, deploy_file):
# ① 提交序列(sha -> 提交时间),用于把一次部署还原成"一批提交"
out = subprocess.run(
["git", "-C", repo, "log", "--reverse", "--format=%H %ct"],
check=True, capture_output=True, text=True,
).stdout.split()
commits = [(out[i], datetime.fromtimestamp(int(out[i + 1]), timezone.utc))
for i in range(0, len(out), 2)]
order = {sha: i for i, (sha, _) in enumerate(commits)}
deploys = [json.loads(line) for line in open(deploy_file, encoding="utf-8") if line.strip()]
deploys.sort(key=lambda d: d["ts"])
start, end = ts(deploys[0]["ts"]), ts(deploys[-1]["ts"])
weeks = (end - start).total_seconds() / 86400 / 7
# ② 按计划内部署切批:一批 = 上次部署之后新增的提交
lead_times, prev = [], -1
for d in deploys:
if not d["planned"]:
continue
idx = order[d["sha"]]
if prev + 1 <= idx: # 本批首个提交 → 部署完成
lead_times.append((ts(d["ts"]) - commits[prev + 1][1]).total_seconds() / 3600)
prev = max(prev, idx)
planned = [d for d in deploys if d["planned"]]
failed = [d for d in deploys if d["status"] == "failed"]
rework = [d for d in deploys if not d["planned"]]
recovered = [d for d in failed if d.get("recovered_at")]
df = len(planned) / weeks
cfr = len(failed) / len(deploys)
fdrt = median((ts(d["recovered_at"]) - ts(d["ts"])).total_seconds() / 60 for d in recovered)
rwr = len(rework) / len(deploys)
print(f"窗口 {start:%Y-%m-%d} ~ {end:%Y-%m-%d}({weeks:.1f} 周);"
f"提交 {len(commits)},部署 {len(deploys)}(计划内 {len(planned)},"
f"计划外 {len(rework)},失败 {len(failed)})")
print(f"部署频率 {df:.1f}/周 | 前置时间 中位 {median(lead_times):.1f}h "
f"P85 {pct(lead_times, .85):.1f}h | CFR {cfr:.0%} | "
f"恢复 {fdrt:.0f}m | 返工率 {rwr:.0%}")
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])
样例仓库是 120 个提交、46 次部署(4 次失败、6 次计划外)的三周数据。本机实测输出(macOS 26 / Python 3.14.8 / git 2.56.0):
窗口 2026-09-02 ~ 2026-09-24(3.2 周);提交 120,部署 46(计划内 40,计划外 6,失败 4)
部署频率 12.6/周 | 前置时间 中位 23.5h P85 64.0h | CFR 9% | 恢复 65m | 返工率 13%
逐条读:
- 12.6 次/周属于“每天 1–2 次”的中间档,不是领先档;前置时间中位 23.5 小时(约 1 天)也正好卡在中间档的边界上。
- P85 = 64 小时,是中位数的 2.7 倍。承诺“这个需求多久上线”时,用中位数会有一半概率打脸;对业务承诺应该用 P85(分布为什么长这样,见 59.2 交付周期与吞吐)。
- CFR 9%(4/46)与返工率 13%(6/46)不一致,正好暴露口径问题:4 次失败之外还有 2 次“计划外的功能返工”,它们没进 CFR。这就是 2024 年要加返工率的现实原因——只盯 CFR 会漏掉“通过了但随后返工”的那部分工作。
怎么读:四指标组合起来才是诊断
单点数字只能定档,组合才能定位:
| 观察到的组合 | 症状 | 先查什么 |
|---|---|---|
| 部署频率骤降 + 恢复时长上升 | 进入冻结期:批量变大、回滚变难 | 回滚成本与审批环节(55.3 回滚设计) |
| 部署频率上升 + CFR 上升 | 带病快跑:门禁被绕过 | 测试门禁与灰度放量 |
| 前置时间上升 + CFR 下降 | 批量变大或评审变长 | 等待时间、批大小 |
| 前置时间长 + CFR 高 + 恢复慢 | 系统性交付能力差 | 价值流与约束(59.4 效能改进方法) |
| CFR 低 + 返工率高 | 失败被重新分类成“正常修复” | 部署的 planned 标记是否可信 |
🔑 四指标是团队自省的仪表盘,不是公司排名的计分板。一旦用于排名,被度量者会去优化口径而不是优化交付——机制见 59.3 度量的陷阱。
边界:什么时候别用这套口径
- 同一 sha 被反复部署(配置漂移修复、重启):每次重部署都算部署,频率会被虚高,需要按“变更集”去重。
- 移动端 / 客户端:商店审核 1–3 天,前置时间里混入外部等待,应该单独度量“提交→提审”和“提审→放量”。
- 库 / SDK:没有“生产部署”这个动作,只能度量发布节奏与下游采用率。
- 跨团队求平均:不同应用、不同约束,平均之后既不能定位也不能行动,DORA 官方明确建议按应用/服务分别度量。
反模式
| 反模式 | 后果 |
|---|---|
| 把四指标做成团队排行榜 | 被度量者刷部署、藏失败、改口径,数字变好交付没变 |
| 只报部署频率 | 拆发布、重复部署即可翻倍,且掩盖了批量变小还是真变快 |
| 用平均恢复时长 | 长尾被平均吃掉;一次 8 小时的故障能被 20 次 20 分钟抵消 |
| 前置时间起点定在“需求提出” | 与 DORA 口径混用,跨季度不可比,改进 CI 看不出效果 |
| CFR 分母随手改(含/不含热修) | 数字漂移 1–3 个百分点,趋势线变成锯齿 |
| 跨团队/跨产品求平均 | 差异被抹平,既不能定位瓶颈也不能分配改进 |
| 只采集不行动 | 采集成本照付,团队只学会“填数”,三个月后数据失真 |
| 把部署频率写进 OKR | 目标化的指标必然被优化,频率涨、价值不变(见 59.3) |
关联
- 前置:55.1 发布策略(发布方式是频率与失败率的结构性约束)、40.5 变更成本(四项指标的成本解释)
- 并列:59.2 交付周期与吞吐(分布与利特尔法则)、59.3 度量的陷阱(指标目标化之后)
- 下游:59.4 效能改进方法(拿到基线之后怎么改)、55.3 回滚设计(回滚成本决定恢复时长)、56.5 事故响应(恢复时长的计时起点)
自检
Q1 团队把部署频率从每周 1 次提到每天 5 次,CFR 从 6% 升到 14%,恢复时长从 30 分钟降到 20 分钟。这是变好还是变坏?
▸ 答案:先算单位时间的失败次数:1×6% = 0.06 次/周,5×14% = 0.7 次/周,失败次数涨了约 11 倍;但每次失败的影响面更小、恢复更快。判定要看单次失败的后果:如果失败都是小问题、灰度能拦住、用户无感,这是“用更快的反馈换更高的失败概率”,整体是变好;如果有资损或数据错乱,先降 CFR。关键不是百分比,而是“失败次数 × 单次影响”。
Q2 为什么 DORA 的变更前置时间从 commit 算起,而不是从需求提出算起?
▸ 答案:它要测的是交付管道的效率。需求提出到 commit 之间由优先级、澄清、排期决定,属于需求侧;混进来之后,“改 CI/CD”的效果会被需求等待淹没,指标失去诊断力。需求侧的时间要单独度量(见 59.2 交付周期与吞吐)。
Q3 为什么“恢复时长”不能继续叫 MTTR?
▸ 答案:传统 MTTR 是“平均修复时间”,把所有故障都算进去,包括机房断电、上游依赖故障——那些与“变更交付能力”无关。2023 年 DORA 改成“失败部署恢复时长”:只算由变更引起、需要立即介入的失败,起点是被发现,终点是服务恢复。口径变了,历史 MTTR 数据不能直接接着比。
Q4 五个指标里,哪个最容易被“做”成好看的数字?防线是什么?
▸ 答案:部署频率。拆发布、同一 sha 重复部署、加一次空跑都能翻倍,成本最低。防线是配对看:每次部署的变更数、返工率、CFR;频率涨而每次部署变更数骤降,多半是在拆发布(推演见 59.3)。
Q5 团队 CFR 从 12% 降到 5%,但返工率从 4% 升到 18%,发生了什么?
▸ 答案:失败被重新分类,不是消失。回滚少了,但“计划外返工部署”多了:问题在部署后暴露,团队用“修复发布”而不是“回滚”处理,于是不计入 CFR。这正是 2024 年增加返工率的原因——CFR 只是返工的代理指标,两个一起看才能发现“数字搬家”。
Q6 本节的样例数据里,前置时间中位 23.5 小时、P85 64 小时。对业务承诺“一周内上线”够不够?
▸ 答案:一周 = 168 小时,P85 是 64 小时,看起来够;但要看承诺的是哪一批变更。如果是含周末、含审批的长尾批次,P95 可能已经接近 75 小时(样例数据 P95 = 74.6 小时),再叠加故障与返工就会破 168。承诺应该用 P85/P95 加缓冲,并且明确“从 commit 起算”还是“从需求受理起算”——两者的差可能是几天。
Q7 一家 iOS 应用为主的公司,DORA 指标还适用吗?怎么改口径?
▸ 答案:适用,但要把“部署”拆成两段:提交 → 提审(团队可控,用 DORA 口径)与提审 → 商店放量(外部约束,单独看时长与拒绝率)。服务端配置、灰度放量仍可按标准口径度量。直接把商店审核时间算进前置时间,会让团队为“审核慢”背锅,也让改进方向失真。
Q8 为什么四指标不能压成一个“研发效能分”?
▸ 答案:因为统计上吞吐与稳定是两个独立因子,压成一个加权分,等于假设它们可以互相补偿——而现实里“高频发布 + 高失败率”和“低频发布 + 低失败率”可以是同一个分数的两种完全不同的团队,改进方向相反。分数适合对外汇报,不适合对内诊断。