🐎 第7章 无为而治:Headless 模式与 CI/CD 集成
📄 一句话概括

长任务最难受的不是慢,是「不知道它在干什么」。stream-json 把每次动作变成一行事件,边跑边吐。 实测两种读法总时长几乎一样(0.775 对 0.770 秒),但流式的第一条 0.527 秒就到了。 它不让你更快,让你更早。

📝 本章和前面几章的关系

上一节流水线跑起来了,这一节解决「跑的时候看得见」这件事。 这一节有真计时:同一个输出,一种等完再读、一种边跑边读。

一、两种读法

等完再读:进程结束,一次性收全部输出,再解析。简单,但中途是黑的。 边跑边读:每来一行就处理一行,进程还没结束就已经有东西了。 两者的总时长几乎一样——流式不加速,它只是把「到手」的时间提前。

而「提前」在长任务里就是全部:知道它跑到第几步,才知道该等还是该掐。

二、真跑:同一段输出,两种读法

演示里十二步、每步二十毫秒,一共十三行事件(最后一行是 done)。 等完再读:第一条要到快一秒才拿到——那其实就是它跑完的时间。 边跑边读:第一条在半程就到了,剩下的边跑边收。

两个总时长几乎一致。差的那一点,是解析开销,不是速度。

三、流式的真正价值

第一,能设「多久没动静就报错」。没有事件流的任务,卡住和慢长得一模一样。 第二,能把中间结果写进日志。事后查问题,能看到它当时在做第几步。 第三,能提前放弃。看见它在反复改同一个文件,就不用等它跑完。

第三条最省钱:一次跑偏的长任务,早三分钟掐掉,就省三分钟的钱。

四、解析事件流的三个坑

半行:读到一半没有换行就不能解析,必须攒够一行再解。 事件类型:别假设只有一种,一定按类型分流,未知类型直接跳过不报错。 错误事件:失败也是事件,从流里出来,不一定是非零退出码。两处都要看。

第三个坑最容易漏:你在检查退出码,而它已经在流里报过一次错了。

五、什么时候不需要流式

任务三秒就结束,流式只增加解析负担。 结果本来就是一整块(比如生成一份文件),中间过程没有意义。 判据:这个任务会不会超过三十秒?会就上流式,不会就等它。

还有一条更实际的:这行日志有没有人看?没人看的进度条,只是多一处要维护的代码。

六、动手 Demo:流式读和等完再读,差的不是速度是「早」

长任务最难受的地方不是慢,是「不知道它在干什么」。 这里真跑同一段输出,一种等完再读、一种边跑边读,各记一次时间。

for line in p.stdout:                    # 边跑边读:来一行处理一行
    line = line.decode(u"utf-8", u"replace").strip()
    if first is None:
        first = time.perf_counter() - t0
    json.loads(line)

实跑输出:

--------------------------------------------------------------------
流式读和等完再读,差的不是速度是「早」
--------------------------------------------------------------------

  长任务最难受的地方不是慢,是「不知道它在干什么」。
  stream-json 把每次动作变成一行事件,边跑边吐,日志里就能看见进度。

  ① 两种读法
  --------------------------------------------------------------------
  等完再读:进程结束,一次性收全部输出,再解析。
  边跑边读:每来一行就处理一行,进程还没结束就已经有东西了。

  ② 真跑:同一段输出,两种读法
  --------------------------------------------------------------------
| 读法 | 第一条到手 | 总共收到 | 中途能看见什么 |
|---|---|:---:|:---:|
| 等完再读 | 0.772 秒 | 13 条 | 结束时才有 |
| 边跑边读 | 0.524 秒 | 13 条 | 第一条就到手 |

  总时长几乎一样(0.771 秒对 0.772 秒)——流式不让你更快,它让你更早。
  而「更早」在长任务里就是全部:看见它已经跑到第几步,才知道该等还是该掐。

  ③ 流式的真正价值
  --------------------------------------------------------------------
  第一,能设「多久没动静就报错」。没有事件流的任务,卡住和慢长得一样。
  第二,能把中间结果写进日志。事后查问题,能看到它当时在做第几步。
  第三,能提前放弃。看见它在反复改同一个文件,就不用等它跑完。

  ④ 解析事件流的三个坑
  --------------------------------------------------------------------
  - 半行。读到一半没有换行就不能解析,必须攒够一行再解。
  - 事件类型。别假设只有一种,一定按 type 分流,未知类型直接跳过不报错。
  - 错误事件。失败也是事件,从流里出来,不一定是非零退出码。两处都要看。

  ⑤ 什么时候不需要流式
  --------------------------------------------------------------------
  任务三秒就结束,或结果本来就是一整块(比如生成一份文件),流式只增加解析负担。
  判据:这个任务会不会超过三十秒?会就上流式,不会就等它。
  ! 事件字段按各家实际格式为准,这里演示的是「一行一个 JSON、按 type 分流」这个形状。

①两种读法:等完再读一次性收全部输出,边跑边读每来一行处理一行。 两者的总时长几乎一样——流式不加速,只是把「到手」的时间提前。 ②十二步、每步二十毫秒,一共十三行事件。 等完再读要等它跑完才拿到第一条,边跑边读第一条在半程就到了。 ③流式的三个价值:能设「多久没动静就报错」、能把中间结果写进日志、能提前放弃。 第三条最省钱——早三分钟掐掉,就省三分钟的钱。 ④三个坑:半行要攒够再解、按类型分流不假设只有一种、错误也是事件(不一定非零退出码)。 第三个最容易漏:你在检查退出码,它已经在流里报过一次错了。

30 秒实验

  1. 把每步的休眠从二十毫秒改成两百毫秒,重跑一遍。等完再读和边跑边读的差距被拉大了多少? 这说明流式的价值跟什么成正比?
  2. 在流里插一条错误事件(type 是 error),重跑一遍。你的解析能发现它吗?如果只看退出码呢?
  3. 把「第一条到手」这个时间点改成「第十条到手」,重跑一遍。 它对「多久没动静就报错」这个策略有什么影响?

常见误区

  1. 以为流式能让任务更快。它只是让你更早看到进展,总时长不变。
  2. 不按类型分流,假设流里只有一种事件。加一种类型就崩。
  3. 只看退出码不看流里的错误事件。失败可能已经在流里报过了。
  4. 解析半行就 json.loads。会抛异常,而异常位置看起来像「数据坏了」。
  5. 给三秒的任务上流式。多一层解析,换不来任何东西。
自测三问(点开对答案) 1. 流式和一次性读取的根本差别是什么? —— 不是速度,是「早」。总时长几乎一样,但流式在任务还没结束时就已经有数据了。 2. 流式的三个价值是什么? —— 能设超时告警(多久没动静)、能把中间结果写进日志、能提前放弃跑偏的任务。 3. 解析事件流要防哪三个坑? —— 半行要攒够再解、按类型分流、错误也是事件(不一定体现在退出码上)。

看得见了,下一节解决下一个问题:多步任务之间,上下文怎么传。

相关笔记