1 为什么把重复操作写成脚本
订单 API 的日常运维经常包含这样的操作:选择业务日期,确认输入文件已经到齐,向对账接口提交任务,检查任务是否完成,最后保存结果。偶尔执行时,工程师可以逐条输入命令;每天凌晨执行时,操作顺序、失败分支与执行证据就不能只保存在人的记忆里。脚本的价值,是把这段操作变成可以阅读、审核、重复执行的流程,并给调用者明确结果。
假设值班人员执行了下载、校验、导入这 3 步。下载失败后,如果还使用目录里昨天留下的文件继续导入,终端最后显示的“完成”会制造假象。问题并非命令不会用,而是命令之间缺少约束:哪个结果允许进入下一步,哪个失败必须停止,重跑时如何识别旧产物。可靠脚本首先描述这些关系,然后才追求少打几个字。

本课沿用前面学过的文件、管道和文本处理能力,重点补上运行契约。可以把脚本看作小型运维程序:它接收参数和环境,执行受控操作,通过标准输出、标准错误与退出码交付结果。终端、cron 和其他脚本都是调用者。终端里有人观察屏幕,定时任务里通常没有;同样的成功条件应该对所有调用者成立。
适合脚本化的是边界清楚、输入明确、步骤相对稳定的操作。例如按业务日期触发订单对账、校验日报是否生成、归档已经确认完成的结果。涉及复杂业务事务和大量并发状态时,应把业务规则放到服务或专用程序里,脚本负责调用与编排。尤其不要让脚本直接修改订单数据库来“补齐”对账结果,否则重试、权限与审计都会变得难以控制。
写脚本前先列出成功条件:指定日期的输入完整,接口接受请求,任务最终完成,输出通过校验。再列出停止条件:参数错误、凭证缺失、网络不可用、任务失败。最后确定重跑规则与证据位置。只要这些条件还说不清,自动运行只会让不确定性按时发生。把流程写下来,也能让接班的人判断脚本是否适合当前事故。
2 shebang、权限与 PATH
脚本文件只是文本,执行它需要解释器。文件首行的 shebang 指明直接执行时使用哪个解释器。例如 #!/bin/bash 要求机器在该绝对路径提供 Bash;#!/usr/bin/env bash 则先启动 env,再按当前 PATH 寻找 Bash。后者便于适配不同安装位置,也意味着解释器选择受环境影响。运维服务器应明确选用哪种方式,并在部署时检查路径。
#!/bin/bash
set -euo pipefail
printf 'order reconciliation starting\n'
保存为 reconcile-orders.sh 后,可以运行 bash reconcile-orders.sh,也可以先执行 chmod u+x reconcile-orders.sh,再用 ./reconcile-orders.sh 启动。前者明确由当前找到的 Bash 读取文件,不依靠脚本的执行位;后者需要执行权限,并让系统依据 shebang 选择解释器。文件读取权限、目录搜索权限也必须满足,不能靠添加执行位解决所有拒绝访问的问题。
sh reconcile-orders.sh 会明确选择 sh,不会因为首行写着 Bash 就自动改用 Bash。某些系统的 sh 不支持 pipefail 或 Bash 数组,因此本课示例约定使用 Bash。不要把脚本在某台机器上的偶然兼容当作部署保证。文件还应使用 Unix 换行;从其他编辑环境带入的回车字符可能让解释器路径或参数包含不可见字符,表现为“文件存在却无法执行”。
PATH 是命令搜索目录列表,不是脚本所在目录。输入 reconcile-orders.sh 时,Shell 按 PATH 查找;输入 ./reconcile-orders.sh 时,路径已明确。不要为图方便把当前目录加入系统任务的 PATH。对于定时任务,可以在脚本顶部设置经过部署约定的精简路径,例如 export PATH=/usr/bin:/bin,同时对业务程序使用确定的绝对路径。
另一个容易混淆的概念是工作目录。脚本放在 /opt/order-ops/bin,并不代表启动后就在这个目录。./data/orders.csv 相对于进程的当前目录解析。更稳妥的接口是要求输入文件使用绝对路径,或在脚本里定义明确的数据目录。若确实需要切换目录,应写成 cd /var/lib/order-ops,并让切换失败终止运行。这样从终端、cron 和部署工具启动时,文件含义保持一致。
3 set -euo pipefail:失败要早停
set -euo pipefail 是 Bash 中常用的防护组合。-e 让许多未被显式处理的非零状态导致退出;-u 让展开未设置变量成为错误;pipefail 让管道中任意命令失败可以反映到整条管道的状态。它们减少静默继续执行的机会,但不能替你定义业务成功,也不构成覆盖所有语法位置的异常机制。
先看最直观的失败。下载返回非零状态后,普通脚本仍可能执行后续校验、发布,最后由成功的 printf 留下退出码 0。启用 -e 后,位于普通顺序语句中的失败会让脚本停在下载处。交互动画比较的是同样的命令序列:错误发生位置不变,后续步骤是否被允许执行发生变化。
-e 有必须理解的边界。命令作为 if 的条件、位于某些 && 或 || 列表中、被 ! 取反等情况时,Shell 需要把失败交给条件判断,因此不会按普通语句那样退出。函数如果整体被放进条件上下文,其内部的 -e 行为也可能受影响。关键操作应显式检查并返回失败,不能仅凭文件顶部有 -e 就断言每条语句都会早停。
if /usr/bin/curl --fail --silent --show-error \
--connect-timeout 5 --max-time 30 \
--output "$response_file" "$status_url"; then
printf 'status response received\n'
else
rc=$?
printf 'status request failed: curl_rc=%s\n' "$rc" >&2
exit 1
fi
这里在 else 的第 1 条命令中保存状态,因为下一条命令会覆盖 $?。--fail 使常见 HTTP 错误响应转为 curl 失败;超时限制避免任务无限挂起。请求成功仍只说明这一层请求通过,响应里的业务状态还需要校验。返回 HTTP 200、内容却表示“对账失败”,不能作为成功发布结果的依据。
-u 对漏拼变量名很有帮助,但可选参数需要默认值。例如 ${1-} 在没有第 1 个参数时展开为空串,${RECONCILE_TOKEN:-} 在变量未设置或为空时都得到空串。随后显式检查是否允许为空。必需配置也可以用 ${API_BASE:?API_BASE is required} 尽早拒绝启动。避免在错误信息里打印凭证内容,变量存在检查只需说明缺少哪个配置。
pipefail 解决的是管道状态。producer | consumer 默认主要由末端命令决定状态,前端失败可能被末端成功掩盖。启用后,管道返回最右侧失败命令的非零状态;所有命令都成功才返回 0。它并不记录每个阶段的完整原因,也可能让 producer | head 因前端收到管道关闭信号而失败。预期会提前结束的管道应重新设计,关键数据可分步落临时文件并逐步校验。
4 参数与退出码:给人和 cron 看的契约
脚本参数应当使操作范围可见。本课对账脚本采用位置参数:reconcile-orders.sh YYYY-MM-DD。业务日期必须显式提供,不能缺省为“现在的日期”后悄悄运行。夜间任务经常处理前 1 个业务日,补跑又可能处理更早日期;把选择规则留在调用层并写入日志,可以避免服务器时区或跨日时刻改变任务含义。
usage() {
printf 'usage: %s YYYY-MM-DD\n' "$0" >&2
}
if [[ ${1-} == --help && $# -eq 1 ]]; then
usage
exit 0
fi
if (( $# != 1 )); then
usage
exit 2
fi
business_date=$1
if [[ ! $business_date =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
printf 'invalid business date format\n' >&2
exit 2
fi
这段只验证格式,并没有证明日期存在,例如 2026-02-31 仍符合表达式。正式入口还应使用部署环境已确认支持的日期工具或业务 API 验证真实日期与允许补跑区间。不要把某个平台特有的日期选项当作所有 Unix 系统共有能力。参数校验完成之前,不发起会改变业务状态的请求,也不创建表示成功的文件。
退出码是短小的机器接口。本课约定 0 表示成功,1 表示运行失败,2 表示参数或配置错误。其他数字可以约定,但应保持文档稳定,避免与 Shell 常见的 126、127 等状态混淆。退出码只能表达粗粒度结果,具体失败阶段和原因要靠日志。cron 通常不会根据非零状态自动重试或通知,必须配置监测、通知或由外部调度系统接收结果。

标准输出适合脚本承诺交付的数据,例如最终结果路径或机器可读记录;标准错误适合进度与诊断。这样调用者重定向结果时,不会把“开始下载”混进 CSV。若脚本只有运维日志,也可统一写入标准错误,并由调度器收集。无论选择哪种方式,先定义契约,再保持一致。
参数展开必须保持边界。调用函数或命令时,用 "$business_date" 传递单个日期,用 "$@" 原样转发每个参数。不要把参数拼成字符串后交给 eval,那会重新解释空格、通配符甚至命令语法。需要组合多个选项时,Bash 数组能保存参数边界。对输出路径、输入日期与操作模式分别验证,比尝试过滤所有“危险字符”更容易审查。
5 函数与复用:别复制粘贴 10 段
当多个步骤都需要日志、配置检查或状态请求时,可以抽成职责单一的函数。函数名应描述动作或判断,例如 log、require_config、fetch_status。函数依靠参数接收输入,以退出码报告成功或失败;如果还要输出数据,明确写到标准输出。把诊断放到标准错误,就能安全地用命令替换接收数据。
log() {
printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >&2
}
require_config() {
if [[ -z ${API_BASE:-} || -z ${RECONCILE_TOKEN:-} ]]; then
log 'stage=config status=failed reason=missing_config'
return 2
fi
}
validate_result() {
local result_file=$1
if [[ ! -s $result_file ]]; then
log 'stage=validate status=failed reason=empty_result'
return 1
fi
# 此处继续按业务结果格式检查状态、日期和记录数。
}
local 使变量局限于当前函数,减少临时变量覆盖外层配置的机会。函数中的 return 返回给调用者,脚本入口中的 exit 结束整个进程。可复用函数通常用 return,让入口决定如何收尾。对关键子命令显式写出失败分支,也能避免函数被 if 调用时受到 -e 条件规则影响。
命令替换也有容易忽略的状态问题。写 local response=$(fetch_status) 时,最终状态可能来自 local 内建命令,掩盖获取失败。更清晰的写法是先 local response,再 response=$(fetch_status) 并检查结果。处理关键文件时,直接让函数写入传入的临时路径,往往比把大段数据装进 Shell 变量更合适。
不要为了消除重复而把全部行为藏进通用函数。若“请求重试”函数不知道调用的是查询还是扣款,就可能把只读查询的重试策略错误用于有副作用的请求。应该复用日志格式、超时参数等稳定机制,同时把重试许可与幂等键留在业务调用处。阅读入口时,应能看见校验、获取锁、请求、确认结果、发布这条主要流程。
脚本需要共享文件时,source 会在当前 Shell 中执行其内容,因此共享文件本身也是代码,不能从可被任意用户写入的目录加载。配置文件也不必默认设计成可执行的 Shell 文件。对于少量设置,使用调度环境或受控包装脚本更直接。权限、所有者和凭证来源都应随部署交付,不能靠“大家平时这样运行”维持。
6 日志与幂等:重跑会不会毁掉订单数据
可靠脚本必须考虑“结果未知”。向订单对账接口提交请求后连接中断,服务端可能根本没收到,也可能已经创建任务但响应丢失。如果客户端简单重发,就可能创建重复任务。幂等表示在约定范围内重复同一操作,其业务效果与执行成功 1 次一致;它需要接口和存储端配合,不能只靠脚本打印“已处理”实现。

例如以 orders-reconcile:2026-10-30:v1 作为业务操作键,同一业务日期与规则版本的补跑沿用该键。服务端需持久记录键与任务的关系,并规定键的有效期及同键不同参数的处理方式。客户端遇到超时后先查询该键的状态;已完成则取回结果,处理中则等待,明确未受理才考虑再次提交。单纯每次生成新的随机键只能标识请求,无法阻止同一业务操作重复发生。
日志至少记录运行标识、业务日期、阶段、耗时与结果。时间建议使用带时区的格式,跨主机排查时更容易对齐。运行标识用于区分每次启动,业务操作键用于连接多次补跑,两者用途不同。不要写入访问令牌、完整订单明细或含敏感参数的请求地址。为了便于检索,错误消息应同时描述动作与原因,而不是只写“失败了”。
本地产物也要防止半成品被当成结果。先在目标目录创建临时文件,写完并验证后再重命名为正式文件。同一文件系统内的重命名可以让读者看到旧文件或新文件,避免看到写了一半的正式文件;这不自动保证断电后的持久性,也不等于远端业务事务完成。正式产物中应能核对业务日期、规则版本与服务端任务标识。
umask 077
result_dir=/var/lib/order-ops/results
tmp_file=$(mktemp "$result_dir/.reconcile.XXXXXX")
cleanup() {
rm -f -- "$tmp_file"
}
trap cleanup EXIT
# 获取结果到 "$tmp_file",并执行完整业务校验。
# 仅在校验成功后执行下面的发布命令。
# mv -- "$tmp_file" "$result_dir/$business_date.json"
这段是临时文件模式示意,获取和校验逻辑需要填实后才能发布。清理函数只删除本次创建的确定路径,不使用拼接出来的宽泛通配符。EXIT 清理不能处理强制杀死或主机断电,部署时仍需安排过期临时文件清理。正式目录必须预先建立并授权给运行账户,脚本不应临时放宽目录权限来绕过失败。
重试次数和退避时间也必须有界。本课把“重试次数”定义为首次尝试之外的次数,设为 2 就最多请求 3 次。若每次请求最多 10 秒,首次退避 2 秒,随后倍增且上限为 8 秒,则总等待上界还要加上各次请求时间。动画只计算预算;真实脚本应依据错误类别、服务端提示与业务幂等保证决定是否允许重试。
同一机器上还应避免定时任务重叠。可以用部署环境支持的 flock 获取非阻塞锁,并把“已有任务运行”记录成可观测事件。锁要覆盖整个关键流程;文件锁通常只能约束共享相应锁机制的进程,不能自动阻止其他主机操作同一批订单。跨主机去重仍由服务端业务键或专用协调机制承担。锁、重试与幂等分别解决不同问题,不能互相替代。
7 常见坑:未引号变量、管道吞退出码、交互假设
第 1 类坑是未加引号的变量。rm $file 可能先进行分词与文件名展开,把 1 个含空格或通配符的路径变成多个参数。通常应写 rm -- "$file",其中 -- 用来结束选项解析。引号保护参数边界,并不证明路径属于允许操作的目录;删除前仍需校验来源与范围。不要使用宽泛删除命令作为所有失败的统一收尾。
第 2 类坑是把管道的可见输出当成成功证据。例如接口请求失败后,末端文本工具仍可能正常结束并产生空文件。pipefail 能暴露部分问题,但格式正确也不代表业务完整。对账结果还要校验日期、状态、必要字段以及约定的记录统计。空数据有时合法,有时表示故障,必须依据接口契约判断,不能一律用文件大小代替业务检查。
第 3 类坑是掩盖原始退出码。command; printf 'done\n' 的最终状态通常来自打印命令。if ! command; then rc=$?; fi 中取到的又是取反后的状态,往往已经成为 0。需要原始状态时,使用 if command; then ...; else rc=$?; ...; fi。把 || true 加到每条命令末尾会让失败消失,只有明确允许忽略的清理动作才适合这种处理。
第 4 类坑是交互假设。脚本依赖登录后设置的别名、自动激活的环境、终端输入密码,或者等待用户输入 yes,到 cron 里就可能失败或挂住。任务入口应从受控来源获取凭证并检测缺失,为网络请求设置超时,禁止意外等待输入。若工具本身需要无人值守选项,应根据其文档明确配置;不能盲目用持续输入确认的命令绕过所有提示。
第 5 类坑是把预期的非零状态当成系统故障。有些查询工具用非零状态表示“没有匹配”,这可能是正常业务分支。应该在条件语句中处理,并区分无结果与执行错误。反过来,脚本依赖的工具即使返回 0,业务字段仍可能表示任务尚未完成。早停选项、退出码检查和业务校验需要共同使用,任何单项都不能包办全部判断。
最后,避免在排障时直接开启包含凭证的全程命令跟踪。set -x 可能把展开后的认证头写入日志。更合适的默认做法是记录脱敏的阶段信息、工具版本、配置是否存在及错误分类;必要的临时调试限制在已确认不会泄露敏感信息的范围。脚本错误信息应帮助下一位值班人员定位,而不是制造新的凭证清理工作。
8 案例:夜间对账脚本漏跑
订单团队发现某个业务日没有对账报告,但定时任务的日志末尾写着“done”。最初有人建议直接重跑。值班人员先查看服务端任务记录和现有产物,确认该日期没有已完成任务,再核对脚本各阶段日志。这样做是为了避免把“报告没看到”误判为“任务从未提交”,从而重复执行有副作用的动作。
证据显示,下载凭证过期,请求返回错误;旧脚本把请求直接接入文本处理管道,末端命令仍返回 0。后续导入读取的是没有清理的旧文件,最后打印成功消息。这里存在 3 个独立漏洞:请求失败未传递,输入未绑定业务日期,成功标记不依赖完整的业务校验。只增加重试次数,无法修复其中任何 1 个根因。
修复先从入口开始:要求显式日期,验证配置与真实日期,生成运行标识并写启动日志。请求加入 HTTP 错误处理、连接超时和总超时;关键响应先写本次临时文件。解析后核对业务日期与任务状态,确认成功才发布正式结果。脚本以非零状态退出时,监测应能定位业务日期与失败阶段,且不输出认证信息。
接着补上业务去重。提交任务使用稳定业务键,超时后查询对应任务;对于服务端已经完成的任务,补跑只取回结果。对于尚未提交的日期,完成凭证修复后再启动。补跑结束不能只检查脚本退出码,还要核对服务端任务、正式产物与报告日期是否一致,确保修复的是缺失结果,而非仅仅得到一次成功的进程退出。
最后增加“结果应该何时出现”的监测。若仅监测脚本报错,cron 条目被删除、机器停机或调度根本没启动时,系统不会产生错误日志。团队约定每个业务日在指定截止时刻之前必须出现成功记录;缺失、失败、仍在运行分别告警。运维目标从“任务每天被安排”变成“每个应处理日期都有可核对的结果”。
9 排查案例:cron 里「能跑」本地却失败
通常听到的是“本地能跑,cron 失败”,反过来同样可能发生。某工程师从自己的终端执行对账脚本,得到配置缺失与目录拒绝访问;昨夜 cron 却留下成功记录。这里的“能跑”先要核实:成功记录是否属于相同脚本版本、相同业务日期,是否存在服务端完成任务和通过校验的产物。不能只凭任务启动日志得出结论。

核对后发现,cron 使用专用账户,通过部署包装脚本提供 API 地址、受控凭证和固定 PATH,入口还明确指定了业务数据目录。工程师直接运行内部脚本,使用个人账户与当前项目目录,既没有配置,也没有结果目录权限。失败符合预期。处理方向是提供可审计的本地诊断入口,按角色授权所需能力;不能把凭证打印出来复制到聊天中,也不能把数据目录改成任意用户可写。
排查时按输入、解释器、身份、环境、目录、依赖、业务结果的顺序对照。记录启动命令与脚本版本,使用 id 查看身份,用 pwd 查看工作目录,用 command -v bash 和 command -v curl 查看工具选择。检查配置时只输出“已设置”或“未设置”。不要直接把全部环境变量导出到日志,因为其中可能含有令牌和连接信息。
若要复现 cron 的精简环境,可在测试配置下使用 env -i PATH=/usr/bin:/bin /bin/bash /opt/order-ops/bin/reconcile-orders.sh 2026-10-30。这会清空继承环境,却不会自动切换用户、目录、时区或权限,因此只是局部复现。缺少必要配置时,预期结果是明确失败;不能为了让演示通过而使用生产凭证执行真实写操作。
定时配置还要注意 cron 实现与运行环境的约定:默认工作目录、Shell、时区、日志投递方式各不相同,不能把个人终端配置当作保证。计划时间也不代表停机后自动补跑,补偿能力需要单独设计。业务日期最好由受控入口明确计算并传入,日期计算中的时区与节假日规则要和订单系统一致。
排查完成后,把可复现的运行条件写进部署说明:运行账户、解释器路径、工具依赖、数据目录权限、配置来源、业务日期规则和成功证据。保留“配置不全就拒绝启动”的行为,同时给出可理解的原因。这样本地失败与 cron 成功不再神秘,而是可以通过条件差异解释;下一次迁移主机也有明确的验收依据。
放进方案里
在订单 API 运维方案中,把夜间对账定义为按业务日期执行的受控任务。入口脚本使用明确的 Bash 路径,启用 set -euo pipefail,启动时检查参数、必要配置、依赖和目录权限。业务日期由调度入口按约定时区传入,脚本验证日期及补跑范围,通过校验后才允许请求接口。运行账户只获得所需访问权限,凭证从受控来源注入,日志只记录配置是否存在。
服务端以业务日期和规则版本形成稳定幂等键,保存该键与任务结果的关系。提交超时后,脚本先查询任务状态;只有允许重试的错误才进入有上限的退避流程。方案明确最大重试次数、请求超时与等待预算,使任务能在截止时刻前结束。同机任务使用锁避免重叠,跨主机的重复提交由服务端幂等契约约束。
结果先写入目标目录内的临时文件,核对日期、任务状态和必要业务字段后,再发布正式产物。退出码约定为 0 成功、1 运行失败、2 参数或配置错误,日志关联运行标识、业务日期、阶段与耗时。定时平台收集输出并触发失败通知,同时按业务截止时间检查成功记录是否缺失,以覆盖任务根本没有启动的情况。
上线前在隔离环境演练缺参、凭证缺失、请求超时、已有任务、重复运行和结果损坏,检查停止位置、退出码与正式产物是否符合约定。补跑流程先查服务端记录,再决定取回结果或重新提交。交付材料记录脚本版本、调度配置、账户权限、配置来源、日志位置与责任人,使值班人员能够依据证据恢复缺失日期,而不是依赖临时口令和个人终端环境。
10 小结:脚本可靠性清单
可靠脚本把重复操作变成有输入、有边界、有结果证据的程序。发布前可以逐项核对:
- 解释器、执行权限、
PATH与工作目录是否明确。 - 参数和配置是否在产生副作用前完成校验。
- 是否理解
-e的条件例外,并显式处理关键失败。 - 管道状态、HTTP 状态与业务状态是否分别检查。
- 变量是否正确引用,日志与数据输出是否分开。
- 请求是否有超时,重试是否有边界并符合幂等契约。
- 是否防止任务重叠,临时文件是否校验后再发布。
- 成功、失败与根本未启动是否都能被发现。
自测时解释 3 个问题:为什么 curl 返回 0 仍不能证明对账完成?为什么超时后重新生成幂等键可能重复创建任务?为什么 cron 日志没有错误仍需要检查成功记录是否缺失?能沿着请求、业务状态与结果证据回答这些问题,就抓住了本课的可靠性重点。
下一课转向 S11 进程与服务管理:把本章验收过的结论带进后续主题,写成可检查的方案约束。