用流量模拟估算 OCR 服务容量
文章以文档数而非请求数衡量 OCR 负载,通过容量计算和蒙特卡洛模拟分析高峰排队与 worker 需求。示例中早高峰 p99 为全天两倍,微批处理在特定参数下可将 worker 数量大致减半,并提供 Python 研究笔记本。
AI 深度解读
给发票 OCR 服务估容量,只看每秒请求数(RPS)容易算偏:一次上传可能是一张 JPEG,也可能是一整年的发票。作者给出的思路是,先把请求换算成文档量,再模拟一天中的流量起伏,用它决定后续该测什么。
这篇 Part 1 做到的是生成负载和估算 worker 数量,还没有测出用户等待时间。文中的数字都是示例假设,但它把一个实用问题拆清楚了:微批处理能提高吞吐,用户是否愿意等它凑满,却需要另算。
一次请求有多重,比来了多少请求更重要
作者假设服务处理 PDF 和 JPEG 发票,一张典型发票只有一页。基线是 1,000 个日活用户,每人每天上传 5 次,每次 7 份文档,因此每天共有 5,000 次上传、35,000 份文档。
把全天摊平,平均上传速率是 0.05787 次/秒。早高峰按平均值的 3 倍计算,得到 0.17361 次/秒,再乘每次上传的 7 份文档,才得到用于规划处理能力的 1.215 份文档/秒。
对于这类服务,请求数描述入口压力,文档数才更接近实际处理量。
这个换算仍有边界。文章固定了每次上传的文档数量,也假设发票通常只有一页,没有继续区分 PDF 页数、图片尺寸或文件质量带来的耗时差异。真实业务若存在大量多页文件,还得继续细分工作量。
微批处理为什么可能省掉一半 worker
作者假设,在线处理一次只处理一份文档,耗时 650 ms,也就是每个持续忙碌的 worker 每秒处理约 1.54 份文档。
微批处理(micro-batching)则先收集少量文档,再一起处理。示例假设同一个 worker 能在 2.6 秒内处理 8 份文档,吞吐约为 3.08 份/秒,恰好翻倍。
下面的函数把峰值文档速率换算成计划 worker 数量,并通过利用率预留余量:
from math import ceil
def planned_workers(peak_docs_per_second, docs_per_batch, batch_seconds, utilization):
docs_per_second_per_worker = docs_per_batch / batch_seconds
return ceil(peak_docs_per_second / (docs_per_second_per_worker * utilization))
要看的是最后一行:处理能力先乘目标利用率,再用 ceil 向上取整。作者采用 70% 的目标利用率,在线参数为 (docs_per_batch=1, batch_seconds=0.65),微批参数为 (8, 2.6)。
按这个公式代入基线,两种方式都只需要 2 个 worker。这是根据示例参数推算的结果,也说明吞吐翻倍不等于任何规模下机器数都减半:向上取整会影响小规模部署。
流量更大时,批次更快凑齐,提高单个 worker 吞吐的收益才更容易体现。这里的 650 ms 和 2.6 秒都是假设,作者明确说,真实部署需要在同一硬件上分别做基准测试。
平均每秒不到一次,为什么还需要模拟
作者把早高峰设在 08:00 至 12:00,共 4 小时。为了维持每天期望上传量仍为 5,000 次,剩余 20 小时的流量系数设为 0.6:
4 × 3 + 20 × 0.6 = 24
接着,每一秒的上传次数都从泊松分布(Poisson)抽样。它允许某一秒没有上传,也允许另一秒突然来好几次,避免把小数形式的平均速率误当成平滑到达的真实请求。
下面的代码生成 100 个可能发生的日子,每行一天、每列对应当天同一秒:
import numpy as np
from scipy import stats
def simulate_uploads(dau, uploads_per_user, days=100, seed=42):
mean_rps = dau * uploads_per_user / 86400
hours = np.arange(86400) / 3600
morning = (hours >= 8) & (hours < 12)
expected_counts = mean_rps * np.where(morning, 3.0, 0.6)
requests = stats.poisson.rvs(
mu=expected_counts,
size=(days, 86400),
random_state=np.random.default_rng(seed),
)
return expected_counts, requests, morning
expected_counts, requests, morning = simulate_uploads(1000, 5)
t = 8 * 3600 # 08:00:00
print(expected_counts[t]) # 0.17361... expected uploads
print(requests[:10, t]) # [0 0 0 0 0 0 0 1 0 0]
print(requests[:, t].mean()) # 0.22 uploads, averaged over 100 days
要分清 expected_counts[t] 和 requests[:, t].mean():前者是模型设定的期望,后者是有限次抽样的均值,两者不必完全相同。
基线下,即使到了早高峰,仍有约 84% 的秒没有上传。每次上传固定为 7 份文档,所以某秒出现两次上传,就会同时带来 14 份文档。对等待时间有影响的,正是这些成组到达。
当用户数改为 10,000、每人每天上传 10 次时,08:00:00 的期望上传数升至 3.472,前十次抽样变为 [5 2 3 3 4 1 0 4 6 2]。
作者再用 np.quantile(counts, [0.50, 0.95, 0.99], method="inverted_cdf") 分别统计全天和早高峰。开头给出的结论是,示例中早高峰 p99 为全天 p99 的两倍;但所附正文没有展示具体分位数输出,不能补成完整结果表,更不能把它当成响应延迟 p99。
我的判断
我认可先理解流量、再决定基准测试范围的顺序。直接拿全天平均 RPS 估机器数,会遗漏上传大小和时段变化;直接相信微批吞吐,又会遗漏凑批时间。
但这仍是一套示例建模。固定每次 7 份文档、采用泊松到达、设置固定早高峰,都需要拿真实日志校验。团队集中上传或定时任务造成的突发,未必符合这些假设。
微批节省约一半 worker 的说法,也只有假设吞吐作支撑,没有真实硬件对照数据。Part 1 尚未把到达流量送进处理队列,因此无法回答用户多久能拿到整次上传的结果。作者把这个问题留给 Part 2,衡量的是每次上传中最后一份文档完成前的等待时间。
能拿来做什么
- 给 Node 上传接口做容量评估时,同时记录每次上传的文档量和到达时间,先算文档/秒,再检查全天与繁忙时段的差异。
- 对支持批量推理的 OCR 流程,在相同硬件上测单份耗时和 8 份批处理耗时,再代入 70% 利用率公式,判断吞吐收益是否会减少实际 worker 数。
- 设计前端批量上传的完成反馈时,把整次上传最后一份文档的完成时间纳入指标;后续模拟也应包含队列等待和凑批等待,才能判断用户是否等得起。