"写了五十个脚本,一半没人用"——问题通常不在写得差,而在该不该写从来没被判断过。写的反馈即时(跑通就有成就感),成本却延迟出现:三个月后参数靠记忆、半年后报错没人修、换人接手直接废弃。等仓库躺满半死脚本,清理成本比当初手写还高。下面给判断框架:收益来源、三个前提、成本构成、失败模式、升级判据。
1. 脚本的收益只来自三处
| 收益来源 | 计算方式 | 典型量级 |
|---|---|---|
| 省下的人工时间 | 单次耗时 × 年执行次数 | 高:每天跑、单次十几分钟 |
| 减少的人为错误 | 错误率降幅 × 单次错误代价 | 高:手工步骤多、易漏 |
| 让流程可复制 | 新人上手时间下降 | 中:需交接或多人执行 |
三项都不成立时,脚本的唯一价值是"写得爽"。顺序固定:先频率,再错误代价,最后可复制性。频率不够的,后两项再好也不值得——维护成本按年计,收益按次计。
2. 三个前提,缺一不可
| 前提 | 判据问句 | 缺了会怎样 |
|---|---|---|
| 重复频率 | 一年执行多少次? | 一年两次的,写比手做更慢 |
| 步骤稳定 | 流程最近半年变过几次? | 每月都变,脚本永远在改 |
| 错误可检 | 跑错了怎么发现? | 静默跑错,比不跑更危险 |
第三个最容易被忽略也最致命:前两个决定"值不值得写",第三个决定"敢不敢用"。
3. 该写与不该写
| 场景 | 判断 | 理由 |
|---|---|---|
| 每天定时取数、聚合、出报表 | 该写 | 高频、固定、结果可核 |
| 发布前固定检查清单 | 该写 | 高频、漏一步就出事 |
| 批量改名、批量转码 | 该写(带确认) | 固定,人工容易漏 |
| 一年两次的年度归档 | 不该写 | 写+调的时间超过手做 |
| 每次都要看情况决定的排查 | 不该写 | 判断密集,脚本只会误导 |
| 需要对外担责的审批动作 | 不该全自动 | 责任必须落在人身上 |
后三行不是"技术难",而是投入产出比或责任结构不允许。这层判断不需要技术能力,只需要诚实。
4. 成本的真实构成
新手只算第一项,所以普遍低估:
| 成本项 | 内容 | 相对量级 |
|---|---|---|
| 编写 | 写通主路径 | 1 |
| 维护 | 依赖升级、参数变、上游改 | 2–3(按年累计) |
| 失效排查 | 定位脚本错还是环境错 | 1–2 |
| 交接 | 让别人能读懂能改 | 1(有文档)否则 3 |
维护成本主要来自外部变化:上游改字段、路径变了、权限模型换了。脚本几乎不会自己坏,坏的是它依赖的世界。设计期压维护面即可:少依赖、少硬编码、出错早暴露。
5. 半自动:机器做枯燥段,人做判断段
全自动与全手动之间有一大片空间,多数场景的最优解在这里:
| 环节 | 谁做 | 形态 |
|---|---|---|
| 收集、汇总、比对 | 机器 | 一条命令出结果 |
| 异常标注 | 机器 | 输出"待人工确认"清单 |
| 最终决策与执行 | 人 | 看清单后确认或修正 |
# 机器只做前半段,判断留给人
report=$(collect_and_diff) # 只读、可重跑
echo "$report" | grep -E '^(DIFF|MISSING)' || { echo "无差异"; exit 0; }
# 有差异不自动处理,落成待确认文件
echo "$report" > /tmp/review-$(date +%F).txt
echo "待确认 $(grep -c '^DIFF' /tmp/review-$(date +%F).txt) 项"
三个好处:判断仍由人做、枯燥段被压缩、失效时后果有限;代价是一次人工确认。
6. 脚本腐化的四个信号
| 信号 | 表现 | 处置 |
|---|---|---|
| 半年没跑过 | 想不起它存在 | 删,或归档注明原因 |
| 报错没人修 | 输出全红字,大家绕开 | 修或删,别留噪声 |
| 参数靠记忆 | 只有作者知道传什么 | 补 --help 与默认值 |
| 需要"先手动改一行" | 每次跑都要编辑文件 | 改成参数,否则不算脚本 |
根因几乎总是一个:没有主人。最有效的团队规则是"每个脚本写一行 owner",而不是要求全部写文档。
7. 失败模式:静默失败最危险
| 失败模式 | 后果 | 防御 |
|---|---|---|
| 静默失败 | 以为成功,实际没做 | 退出码 + 结果断言 |
| 部分成功 | 一半改了、一半没改 | 幂等 + 可重入 |
| 越权执行 | 一次错操作被放大 | 最小权限账号 |
| 重复执行 | 重跑产生双份数据 | 幂等键/前置检查 |
原则只有一条:**宁可吵醒,也不要沉默。**脚本要自己声明成功,不能靠"没报错"推断成功。
#!/usr/bin/env bash
set -euo pipefail # 任一步失败即中断,未定义变量报错
target=/data/out/report.csv
before=$(wc -l < "$target" 2>/dev/null || echo 0)
do_the_work
after=$(wc -l < "$target" 2>/dev/null || echo 0)
[ "$after" -gt "$before" ] || { echo "FAIL: 行数未增长" >&2; exit 2; }
echo "OK: $before -> $after"
set -euo pipefail 挡住最常见的事故:中间命令失败、脚本继续跑,最后打印"完成"。
8. 幂等与可重入是硬要求
| 操作 | 幂等做法 | 不幂等的后果 |
|---|---|---|
| 创建资源 | 先查存在性,存在即成功 | 重跑产生重复条目 |
| 更新配置 | 全量覆盖(目标态) | 追加式改配置越跑越乱 |
| 发通知 | 带去重键,或先标记已发 | 群里刷屏,读者开始无视 |
判断幂等有个简单测试:**连跑两次,结果应一致。**不一致处就是重跑会出事的地方,而自动化里最难排查的故障几乎都来自"重跑一次"。
9. 触发方式的三种与各自风险
| 触发方式 | 优点 | 风险 | 适用 |
|---|---|---|---|
| 手动 | 完全可控,出事有人在场 | 依赖人的记忆 | 低频、有判断 |
| 定时 | 稳定,不依赖人 | 环境变了照跑,可能跑错窗口 | 取数、巡检、备份 |
| 事件 | 实时,耦合低 | 风暴、重复、乱序 | 数据同步、通知 |
定时任务的坑是跑在错误的时刻:节假日、月初、上游维护窗口。规避靠给脚本一个"允许运行时间窗",窗外退出并留一行日志。
事件触发的坑是重复与乱序:同一消息投两次,或先到的后处理。只能靠幂等与版本号解决,靠脚本内部状态机硬扛会失败。
10. 告警与兜底:脚本挂了谁知道
没有告警的定时任务等于没有。最小可用方案是心跳:
# 成功执行时才刷新心跳
date +%s > /var/run/report-gen.heartbeat
# 独立检查:心跳过期就告警(比查进程可靠)
now=$(date +%s); last=$(cat /var/run/report-gen.heartbeat 2>/dev/null || echo 0)
[ $((now - last)) -lt 90000 ] || echo "report-gen 超 25h 未成功" | notify
心跳比"进程在不在"可靠:进程在但卡住、跑完但结果错,查进程都看不出来。兜底还要回答:**脚本没跑成,流程怎么继续?**通常是"有手动路径",且要写下来、偶尔演练;否则只是把单点故障从人搬到了脚本。
11. 权限与审计
| 做法 | 目的 | 代价 |
|---|---|---|
| 专用账号而非个人账号 | 人走脚本能跑,权限可追溯 | 一次授权配置 |
| 最小权限 | 出错时影响面有限 | 需拆分脚本职责 |
| 动作留日志(谁、何时、改了什么) | 事后可归因 | 一点存储与实现成本 |
| 高危动作走人工闸门 | 不可逆操作不自动执行 | 每次几十秒确认 |
# 用固定账号跑,脚本里不出现个人凭证
sudo -u opsbot /opt/ops/bin/report-gen.sh
# 高危动作:只打印将要执行的内容
echo "将删除 $n 条记录:$(echo "$ids" | head -3)..."
echo "确认后执行:./purge.sh --ids-file /tmp/ids.txt"
用个人账号跑定时任务是隐性债:此人调岗或离职,任务悄然停摆且无人知晓。
12. 从脚本到工具的升级判据
| 判据 | 脚本够用 | 该升级为工具 |
|---|---|---|
| 使用者 | 只有我 | 第二个人开始用 |
| 输入形态 | 固定参数 | 组合多变,需校验与提示 |
| 失败处理 | 我看日志就行 | 别人看不懂日志 |
"有第二个人用"是最清晰的信号:从这一刻起 --help、错误信息、默认值、退出码都是接口,而接口要维护。升级不等于重写,多数情况下把参数解析与错误输出做规范就够。
13. 度量
| 指标 | 口径 | 判读 |
|---|---|---|
| 节省时间 | 单次节省 × 次数 − 维护工时 | 为负就该删 |
| 失败率 | 失败次数 ÷ 执行次数 | 上升说明环境在变 |
| 无人值守率 | 无需人工介入即成功的比例 | 低说明半自动环节太多 |
| 维护工时 | 每月花在修它的时间 | 超过收益一半就该重估 |
saving = per_run_saved * runs_per_year - maintenance_hours
maintain_share = maintenance_hours / total_hours_saved
maintain_share 超过 0.5 是明确信号:它在拖累你而不是帮你。此时正确动作通常是删掉它、改回清单式手动流程,而不是继续修。
14. 排错
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 跑完但结果不对 | 静默失败,缺断言 | 加行数/校验和断言,失败即退出 |
| 重跑一次数据翻倍 | 非幂等写入 | 改目标态覆盖或加去重键 |
| 换人接手就废 | 参数靠记忆、无 owner | 补 --help、默认值、owner 行 |
| 定时任务悄悄停了 | 个人账号、无心跳 | 专用账号 + 心跳告警 |
| 每月都在改 | 步骤不稳定,本不该自动化 | 降级为清单 + 半自动 |
15. 边界:这些环节手做更划算
| 场景 | 为什么不该写 | 替代 |
|---|---|---|
| 一年跑一两次 | 编写调试超过手做 | 写成清单照着做 |
| 步骤每月都在变 | 维护成本永远高于收益 | 半自动:机器收集,人执行 |
| 判断密集、每次不同 | 脚本覆盖不了分支 | 只工具化"信息收集" |
| 需现场判断与担责 | 责任无法转移到脚本 | 人执行,脚本做记录 |
| 环境本身不稳定 | 失效排查成本高 | 先修环境,再谈自动化 |
一句话收口:自动化的门槛不是"能不能写出来",而是"一年跑几次、错了谁发现、坏了谁修"。三问答不上来,就先别写。
16. 自测
- 你手上的脚本,收益来自省时间、减错误还是可复制?哪一项其实不成立?
- 三个前提里,你的脚本最常缺哪个?缺它时发生了什么?
- 维护成本的主要来源是什么?举一个"脚本没坏、世界变了"的例子。
- 什么失败最危险?你的脚本怎么声明"成功"?
- 怎么用一个简单测试判断脚本是否幂等?
- 哪些环节手做比写脚本划算?你打算删掉哪个脚本?