工作流这三个字,这两年是被说烂了的。招聘要求里写,产品文档里写,做内容的、做运营的、写代码的都在聊。但我发现一个挺有意思的现象:很多人嘴上说着工作流,脑子里想的其实是"自动化脚本",还有人把它等同于"审批流程",甚至有人觉得建个工作流就是把几个软件连起来点点鼠标。真上手搭的时候,第一步就卡住了——不知道先画流程还是先打开工具,不知道节点之间数据怎么传,不知道跑失败了从哪儿查。
我干这行十来年,从早期自己写定时脚本拼流程,到后来用可视化编排平台给团队搭业务流水线,再到把工作流嵌进代码里做数据管道,几个阶段都趟过。这篇就聊三件事:工作流到底是什么(不是背定义,是讲清楚它和周边概念的边界)、一条工作流由哪几个零件组成、以及从零搭一条能上生产的工作流具体怎么走。不管你是完全没接触过的新手,还是已经搭过几条但总在半夜被告警叫醒的老手,下面这些内容应该都能抠出点能直接用的东西。
1. 先把概念捋直:工作流到底在解决什么问题
1.1 我理解的工作流,本质是"把不确定性收进确定的框里"
抛开那些教科书定义,我用一句话概括:工作流是把一件需要多步骤、多人或多系统协作的事,拆成有明确顺序、明确输入输出、明确失败处理方式的执行单元,然后交给一个统一的调度者去跑。
注意这里面有三个关键词,缺一不可。第一个是多步骤,单个动作不叫工作流,那叫一次操作。第二个是明确的输入输出,也就是每个环节都得说清楚"我拿到什么、我交出什么",这是后面所有排障的基础。第三个是统一的调度者,也就是得有一个东西负责记录"现在跑到哪一步了、下一步该谁了、上一步失败了怎么办"。很多人搭工作流只搭了前两个,第三个全靠人肉盯着,那不叫工作流,那叫"带着清单干活"。
为什么第三个最关键?因为工作流真正的价值不在于"快",而在于可观测和可恢复。一件事如果只有三步,人手动做可能比搭工作流还快。但如果这件事每周要做五十次,每次三步,任何一步出错都要从头查起,那人工的成本就爆炸了。工作流把每一步的状态都落下来,出错的时候你能精确知道是第几步、什么参数、报了什么错,重跑的时候也不用从头再来。
所以判断一件事该不该做成工作流,我的经验标准是:步骤数乘以执行频次,再乘以单次出错后的排查成本。这个乘积上去了,就值得做;上不去,写个脚本甚至手动干反而更划算。
1.2 工作流、自动化脚本、审批流,三者的边界在哪
这三个概念经常被混着用,我在实际项目里是这么区分的。
自动化脚本是"一次性把一串动作跑完",它通常没有状态持久化,跑完就结束了,中途挂了就得从头再来。脚本的优势是轻、快、灵活,几十行代码能搞定的事没必要上工作流平台。缺点是它没有"记忆",一旦涉及等待人工确认、跨天执行、失败后从断点续跑,脚本写起来就开始难受了。
审批流是工作流的一个子集,特点是中间有"人等系统"的环节,也就是人工节点。它的核心是权限、流转规则和留痕,对执行性能要求不高,但对"谁在什么条件下能批、批完流转给谁"要求极严。很多办公协同工具里的流程引擎就是干这个的。
工作流这个概念更宽,它既包含纯自动化的机器节点链路,也包含带人工节点的混合链路。可以理解成:审批流是工作流的特例,自动化脚本是工作流的实现手段之一。
举个我实际遇到的例子。有个团队要做"每周素材收集—初筛—加工—发布—归档"这件事。最初他们写了个脚本,把收集和发布自动化了,中间的初筛和加工靠人。结果问题来了:脚本不知道人什么时候筛完,只能定个死时间跑,经常跑到一半素材还没准备好,报错。后来改成工作流,中间插了人工节点,人工点"完成"才触发下一步,失败还能单独重跑某一段,整个链路才稳下来。
提示:如果你发现自己的"工作流"里有大量"等一下再跑""看看好了没"这种人工判断,那多半是缺了状态管理和人工节点,硬用脚本顶会越来越痛苦。
1.3 什么场景不值得上工作流
这条我想单独说,因为见过太多过度设计的案例。有团队为了"显得规范",把一件每天只做一次、三步就能完成的事,搭成了一个十几个节点的工作流,维护成本比手工做还高。
我的判断清单是这样的:执行频次低于每周一次的、步骤固定且极短的(三步以内)、失败后重做成本很低的、参与方只有一个人的,这四类情况我基本不会去做工作流,写个脚本或者干脆手动更省事。反过来,只要命中"跨系统""跨人""执行频次高""失败代价大"里的任意两条,就值得认真设计。
还有一类特殊情况:探索期的业务。流程还没稳定下来,规则一天一变,这时候搭工作流就是给自己挖坑,因为每改一次规则就要改一次流程。稳妥的做法是先手动跑通、把规则沉淀下来,等流程稳定两周以上再固化。
2. 拆开来看:一条工作流的四个核心构件
2.1 触发器:定时、事件、手动,三种入口的取舍逻辑
触发器决定工作流什么时候开始跑。看起来最简单,实际上选错了会让后面所有设计都别扭。
定时触发最适合"周期固定、上游数据有明确产出时间"的场景。这里有个细节:定时时间不要卡在上游产出时间点上,要留缓冲。比如上游每天早上八点出数据,你定八点触发,大概率跑到的时候数据还没写完。我的习惯是留出上游平均耗时的1.5倍作为缓冲,八点出数据就定九点半。另外定时任务要避开整点,整点是各类系统的拥堵高峰,错开到 :07、:23 这种时间点,能减少很多莫名其妙的超时。
事件触发是上游发生动作时立刻拉起流程,比如"有新增记录就处理"。它的好处是实时,坏处是容易雪崩。上游一秒钟推一千条事件过来,你的流程如果没有并发控制和排队机制,直接被压垮。用事件触发必须配三样东西:消息队列做缓冲、并发上限做保护、失败重试做兜底。
手动触发看着最土,但在实际项目里用得最多,尤其是调试阶段和人工确认后的续跑。我给团队定过一个规矩:所有工作流上线前,必须保留一个手动触发入口,方便排障时单独验证某一段。
三种可以混用。比如一条内容处理流水线,主链路是定时触发拉取当日素材,中途有人工审核节点,审核通过后手动触发发布段。这种混合设计比纯自动更贴近真实业务。
| 触发方式 | 适合场景 | 主要风险 | 必须配套的措施 |
|---|---|---|---|
| 定时触发 | 周期性批处理、日报周报生成 | 上游未就绪、整点拥堵 | 时间缓冲、错峰、上游就绪检查 |
| 事件触发 | 实时响应、增量处理 | 流量突增压垮下游 | 队列缓冲、并发上限、死信队列 |
| 手动触发 | 调试、人工确认后续跑 | 依赖人的记性,容易漏跑 | 待办提醒、超时自动升级 |
| 混合触发 | 主链路自动加人工节点 | 状态管理变复杂 | 统一的状态存储、幂等设计 |
2.2 节点:动作、判断、人工、子流程,四类够用了
节点是工作流里的执行单元。工具五花八门,但节点类型其实就那么几类,认清楚类型比记某个具体工具的操作重要得多。
动作节点干实事,调接口、读写数据库、跑脚本、发消息都算。设计动作节点最重要的原则是单一职责:一个节点只干一件事。我见过有人把一个节点里塞了"取数据+清洗+入库+发通知"四件事,结果某天发通知失败了,整条流程重跑,数据被入库了两遍。拆成四个节点,失败就只重跑失败的那个。
判断节点做分支。这里最常见的坑是分支条件不互斥,导致一条数据同时命中两个分支,或者一个都不命中。我的做法是每个判断节点最后一定挂一个"默认分支"兜底,条件写不出来的一律走默认分支并打日志,这样至少不会静默丢失数据。
人工节点是等人操作的环节。设计人工节点的核心是超时机制:人等系统不能无限等,必须设定超时时间,超时后要么自动升级给上一级,要么走兜底分支,要么直接标记失败告警。我踩过最惨的坑就是一个人工节点没设超时,卡了三天没人管,整条链路积压了几百条任务。
子流程节点是把一段可复用的逻辑包起来。比如"发送通知"这个动作在十条流程里都要用,就抽成子流程,改的时候改一处。子流程的坑在于参数传递,调用方传进去什么、子流程返回什么,必须约定清楚,不然排查起来像查俄罗斯套娃。
注意:判断节点的分支一定要做穷举覆盖测试。我习惯的做法是把所有分支条件列成一张表,逐条造数据验证,包括"所有条件都不满足"这种情况。
2.3 数据上下文:节点之间的"接力棒"怎么传
这是新手最容易迷糊的地方,也是我认为最该提前想清楚的地方。
节点之间传数据,本质上只有两种模式:共享上下文和显式传参。
共享上下文就像一个公用的记事本,所有节点都能往上写、都能往上读。好处是省事,不用每个节点都声明输入输出。坏处是耦合严重,节点A改了个字段名,节点C莫名其妙就报错了,而且很难查,因为它没在接口上体现出来。
显式传参就是每个节点明确声明"我需要哪些输入、我输出哪些字段"。麻烦一点,但可维护性高一个数量级。我现在的习惯是:主干链路一律用显式传参,只在同一个子流程内部用共享上下文。
还有一个绕不开的问题是数据体积。上下文里不要塞大对象,比如整个文件内容、上千条的列表。上下文通常存在数据库或缓存里,塞太大既慢又容易超限。正确做法是传引用,比如把文件存到对象存储,上下文里只放一个地址;把大批量数据落到临时表,上下文里只放一个批次号。
另外要养成给上下文字段加版本的习惯。工作流改版的时候,老版本还在跑的任务用的是老结构,新节点读老结构就会挂。加个 version 字段,节点里做兼容判断,能省掉大量深夜救火。
2.4 状态与幂等:为什么你的流程总在重复执行
这条是我最想强调的,因为它是"能跑"和"能上生产"的分水岭。
工作流必须持久化状态,至少要记录:当前跑到哪个节点、每个节点的开始时间结束时间、输入输出快照、失败原因、重试次数。没有这些,出问题就是黑盒。
幂等性是另一个必须提前设计的点。所谓幂等,就是同一个任务执行一次和执行三次,结果一样。为什么重要?因为工作流的重试机制天然会导致重复执行。你设了失败重试三次,那第一次其实已经部分成功了(比如消息发出去了但响应超时),重试就会导致消息发两遍。
保证幂等的常规手段有三种。一是唯一键去重,每次执行带一个全局唯一的执行ID,下游根据这个ID判断是否处理过。二是状态机约束,只有处于特定状态的数据才允许被处理,处理完立刻改状态,重复请求自然被挡掉。三是操作本身设计成幂等,比如"设置为X"是幂等的,"增加1"不是。
我一般会要求:任何涉及写数据、发消息、扣费用的节点,上线前必须回答一个问题——这个节点被执行两遍会怎样?如果答案是"会出问题",那就得补去重逻辑。
3. 搭建实操:从纸面草图到跑通第一条链路
3.1 别急着打开工具,先在纸上把四件事写清楚
我见过太多人一上来就打开流程编辑器开始拖节点,拖到一半发现漏了个分支,推倒重来。正确的顺序是先梳理,梳理清楚四件事再动手。
第一件,入口和出口。这条流程从哪儿开始、到哪儿算结束、产出物是什么、交给谁。这个必须一句话说清楚,说不清说明还没想明白。
第二件,参与方。哪些是系统、哪些是人、每个参与方负责哪一段。这里要特别注意跨团队的情况,凡是涉及别的团队的环节,都要单独标注并提前沟通,不然流程搭好了对方不配合,白干。
第三件,异常路径。这是最容易被跳过的一步。我习惯用"如果这一步失败了会怎样"逐节点问一遍,把答案写成表。大部分工作流的健壮性差距,就在这张表上。
第四件,数据流。每个节点吃什么、吐什么,字段名和类型是什么。这一步做扎实了,后面配置节点就是填表。
我自己的习惯是用一张纸画方框加箭头,方框里写"谁做什么",箭头上写"传什么数据"。画完对照检查一遍:有没有断头路(有入口没出口)、有没有死循环、有没有某个节点没有任何输入。
3.2 三条技术路线怎么选,别迷信"低代码"
搭建工作流大体有三条路:可视化编排平台、代码编排框架、业务系统内嵌。
可视化编排平台的优势是上手快、非技术人员能看懂、调试直观。适合业务规则多变、参与方包含非技术人员的场景。缺点是复杂逻辑表达起来别扭,条件一多就是一堆嵌套的判断节点,看得人头大;另外平台的执行能力和扩展性有天花板。
代码编排框架用代码定义流程和依赖,适合逻辑复杂、需要精细控制的场景,比如数据管道、批处理。优势是灵活、可测试、能进版本管理。缺点是对非技术人员不友好,改个流程要发版。
业务系统内嵌是在自己的系统里实现一套轻量的流程引擎,适合流程和业务强耦合、需要深度定制的场景。成本最高,但如果你的业务流程本身就是核心竞争力,这条路值得走。
| 路线 | 上手成本 | 复杂逻辑表达 | 可维护性 | 适合的团队 |
|---|---|---|---|---|
| 可视化编排平台 | 低,半天能跑通 | 一般,嵌套多了难读 | 中,配置散在界面里 | 业务方主导、规则常变 |
| 代码编排框架 | 中,需要开发参与 | 强,逻辑随意组合 | 高,可测试可版本化 | 有研发、逻辑复杂 |
| 系统内嵌引擎 | 高,要自己造轮子 | 最强 | 取决于自身工程质量 | 流程即核心业务 |
我的实际选择通常是混合:主干用代码编排保证可靠性和可测试性,把需要业务方调整的部分(比如分支阈值、通知模板)暴露成配置,业务方在界面上改配置,不用碰代码。这样两头的好处都能拿到。
3.3 一条完整工作流的配置实录:素材收集到归档
下面用一条真实存在过的链路做演示,脱敏之后是这样的:每天早上从几个来源收集素材,去重后落到素材库,人工挑选,选中的走加工,加工完发布,最后归档并生成日报。
先看整体定义。我用 YAML 描述流程,这在代码编排框架里很常见:
name: daily_content_pipeline version: 3 trigger: type: schedule cron: "23 8 * * *" # 错开整点,8:23 触发 timezone: Asia/Shanghai readiness_check: target: upstream_source_status expect: ready max_wait_seconds: 1800 # 上游没就绪最多等 30 分钟 concurrency: max_running: 1 # 同一天只允许一条实例在跑 policy: skip_if_running # 已有实例在跑则跳过,避免重复 defaults: timeout_seconds: 600 retry: max_attempts: 3 backoff: exponential base_seconds: 2 # 2s / 4s / 8s steps: - id: fetch_sources type: action handler: fetch_all_sources params: sources: [src_a, src_b, src_c] since_hours: 24 - id: dedupe type: action handler: dedupe_by_hash depends_on: [fetch_sources] - id: save_pool type: action handler: save_to_pool depends_on: [dedupe] - id: human_pick type: human depends_on: [save_pool] assignee_role: editor timeout_seconds: 14400 # 人工环节 4 小时超时 on_timeout: notify_and_skip - id: process type: action handler: process_selected depends_on: [human_pick] when: "human_pick.selected_count > 0" - id: publish type: action handler: publish_batch depends_on: [process] idempotency_key: "${run_date}-${batch_id}" - id: archive type: action handler: archive_batch depends_on: [publish] - id: report type: action handler: send_daily_report depends_on: [archive] always_run: true # 无论前面成功失败都要出日报几个设计点单独说一下背后的理由。
cron 定在 8:23 而不是 8:00。整点是各类系统批量任务的集中时间,数据库连接池、消息队列、第三方接口都在这个时间点承压。错开二十分钟,超时率能明显下降,这个我做过对比,不是玄学。
max_running: 1 加 skip_if_running。这是防重复执行的第一道闸。如果昨天那条还没跑完,今天这条直接跳过并告警,而不是两条并行把素材处理两遍。没有这个配置,人工节点一卡住,第二天立刻并发。
readiness_check。上游没就绪就等着,最多等半小时。这比"跑失败了再重试"优雅得多,因为它把失败前置成了等待。
human_pick 的 on_timeout: notify_and_skip。人工环节超时不能死等,我的选择是通知加跳过。也考虑过自动升级给上级,但实际用下来发现升级会制造更多沟通成本,跳过并在日报里标注"今日未挑选"反而更清晰。
publish 的 idempotency_key。这是幂等的关键。用日期加批次号做唯一键,下游发布接口收到重复键直接返回上次结果,不会发两遍。
report 的 always_run: true。日报这个节点必须无条件执行,不然一旦前面某个环节挂了,你连"今天挂了"这个消息都收不到,只会发现今天很安静——这是最可怕的故障。
再看一个动作节点的具体实现,用 Python 示意:
def save_to_pool(ctx): items = ctx.get("deduped_items") or [] if not items: ctx.set("saved_count", 0) return {"status": "skipped", "reason": "empty_input"} existing = load_existing_hashes() # 从库里捞已存在的指纹 new_items = [i for i in items if i["hash"] not in existing] batch_id = f"pool_{ctx.run_id}" write_batch(batch_id, new_items) # 按批次写,便于回滚 ctx.set("batch_id", batch_id) ctx.set("saved_count", len(new_items)) return {"status": "ok", "saved": len(new_items)}这段代码里有两个细节值得说。空输入直接返回 skipped 而不是 ok,这样在流程视图里一眼能看出"今天没素材",而不是混在一堆成功里。按批次写入,每批带一个 batch_id,出问题的时候能精确按批次回滚,不用一条条删。
3.4 参数怎么定:超时、重试、并发都要有依据
很多人配参数是拍脑袋,超时写 300 秒、重试写 3 次,问为什么,答"大家都这么写"。这几个参数其实都有算法。
超时时间:取该节点下游依赖的 P99 耗时,乘以 2。为什么是 2 而不是 1.5?因为工作流本身的调度有开销,网络抖动也有长尾,2 倍是比较稳的。如果你的下游 P99 是 90 秒,超时定 180 秒。定太短会误杀正常请求,定太长会让故障发现变慢。
重试次数:一般 3 次足够,配合指数退避(2 秒、4 秒、8 秒,最多到 60 秒封顶)。注意重试只对可恢复错误有意义,比如网络超时、下游限流。对参数错误、权限不足这类确定性失败,重试一百次也没用,反而制造噪音。所以重试策略必须能识别错误类型,我习惯把错误码分三类:可重试(超时、5xx、限流)、不可重试(参数、权限、404)、不确定(部分成功),第三类要人工介入。
并发数:从下游的承受能力倒推。下游每秒能处理 20 个请求,那并发就控制在 15 以内,留点余量给其他调用方。另外注意并发和幂等的配合,同一个业务键的并发处理必须串行,不然去重逻辑会失效。
3.5 上线前的自测清单,我每次都要过一遍
搭完不等于能上,我有一条固定的自测清单,每次上线前逐项打勾。
- 空数据跑一遍:上游没数据时流程能不能正常走完,不报错也不静默成功
- 单节点失败跑一遍:故意让中间某个节点报错,验证告警是否触发、重试是否生效、后续节点是否被正确阻断
- 重复触发跑一遍:手动连续触发两次,检查有没有产生重复数据或重复消息
- 超时跑一遍:把人ぬ节点的超时改短(比如 30 秒),验证超时分支是否按设计执行
- 回滚跑一遍:模拟某批数据有问题,验证能不能按批次回滚干净
- 权限验证:用最小权限的账号跑,确认没有依赖某个管理员权限才能跑通的隐藏路径
这六条里,空数据和重复触发这两项最容易暴露问题,我几乎每次都能查出点东西。有一次就是空数据场景下一段聚合逻辑除零报错,人工测试永远测不到,因为人总是拿有数据的用例去测。
4. 排查实录:跑起来之后才是真正的开始
4.1 五个高频故障,先看这张表
工作流出问题,八成集中在下面五类。我把它们整理成速查表,出问题先对号入座,能省掉大量瞎猜的时间。
| 故障现象 | 最可能的原因 | 快速验证方法 | 处理方式 |
|---|---|---|---|
| 同一批数据被处理两遍 | 缺少幂等,重试导致重复执行 | 查执行记录里同一业务键的执行次数 | 补唯一键去重,改状态机约束 |
| 流程卡住不动 | 人工节点未设超时,或节点在等一个永远不来的回调 | 看节点开始时间,超过阈值就是卡住 | 补超时,补兜底分支 |
| 定时任务偶尔漏跑 | 上一实例未结束被跳过,或调度器本身故障 | 查调度日志有无 skip 记录 | 加漏跑告警,检查实例时长 |
| 部分成功但整体报失败 | 事务边界没划清,多步操作未做补偿 | 查日志里成功和失败的步骤分界 | 补补偿逻辑,拆分事务 |
| 下游被打挂 | 事件触发没有并发控制 | 看下游 QPS 曲线是否尖峰 | 加队列,限并发,加退避 |
4.2 排障三步法:定位、重放、归因
我的排障流程固定三步,顺序不能乱。
第一步定位。看状态表,找出第一个失败的节点。注意是第一个,不是最后一个。很多人看日志从后往前翻,容易被后续的连锁错误带偏。第一个失败的节点才是根因所在,后面的都是它的后果。
第二步重放。定位到节点之后,用当时的输入快照单独重放这个节点,不改任何东西跑一遍。这一步的作用是区分"偶发"和"必现"。偶发的多半是超时、网络抖动;必现的才是逻辑问题。我特别强调要用输入快照重放,不要用当前数据,因为当前数据可能已经变了,重放结果没有参考价值。这也是为什么前面强调节点要记录输入输出快照。
第三步归因。跑通了要问一句"为什么之前失败",跑不通要问"哪个参数不对"。归因这一步最容易偷懒,看到重放成功就当没事了,结果第二天同样的错再来一遍。我的要求是:任何一次故障都要落一条结论,要么是"已修复",要么是"已知问题加监控",不能是"再看看"。
4.3 四个我亲自踩过的坑
这几个都是文档里不会写、但实际会让人加班到凌晨的东西。
第一个坑:以为重试是安全的。早期我配了个节点重试三次,那个节点做的事是"给用户发提醒"。结果某天下游响应慢,第一次其实发出去了只是响应超时,重试又发了两次,用户收到三条一样的提醒。教训是:任何有外部副作用的节点,重试前必须先确认幂等。没有幂等能力,宁可失败告警也不要盲目重试。
第二个坑:超时设得比下游还短。有次把一个接口调用节点超时设成 30 秒,那个接口的 P99 是 45 秒。结果每天都有百分之几的请求被我们自己掐断,然后走重试,重试又超时,最后判定失败。表面看是"接口不稳定",实际是我们的超时参数把自己坑了。后来按 P99 乘 2 改到 90 秒,失败率直接掉到接近零。
第三个坑:权限用了超级账号。开发阶段图省事,流程跑在一个权限很高的账号下,上线后一直没问题。直到某次安全整改把这个账号权限收紧,整条链路全挂,而且报的错是"记录不存在",完全看不出是权限问题。现在我们所有流程都跑在独立的最小权限账号下,虽然配置麻烦,但不会出现这种断崖式故障。
第四个坑:脏数据让流程静默出错。有个上游字段偶尔会传空字符串,我们的逻辑里判断用的是"字段存在就处理",空字符串也算存在,结果处理了一批空数据,产出的报表全是零。流程全程没报错,因为技术上没出错,是业务上错了。从那以后我要求所有关键字段做类型和范围校验,校验不过直接失败并告警,宁可停下来也不要带着脏数据往下跑。
提示:静默错误比崩溃更危险。崩溃你会立刻知道,静默错误可能一周后才被发现,而且那时候数据已经污染了一大片。宁可让流程多停几次,也不要让它带着问题往下走。
5. 让它活得久一点:版本、监控与演进
5.1 版本管理:改流程比建流程更需要纪律
工作流上线之后一定会改,改的时候最容易出的事是"新版本把正在跑的老实例搞挂了"。原因是节点逻辑变了,但老实例的上下文还是老结构。
我的做法有三条。一是流程定义必须进版本管理,配置文件也好、导出的定义也好,都要有版本号,能追溯每次改动。二是改结构要向后兼容,加字段可以,删字段和改字段名必须走过渡期,节点里同时兼容新旧两种结构,等老实例跑完再清理。三是保留快速回滚能力,每次发布新版本前把上个版本的定义存一份,出问题五分钟内能切回去。
还有一个细节:长跑实例的版本冻结。一条实例如果已经跑起来了,应该继续用启动时的那个版本定义跑到结束,而不是中途切换到新版本。这个能力取决于你用的框架,如果没有,那就在发布时机上避开正在跑的时间段。
5.2 该盯哪几个指标,别做无用监控
工作流的监控不要贪多,我只看四类。
执行成功率,按天看,低于阈值告警。这个指标能反映整体健康度,但它有滞后性,挂了才知道。
执行时长,重点看 P95 和 P99,而不是平均值。平均值会把长尾吃掉,而长尾才是问题所在。我一般会设一个时长基线,超过基线 1.5 倍就告警,这样能在故障扩散前发现苗头。
积压量,也就是待处理任务数。这个是最有价值的先行指标,积压一涨,说明处理能力跟不上了,或者某个环节卡住了,这时候还没出故障,有时间处理。
重试率,重试次数除以总执行次数。重试率上升往往意味着下游开始不稳定,是故障的前兆。
对应的告警策略也要分级别。积压超过阈值、重试率突增,走低优先级通知,工作时间处理;执行失败连续出现、关键链路中断,走紧急告警,立刻处理。分级的意义在于避免告警疲劳,什么都是紧急,最后什么都当没看见。
5.3 什么时候该拆,什么时候该合并
工作流跑久了会越来越大,到最后一张图上有几十个节点,谁也不敢动。这时候要判断该拆还是该合。
我的拆分信号是:节点数超过二十、单个流程跨越三个以上业务域、不同环节的修改频率差异很大。满足其中两条就该拆。拆的原则是按业务域拆成主流程加若干子流程,主流程只负责调度和状态汇总,子流程负责具体业务。这样改某个域的时候不影响其他域。
合并的信号正好相反:两条流程有八成以上的节点重复、两条流程的触发条件和数据源完全一致。这种情况说明当初拆早了,合回去能减少维护点。
还有一个容易被忽略的点:流程的生命周期管理。业务停了、流程废弃了,一定要显式下线,不要留着不管。留着的废弃流程会继续按定时触发跑,消耗资源、产生告警、污染监控数据,最后变成没人敢删的历史包袱。我们现在的做法是每个流程定义里带一个 owner 字段和一个复核日期,到期自动提醒 owner 确认是否还活着。
最后分享一个我自己一直在用的习惯:每搭完一条新流程,花十分钟写一段"如果三个月后接手这条流程的人是我,我需要知道什么"。内容就三件事——这条流程为什么这么设计、哪几个参数是关键的不能乱改、出问题先看哪里。写完贴在流程说明里。这十分钟投入,在后续每一次排障和交接里都能翻倍赚回来。踩过的坑多了就会明白,工作流真正的门槛从来不在拖节点,而在于你有没有把那些"只有踩过才知道"的细节提前想清楚。