ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Shell循环脚本生产化改造:从课堂demo到可靠定时任务

Shell循环脚本生产化改造:从课堂demo到可靠定时任务 直接开工。咱们聊一个我特别有感触的话题Shell 循环脚本怎么从课堂那种“能跑出结果就行”的状态改成生产环境里敢让定时任务、报警系统、同事和领导都放心的状态。我见过太多项目业务逻辑不复杂就是一堆for和while循环在批量处理文件、批量调接口、批量巡检机器但脚本本身还是课堂实验的水平。今天这篇东西我就结合自己的实际改造经验把循环脚本生产化这条路走一遍讲清楚为什么改、怎么改、有哪些坑。这篇内容适合的人很具体刚入职一两年的运维、后端和测试同学以及那些已经在生产环境里跑过 Shell 脚本、但被“脚本又挂了”“日志什么都看不出来”“重复执行出了两份结果”这些问题折腾过的人。你会看到一个从零开始的改造全过程包括代码、参数、设计思路以及在真实服务器上踩过的坑。文章里所有例子都是我实际改过的模式脱敏后的版本可以直接抄作业。1. 从课堂脚本到生产脚本差距到底在哪1.1 课堂脚本的典型画像先还原一个典型的课堂级脚本。老师布置任务用 Shell 写一个批量备份脚本把/data/logs下的所有子目录打包成 tar.gz 存到/backup。学生通常几行就写完了for dir in /data/logs/*/; do name$(basename $dir) tar -czf /backup/$name.tar.gz $dir done echo 备份完成这段代码在课堂演示环境里什么问题都没有目录名没有空格备份目录容量足够没有并发写只跑一次跑了以后看一眼 tar 包在不在就算完事。但你把同样的逻辑丢到生产服务器上它会在一周内给你上够三节课的课。第一节课叫容错。tar命令执行失败时脚本根本不会停下来因为 Shell 默认每条命令的退出码不会中断脚本。你看到的可能是一堆tar: Cannot open: Permission denied刷屏但最后依然会打印“备份完成”然后监控系统没有告警备份清单里多了一个空文件。等哪天要恢复数据的时候才发现那个 tar 包是坏的。第二节课叫可重复性。生产任务不是跑一次就结束的定时任务每天凌晨跑一次。同一个目录被重复打包第二次生成的 tar 包会覆盖第一次。如果当天日志文件还在写入打包出来的就会是“半个文件”校验和永远对不上。课堂环境里不会有人关心你同一个脚本跑两次结果是否一致生产环境天天都在关心。第三节课叫可观测性。课堂脚本输出打印到终端人看着就行。生产脚本跑在 crontab 里输出进了邮件或者日志文件出了问题是没有人守着看的。你必须自己把日志写成方便 grep、方便按时间切分、方便统计成功失败数量的格式。我见过太多生产脚本出错信息全是tar: error while loading shared libraries这种直接抛给终端的东西事后排查完全靠猜。我随手梳理了一下课堂脚本和生产脚本的基本差异其实就浓缩在这张表里维度课堂脚本生产脚本退出码几乎不关注每条关键命令都要检查整体退出码要能反映成败运行次数跑一遍可能定时反复跑必须幂等输入数据干净的演示数据可能含空格、换行、大体积、权限不足的情况错误处理直接报错继续跑失败要重试、跳过、记录并最终汇总日志打印到终端分级日志、按时间命名、可检索、带告警并发单任务跑可能要并行处理但要控制并发数和资源锁不考虑必须防重入避免两个任务实例打架配置写死在脚本里环境变量、配置文件、命令行参数分离别小看这张表里的每一项每一项背后都有真实的故障案例。我自己就经历过一次凌晨的备份脚本在一个目录名带空格的目录上炸了整个备份目录缺了三个文件因为当时脚本用的是for dir in $(ls /data/logs)列目录永远按空格切分。后面排查的时候我盯着脚本看了好久才反应过来——这不就是课堂里学过的默认分隔符问题吗只是课堂里指导老师不会给你造一个带空格的文件名。1.2 生产环境的硬性约束课堂环境里你只需要对结果负责生产环境里你要对“整个过程是否可控”负责。这四个约束必须刻进脑子里。第一个约束是幂等。同一个任务重复跑结果不能叠加、不能冲突、不能产生不一致。对于循环脚本来说幂等设计通常要走到“先检查再执行”的思路上。批量导入任务就要先查一下记录是否已存在批量打包任务就要设计成“每次生成不同的文件名或者先写临时文件再原子改名”。我之前改造一个报表生成任务方案就是让每次运行输出带时间戳的文件而不是固定覆盖一个老文件这样即使同一分钟跑了两遍也不会互相覆盖人工比对时还能看出是哪次运行产生的。第二个约束是出错快速暴露。生产脚本出错了不能静默带过。这里不只是set -e这么简单而是要设计好“哪些错误可以继续、哪些必须终止、哪些要重试”。比如批量调外部接口单个接口返回超时可能是偶发可以重试两三次但如果循环到三分之一的时候磁盘写满了再重试就没有意义要立刻停下来并发出告警。合理的错误分级比一个全局“碰到错误就退出”的开关要可靠得多。第三个约束是资源可控。循环脚本跑在生产服务器上数据量和课堂完全不一个量级。一个for循环同时把所有任务丢到后台几百个进程一起跑起来CPU、内存、文件句柄分分钟被打满。所以生产改造非常重要的一个工作就是给循环加并发限制——比如用xargs -P限制同时跑几个或者用队列配合wait控制后台任务数量。这个我会在后面专门用一小节讲。第四个约束是可追溯。跑过的任务哪些成功了、哪些失败了、失败原因是什么、耗时多久都要有迹可循。这个看着是在说日志但实际是贯穿整个脚本设计的事情。你要从一开始就给每一条任务分配一个可识别的上下文比如文件名、任务 ID、来源 IP然后日志里每一行都要带上这个上下文。等出故障要排查的时候你会发现这种做法省下的时间是以小时计的。这四个约束不是空对空的原则。接下来我会用两个最常见的循环场景把约束落到具体代码上。2. 循环脚本的常见模式与生产改造思路2.1 批处理类循环从“跑完就行”到“可断点续跑”批处理循环几乎是所有 Shell 生产脚本里最核心的模式典型长相是这样有一批文件要处理或者有一批数据要清洗脚本遍历这批数据对每一个执行同样的操作。课堂版大概长这样for file in /data/input/*.csv; do python3 process.py $file done我来拆解一下生产版要做什么。先说断点续跑。生产批处理最常见的问题是任务跑到一半服务器重启了、依赖服务挂了、或者某个数据源卡死了整个任务中断。如果是课堂脚本中断就中断了重新跑一遍就行但生产任务如果重新跑一遍会导致一批数据被重复处理。解决办法通常有两种一是处理前检查目标是否已存在也就是幂等二是记录处理进度中断后可以接着跑。我给一个具体的进度记录方案。假设任务是对一批 CSV 执行数据清洗输出目录是/data/output那么每处理完一个文件就在另一个目录里写入一个标记文件标记文件名就是源文件名加上.done后缀。下次脚本启动时先检查标记文件是否存在存在就直接跳过。这就是最简单的断点续跑不需要引入数据库但已经能扛住绝大多数中断场景。input_dir/data/input output_dir/data/output marker_dir/data/markers mkdir -p $output_dir $marker_dir for file in $input_dir/*.csv; do name$(basename $file) marker$marker_dir/$name.done if [[ -f $marker ]]; then log INFO skip $name (already done) continue fi if ! python3 process.py $file -o $output_dir/$name.out; then log ERROR failed to process $name continue fi : $marker done注意一个细节标记文件必须等处理成功之后再创建而不是在处理前就建好。否则任务中断后你留下的是一堆“假完成”的标记排查的时候很容易被误导。另外一个细节是标记文件里最好写上处理时间、源文件校验和这些元信息后续如果要做对账这些都是好东西。再说重复数据保护。批处理任务重跑时最怕“重复生成”。比如批量生成报表如果输出路径固定第二次运行会把第一次的报表覆盖掉可能因为数据源状态不同导致两次报表对不上。改造方式很简单输出文件名带运行批次号。用时间戳就行但注意精确到秒可能不够因为同一个循环任务完全有可能在一秒内被触发两次建议带上进程 ID 或者纳秒时间戳。我个人常用这种run_id$(date %Y%m%d_%H%M%S)_$$$$是当前 Shell 的进程 ID保证同一秒内多次运行也不会重名。再配合输出目录按run_id分目录整个批处理任务的所有产物都能追溯。2.2 巡检类循环从“打印结果”到“结构化报告告警”第二种典型循环是巡检循环比如批量检查一批服务器的磁盘使用率、批量测试一批接口的连通性。课堂脚本一般是这样for host in $(cat hosts.txt); do ping -c 1 $host /dev/null echo $host ok || echo $host down done放到生产环境中最大的问题是输出不可消费。人眼扫一屏可以但几十上百台机器输出几百行你想知道“到底有多少台挂了哪几台挂得最严重”靠眼睛是不可能的。改造方向是输出结构化数据做汇总统计并且和告警通道打通。我的做法是在循环里把每一台的巡检结果写到临时目录下的独立小文件里所有循环结束后统一合并。这样做的好处有两个第一循环里不需要维护全局变量避免了 Shell 里管道子进程作用域导致的变量丢失问题第二合并阶段可以对结果做统计和排序打印出“摘要”而不是“流水账”。tmp_dir$(mktemp -d) trap rm -rf $tmp_dir EXIT for host in $(hosts.txt); do ( if ping -c 1 -W 2 $host /dev/null 21; then echo host: $host status: ok else echo host: $host status: fail fi ) $tmp_dir/$host.result done echo 巡检摘要 grep -h status: ok $tmp_dir/*.result | wc -l echo 台主机正常 grep -h status: fail $tmp_dir/*.result | wc -l echo 台主机异常 echo 异常列表 grep -h status: fail $tmp_dir/*.result || true这里我刻意把每一台的结果写到独立文件而不是直接追加到同一个文件是因为如果直接 result.txt多个子进程同时写同一个文件时可能出现交错。用独立文件就完全规避了并发写问题最后grep合并即可。这个技巧在处理大量循环任务时非常管用。巡检结果出来了还要解决“怎么让人知道”的问题。生产上通常有两个通道短信/企业微信/钉钉告警以及邮件日报。Shell 脚本里最轻量的做法是正常结果走日志异常结果走告警。告警的判断逻辑不要散落在循环里统一在汇总阶段判断——异常数量超过阈值就调用一次告警脚本避免同一个故障触发几十条重复告警。下面这段是我常用的判断模式fail_count$(grep -h status: fail $tmp_dir/*.result | wc -l) if (( fail_count 0 )); then /usr/local/bin/send_alert 巡检异常$fail_count 台主机不可达详见日志 $tmp_dir/summary.txt fi这里有个容易被忽略的点grep没匹配到内容时会返回非零退出码如果前面没加|| true或者没放进if判断里整个脚本可能会被set -e直接终止导致明明是想忽略“没有异常”的情况结果系统反而把脚本跑断了。这个细节我后面会单独拎出来讲。3. 实战改造案例把一串课堂 for 循环改造成生产级脚本3.1 原始脚本的问题清单这一节我完整走一遍改造流程用的是一个很典型的批量图片压缩任务。原始脚本长这样看起来还挺简洁#!/bin/bash for img in /data/images/*.jpg; do convert $img -resize 800x ${img%.jpg}_small.jpg done这个脚本确实能用但拿到生产环境里我会直接打回。问题清单如下第一没有全局错误控制。convert是对图片做转换的 ImageMagick 命令不是每张图都能转换成功。如果某张图片本身就是损坏的convert会报错退出但脚本不会停后面所有图片继续处理。最后从日志里根本看不出哪张失败、失败了多少张。第二没有幂等保护。脚本重复执行时会对同一批图片反复生成_small.jpg白白消耗 CPU而且如果原图已经被替换过生成出来的缩略图可能和旧文件时间戳不一致导致 CDN 缓存混乱。更严重的是如果源图已经删了但缩略图还在convert命令就会执行失败而这个失败同样不会被记录。第三没有日志。脚本跑完就是跑完了一行输出都没有。半小时之后人来看根本不知道这个任务是否跑过、消耗了多少时间、产生了多少产物。第四没有并发控制。一张一张处理在小数据量时没问题但如果目录下有几千张高清图单线程处理可能要一两个小时而生产环境完全可以并行跑只要控制并发数不把 CPU 打满。第五没有防重入机制。如果定时任务调度和手动执行撞在一起两个脚本实例同时跑就会同时操作同一批文件轻则浪费资源重则生成的文件相互覆盖、半截文件直接流入线上。第六没有处理文件名特殊字符。/data/images/*.jpg这个通配符展开在文件名包含空格和换行时会被切碎。用*展开还好一点但如果你改成for img in $(find /data/images -name *.jpg)遇到带空格的文件名就彻底炸了。这个属于for循环的典型坑。3.2 逐步改造从循环骨架到生产细节改造后的目标很明确能无限重跑、出问题能快速定位、多实例不会打架、资源占用可控。我的改造是按下面几个步骤叠加的每一步都有明确的理由。第一步把脚本头部搞干净加上严格模式和可配置项#!/bin/bash set -euo pipefail input_dir/data/images output_suffix_small max_width800 concurrency4 log_dir/var/log/image_compress run_id$(date %Y%m%d_%H%M%S)_$$ mkdir -p $log_dir log_file$log_dir/compress_${run_id}.logset -euo pipefail这三个选项我拆开解释一下set -e让脚本在命令出错时退出set -u让未定义变量直接报错而不是当成空值继续跑这个选项能抓出一堆因为变量名拼写错误导致的隐蔽问题set -o pipefail让管道命令的退出码取管道中最后一个非零退出码而不仅仅是最后一条命令的像python3 process.py | tee -a log这类写法如果前面的 python 挂掉了课堂里几乎不会被发现。这三个选项一起设是生产 Shell 脚本的第一道防线但注意set -e有它自己的局限性我会在常见问题里细说。第二步写日志函数。生产脚本里我强烈建议不要用散落的echo直接打输出而是走一个统一的日志函数带时间戳、级别、run_id。这样日志文件可以按运行实例分开排查时直接 grep 某一个 run_id 就能找到一次完整运行的全部记录。log() { local level$1 local msg$2 echo $(date %Y-%m-%d %H:%M:%S) [$level] [run:$run_id] $msg $log_file }第三步实现防重入锁。用flock命令把锁文件和应用绑定如果锁被占用就立刻退出。flock是系统层面的文件锁比单纯判断锁文件存不存在可靠得多——进程意外退出时内核会自动释放锁不会出现“锁文件残留导致任务永远跑不了”的经典问题。exec 9$log_dir/compress.lock if ! flock -n 9; then echo 另一个压缩任务正在运行退出 $log_file exit 1 fi这里exec 9的意思是以追加模式打开文件描述符 9然后flock -n 9尝试获取这个文件描述符上的排他锁。-n表示非阻塞拿不到就直接失败。注意锁文件不要放在会被清理的临时目录最好放在固定的日志或运行目录下避免因为目录被删导致锁失效。第四步循环体改造。这里是整个过程的核心我觉得全贴出来比拆着讲更直观find $input_dir -maxdepth 1 -type f -name *.jpg -print0 | while IFS read -r -d img; do name$(basename $img) out${img%.jpg}${output_suffix}.jpg if [[ -f $out ]]; then log INFO 跳过已存在: $name continue fi log INFO 开始处理: $name if ! convert $img -resize ${max_width}x $out $log_file 21; then log ERROR 转换失败: $name continue fi log INFO 处理完成: $name done这一个循环体里塞进了三个课堂脚本没有的关键设计find ... -print0配合while IFS read -r -d 是 Shell 处理文件名含空格、换行、特殊字符的标准做法-print0意味着文件名用空字符null分隔而不是换行read -d 按空字符读取IFS关掉输入字段分隔符-r防止反斜杠转义。这个组合写起来繁琐但它是处理任意文件名的正确姿势不是靠加两个引号就能替代的。if [[ -f $out ]]是幂等检查。已经生成过的缩略图直接跳过任务重跑不会重复消耗 CPU。注意我把判断放在日志之后、convert之前这样日志里能看到“为什么跳过”。检查方式不是检查源文件有没有变化而是检查输出文件是否存在因为图片压缩的需求通常是“缺什么补什么”这个逻辑更贴合实际。if ! convert ...把错误处理显式化。if !的意思是取命令退出码的反义——命令失败时进入分支记录错误后continue跳过当前文件继续下一个而不是整个脚本崩掉。这里也回答了“为什么不用set -e 不检查”这个场景里单张图片失败是可容忍的不应该终止整个任务所以要手动捕获。第五步并发控制。在循环结构里加并发最简单的办法是改成xargs -P把循环体抽成独立函数然后交给xargs并行调用。我用下面的写法保持日志顺序不乱process_one() { local img$1 # 函数内部包含幂等检查、convert、日志和上面循环体逻辑一致 } export -f process_one find $input_dir -maxdepth 1 -type f -name *.jpg -print0 | xargs -0 -I{} -P $concurrency bash -c process_one $ _ {}这里-P 4表示同时跑 4 个进程。你也许注意到了xargs -I{}会为每个参数启动一个子 shell开销比循环大一些但对图片压缩这种 IO 密集任务来说并发收益远大于额外开销。真正要小心的是进程间日志写入问题所以我在函数内部把日志写入统一到同一个日志文件并用“单条短日志多次写入”的方式尽量避免交错——日志每行都带时间戳和进程上下文个别几行交错也不影响排查。如果你想做到严格顺序可以给每个子进程分配一个 id写在日志行里最后用sort按 id 和行号收拢。3.3 验证和灰度改造完不能直接全量跑这是改造里最容易忽视、但我觉得最值得写的一环。新脚本改造完千万不能直接丢到生产环境全量跑。我自己的习惯是做三层验证。第一层用一个小样本集做功能验证。把环境变量指向一个只有几十张图的测试目录跑一遍确认产物、日志、退出码都符合预期。这里我建议加一个dry_run开关脚本只打印“要做什么”而不实际执行用来在代码 review 时快速说明行为也方便别人接手时安全试运行。dry_run${DRY_RUN:-false} if [[ $dry_run true ]]; then log INFO [DRY RUN] 将处理: $img continue fi第二层检查并发数是否符合资源预期。用top -d 1或者pidstat观察几个convert进程启动后的 CPU 占用和平均负载如果并发 4 就把 CPU 打满就要把concurrency调小。机器核心数是 4 的 8 核服务器并发压到 2 也完全合理因为生产服务器还有别的业务在跑不能把一个定时任务跑成资源杀手。第三层和旧结果做对账。如果改造前已经有旧版本的产物跑完新脚本后对比新旧缩略图的文件数、文件大小分布、缺失列表确认没有多产生一份、也没有漏掉原来有的一份。这一步很重要它本质上是验证幂等改造是否成功。我自己的灰度策略是先跑目录下十分之一的文件观察一天再逐步放大到全量。定时任务调度平台如果支持分批参数就把输入目录切换成不同的子目录。4. 循环脚本生产改造中的常见坑与排错经验4.1 常见坑速查表这一节我整理一下做 Shell 循环改造时最高频踩中的坑每一行都是真实事故的浓缩。坑 1set -e在循环里经常失灵。具体表现是脚本中途出错但整体退出码还是 0。原因在于set -e不会在if条件、while条件、或||列表的左侧、以及管道非最后一个命令这些位置触发退出。典型例子while read line循环里read读到文件末尾会返回非零set -e如果严格生效循环根本没法正常结束。所以别迷信set -e它只是兜底真正的错误处理还得靠显式判断。坑 2$(...)里的变量在子 shell 中丢失。for循环里如果写成cat file.txt | while read line; do ... donewhile是放在管道右侧的子 shell 里的循环里赋值的变量在循环结束后全部丢失。解决办法有两个一是换成进程替换while read ...; done (cat file.txt)这样while在主 shell 里执行二是把需要保留的结果写入外部文件。我强烈建议用第二种方式积累结果因为即使解决了作用域问题把大量结果塞进 Shell 变量也容易碰到内存和引用问题。坑 3echo输出里有特殊字符导致判断失灵。比如if [ $(command) ok ]如果command的输出里带了尾随空格或不可见字符判断直接就 false。生产数据里这种问题尤其多。我的经验是不要用字符串相等判断命令输出改用退出码必须用输出时用[[ ... ]]加正则或者先awk {print $1}把多余部分剥掉。坑 4循环大批量执行命令时不限制并发导致系统 OOM。很多人图省事用for 循环 把所有任务丢后台然后wait结果几百个进程一起起来单台服务器内存瞬间被打满连 SSH 都连不上。这是我在生产环境见过最危险的写法。能救命的只有一个原则永远用xargs -P或sem给并发加闸后台任务数要时刻盯住。坑 5文件名里包含换行/空格for i in $(find ...)直接切碎。这个坑前面已经踩过一遍了这里再说一次因为太常见。凡是遍历文件名一律find -print0read -d 或者直接for配合通配符展开加引号。千万不要用for i in $(command)的方式拿一堆文件名。坑 6trap只写在脚本最后。很多课堂脚本没有trap一旦中途exit临时文件永远残留。生产脚本要在一开始就写好trap并且覆盖EXIT、INT、TERM三个信号。像trap rm -rf $tmp_dir EXIT不管正常结束还是被信号杀掉都会清掉临时目录。这里注意trap里如果用了未定义的变量在set -u下可能报错所以临时目录变量必须提前定义。我把这些坑整理成一个速查表方便你贴到工位上常见坑典型后果正确姿势set -e失灵出错继续跑退出码为 0显式检查退出码if ! cmd捕获错误管道内 while 变量丢失结果统计为 0用进程替换或结果落文件for i in $(find)文件名被空格切碎find -print0read -d 无并发限制的后台任务内存打满系统卡死xargs -P限定并发数无 trap 清理临时文件残留开头设置trap ... EXIT无锁直接跑多实例互相覆盖flock加锁非阻塞抢锁日志不统一排查靠猜统一日志函数带 run_id4.2 排错实操记录这里我想记录两个我自己实际排查过的案例都很能代表生产循环脚本的真实故障现场。第一个案例是“凌晨定时任务偶尔失败但日志没有任何输出”。我接手的时候脚本长这样for host in $(cat host_list.txt); do ssh user$host df -h /data /var/log/check_disk.log done症状是每天早上大部分机器都有记录但有那么两三台机器偶尔缺席登录历史显示那几台机器当时是正常在线的。排查过程是这样的第一步手动跑脚本发现ssh命令偶尔出现Connection timed out第二步确认是网络波动导致ssh连接偶发失败第三步检查日志发现因为ssh失败后退出码非零但脚本没做任何处理只是安静地跳过第四步也是最关键的——当初写日志时没有记录退出码所以日志里既看不到“超时”也看不到“跳过”根本分不清是“机器没响应”还是“任务没跑到”。修复方法是在循环体里补上结果记录if ! ssh user$host df -h /data $log_file 21; then log ERROR $host ssh 失败退出码: $? fi注意$?必须立刻在命令执行后取中间不能插任何其他命令否则取到的是别的东西的退出码。这个案例教会我的道理是日志不但要记录成功更要记录失败和失败原因而且失败原因越具体越好。第二个案例是“并发循环偶尔生成重复的文件”。当时脚本用了一种看起来很聪明的写法for url in $(cat urls.txt); do curl -s $url -o /data/$(basename $url) done wait多个后台任务同时下载并写入同一个输出路径结果就是两个任务同时打开同一个文件互相覆盖最终文件可能是两个响应的内容拼接的。这个问题的本质是无并发限制与无冲突写入策略。修复时我做了两件事第一改用xargs -P限制并发第二每个下载任务先写临时文件再原子改名find /data/tmp -name *.part -type f -delete xargs -P 5 -I{} bash -c url$1 out/data/$(basename $url .tmp) tmp$out.$$.part curl -s $url -o $tmp mv $tmp $out _ {}mv在同一个文件系统里是原子操作所以不会出现“别人读到一个写了一半的文件”的情况。这个案例我总结出的通用经验是并发循环里所有“先写后读”的产物都应该走“临时文件 原子改名”的路线。5. 从脚本到工程后续还能怎么扩展5.1 把循环脚本沉淀成可复用模块改完一个脚本之后最容易犯的错误是把它当孤品这个业务要处理图片那个业务要处理日志各自写各自的循环。两个脚本里重复的逻辑其实非常多日志函数、锁逻辑、并发框架、幂等判断、临时文件处理。我的建议是花一点时间把公共部分抽出来单独放到一个/usr/local/lib/shell_lib.sh之类的公共文件里脚本通过source引入。# /usr/local/lib/shell_lib.sh init_log() { ... } acquire_lock() { ... } log_info() { ... } log_error() { ... }抽公共库之后有一个直接的好处公司安全规范要求日志格式统一你只需要改一个文件所有脚本同步生效不需要逐个去改几十个脚本里的echo。这也符合“同一段逻辑不要维护两份”的工程原则。脚本的循环体本身可以抽成函数放到公共库里业务差异只体现在具体的处理函数上。这里我必须提醒一句公共库不是越抽象越好。Shell 脚本的调试成本本来就比 Python 高如果为了复用写出一堆带回调函数、多级调用的“框架”出了问题后排查难度会呈指数级上升。我自己的标准是公共库只放三样东西——日志、锁、时间日期格式化最多加一个并发函数其余能写直白就不要封装。5.2 和定时任务系统、监控系统衔接生产脚本改造最终要落进调度系统。市面上的定时任务平台基本都是调用你写好的入口脚本并接收退出码。所以你在改造时要保证脚本在成功时返回 0在处理了部分失败但业务允许完成时返回 0在“整体严重失败”时才返回非零。我建议为脚本设计三种退出码语义写进文档并强制遵守退出码语义调度平台动作0全部成功或有失败但都在容忍范围内无动作1发生错误且需要人工介入告警2锁被占用、参数错误等环境问题告警并禁止自动重试退出码定义好后调度平台就可以根据退出码决定是否告警、是否触发重试。这里有一个容易忽略的点重试策略不要一刀切。锁冲突退出码 2建议不要重试等下一个周期即可而网络超时退出码 1可以分两次重试因为多数故障是偶发的。重试时最好带timeout限制防止一个任务被重试到天荒地老。再进一步脚本可以主动把处理结果推送到监控系统。最轻量的集成方式是把关键指标输出成监控采集格式例如echo image_compress_success $(grep -c 处理完成 $log_file) echo image_compress_failed $(grep -c 转换失败 $log_file)这样监控系统可以直接采集计数器按天观察任务的成功率和耗时趋势。生产脚本改造到此就已经不是“循环能跑”的水平而是纳入整个可观测体系了。定时任务从批量循环升级为数据采集源机器脚本从 crontab 里的黑盒变成数字指标这种质变是我觉得最有成就感的地方。回到文章开头那个图片压缩任务。改造完之后这个脚本能扛住几千张图片、几十个并发、文件里带空格带中文、服务器重启、定时任务重跑、甚至两个实例误调度同时运行——几乎每个课堂场景里不会出现的问题都在生产场景里验证过了。我个人在实际操作中最大的体会是Shell 循环脚本的生产改造难点从来不是语法而是思维转变。你不再是个写循环的人而是个设计“数据处理链路”的人得时刻想着失败路径、重跑行为、可观测性和资源边界。这种转变一开始会觉得很繁琐但等你被生产故障教育过两三次就会发自内心地认同。最后再分享一个小技巧每次改完循环脚本先在服务器上开着bash -x跑一遍小样本那些“以为没问题实际上执行路径和想象完全不同”的细节全是靠这个命令抓出来的。这个东西我到现在都还在用。
返回列表