Skip to content

交付周期是多少天:先说清楚从哪个事件算到哪个事件 ​

两个团队都说自己的交付周期是「中位数 3 天」,一比较才发现,一个从需求被排进迭代算起,一个从开发者开始写代码算起;一个只统计已经上线的需求,另一个把重开过的需求按第一次上线算。看板改版后周期「缩短了一半」,后来查明是新看板每天只同步一次状态,日内的反复全都看不见了。

交付周期、吞吐量、在制工作这些流转指标,用来回答「需求从提出到上线要多久、卡在哪里」。它们的计算本身很简单,难的是口径:工作项的边界、开始和结束各取哪个事件、重开怎么算、取消和未完成的怎么办、数据是事件历史还是每日快照。口径不同,同一批工作得出的数字可以差好几倍,而且每一种都「算对了」。

本文用 7 个工作项的合成状态事件,把这些口径逐一算一遍。数据是手写的,只用来说明口径怎样改变数字,不代表任何团队的真实情况。实验见文末。

A排队处理等评审B重开C排队 45hG一直排队,未完成,不进入周期统计第 1 天结束:快照记下 B = done统计时刻 96h排队与等评审处理重开后的返工已完成 4 项的中位周期:从 ready 算 36.5h,从 doing 算 13h;同一份数据,口径不同
图 1 · 实测(合成事件,时间单位为小时):A 与 C 的大部分时间在排队和等评审;B 在 12h 完成、14h 重开、20h 再完成,每天结束时的快照两天都是 done;G 从 1h 起一直在 ready,到第 96h 仍未开始,只统计已完成项时它不出现。

一、数据:状态事件,而不是状态字段 ​

每条事件记录:事件 ID、工作项、发生时刻、到达时刻(进入统计系统的时刻)和新状态。状态有 ready(排队)、doing(处理中)、review(等评审)、done 与 cancelled。

python
# (事件 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"),   # 同一事件重复投递

只保存「当前状态」的字段,回答不了「这一项在评审环节等了多久」;所以流转指标的数据源应该是带时刻的状态事件。事件本身也有两个常见问题,后面会看到:重复投递和乱序到达。

二、起点:同样的工作,两种周期 ​

text
从进入 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 个小时。

三、重开、快照与乱序 ​

text
B 第一次完成在 12h、重开后最后完成在 20h:按第一次完成算周期 10h,按最后一次算 18h;重开次数 1
B 在每天结束时的快照:['done', 'done'];事件历史里同一天经历了 ['ready', 'doing', 'review', 'done', 'doing', 'done'],快照看不到重开

重开的工作项,按第一次完成算会让周期看起来更短,还会把返工藏起来。更稳妥的做法是按最后一次完成计算周期,同时单独统计重开次数。

每日快照只记录每天结束时的状态,B 当天上午完成、下午重开、晚上再完成,在快照里就是一直「完成」。开头那次「周期缩短一半」,就是快照丢掉了这些往返。快照适合看某个时刻的积压,不适合重建流转过程。

text
按到达顺序处理 C 的事件:['ready', 'doing', 'done', 'review'],最后状态 review(晚到的评审事件把已完成的工作项拉回 review)
按发生时刻排序:['ready', 'doing', 'review', 'done'],最后状态 done
D 的事件不去重:['ready', 'doing', 'doing', 'cancelled'];按事件 ID 去重:['ready', 'doing', 'cancelled']

事件从多个系统汇总时,到达顺序不等于发生顺序。按到达顺序重放,晚到的评审事件会把已经完成的 C 拉回「等评审」。所以重建状态要按发生时刻排序,并且按事件 ID 去重。这和消息乱序时用版本号拒绝旧值是同一类问题。

四、处理与等待 ​

text
A:处理 10h、等待 60h(排队与等评审),处理时间占端到端 14%
C:处理 4h、等待 51h(排队与等评审),处理时间占端到端 7%
F:处理 2h、等待 2h(排队与等评审),处理时间占端到端 50%

把端到端周期拆成「处理」和「等待」两部分,问题的位置就清楚多了:A 和 C 的大部分时间花在排队和等评审上,让开发写得更快,对它们几乎没有帮助。这个比例常被称为流动效率。它能帮助形成「评审是不是瓶颈」这样的假设,但不能直接证明因果,评审等待长,也可能是因为评审人同时在处理线上故障。下一步应该去看具体的工作项,而不是直接调整流程。

五、没完成的不能不算 ​

text
只统计已完成的 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 去重是否用了快照
统计量中位数与分位数,而不只是平均样本数与时间窗口

这些指标描述的是工作在系统里怎样流动,用来发现排队和返工。它们不适合给个人排名:一个人的周期长,可能是因为他接的是最难的那一项,或者在等别人;把指标变成考核,团队很快会学会拆小工作项、晚一点把状态改成「处理中」,数字变好看了,流转并没有变快。


配套实验

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权