交付周期是多少天:先说清楚从哪个事件算到哪个事件
两个团队都说自己的交付周期是「中位数 3 天」,一比较才发现,一个从需求被排进迭代算起,一个从开发者开始写代码算起;一个只统计已经上线的需求,另一个把重开过的需求按第一次上线算。看板改版后周期「缩短了一半」,后来查明是新看板每天只同步一次状态,日内的反复全都看不见了。
交付周期、吞吐量、在制工作这些流转指标,用来回答「需求从提出到上线要多久、卡在哪里」。它们的计算本身很简单,难的是口径:工作项的边界、开始和结束各取哪个事件、重开怎么算、取消和未完成的怎么办、数据是事件历史还是每日快照。口径不同,同一批工作得出的数字可以差好几倍,而且每一种都「算对了」。
本文用 7 个工作项的合成状态事件,把这些口径逐一算一遍。数据是手写的,只用来说明口径怎样改变数字,不代表任何团队的真实情况。实验见文末。
一、数据:状态事件,而不是状态字段
每条事件记录:事件 ID、工作项、发生时刻、到达时刻(进入统计系统的时刻)和新状态。状态有 ready(排队)、doing(处理中)、review(等评审)、done 与 cancelled。
# (事件 ID, 工作项, 发生时刻(小时), 到达时刻, 新状态)
("e5", "B", 2, 2, "ready"), ("e6", "B", 4, 4, "doing"), ("e7", "B", 10, 10, "review"),
("e8", "B", 12, 12, "done"), ("e9", "B", 14, 14, "doing"), ("e10", "B", 20, 20, "done"),
("e13", "C", 54, 90, "review"), # 评审事件晚到 36 小时
("e16", "D", 8, 8, "doing"), ("e16", "D", 8, 9, "doing"), # 同一事件重复投递只保存「当前状态」的字段,回答不了「这一项在评审环节等了多久」;所以流转指标的数据源应该是带时刻的状态事件。事件本身也有两个常见问题,后面会看到:重复投递和乱序到达。
二、起点:同样的工作,两种周期
从进入 ready 算起:A 70h、B 18h、C 55h、F 4h;中位数 36.5h
从第一次进入 doing 算起:A 40h、B 16h、C 10h、F 3h;中位数 13h从 ready 算起的是用户视角的等待:需求被接受后多久能用上。从 doing 算起的是开发视角的处理周期,排队的时间不在里面。两者都有用,但不能混着比较,开头那两个「中位数 3 天」说的就是不同的东西。C 的差别最大:它在 ready 里排了 45 个小时,真正处理只用了 4 个小时。
三、重开、快照与乱序
B 第一次完成在 12h、重开后最后完成在 20h:按第一次完成算周期 10h,按最后一次算 18h;重开次数 1
B 在每天结束时的快照:['done', 'done'];事件历史里同一天经历了 ['ready', 'doing', 'review', 'done', 'doing', 'done'],快照看不到重开重开的工作项,按第一次完成算会让周期看起来更短,还会把返工藏起来。更稳妥的做法是按最后一次完成计算周期,同时单独统计重开次数。
每日快照只记录每天结束时的状态,B 当天上午完成、下午重开、晚上再完成,在快照里就是一直「完成」。开头那次「周期缩短一半」,就是快照丢掉了这些往返。快照适合看某个时刻的积压,不适合重建流转过程。
按到达顺序处理 C 的事件:['ready', 'doing', 'done', 'review'],最后状态 review(晚到的评审事件把已完成的工作项拉回 review)
按发生时刻排序:['ready', 'doing', 'review', 'done'],最后状态 done
D 的事件不去重:['ready', 'doing', 'doing', 'cancelled'];按事件 ID 去重:['ready', 'doing', 'cancelled']事件从多个系统汇总时,到达顺序不等于发生顺序。按到达顺序重放,晚到的评审事件会把已经完成的 C 拉回「等评审」。所以重建状态要按发生时刻排序,并且按事件 ID 去重。这和消息乱序时用版本号拒绝旧值是同一类问题。
四、处理与等待
A:处理 10h、等待 60h(排队与等评审),处理时间占端到端 14%
C:处理 4h、等待 51h(排队与等评审),处理时间占端到端 7%
F:处理 2h、等待 2h(排队与等评审),处理时间占端到端 50%把端到端周期拆成「处理」和「等待」两部分,问题的位置就清楚多了:A 和 C 的大部分时间花在排队和等评审上,让开发写得更快,对它们几乎没有帮助。这个比例常被称为流动效率。它能帮助形成「评审是不是瓶颈」这样的假设,但不能直接证明因果,评审等待长,也可能是因为评审人同时在处理线上故障。下一步应该去看具体的工作项,而不是直接调整流程。
五、没完成的不能不算
只统计已完成的 4 项,中位周期 36.5h;在第 96h 时仍未完成的 2 项已等待 G 95h、E 86h,其中 G 从未开始;取消 1 项(D)不计入周期
各时刻处在 ready、doing、review 的工作项数:{12: 6, 14: 7, 24: 5, 48: 4, 72: 2, 96: 2}(B 在 12h 完成、14h 重开,又回到在制)周期只能对已完成的工作项计算,这天然漏掉了还卡着的那些:G 已经排了 95 个小时,比任何一个已完成项都久,却不会出现在周期统计里。只看已完成项,最慢的工作永远在统计之外。所以要同时报告在制工作的已等待时长(工作项年龄),以及在制工作的数量。取消的工作项单独计数,不计入周期,也不能直接从分母里消失。
周期、吞吐量和在制数量之间有一个已知关系(利特尔法则):在一个稳定的系统里,长期平均的在制数量等于吞吐量乘以平均周期。它的前提是三者使用同一套口径、在足够长且稳定的时间窗口上统计。如果算出来对不上,先怀疑口径,而不是怀疑这个关系。
六、口径表与使用边界
| 要定的口径 | 选项 | 写在报告里 |
|---|---|---|
| 工作项 | 需求、用户故事、缺陷,是否混在一起 | 类型与粒度 |
| 起点与终点 | 进入 ready、第一次 doing;第一次或最后一次 done、上线 | 起止事件 |
| 重开 | 按最后一次完成,另计重开次数 | 重开次数 |
| 未完成与取消 | 单独报告年龄与数量 | 截止时刻 |
| 数据源 | 事件历史,按发生时刻排序、按 ID 去重 | 是否用了快照 |
| 统计量 | 中位数与分位数,而不只是平均 | 样本数与时间窗口 |
这些指标描述的是工作在系统里怎样流动,用来发现排队和返工。它们不适合给个人排名:一个人的周期长,可能是因为他接的是最难的那一项,或者在等别人;把指标变成考核,团队很快会学会拆小工作项、晚一点把状态改成「处理中」,数字变好看了,流转并没有变快。
配套实验
- codesphere-labs/engineering/delivery-flow-metrics:Python 3.14.8 标准库,7 个工作项的合成事件:起点定义、重开、每日快照、处理与等待、只统计已完成项、乱序与重复事件、在制工作(验证记录)
参考资料
- John D. C. Little,A Proof for the Queuing Formula: L = λW
- DORA:Software delivery performance metrics
- Daniel S. Vacanti,《Actionable Agile Metrics for Predictability》(工作项年龄与周期分布)