「我们团队效率怎么样?」这个问题在工程团队里很难回答。代码行数不能衡量价值,任务完成数不能反映质量,部署次数也可能是小修小补堆出来的。DORA(DevOps Research and Assessment)研究团队从 2014 年开始分析几千个工程团队的数据,最终识别出四个指标,能有效区分高效能团队和低效能团队。这四个指标本质上都在衡量同一件事:用户价值从代码提交到上线的流速,以及出问题后的恢复能力。
为什么 DORA 而不是其他指标#
工程管理者常用的「效率指标」有很多问题:
- 代码行数:激励写冗余代码,重构会「减少」产出
- Story 点数:各团队评分标准不同,无法横向比较
- Bug 修复数:会激励多报 Bug 再关闭
- 测试覆盖率:容易写无意义的测试来提升数字
DORA 指标的核心洞察是:把软件交付看作一条价值流,用流速和质量来衡量效能。四个指标覆盖了这条价值流的两个维度——吞吐量(Throughput)和稳定性(Stability):
- 吞吐量:部署频率、变更前置时间
- 稳定性:变更失败率、故障恢复时间(MTTR)
高效能团队这两个维度都高,低效能团队通常会以牺牲一个维度来换取另一个。
四个指标详解#
1. 部署频率(Deployment Frequency)#
定义:生产环境的代码变更部署频率。
| 绩效级别 | 部署频率 |
|---|---|
| Elite | 每天多次 |
| High | 每天一次到每周一次 |
| Medium | 每周一次到每月一次 |
| Low | 每月一次或更慢 |
部署频率低的团队通常有以下特征:
- 发布流程复杂、需要人工审批多个环节
- 测试套件太慢(超过 30 分钟),开发者不愿意频繁触发
- 发布批次大(「攒够了再一起发」),导致风险集中
部署频率本身不是目的。频繁部署的意义在于:每次变更更小,出问题更容易定位,回滚风险更低。
2. 变更前置时间(Lead Time for Changes)#
定义:从代码提交到该代码在生产环境运行的时间。
| 绩效级别 | 变更前置时间 |
|---|---|
| Elite | 小于 1 小时 |
| High | 1 天到 1 周 |
| Medium | 1 周到 1 个月 |
| Low | 超过 1 个月 |
变更前置时间是「从想法到用户」的时间代理指标。长前置时间意味着:
- 快速实验的成本很高(验证一个假设要等几天)
- 紧急修复无法快速到达用户
- 开发者的上下文切换成本高(提交代码后要等很久才知道结果)
前置时间 = Code Review 等待时间 + CI 构建时间 + 部署等待时间 + 人工审批时间。每个环节都是可以优化的。
3. 变更失败率(Change Failure Rate)#
定义:导致生产环境降级、需要紧急修复或回滚的变更比例。
| 绩效级别 | 变更失败率 |
|---|---|
| Elite | 0-5% |
| High | 5-10% |
| Medium | 10-15% |
| Low | 15-45% |
变更失败率高说明质量保障环节有漏洞:测试不充分、缺乏金丝雀/灰度发布、代码审查流于形式等。
注意:降低变更失败率不是靠减少部署频率(攒批次发布会让每次变更更大,实际上更容易出问题)。正确的方法是加强测试自动化、引入金丝雀发布。
4. 故障恢复时间(Time to Restore Service,即 MTTR)#
定义:从生产环境发生故障到恢复服务的时间。
| 绩效级别 | MTTR |
|---|---|
| Elite | 小于 1 小时 |
| High | 小于 1 天 |
| Medium | 1 天到 1 周 |
| Low | 超过 1 周 |
MTTR 高的常见原因:
- 告警滞后(用户先发现,而不是监控系统)
- 缺少 Runbook(on-call 需要临时找人咨询)
- 回滚流程复杂(没有一键回滚)
- 跨团队协作链路长(需要多个团队确认才能操作)
指标采集设计#
DORA 指标的采集需要从 CI/CD 系统和 Incident 管理系统获取数据,然后关联分析。
部署频率采集#
从 CI/CD 流水线的完成事件中提取:
# 伪代码:从流水线事件中统计部署频率
from datetime import datetime, timedelta
def calculate_deployment_frequency(deployments: list[dict], days: int = 30) -> dict:
"""
deployments: [{"env": "prod", "timestamp": "2026-04-01T10:00:00Z", "service": "api", "status": "success"}]
"""
cutoff = datetime.utcnow() - timedelta(days=days)
prod_deploys = [
d for d in deployments
if d["env"] == "prod"
and d["status"] == "success"
and datetime.fromisoformat(d["timestamp"]) > cutoff
]
return {
"total": len(prod_deploys),
"per_day": len(prod_deploys) / days,
"level": classify_deployment_frequency(len(prod_deploys) / days)
}
def classify_deployment_frequency(per_day: float) -> str:
if per_day >= 1:
return "Elite" # 每天至少一次
elif per_day >= 1/7:
return "High" # 每周至少一次
elif per_day >= 1/30:
return "Medium" # 每月至少一次
else:
return "Low"
数据来源:流水线 Webhook 事件推送到消息队列,存入时序数据库(Prometheus/InfluxDB)或数据仓库。
变更前置时间采集#
需要关联 git commit 时间和部署完成时间:
def calculate_lead_time(commits: list[dict], deployments: list[dict]) -> list[float]:
"""
commits: [{"sha": "abc123", "timestamp": "2026-04-01T09:00:00Z"}]
deployments: [{"sha": "abc123", "prod_timestamp": "2026-04-01T10:30:00Z"}]
"""
sha_to_commit_time = {c["sha"]: c["timestamp"] for c in commits}
lead_times = []
for deploy in deployments:
if deploy["sha"] in sha_to_commit_time:
commit_time = datetime.fromisoformat(sha_to_commit_time[deploy["sha"]])
deploy_time = datetime.fromisoformat(deploy["prod_timestamp"])
lead_time_hours = (deploy_time - commit_time).total_seconds() / 3600
lead_times.append(lead_time_hours)
return lead_times
关键点:取 中位数(p50)而不是平均值。一次大型特性分支合并可能有几十个 commit,平均值会被这些老 commit 拉高,不反映正常情况。
变更失败率采集#
需要关联部署事件和 Incident/回滚事件:
def calculate_change_failure_rate(
deployments: list[dict],
incidents: list[dict],
rollbacks: list[dict],
window_hours: int = 24
) -> float:
"""
判断一次部署是否是「失败变更」:
- 部署后 24 小时内有 P1/P2 Incident
- 或者触发了回滚
"""
failure_deploys = set()
for deploy in deployments:
deploy_time = datetime.fromisoformat(deploy["timestamp"])
window_end = deploy_time + timedelta(hours=window_hours)
# 检查窗口内是否有高优先级 Incident
for incident in incidents:
incident_time = datetime.fromisoformat(incident["created_at"])
if (deploy_time <= incident_time <= window_end
and incident["severity"] in ["P1", "P2"]):
failure_deploys.add(deploy["id"])
break
# 加上所有回滚的部署
for rollback in rollbacks:
failure_deploys.add(rollback["original_deploy_id"])
return len(failure_deploys) / len(deployments) if deployments else 0
MTTR 采集#
def calculate_mttr(incidents: list[dict]) -> dict:
"""
incidents: [{"id": "INC-123", "created_at": "...", "resolved_at": "...", "severity": "P1"}]
"""
recovery_times = []
for incident in incidents:
if not incident.get("resolved_at"):
continue # 未解决的不计入
created = datetime.fromisoformat(incident["created_at"])
resolved = datetime.fromisoformat(incident["resolved_at"])
recovery_hours = (resolved - created).total_seconds() / 3600
recovery_times.append(recovery_hours)
if not recovery_times:
return {"mttr_p50": None, "mttr_p90": None}
recovery_times.sort()
n = len(recovery_times)
return {
"mttr_p50": recovery_times[int(n * 0.5)],
"mttr_p90": recovery_times[int(n * 0.9)],
"sample_size": n
}
从指标到行动:找到真正的瓶颈#
DORA 指标只是告诉你「哪里有问题」,真正的价值在于顺着指标找到根因。
部署频率低#
排查路径:
- Code Review 等待时间:平均 PR 在 Review 前等多久?> 4 小时说明 Review 资源不足或流程有问题
- CI 时间:构建 + 测试总时长 > 15 分钟,开发者就会倾向于「攒着发」
- 发布窗口限制:只允许工作时间发布、需要多人审批,都会降低频率
- Feature Flag 缺失:没有 Feature Flag 时,未完成的功能无法合并主干,导致长期 Feature Branch
改进方向:
CI 优化 → 并行测试、缓存依赖、分层构建
Review 效率 → 小 PR 文化、自动化 lint/format 减少 nit 评论
发布自动化 → 零人工干预的自动发布到 QA/PRE,减少审批环节
变更前置时间高#
细化测量每个阶段的时间分布:
提交 → PR 创建:开发延迟(通常忽略)
PR 创建 → 首次 Review:Review 等待时间
首次 Review → PR 合并:Review 轮次 × 每轮时间
PR 合并 → 部署完成:CI 构建 + 排队等待 + 部署时间
用 Grafana 画出每个阶段的分布图,哪个阶段的 p90 最长,就重点优化哪个。
变更失败率高#
首先确认度量本身是否准确:一次部署后 1 小时的 Incident 是因为这次部署吗?如果不是,不能归入变更失败。
真正的改进方向:
- 测试覆盖不足:统计哪类功能的失败率高,对应加集成测试
- 灰度发布缺失:引入金丝雀发布,让 1% 流量先验证,失败了自动回滚
- 配置变更:配置变更导致的故障很常见,引入配置变更的 Dry-run 验证
MTTR 高#
MTTR 高通常有几个明确的卡点:
| 卡点 | 典型症状 | 改进措施 |
|---|---|---|
| 告警滞后 | 用户先反馈,监控后触发 | 主动探测(黑盒监控)、SLO 告警 |
| 定位慢 | on-call 需要 30 分钟才找到原因 | 完善 Dashboard、结构化日志 |
| 缺少 Runbook | 每次都需要找专家 | 为高频故障写 Runbook |
| 回滚慢 | 需要手动操作多个步骤 | 一键回滚、ArgoCD 自动同步 |
Grafana Dashboard 构建#
把四个指标放到一个 Dashboard,实时可见:
# Prometheus Recording Rule:预计算 DORA 指标
groups:
- name: dora_metrics
interval: 1h
rules:
# 过去 30 天部署次数(每日)
- record: dora:deployment_frequency:daily
expr: |
increase(cicd_deployments_total{env="prod",status="success"}[1d])
# 变更失败率(7 天滚动窗口)
- record: dora:change_failure_rate:7d
expr: |
(
increase(cicd_deployments_total{env="prod",status="failed"}[7d])
+ increase(rollbacks_total{env="prod"}[7d])
)
/
increase(cicd_deployments_total{env="prod"}[7d])
Dashboard 面板建议:
- 部署频率:折线图,按天展示 30 天趋势,加上目标线(比如每天 3 次)
- 变更前置时间:热力图,x 轴时间,颜色深度表示 p50 时长
- 变更失败率:百分比折线图,标出 Elite/High/Medium 的阈值区域
- MTTR:箱线图,展示 p50 和 p90,区分 P1/P2/P3 故障等级
DORA 与 OKR:把改进目标写入团队规划#
DORA 指标是很好的 OKR Key Result 载体,因为它们可量化、有基线、改进方向明确:
Objective: 提升平台工程效能,加速价值交付
KR1: 变更前置时间(p50)从当前 4 小时降低到 1 小时
KR2: 部署频率从每天 2 次提升到每天 5 次
KR3: 变更失败率从 12% 降低到 5% 以下
KR4: MTTR(P1 故障)从 45 分钟降低到 20 分钟以内
每个 KR 对应具体的 Initiative:
- KR1:优化 CI 并行度(目标:CI 时间从 20 分钟→8 分钟)+ 减少 Review 等待
- KR2:引入自动部署到 PRE 环境(合并主干后自动触发)
- KR3:上线金丝雀发布能力,关键服务必须经过金丝雀阶段
- KR4:为 TOP 5 高频故障场景写 Runbook,上线主动探测告警
常见误用:DORA 不是考核工具#
最后这点很重要,也是很多团队踩的坑:
不要用 DORA 做跨团队横向排名。基础设施团队的部署频率天然比业务团队低,不能因此说基础设施团队效能差。不同业务属性、不同阶段的团队,DORA 基线完全不同。
不要让指标成为目标本身。如果团队为了提升部署频率,把每次提交都触发一次部署(包括文档修改、注释变更),数字好看了但没有实际价值。DORA 度量的是「有意义的生产变更」。
DORA 是诊断工具,不是评分工具。发现某个指标差,下一步是找根因,而不是给团队打低分。团队看到自己的数据后,应该有「原来瓶颈在这里,我们来修它」的动力,而不是「怎么把数字刷好看」的焦虑。
正确的使用姿势:团队自己看自己的数据,横向比较只和自己的历史基线比,平台工程团队的价值体现在帮所有团队提升这些指标。
DORA 的研究结论非常明确:高效能团队的四个指标都高,而不是以稳定性换吞吐量,或者以吞吐量换稳定性。这说明软件交付效能的提升不是零和博弈——好的工程实践让速度和质量可以同时提升。






