59.2 交付周期与吞吐
一句话
交付周期是一条分布,不是一个平均数;吞吐由瓶颈决定,不由总人数决定;两者被利特尔法则锁死(
W = L / λ),所以想同时要短周期和高吞吐,杠杆只有两个:降在制品或提瓶颈产能。
三种“周期”别混着用
| 名称 | 起点 | 终点 | 回答什么问题 | 谁在用 |
|---|---|---|---|---|
| 需求前置时间 | 需求受理 | 生产可用 | 用户从提需求到能用要多久 | 业务、产品 |
| 变更前置时间(DORA,口径见 59.1 DORA 四指标) | 提交进版本库 | 生产可用 | 交付管道本身有多快 | 工程团队 |
| 开发周期(cycle time) | 开始动工 | 完成 | 看板上的流动是否顺畅 | 团队日常 |
三者的差值就是等待:需求前置时间 − 变更前置时间 ≈ 排队等排期、等澄清、等评审;变更前置时间 − 开发周期 ≈ 等合并、等流水线、等发布窗口。改进要先看差在哪一段,否则会把“等排期”的锅算到 CI 头上。
🔑 统一口径比精确数字重要:同一张图上的两条趋势线,若起点定义变过,趋势就是假的。指标定义写进看板说明,改口径要像改 API 一样公告。
为什么必须看分布
本节的样例数据(12 周、174 件完成,仿真实测见 59.3 度量的陷阱):
| 分位 | 小时 | 天 | 含义 |
|---|---|---|---|
| P50 | 80.7 | 3.4 | 一半的工单在此时间内完成 |
| P85 | 160.2 | 6.7 | 85% 的工单在此时间内完成 |
| P95 | 193.0 | 8.0 | 95% 的工单在此时间内完成 |
| 均值 | 96.3 | 4.0 | 被长尾拉高,不代表“典型体验” |
| 最长 | 299.9 | 12.5 | 一条工单拖了三周半 |
均值和 P50 差 20%,P85 是 P50 的 2 倍。对业务承诺用均值,等于承诺了一个 40% 的工单都达不到的时间;用 P85 才是“十次有八次能兑现”。
利特尔法则:三个数被一个等式锁住
在稳态下,排队系统里长期平均:
L = λ × W
L:系统内的平均工作项数(在制品,WIP)
λ:平均吞吐率(件/周)
W:平均交付周期(周)
推导只需一步:把时间切成小段,Σ 工单耗时 = ∫ 在制品数 dt。两边同除以窗口长度 T,左边是“平均工单耗时 × 吞吐”,右边是“时间平均在制品数”,于是 W = L / λ(公式前提与排队论推导见 34.2 性能建模)。
它给的是算术约束,不是建议:
| 想达到 | 必须满足 | 在本节样例(λ = 14.5 件/周) |
|---|---|---|
| 平均周期 ≤ 2 天 | WIP ≤ 14.5 × 2/7 ≈ 4.1 件 | 当前 7.6 件,要砍掉近一半 |
| 平均周期 ≤ 3 天 | WIP ≤ 6.2 件 | 当前偏高超 20% |
| 吞吐翻倍(14.5 → 29 件/周) | 瓶颈产能翻倍,或 WIP 翻倍(周期也翻倍) | 加人若不动瓶颈,只是把队列变长 |
它什么时候不成立
| 失效条件 | 表现 | 处理 |
|---|---|---|
| 非稳态 | 刚加人、刚改流程、需求突增,WIP 在剧烈变化 | 只用长期平均;看趋势不看单周 |
| 窗口不一致 | 用 12 周算吞吐、用排空后的窗口算周期 | 三个量取同一时间窗(本节实测正是这个坑) |
| 工作项类型混装 | 大需求和小 bug 混在一起算平均 | 按类型分层,或统一折算后声明折算规则 |
| 只统计已完成 | 卡住不动的工单被排除,周期看起来很好 | 用“截至今天的存活时间”纳入未完成项 |
| 返工重开 | 同一需求做两遍,只算一次 | 每段开工到完成各计一次,或单列返工率 |
吞吐由瓶颈决定,不由总人数决定
约束理论(TOC)的一句话版本:系统的产出等于瓶颈的产出,非瓶颈上的改善不增加产出,只增加库存。
| 瓶颈位置 | 加开发的后果 | 正确动作 |
|---|---|---|
| 需求澄清 | 开发等需求,WIP 堆在“待开发” | 加产品/分析,或先做需求就绪定义 |
| 代码评审 | 提交排队等评审,周期长尾 | 缩小 PR、评审 SLA、配对评审 |
| 测试环境 | 环境排队,集成等待 | 环境即服务、按需隔离 |
| 发布审批 | 批量变大,前置时间上升 | 风险分级、自动化放行(见 55.1 发布策略) |
🔑 判断瓶颈的方法不是猜,是看队列堆在哪:看板上某一列长期堆积、且下游经常空闲,那一列就是约束。加人加到非瓶颈上,只会让在制品更多、周期更长。
利用率陷阱:把人排满,周期爆炸
单队列、单服务台的平均排队时间(以服务时间为单位):
Wq / S = ρ / (1 − ρ) ρ = 利用率
| 利用率 ρ | 平均排队时间(× 服务时间) | 直观解释 |
|---|---|---|
| 50% | 1.0 | 来一件做一件,几乎不排 |
| 70% | 2.3 | 开始有感 |
| 80% | 4.0 | 排队时间是干活的 4 倍 |
| 90% | 9.0 | 周期是服务时间的 10 倍 |
| 95% | 19.0 | 一点抖动就雪崩 |
| 98% | 49.0 | 实际上已经不可用 |
多服务台(c 个并行开发位)会把这个曲线压平一些,但不会改变形状:接近饱和时,等待时间仍然发散。所以“提升资源利用率”这个财务指标,和“缩短交付周期”在 80% 以上是直接冲突的。
仿真实测:周期、WIP 与利特尔法则
生成 12 周的工单流:6 个并行开发位,服务时长对数正态(中位 2.2 天),前 6 周到达 1.6 件/天、后 6 周 2.3 件/天。本机实测输出(Python 3.14.8):
12 周完成 174 件;吞吐 λ = 14.50 件/周
时间加权平均在制品 L = 7.61 件
交付周期(小时):
P50 80.7 约 3.4 天
P85 160.2 约 6.7 天
P95 193.0 约 8.0 天
均值 96.3 约 4.0 天;最长 299.9 小时
排队等待占周期 39%;中位等待 14.1 小时
窗口:首件到达到最后一件完成 = 91.7 天(比 84 天多出排空时间)
利特尔法则(同窗口):L/λ_T = 7.61/13.28 = 96.3 小时,实测均值 96.3 小时
若错用 84 天窗口:88.2 小时 —— 与实测差 -8%,是窗口不一致,不是法则失效
若吞吐不变、在制品减半:W ≈ 48.2 小时(2.0 天)
读法:
- 利特尔法则精确成立(96.3 vs 96.3),但用错窗口就出现 8% 的“偏差”。现实里团队经常拿“本季度吞吐”配“上季度周期”,误差不是法则的错,是口径的错。
- 等待占 39%:即使没有大排队,仍有近四成时间在等。VSM 的典型结论是等待占 70%–95%,这个仿真已经算“流畅”的了;改进先砍等待。
- 在制品减半 → 周期减半(96.3 → 48.2 小时),前提是吞吐不变。如果砍 WIP 时把开发位也砍了,吞吐会掉,法则照样成立,只是两边一起变差。
生成与统计的完整脚本:
#!/usr/bin/env python3
"""flow_stats.py —— 事件仿真生成工单流,再算吞吐 / 时间加权 WIP / 周期分布。"""
import heapq
import math
import random
from statistics import median
random.seed(59)
C, DAYS, BASE, BUMP = 6, 84.0, 1.6, 2.3 # 6 个开发位;12 周;前后两段到达率
MEDIAN_SERVICE = 2.2 # 单件服务时长中位数(天)
def service_days():
return math.exp(math.log(MEDIAN_SERVICE) + random.gauss(0, 0.55))
def simulate():
t, items, events = 0.0, [], []
while t < DAYS:
t += random.expovariate(BASE if t < 42 else BUMP)
if t >= DAYS:
break
items.append({"id": len(items), "arrive": t, "start": None, "done": None})
events.append((t, "arrive", items[-1]["id"]))
heapq.heapify(events)
by_id = {it["id"]: it for it in items}
queue, free = [], C
while events:
t, kind, i = heapq.heappop(events)
if kind == "arrive":
if free:
free -= 1
by_id[i]["start"] = t
heapq.heappush(events, (t + service_days(), "done", i))
else:
queue.append(i)
else:
by_id[i]["done"] = t
if queue:
j = queue.pop(0)
by_id[j]["start"] = t
heapq.heappush(events, (t + service_days(), "done", j))
else:
free += 1
return [it for it in items if it["done"] is not None]
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)
items = simulate()
weeks, n = DAYS / 7, len(items)
throughput = n / weeks
# 时间加权平均在制品:每条工单在系统内的区间对时间积分
points = sorted({p for it in items for p in (it["arrive"], it["done"])})
area = sum(sum(1 for it in items if it["arrive"] <= (a + b) / 2 < it["done"]) * (b - a)
for a, b in zip(points, points[1:]))
avg_wip = area / (points[-1] - points[0])
cycle = [(it["done"] - it["arrive"]) * 24 for it in items]
wait = [(it["start"] - it["arrive"]) * 24 for it in items]
print(f"12 周完成 {n} 件;吞吐 λ = {throughput:.2f} 件/周")
print(f"时间加权平均在制品 L = {avg_wip:.2f} 件")
print("交付周期(小时):")
for p in (0.5, 0.85, 0.95):
print(f" P{int(p*100):<3} {pct(cycle, p):>6.1f} 约 {pct(cycle, p)/24:>4.1f} 天")
print(f" 均值 {sum(cycle)/n:>6.1f} 约 {sum(cycle)/n/24:>4.1f} 天;最长 {max(cycle):.1f} 小时")
print(f" 排队等待占周期 {sum(wait)/sum(cycle):.0%};中位等待 {median(wait):.1f} 小时")
t0, t1 = min(it["arrive"] for it in items), max(it["done"] for it in items)
lam_T = n / ((t1 - t0) / 7)
correct = (avg_wip / lam_T) * 7 * 24
wrong = (avg_wip / throughput) * 7 * 24
print(f"窗口:首件到达到最后一件完成 = {t1-t0:.1f} 天(比 84 天多出排空时间)")
print(f"利特尔法则(同窗口):L/λ_T = {avg_wip:.2f}/{lam_T:.2f} = {correct:.1f} 小时,"
f"实测均值 {sum(cycle)/n:.1f} 小时")
print(f"若错用 84 天窗口:{wrong:.1f} 小时 —— 与实测差 {(wrong/(sum(cycle)/n)-1)*100:.0f}%,"
f"是窗口不一致,不是法则失效")
print(f"若吞吐不变、在制品减半:W ≈ {correct/2:.1f} 小时({correct/2/24:.1f} 天)")
怎么用:三个数、两张图、一条公式
| 要回答的问题 | 看什么 | 判据 |
|---|---|---|
| 该承诺多久 | P85 / P95 | 承诺值 = P85 + 缓冲,不用均值 |
| 现在堵在哪 | 累积流图(CFD)中变宽的列 | 最宽且持续增长的那列是约束 |
| 该降到多少 WIP | WIP = λ × W目标 | 例:λ = 14.5/周、目标 3 天 → WIP ≈ 6.2 |
| 改进有没有效 | 周期分布 + 吞吐一起看 | 周期降但吞吐崩了 = 砍了产能,不是改进(改进顺序见 59.4 效能改进方法) |
WIP 上限不是“不超过人数”,而是由目标周期反算:WIP = 吞吐 × 目标周期(排期上的用法见 1.3 需求优先级)。上限设好之后,只允许“做完一件、拉入一件”,不允许“有空位就塞”。
反模式
| 反模式 | 后果 |
|---|---|
| 用平均交付周期对业务承诺 | 一半以上的工单超时;长尾被平均掩盖 |
| 把利用率提到 90% 以上当目标 | 排队时间放大 9–19 倍,周期爆炸、故障恢复也更慢 |
| 瓶颈在评审时加开发 | WIP 上升、周期变长、吞吐不变(TOC:非瓶颈改善不增产) |
| WIP 上限按人数拍 | 上限与目标周期脱钩,等于没有约束 |
| 只统计“开发动工→完成” | 漏掉排队与等待,通常是总周期的 60% 以上 |
| 大需求和小 bug 混算周期 | 均值随需求构成波动,趋势线无意义 |
| 用故事点/人天当吞吐 | 单位不可比,跨迭代“提速”是估算口径变了 |
| 三个数取自不同窗口 | 利特尔法则“失效”,得出“降 WIP 没用”的错误结论 |
关联
- 前置:59.1 DORA 四指标(前置时间的口径)、34.2 性能建模(利特尔法则、排队论)(公式与前提)、55.1 发布策略(批量与发布窗口)
- 并列:1.3 需求优先级(
W = L/λ在排期上的用法) - 下游:59.3 度量的陷阱(周期一旦成为考核目标)、59.4 效能改进方法(先降哪个约束)
自检
Q1 团队平均交付周期 6 天,P85 是 14 天。业务要求“两周内必须上线”,现在能满足吗?
▸ 答案:不能按“平均 6 天”回答。P85 = 14 天意味着约 15% 的工单会超过两周;如果业务要求是“绝大多数按时”,至少要看到 P95。要么把承诺改成 P85 口径并补缓冲,要么降 WIP 把整条分布左移——只压均值不改分布,尾部照样违约。
Q2 经理想“提高吞吐”,给团队加了两名开发。按本节模型,什么情况下吞吐不变、什么情况下反而下降?
▸ 答案:瓶颈不在编码时吞吐不变——新增的人只会让“待评审/待测试”队列更长,WIP 上升、周期变长。若新人还需要老成员带,短期内瓶颈产能反而下降(指导占用),吞吐会先跌后升。正确顺序:先找队列堆在哪,再决定加谁。
Q3 为什么把利用率从 70% 提到 90%,交付周期会涨 4 倍以上?
▸ 答案:按 Wq/S = ρ/(1−ρ),70% 时排队是服务时间的 2.3 倍,90% 时是 9 倍;周期 = 服务 + 排队,所以从 3.3 倍涨到 10 倍,约 3 倍。再叠加需求波动和服务时间波动,实际更差。利用率越高,系统对波动的吸收能力越弱——这也是故障恢复变慢的原因。
Q4 本节实测里,用 84 天窗口算出的 L/λ 比实测均值低 8%。这说明利特尔法则错了吗?
▸ 答案:没有错,是窗口不一致:λ 用了 84 天(含尚未排空的尾部),而平均周期只统计了已完成工单,这些工单的完成时间拖到了 91.7 天。三个量必须取同一窗口。修正后 7.61/13.28×168 = 96.3 小时,与实测完全一致。
Q5 团队把 WIP 从 12 降到 6,周期从 8 天降到 5 天,没有降到 4 天,为什么?
▸ 答案:利特尔法则给的是稳态下的比例关系,前提是吞吐不变。降 WIP 的过程中吞吐通常会先掉一点(开发位短暂空闲、批次变小带来额外切换),稳态要几周才到;另外 8 天里若有 30% 是外部等待(等审批、等环境),这部分不随 WIP 变化。先把不变的等待剔掉,再看比例是否成立。
Q6 看板上“待评审”一列长期堆 30 多个 PR,“开发中”只有 5 个。吞吐的约束在哪?该动什么?
▸ 答案:约束在评审。开发不是瓶颈,加开发只会让待评审队列更长。动作顺序:① 限制同时在评审的 PR 数(WIP 上限);② 缩小 PR 体积、设评审 SLA;③ 增加评审能力(轮值、配对评审);④ 若评审是必要的质量门禁,考虑自动化替代部分人工检查(静态检查、契约测试)。
Q7 为什么不能拿“故事点/周”当吞吐,再和别的团队比?
▸ 答案:故事点是团队内部的相对估算,锚点、拆分粒度、包含的隐性工作都不同;两个团队的“20 点/周”不可比。吞吐要用可数的完成项(需求数、PR 数、部署数),并注明工作项类型的构成;跨团队比较只能用 DORA 这类有统一采集口径的指标,且仍要按应用分别看。
Q8 需求前置时间 30 天、变更前置时间 2 天,改进应该先做哪段?
▸ 答案:先做 30 天那段。它占总周期的 94%,即使把交付管道从 2 天压到 1 天,总周期也只从 30 降到 29。要在 30 天里找等待:排期队列、澄清往返、跨团队依赖。判断依据是哪一段的等待最长,不是哪一段最容易改。