首页 / 资讯中心 / 文章详情

rea:用三条命令实现本地任务编排与自动化执行

rea:用三条命令实现本地任务编排与自动化执行 ★ FEATURED ARTICLE
最近我给自己写了个内部小工具代号叫 rea用来解决每天重复执行一堆固定命令的问题。先交代一下背景最近一段时间我手头有几个数据任务看起来完全不相关但都有同一个共性——它们都靠一套固定流程在转。每天早上要同步上游数据午间要跑当天的统计摘要晚上要做一个归档备份。之前这些动作分散在好几个窗口里有的用界面按钮有的用命令行还有的干脆是日历提醒我去执行。1. 从每天手动点确认到 rea这个项目到底在解决什么问题1.1 一个真实到不好意思分享的早晨某个工作日我收到预警发现昨夜定时归档没能跑起来。排查结果十分尴尬不是数据源的问题也不是机器宕机只是因为我上周重构目录时把一个脚本的路径改了却没有把另一处写死的路径同步更新。数据继续往旧目录写没有报错但归档动作追踪的是新目录所以整晚都空转。那种“烂得很不起眼”的故障比大崩溃难处理得多因为没有人愿意盯着屏幕看它怎么悄悄丢东西。这种事发生几次以后我开始意识到“按流程办事”这个动作本身就是可以被系统化的。单个步骤拆开都很简单手工做也就几秒问题出在它们的组合和顺序。只要步骤超过四个人的记忆就开始不可靠。这不是注意力问题而是认知负担问题。我需要把“按顺序执行这些步骤”这件事从大脑里搬到程序里。这个念头就是 rea 项目的起点。rea 这个名字的来源很简单——“Recurring Execution Assistant”三个单词的开头字母抽出来正好拼成 rea。翻译过来就是“重复执行助手”。它的定位很清楚不做一个覆盖广泛的自动化平台只做一台你自己能看懂、能修改的本地执行编排器。1.2 为什么我不直接去搭一套更复杂的调度系统如果你也遇到同样的问题可能会说这不是现成的任务调度工具就能做吗。确实能但它们的重点在“按时触发”而我的难点其实横跨了两块第一块是按顺序执行一批动作第二块是执行完以后留下可查的日志和结果摘要。我真正缺的是一层“命令编排层”可以让我在项目目录里定义“接下来几步做什么”而不是去配置一个远程大系统。我还考虑过直接写一堆 Shell 脚本每个项目一套。缺点是同一个物种的脚本会在复制中被改出好几个版本。今天你补了新的检查逻辑明天你可能忘了同步到另一份副本。到头来你依然要记住“哪个脚本是最新版”这又从执行负担变成了管理负担。所以最后我定了一个很朴素的方案写一个本地命令行工具。它做的事情限定在三件事——初始化项目环境、按配置跑步骤、生成运行后的汇总。没有图形界面不需要常驻服务也不碰云端同步。本质上它是一条命令把你内部事先装好的步骤跑完。1.3 rea 的边界解决什么问题不解决什么问题rea 更适合下面这几类场景个人或小组内部的项目初始化、定期数据更新、构建发布前的多个检查步骤又或者只是想把“打开终端、敲几行命令、等待确认”压成“敲一行命令”。如果你是维护大规模集群或者在几百台机器上跑同一个流程那确实应该去用正统的分布式编排系统。rea 的目的不是替代它们而是填补它们覆盖不到的长尾场景。代码量不大第一版加起来一二百行核心逻辑只有几十行。这个特点带来的好处是任何会写基本脚本的同事都能看懂它的行为觉得哪里不对还能直接拉出来复盘不需要打开一个黑盒调半天。2. 为什么我把工具收敛成 init / run / report 三条命令2.1 命令越少越容易坚持使用任何一个工具刚做出来都有点“什么都想干”的趋势。我在第一版差点加了一堆子命令比如“初始化某个子项目”“执行单个任务”“查看上一轮日志”“清理临时文件”。后来想想不对维护工具的人也是人需要记住的命令越多工具最终被使用的可能性就越低。我于是砍到三个子命令init、run、report。init 负责把一个项目目录变成 rea 可以管理的环境run 负责执行配置里定义的任务序列report 负责把最近一次执行的结果打印成摘要。每一条命令的边界都很清楚不用翻文档也能猜个大概。有人会说三条命令也太少进度条、任务暂停、后台驻留这些功能都没有。我的观点是第一版做得足够简单才敢天天用复杂能力可以以后再往上加但一开始就铺得太大会让每个环节都难测试。项目做成一件靠谱的事比做成十件半吊子的事更重要。2.2 一眼能看懂的配置体系先看 rea 的配置长什么样。以前每次加新任务我都会改脚本里的参数现在换成在项目根目录维护一份 rea 配置。下面是一个最小示例project: demo cwd: ./workspace timeout: 120 tasks: - name: sync-data cwd: ./data cmd: sh sync.sh - name: run-analysis cwd: ./analysis cmd: python analysis.py - name: archive cwd: ./output cmd: tar -czf result.tar.gz result.csv这是一份 YAML 文件里面定义了三个阶段先同步数据再跑分析最后归档。每一个阶段都有独立的工作目录。rea run 读到这份配置就按顺序执行如果某一步失败默认会停下来。2.3 配置项里的一个关键细节cwd有个细节非常值得聊很多手动执行的错误不是命令写错了而是“当前目录不对”。配置文件里给每个任务单独配了 cwd这样我不需要依赖运行人当时站在哪个目录下。执行前先切到对应目录再启动具体命令日志里也会记录下当时的工作目录。一句话总结先把位置摆对再干正事。另一个同样重要的配置项是 timeout。任务可能因为外部依赖卡住或者纯粹因为数据量临时变大而变慢。全局默认值给一个不太长的 60 秒单个任务可以覆盖。这个参数不复杂但能防止一条命令把整条流水线堵死。2.4 默认值和安全边界为了让工具“约定优于配置”我给 rea 定了几条默认规则。第一不指定 cwd 时默认使用配置文件所在目录第二不指定 timeout 时默认给 60 秒第三rea 不会主动扫描项目目录之外的文件只处理配置里明确提到的路径。最后一条尤其重要。像这种自动化工具如果因为权限设置过于宽松而误删了无关目录一旦打开那个口子下次就不知道会碰坏什么。保守一点宁可让某些任务因为路径写错而失败也比误操作波及全盘强。这个安全边界可能显得执行力低但自动化工具的信任感就是这么一点点建立的它必须像一位非常礼貌的助手只做你写在纸上委托的事不多碰任何东西。2.5 把配置文件放进版本控制的连锁收益rea 的配置文件从一开始就被我放进了版本控制。这个决定在最初看起来没什么后来却是提升信任感的关键。每次改动流程我都能看到 diff哪一步的 cwd 变了哪条命令从 A 换成了 B。出了问题不再是“某个脚本被谁改过了”而是“某一次提交改了这个配置我们可以回滚”。对于团队协作效果更明显。新成员接手项目时不用四处打听“平时你都是怎么跑的”项目仓库里已经自带一份可执行的说明书。他们把仓库拉下来运行 rea init 生成本地状态再看一眼 run 的结果就完成了从陌生到接手的过程。这个价值往往比工具本身还大。3. 骨架实现执行器是怎么把活一步一步干完的3.1 先解决“顺序执行”这个基础问题rea 的核心是执行器。上面配置里的 tasks 列表执行顺序就是配置文件里的顺序。原理层面并不复杂打开配置、解析任务、逐个启动子进程、拿到返回码、写入日志、最后汇总状态。复杂一点的是依赖关系。我在第一版里不考虑并行因为并行带来的问题——资源竞争、输出交错、结果汇总要合并——会把事情彻底复杂化。而绝大多数“每天重复的流程”都是顺序执行的。如果你真的有一批互相独立的步骤想并行可以在 rea 外面套一层并发调用或者把它当作 v2 的方向。把事情拆成“先保证稳步跑完再谈加快速度”是最稳妥的路径。任何时候引入并行都要给足可观测性。两个任务同时写同一个文件、同时往终端打日志排查起来会非常痛苦。我对 rea 的要求是“结果可复现”——只要配置不变输入不变跑出来的日志序列始终稳定。3.2 执行器的核心骨架下面用 Python 简单表达一下实现的骨架。这是一个示意结构不是完整代码但能看出整体思路import os, subprocess, time, json, yaml def load_config(path): with open(path, encodingutf-8) as f: return yaml.safe_load(f) def run_task(task): start time.time() log_path os.path.join(logs, task[name] .log) cwd task.get(cwd, .) try: with open(log_path, wb) as fout: proc subprocess.Popen( task[cmd], shellTrue, cwdcwd, stdoutfout, stderrsubprocess.STDOUT ) proc.wait(timeouttask.get(timeout, 60)) return {name: task[name], ok: proc.returncode 0, duration: time.time() - start, log: log_path} except subprocess.TimeoutExpired: return {name: task[name], ok: False, reason: timeout, duration: time.time() - start} cfg load_config(rea.yaml) results [run_task(t) for t in cfg[tasks]] for r in results: print(r[name], OK if r[ok] else FAIL, r[duration]) failed [r[name] for r in results if not r[ok]] if failed: print(failed:, failed)核心逻辑集中在 run_task 上每个任务一个日志文件把标准输出和标准错误合并到同一份文件里面去避免定位问题时在多个输出流之间来回翻找。Python 的 Popen 直接负责启动子进程wait 之后再查返回码。整个循环看上像流水线每个环节留下的痕迹都很清楚。3.3 日志和退出码为什么比人眼观察更可靠最容易被低估的是退出码。很多刚接触这类工具的人习惯靠“日志最后有没有 ERROR”来判断任务是否成功这个方式很脆弱因为同一句话有时只是警告有时却是致命错误。更稳的判断标准是进程的返回码0 表示正常结束非 0 表示发生异常。rea 默认以返回码为准把输出文本留给人工来读。它的判断逻辑因此非常稳定不依赖字符串匹配出 WiFi 谁的运气。日志方面我刻意让每个任务单独落一个日志文件因为步骤多了以后把输出全部混在一起会把任务边界搅浑。每一步从什么时间开始、花了多久、最后返回什么状态都要能照着文件反查。下次回头看就像翻快递记录一样每个包裹什么时候揽收、配送、签收都一目了然。3.4 状态文件让 report 有据可依run 结束以后rea 会把本次运行的状态写成一份 last_run.json。这个文件很小大概长这样{time: 2025-06-01T08:30:00, ok: true, tasks: [ {name: sync-data, ok: true, duration: 3.2}, {name: run-analysis, ok: true, duration: 12.7}, {name: archive, ok: true, duration: 0.8} ]}report 在输出摘要时去读这个文件把结果拼成表格或者普通文本。为什么要把状态单独落一份而不是让 report 直接从另一个内存对象里拿数据因为工具退出后主进程里的一切就消失了想要“事后查证”就必须靠磁盘上的材料。把状态写成 JSON成本极低但让工具的所有下游输出都有了统一的出处。3.5 report 输出的两种形态report 命令可以按场景输出两种形态。平时我直接用纯文本眼睛扫一遍就能确认有没有失败任务需要同步给同事时我会让它输出一份 Markdown 表格顺手贴进协同工具的评论区。两种形态共享同一份 last_run.json只是渲染方式不同实现层面反射出一个很朴素的理念结构化的数据永远比格式化的文本值钱。所以我在设计时坚持不把“输出格式”和“数据来源”耦合在一起。现在看起来这个决定帮了大忙后来至少换过三种摘要样式数据文件一次都没有动过。4. 真实场景下踩到的三个坑和一个隐藏经验4.1 失败即停止还是继续执行我改了一次默认策略刚上线的时候rea 默认会跑完全部任务最后统一输出失败清单。后来我发现这是不对的。有一次同步任务已经失败后面的分析任务还继续跑拿着陈旧输入就算出了一份完整报告报告看起来十分漂亮但底数不对相当于生产了一堆废数字。真正的错误早就发生了又白白多算了二十多分钟。后来我把默认逻辑改成了失败即停止终止信息里会写清楚是哪个步骤出了错。这个逻辑虽然保守但理念很干净缺了前面的原料后面的加工就是浪费。如果你明确知道某些步骤即便前面失败也能照跑可以在命令后面加 --continue 打开“继续模式”。这是工具设计里很典型的“默认安全显式放权”。4.2 锁文件和日志名处理重复启动造成的混乱另一件发生概率很高的事是自动触发导致的重复执行。有几次我用脚本调用 rea 时上一轮还没跑完下一轮又被启动了。两个进程同时往日志目录里写文件命名规则一旦一样文件就会互相覆盖。最后我甚至找不到某一步到底有没有真的执行。解决方案分两层。第一rea 运行开始时先写一个锁文件如果检测到已有实例正在执行就直接退出并提示。第二日志文件名加上时间戳和随机短串避免不同实例的日志互相覆盖。前者防止重入后者保证即使误入了痕迹也能分开查。这两个改动用不了多少时间但体验提升非常明显。4.3 Ctrl-C 之后子进程并不是真的停下来了使用命令行工具的人偶尔会按 Ctrl-C 取消任务。我发现直接中断时主进程确实退出了但通过 Popen 拉起的子进程往往还留在后台继续占用 CPU。更麻烦的是下次我要手动清理时还得按名字去一堆进程里慢慢找。修复方案也比较经典给主进程注册信号处理器收到中断信号后先往子进程组发中断信号等一小段时间再发强杀信号如果仍不退出就标记为“强制结束”。简单说就是“先礼貌请它停再确认它停”。这里用到了进程组的概念让所有被 rea 拉起的子进程归属同一个进程组这样能一次给整组发信号不至于漏掉某个孙进程。4.4 超时和卡死判断最后一个困扰是某些任务会像轴承卡住一样既不退出也不报错就挂在那里。如果统一把超时时间设得很短那些真正需要长时间运行的任务又会被误杀设长了一个卡死的任务又会拖慢整个流程。我的权衡是配置文件里每个任务都单独指定 timeout全局默认 60 秒超过时限就按失败处理并在摘要里写清理掉“timeout”原因。任务是否卡死在本地流程里更多表现为“没有在预期时间内返回”而不是真正的死锁。所以只要 60 秒左右跑不完我宁可报错也不想盯着它干等。4.5 用“病号任务”复现边界情况的测试方法如果你也在写类似的工具我建议直接制造几个“坏任务”当试验品。配置里故意放一条会返回错误的命令把它夹在几条成功任务中间再放一条永远不退出的 sleep 任务按 Ctrl-C 看看会发生什么。有了这两个“病号”上面提到的大多数边界情况都能在十几分钟内复现并验证。测试自动化工具最怕的其实是“全流程都是绿灯”。那种环境下你永远看不到任何分支逻辑真正上线才发现各种意外路径都没有被覆盖过。把失败当作一等公民来测试是我在这次迭代里最深的体会。5. 项目跑顺以后什么值得保留哪些我劝你别急着加5.1 连续跑三个月实际省下的东西rea 在我这边连续运行了三个月最直接的变化是“早晨判断项目状态”从半个多小时缩短到按一次回车、再看一眼 report。以前我每天先看通知、再开终端、逐个手动确认现在直接 rea report几分钟内就能知道昨晚到今早的流程里到底是哪一步出了问题。对协作而言好处同样明显。我只需要把配置文件加一份简要说明放进项目仓库新接手的人执行 init run就能看到完全一样的执行环境和日志格式。环境差异、路径差异都被收敛到一个配置里不再散落在好几条不知道谁留的注释中。5.2 如果重新写一轮我会从第一天就做的三件事第一加上 dry-run 模式。它不真正执行任何命令只打印每个任务的名字、工作目录和将要执行的命令提前暴露路径写错或命令名拼错的问题。第二从一开始就定好 report 的摘要结构而不是在跑了两周之后翻着日志再想办法拼凑。第三把配置版本号或者日期写进去。否则时间一久你自己都分不清当前到底跑的是哪一份配置。这三件事单看都不难但排在项目后期去补就要额外处理兼容问题改动成本比第一天就做好高得多。5.3 可视化面板、插件系统这些“王冠功能”请冷静每隔一段时间就会有人建议我加一个 Web 页面做可视化或者做成插件系统再或者把并行依赖图画得更复杂。我的态度很明确这些可以做但不要在只有一两个人用得舒服的时候做。可视化查看的前提是你已经有一份可靠的结构化状态插件系统更不用说了它解决的是生态问题不是执行问题。如果真觉得某个项目需要更复杂的流程调度请优先考虑成熟的流程平台或持续集成系统。它们的并发、重试、告警都经过大量真实场景打磨。rea 的价值在于“给你一个能看懂的抽象层”而不是变成一个需要专门培训才能上手的复杂堡垒。5.4 关于工具的寿命管理舍得砍也要敢于留小工具最容易犯的毛病是“慢慢长胖”。今天想加一个参数明天想多一种格式最后原本清晰的三条命令变成十几个子命令和几十个开关。我给 rea 设下的一个软性约束是如果往里面加新功能必须同时砍掉一个旧功能如果找不到可砍的说明新功能还不够重要。所以到目前为止rea 依然保持着非常少的命令和很短的配置文件。它在技术上没有炫技但稳定、可见、好修这在我看来比任何复杂架构都要值钱。它不是那种“设计出来展示给同行看”的项目而是真正每天被我的手指头使用的工具。5.5 我的现实收尾这个项目给我的最大收获并不是“少按了几次回车”而是意识到很多所谓繁琐日常都可以拆成一组能验证的步骤再把顺序交给机器。它没有取代那些大型平台也没有让所有问题消失——比如我还是会偶尔发现某个脚本的路径写错了但排查时间明显缩短因为每一步都有日志和退出码出错的位置变得非常容易定位。现在每个工作日早上我的固定动作只有一个入口rea run然后去看 report。多出来的时间可以用来把早饭吃完。如果你也有几条高频重复流程刚好卡在“手工跑嫌烦、上平台又太重”的中间地带也许可以照着这个思路给自己写一个类似的小助手。叫什么名字都行重要的是它由你的流程生长出来也只服务你的流程。
阅读完成 · 觉得有帮助?
咨询建站