Skip to content

第2章 Shell 与部署脚本(批量部署 · 环境初始化 · 上线自动化) ​

对应简历:技能第8条「熟练编写 Shell 脚本实现批量部署、环境初始化、日志清理等实施自动化」;技能第14条「熟悉 Git、Jenkins、Ansible 等自动化工具」;项目一「给数据面节点写批量 docker load 镜像加载脚本」「编写《智能合约引擎安装部署指南》」;项目二「全节点标准化初始化」。 实施版口径:本章倾斜批量部署、环境初始化、上线自动化三类实施脚本;三剑客作为脚本的零件保留核心节;Python/Ansible 只讲选型边界。手写题模板(2.11/2.12)要求能默写。 使用提醒:每个"标准回答"按可口述 45–120 秒写;模板是交付资产,背到能白板手写。

2.1 Shell 变量与特殊变量(部署脚本的地基) 【必背】 ​

标准回答:先说结论:特殊变量和退出码判断是所有部署脚本的骨架,我这三年写的每一个生产脚本开头都是同一套结构。规则层:name=value 赋值等号两边绝不能有空格;取值 $name;export 升级全局让子进程可见;引号三分——单引号原样、双引号解析变量、$() 执行命令替换(我用 $() 不用反引号,可读且能嵌套)。特殊变量必背组:$0 脚本名、$1 到 $n 位置参数、$# 参数个数、$@ 每个参数独立、$? 上一条命令退出状态(0 成功非 0 失败)、$$ 当前 PID。脚本容错基石是判断 $?:关键命令后 [ $? -eq 0 ] || exit 1,让脚本失败即停。我的部署脚本固定骨架五段:shebang、set 安全项、参数检查($# 不够就打印 usage 退出)、变量区、主逻辑——这套骨架是在银行客户现场被"脚本跑一半挂了没人知道"教育出来的。实际例子:银行项目批量加载镜像的脚本里,每个 docker load 之后都判 $?,失败立刻停下来打日志,9 个节点哪个包没进去一目了然;上线脚本里启停动作后判返回码决定走"继续发布"还是"立即回滚"。特殊变量不是考点,是脚本可信度的地基。

追问链:

  • 追问1:$* 和 $@ 的区别? → 答:不加引号行为一样;加引号时 "$*" 把所有参数拼成一个整体字符串,"$@" 保持每个参数独立。遍历参数必须 for arg in "$@",带空格的路径才不会被拆碎。我批量脚本的节点清单传参全用 "$@"——路径参数一旦被拆碎,rm/scp 类命令的行为就不可预期,这种细节是脚本能不能交给别人用的分水岭。
  • 迭问2:怎么给脚本传默认值、防空变量? → 答:${var:-默认值}——变量为空就用默认值,我的脚本里路径类变量统一加这层保护;再加 [ -n "$DIR" ] || exit 1 非空校验和 rm -rf -- "$DIR" 的 -- 分隔,三层下来"空变量拼出 rm -rf /"这类灾难就杜绝了。脚本的自卫能力是我从批量部署的坑里攒出来的写法习惯。
  • 迭问3:字符串处理在部署脚本里常用什么? → 答:${} 家族——${var##*/} 剥路径拿文件名、${var%%.*} 去后缀、${var/old/new} 替换。我的镜像加载脚本就是用这组从 tar 包文件名提取镜像名和版本做核对清单的,比 awk 切割直观,还不产生子进程,循环里大规模调用时性能优势明显。

关联话术:"面试被问脚本能力,我不背语法,直接讲结构:'我的每个交付脚本都是五段式骨架——shebang、set 安全项、参数校验、变量区、主逻辑,客户自己拿去重跑结果和交付时一致。'这句话在实施岗特别值钱,因为实施脚本最终是要交给客户用的。"

2.2 条件、循环、函数与脚本骨架 【理解】 ​

标准回答:条件用 if [[ 条件 ]],条件三组:数值 -eq/-gt、文件 -f/-d/-e、字符串 =/-z;[[ ]] 支持逻辑组合和 =~ 正则,比单括号安全,我的新脚本一律用它。多分支用 case——写 start/stop/status 启停脚本的天然结构,我交付的每个应用都有这么一个启停脚本配 systemd 或独立使用。循环三种我都在用:for i in 列表 遍历节点清单是批量脚本的主体;while read line 逐行读文件必须用 done < file 的重定向写法——接在管道后面循环体跑在子 shell 里,循环内改的变量出不来,这是踩过才记住的坑;C 风格 for ((i=0;i<n;i++)) 做计数和重试。函数把动作封装复用——deploy()、check()、rollback() 各管一段,主流程里按序调用,脚本结构立刻清晰;注意 return 只能返回 0-255 状态码,要"返回值"就 echo 输出配 result=$(func) 接。这些语法对我而言不是知识点而是肌肉记忆:三年里写的脚本 90% 就是"for 循环 + ssh/scp + 判断 + 日志"的组合,批量部署、批量巡检、批量加载镜像骨架全一样。

追问链:

  • 迭问1:批量 ping 一批 IP 输出结果怎么写? → 答:一行核心 for ip in $(cat ip.txt); do ping -c1 -W1 $ip &>/dev/null && echo "$ip ok" || echo "$ip fail"; done > ping.txt。两个要点:-c1 -W1 限制一个包一秒超时,100 台也不拖死脚本;cmd && a || b 按退出码分流。这个结构稍加改造就是我的"批量环境探测"脚本:装之前先把全部节点的连通性、磁盘空间、架构打一张表,装起来心里有底——实施前先探测环境,是我固定的工作顺序。

关联话术:"现场手写脚本我不慌,因为我真的天天写。只是我的每个脚本都比课堂版多三样东西:日志、防重、失败处理——这三样恰恰是面试官区分'背过题'和'真交付过'的标志。"

2.3 sed:批量改配置的主力 【必背】 ​

标准回答:sed 是流式行编辑器,我的口诀"查 p、删 d、改 s、增 i/a"。实施场景里它就是批量改配置的主力:15 台机器统一改 sshd 参数、统一换软件源地址、统一关 SELinux,全是 sed 干的。查:sed -n '/error/p',-n 抑制默认输出配 p 用;删:sed '/^#/d;/^$/d' 删注释空行——我看任何配置文件有效内容的第一条命令。改:sed 's/old/new/g' 全局替换;sed -i 直接改源文件,生产规范 sed -i.bak 自动留备份再落盘。最高频的实战写法是反向引用处理注释行:sed -ri 's/^#?(Port ).*/\1 2222/'——#? 匹配可能存在的注释符,(Port ) 分组用 \1 引回来,一条命令同时覆盖"已注释"和"未注释"两种形态,批量改 15 台国产化虚拟机配置时这是我的主力写法。批量变更的纪律我固定四步:单台验证 → diff 对比确认 → 批量执行 → 抽查复核,这在银行客户环境是变更纪律不是可选项。

追问链:

  • 迭问1:sed -i 有什么风险? → 答:直接改源文件、正则写错就把配置改坏且没有撤销。防线两道:sed -i.bak 自动备份;或者先不加 -i 跑一遍看标准输出,确认替换符合预期再落盘。批量场景再加单台先行验证。我给团队立的规矩就是这四步——变更类脚本的价值在"敢重跑、可回退"。
  • 迭问2:sed 的工作原理? → 答:逐行处理:当前行读进"模式空间",执行脚本命令,处理完默认打印再读下一行;-n 关掉默认打印所以必须配 p。理解这个模型就能解释 sed 为什么流式、为什么大文件也稳——它不整读内存,这也是它适合脚本批处理几十台机器的根本原因。知其然也知其所以然,这题就不丢分。

实操模板:

bash
sed -n '/ERROR/p' app.log              # 查
sed -i.bak 's/^SELINUX=.*/SELINUX=disabled/' /etc/selinux/config   # 改+备份
sed -ri 's/^#?(Port ).*/\1 2222/' sshd_config   # 反向引用兼容注释行
sed '/^$/d;/^#/d' conf > conf.clean    # 删空行+注释行

2.4 awk:取列与统计 【必背】 ​

标准回答:awk 是文本分析语言,实施视角我掌握它三个核心能力。第一取列:awk '{print $1,$7}' 按空白分列,-F: 改分隔符,$NF 是最后一列、NF 字段数、NR 行号。第二条件过滤:awk '$9==500' 按字段值筛。第三关联数组统计——awk 的灵魂:awk '{ip[$1]++} END{for(i in ip) print i,ip[i]}',一个数组完成分组统计。执行模型三段式 BEGIN{预处理} 模式{逐行} END{收尾}。这把钥匙在实施场景的用法:联调时统计 Nginx 日志给客户出访问报告——TOP10 IP、状态码分布、5xx 数量,三件套(awk 取列、sort/uniq -c 计数、head 截取)一条管道出结果;装机核对时从配置文件按列提取信息做比对表;批量部署完统计成功/失败台数。统计题的万能规律:数组下标换成你要统计的维度——IP 换成 URL 就是 URL TOP10、换成状态码就是错误分布,不用背 N 道题,只背一个模式。

追问链:

  • 迭问1:统计访问最多的前 10 个 IP? → 答:管道版 awk '{print $1}' access.log | sort | uniq -c | sort -nr | head——取列、排序、去重计数、数值倒序、截取五连;纯 awk 版 awk '{ip[$1]++} END{for(i in ip) print ip[i],i}' | sort -nr | head。面试现场我写管道版——出错率低还同时展示 sort/uniq 功底,再口头补一句"纯 awk 版少两个进程",两个版本都拿得出手。
  • 迭问2:uniq 前为什么必须 sort? → 答:uniq 只比较相邻行,相同行不相邻就不合并,计数也按相邻段算。漏掉 sort 的现象很有迷惑性:输出出现多行"1 相同IP",命令都"成功"了但结果错。这个坑我在给同事做脚本评审时指出过——管道每一环的语义要真懂,不然错了都不知道错在哪。

实操模板:

bash
awk '{print $1}' access.log | sort | uniq -c | sort -nr | head -10   # TOP10 IP
awk '{s[$9]++} END{for(c in s) print c,s[c]}' access.log | sort -k2 -nr  # 状态码分布
awk -F: '$3>=1000{print $1,$7}' /etc/passwd    # 普通用户盘点(装机核对)

2.5 grep 与管道家族 【理解】 ​

标准回答:grep 是行过滤器,参数组:-i 忽略大小写、-v 反选、-n 行号、-w 单词精确、-c 计数、-r 递归、-A/-B/-C N 上下文——排障看报错上下文我最常用 grep -C 3 error,因为一行报错往往看不出调用链。正则分两档:基础正则 grep 默认,扩展正则加 -E(+、?、{n,m}、|、()),sed 要 -r,awk 默认就是 ERE——"哪个工具默认哪档"是加分细节。管道家族补齐统计流水线:sort 排序(-n 数值、-r 倒序、-k 列)、uniq -c 计数、cut 按分隔符切列、tr 转换字符(tr -d '\r' 删 Windows 换行——处理客户传来脚本文件必用)。我日常工作的招牌五连:grep 按条件过滤 → awk 取列 → sort 加 uniq -c 计数 → sort -nr 排 TOP → head 截取,联调时给客户出访问报告、装机后核对配置清单,全是这条管道的变体。还有一条高频到肌肉记忆的:grep -Ev '^#|^$' 配置文件——看任何服务"真正生效的配置",改配置前先跑一遍,避免被一屏注释干扰判断。

追问链:

  • 迭问1:统计日志里 502 的次数? → 答:快速版 grep -c ' 502 ' access.log(两侧空格按字段边界匹配);严谨版 awk '$9==502' access.log | wc -l 按第九列值判断。联调排障时我先用快速版估量级,要出报告给客户就用 awk 版——精确字段语义,数字才敢写进交付物。

关联话术:"三剑客我的用法一句话:grep 负责找到、sed 负责改掉、awk 负责算清楚。联调排障的真实工作流:先 grep -E ' 50[0-9] ' 抓 5xx、再 awk 按分钟数组统计出故障时段请求曲线——三板斧串成管道,是我在客户现场出排障报告的标准动作。"

2.6 批量分发:免密钥 + for 循环 ssh/scp + rsync 【必背】 ​

标准回答:批量部署的第一块基石是 SSH 免密通道,标准四步:ssh-keygen -t rsa 生成密钥对 → ssh-copy-id user@节点 把公钥推过去 → 验证免密登录 → 之后 for 循环里 ssh/scp 全部免交互。批量执行的标准形态是"清单 + 循环":节点清单文件每行一个 ip 或 ip:user,for host in $(cat nodelist.txt) 循环里做三件事——scp 分发包、ssh 远程执行、结果回收。两个必配参数:ssh -o ConnectTimeout=5 限制连接超时,防一台失联拖死整个脚本;-o StrictHostKeyChecking=no 跳过首次确认,保证批量场景不卡交互。分发大目录用 rsync:增量只传差异,--exclude 排除日志、--bwlimit 限速保护客户业务网络。结果回收必须可观测:每台执行完 && echo "$host ok" || echo "$host FAIL",统一 tee 到带日期的日志,跑完 grep FAIL 一眼拿到失败清单。这套"清单即文档、失败即记录"的形态就是我在银行项目 15 台国产化虚拟机上做批量部署的实际做法——批量任务的完成度必须可验证,"跑完了"和"都成功了"是两件事。

追问链:

  • 迭问1:批量执行有一台 SSH 不通怎么办? → 答:原则是"一台失败不拖死全局"。ConnectTimeout 限制等待,命令包在 || echo unreachable 里记录后继续下一台,最后 grep 日志出失联清单单独处理。银行项目跑批量巡检时个别节点关机是常态,这个设计保证报告永远出得来、还顺手暴露失联节点——这两行代码的工程意识,比多背十个命令值钱。
  • 迭问2:为什么优先密钥不用 expect 传密码? → 答:三个理由:安全——expect 脚本里是明文密码,落盘即泄密;可靠——expect 依赖提示符精确匹配,系统提示语一变就失效;生态——密钥之上直接跑 scp/rsync/Ansible 全家桶。金融环境密钥分发要走变更申请,我都是提前申请好再做批量操作;实在受限才 expect 临时顶一下并限期替换——"会用但尽量不用",这句话体现的是选型判断力。
  • 迭问3:清单文件怎么设计? → 答:我的格式"每行一个节点 ip 或 ip:user:port",进阶版按组分段([master]、[worker])不同组执行不同动作——这个设计受 Ansible inventory 启发。清单和脚本一起进 git,加节点改一行。配置和逻辑分离就这么一小步,省掉的是"改脚本里的 IP 列表忘了备份"这类低级事故。

实操模板(批量执行骨架,可默写):

bash
#!/bin/bash
# batch_exec.sh —— 分发包并远程执行,失败不中断
set -u
LIST=/opt/scripts/nodelist.txt; PKG=/opt/pkg/app.tar.gz; LOG=/var/log/batch_$(date +%F).log
for host in $(cat $LIST); do
  echo "===== $host ====="
  scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no "$PKG" $host:/tmp/ \
    && ssh -o ConnectTimeout=5 $host "tar -xzf /tmp/app.tar.gz -C /opt && /opt/app/install.sh" \
    && echo "$host ok" || echo "$host FAIL"
done | tee $LOG
grep -c FAIL $LOG || echo "ALL SUCCESS"

2.7 环境初始化脚本的设计要求(幂等/日志/自检/防重入) 【必背】 ​

标准回答:环境初始化脚本能不能交付给客户重复使用,取决于四个工程属性,这是我把初始化做成"交付资产"的核心经验。第一幂等:跑多少遍结果一致。实现手段:配置文件用"整段覆盖写"(cat > 文件)而不是追加(>>);追加类配置先 grep -q 判断存在与否再写;目录创建用 mkdir -p;服务启用用 enable --now 本身幂等。不幂等的脚本第二次跑就重复追加出一堆脏配置,客户一重跑就出问题——这种脚本不敢交付。第二日志留痕:exec >>$LOG 2>&1 把全部输出落文件,每台机器的初始化日志归档进交付材料,出问题可回溯到具体某条命令。第三收尾自检:脚本末尾自动打印关键状态摘要(df、free、时间同步、关键参数),我拿它当每台机器的验收截图,验收材料直接引用。第四防重入:crontab 调度的长任务用 flock -n /tmp/xxx.lock -c '脚本',上一轮没跑完新触发自动退出——两个实例同时写配置是灾难。这套设计我在移动云项目 7 台节点上验证过,也是公司后续外包项目复用的标准底座。讲这题时我的总结句是:脚本能力和脚本交付能力是两回事,实施工程师要的是后者。

追问链:

  • 迭问1:怎么验证一个脚本是不是幂等的? → 答:最直接的办法——连跑两遍,第二遍结束后 diff 关键配置文件,没有任何差异才算幂等;再检查日志输出,第二遍不应该报"已存在"类错误或产生重复行。我把"连跑两遍加 diff"写进了交付前的自测步骤,脚本没有过这一关不出测试机。
  • 迭问2:set -euo pipefail 各管什么? → 答:-e 任何命令非零立即退出,防带病执行;-u 引用未定义变量报错,防手误空变量扩散;-o pipefail 管道任一环失败整条算失败——默认只看管道最后一个命令。三件套放 shebang 下一行是我所有生产脚本的标配。注意 -e 的例外位置(if 条件、&& 链里不触发),允许失败的命令显式 || true 声明意图——这个坑我踩过一次,之后团队规范加了"允许失败必须显式声明"。

关联话术:"'我交付的脚本客户自己重跑,结果和交付时完全一致'——这句话我在面试里说过,它是简历技能第 8 条'批量部署、环境初始化自动化'的信用背书:脚本不是写完就完了,是写到别人敢用为止。"

2.8 上线自动化:启停、健康检查与回滚脚本 【必背】 ​

标准回答:上线脚本是实施自动化里要求最高的一类,因为它直接面对生产窗口。我的上线发布脚本固定四段结构:预检 → 发布 → 验证 → 回滚入口。预检段:检查包是否存在、校验 MD5、检查目标目录空间、确认当前版本记录——预检不过根本不进入发布,把"包传错了""磁盘满了"这类事故挡在动作之前。发布段:备份当前版本目录(带时间戳改名)、解压新版本到发布目录、软链接 current 切换指向——用软链接做版本切换是关键设计,回滚就是把链接切回去,秒级完成;然后按序重启服务(kill -15 → 确认退出 → 启动)。验证段:轮询健康检查——端口起来没有(ss)、接口返回 200 没有(curl 带 --max-time 和重试次数)、日志有没有刷 ERROR,连续 N 次通过才算成功,任何一步失败自动触发回滚。回滚段:链接切回旧版本、重启、再验证,全程打日志。这套结构在银行项目数币平台后端的上线里反复使用过——上线动作标准化之后,割接窗口里人只做决策不做操作,手抖类事故就没有了生存空间。这也是第 15 章割接方案里"技术回滚"一节的具体实现。

追问链:

  • 迭问1:健康检查怎么设计才靠谱? → 答:三层递进:进程层 ss 查端口监听(起来没有);应用层 curl 健康检查接口(不光起了,还能正确响应);业务层抽一个核心接口验证(能响应且行为正确)。必须带超时和重试——服务启动有预热时间,一次 curl 失败就回滚会误杀。我的参数习惯:单次 --max-time 5,间隔 5 秒重试 12 次,约一分钟内出结论。健康检查脚本也是 Keepalived 和负载均衡健康检查的同一套思路——检查要贴近真实用户路径。
  • 迭问2:回滚为什么用软链接切换而不是重新解压旧包? → 答:速度和确定性。软链接切换是改名级操作,秒级完成,且旧版本目录原封没动过,状态确定性高;重新解压要传包、解压、改配置,窗口长且多变量。我的发布目录结构固定是 /opt/app/releases/版本号 加一个 current 软链接,保留最近三版——这套结构本身就是为"随时可回滚"设计的,回滚不是应急预案,是发布结构里的内置能力。
  • 迭问3(深挖):数据库变更怎么回滚? → 答:如实分层:应用和配置的回滚用软链接切换这套,我很熟;数据库 DDL 多数不可逆(drop 了就是没了),我的做法是把数据变更和应用变更分开窗口执行,DDL 前强制备份、先在测试库演练,并准备逆向 SQL(建表语句和数据导出)作为回滚手段。银行项目割接方案里我们就是这样分级的——能自动回滚的自动回滚,不能自动的走人工预案,预案里写明每个数据操作的风险等级和恢复手段。

实操模板(上线发布脚本核心段,可背):

bash
#!/bin/bash
# release.sh —— 预检/发布/验证/回滚 四段式(核心段)
set -euo pipefail
APP=/opt/app; REL=$APP/releases/$(date +%F_%H%M); CUR=$APP/current
PKG=$1; [ -f "$PKG" ] || { echo "包不存在"; exit 1; }        # 预检
mkdir -p $REL && tar -xzf "$PKG" -C $REL
[ -L $CUR ] && mv $CUR $APP/releases/prev_link_bak            # 备份
ln -sfn $REL $CUR                                             # 切换
$APP/bin/restart.sh                                           # 重启(15→确认退出→启动)
for i in $(seq 1 12); do                                      # 健康检查
  curl -sf --max-time 5 http://127.0.0.1:8080/health && { echo "RELEASE OK"; exit 0; }
  sleep 5
done
echo "CHECK FAILED -> ROLLBACK"; ln -sfn $APP/releases/prev_link_bak $CUR   # 回滚
$APP/bin/restart.sh && echo "ROLLBACK DONE"

2.9 crontab 五段式与日志清理 【必背】 ​

标准回答:crontab 表达式五段:分、时、日、月、周,五个符号:* 任意、*/n 每隔、- 范围、, 列表。示例背三个:30 3 * * * 每天凌晨 3:30;*/5 * * * * 每 5 分钟;0 2 * * * 每天凌晨 2 点。操作:crontab -e 编辑、-l 列出、-r 全删无确认(我只敢用 -l 和 -e);银行环境按客户审计要求放 /etc/cron.d 并指定运行用户。四个经典坑必须会讲:第一,% 在 crontab 里是特殊字符表示换行,date 里的 % 必须转义 \%,否则命令被截断——这是我真实踩过然后写进规范的坑;第二,cron 的 PATH 极小,脚本内外全用绝对路径;第三,cron 是非登录 shell 不加载 profile,需要的环境变量在脚本里自己 source;第四,排查分两步——/var/log/cron 确认任务有没有触发,"没触发查 cron,触发了没结果查脚本日志"。实施交付里 crontab 的三大用途:日志清理、备份任务、巡检定时。日志清理的生产版我用 find /目录 -type f -name "*.log" -mtime +7 -delete——按修改时间只删文件不动目录结构,比裸 rm -rf 可控得多;再加上"crontab 只调脚本、不写长命令"的铁律,% 的坑和路径的坑一次全避开。

追问链:

  • 迭问1:怎么确认定时任务执行了? → 答:三步验证:/var/log/cron 看触发记录;脚本自己的日志(我在每个 cron 任务里强制加 >> 日志 2>&1);看产物时间戳。经验之谈:cron 排查一半的问题第一步就是"任务行里加了重定向吗"——没有日志的 cron 任务等于盲跑,我的交付脚本一律自带日志。
  • 迭问2:日和周同时指定会怎样? → 答:这是 cron 的语义陷阱:日和周同时指定时是"或"的关系,任一条件满足就执行,不是"与"——0 0 1 * 1 是每月 1 号或每个周一都跑,而不是"每月 1 号且是周一"。要实现"与"逻辑只能在脚本里自己判断,比如开头加 [ "$(date +%d)" -eq 01 ] && [ "$(date +%u)" -eq 1 ] || exit 0。知道这个坑的存在比背答案重要,因为它会以"任务为什么多跑了"的形态出现在排障里,我就把它写进了交付脚本的注意事项。

实操模板:

bash
# 每天 2:00 日志清理(封装进脚本,crontab 只调脚本)
0 2 * * * /opt/scripts/clean_log.sh >> /var/log/clean.log 2>&1
# clean_log.sh 核心行
find /var/log/app -type f -name "*.log" -mtime +7 -delete

2.10 脚本调试与质量保障 【理解】 ​

标准回答:调试工具两件:bash -x script.sh 打开执行回显,每条命令带展开过程打印,是查变量值和执行路径的第一利器;bash -n 只做语法检查不执行,发布前必跑。还有一类高频问题——"cron 里跑不起来、手动正常",按三步收窄:bash -x 对比手动和 cron 的输出在哪一步分岔;重点查环境差异(cron 的 PATH 小、不加载 profile);crontab 行加日志重定向对比 stderr。九成原因落在路径和环境变量上。质量保障我在工具和文化上各有一层:工具层用 shellcheck 静态分析,未加引号的变量、可能为空的展开这些"平时不炸、特定输入才炸"的隐患它都能抓出来(SC2086 就是变量未加双引号),新脚本交付前过一遍清零 warning;文化层是"评审加留痕"——重要脚本互相 review 重点看 rm、delete 类写操作,所有变更脚本归档带版本。再加 dry-run 模式:批量删除、批量变更类脚本带 DRYRUN=1 只打印将执行的命令不真执行,两段式"先预览后执行"是保命流程。这三层不是形式主义——银行环境的脚本客户自己要反复重跑,质量标准是被客户逼出来的。

追问链:

  • 迭问1:脚本陷阱里最致命的是哪条? → 答:变量不加引号排第一——rm -rf $DIR 在变量为空时变成裸 rm -rf,灾难级。我的防线三层:变量展开一律双引号;危险命令的路径变量先非空校验;rm -rf -- "$DIR" 的 -- 防变量以 - 开头被当选项。成本几乎为零,杜绝一整类事故——安全性和复杂度不一定正相关。
  • 迭问2:脚本防重复执行还有什么办法? → 答:除了 flock(2.7 讲过),脚本开头 pgrep -f "$0" | grep -v $$ 自查已有实例在跑就退出。备份脚本最需要这个——全备跑 40 分钟、cron 每 10 分钟触发一次、没有锁就是灾难。这个细节在面试里说出来很加分,因为它是真实事故才会教给你的。

2.11 部署脚本模板库一:批量部署与环境初始化 【必背】 ​

标准回答:这一节是我把项目里真实用过的批量脚本整理成的可背模板,覆盖"批量部署类"三件套。模板 A 是批量初始化执行:读节点清单,逐台 scp 初始化包、远程执行 init 脚本、回收结果,配套 ConnectTimeout 防拖死、FAIL 记录防漏——银行项目 15 台国产化虚拟机的基线统一就是它的原型。模板 B 是批量镜像加载:for 遍历镜像目录、case 排除特定镜像、cat 包 | docker load 逐个导入、$? 判断失败即停——这是我在 ARM 内网环境实际写的脚本,BCS 数据面 9 个节点一次铺通,脚本和镜像清单一起沉淀进部署指南,是简历技能第 8 条最实证的一条。模板 C 是批量巡检:循环 ssh 拿回负载、内存、磁盘、失败服务四项,tee 到带日期日志——交付后移交客户前的"健康快照"就靠它出。三个模板共同的结构要求:shebang、set 安全项、变量区、清单驱动、结果可验证——面试手写任选一个,结构分加细节分都有了。

实操模板A:批量初始化部署(可默写)

bash
#!/bin/bash
# deploy_all.sh —— 分发初始化包并执行,清单驱动
set -euo pipefail
LIST=/opt/scripts/nodelist.txt
PKG=/opt/pkg/init_bundle.tar.gz
for host in $(cat $LIST); do
  echo "===== $host ====="
  scp -o ConnectTimeout=5 "$PKG" $host:/tmp/ \
    && ssh -o ConnectTimeout=5 $host \
      "tar -xzf /tmp/init_bundle.tar.gz -C /tmp && bash /tmp/init_node.sh $host" \
    && echo "$host ok" || echo "$host FAIL"
done | tee /var/log/deploy_$(date +%F).log

实操模板B:批量镜像加载(银行项目原型)

bash
#!/bin/bash
# load_images.sh —— 批量离线导入镜像(排除管理面专用镜像)
set -euo pipefail
for f in /opt/images/*.tar.gz; do
  case "$f" in *edge-manager*) echo "skip: $f"; continue;; esac
  echo "loading: $f"
  cat "$f" | docker load || { echo "FAILED: $f"; exit 1; }
done
docker images | wc -l    # 收尾核对镜像数

实操模板C:批量巡检

bash
#!/bin/bash
LIST=/opt/scripts/nodelist.txt
for host in $(cat $LIST); do
  echo "===== $host ====="
  ssh -o ConnectTimeout=5 $host \
    "hostname; uptime | awk -F'load average:' '{print \$2}'; \
     free -m | awk '/Mem/{printf \"mem %s%%\n\", \$3/\$2*100}'; \
     df -h | awk '\$6==\"/\"{print \$6, \$5}'; systemctl --failed --no-legend"
done | tee /var/log/patrol_$(date +%F).log

追问链:

  • 迭问1:巡检几十台里有一台挂了怎么办? → 答:ConnectTimeout 限等待、|| echo unreachable 记录后继续,报告永远出得来。交付场景这个设计还有一个价值:移交客户前的巡检报告能顺带暴露失联或异常节点,作为"移交时环境状态"的存档证据。
  • 迭问2:批量脚本怎么做到"敢交给客户重跑"? → 答:三个条件:幂等(2.7 的四属性)、日志全落盘、失败即停不产生半成品状态。我的批量镜像脚本失败 exit 1 停住,客户重跑时 docker load 对已导入镜像幂等(报 already exists 但不报错),所以整个脚本天然可重入——设计脚本时就把"重跑行为"想清楚,是交付脚本和自用脚本的区别。

2.12 部署脚本模板库二:上线自动化与常用小件 【必背】 ​

标准回答:第二个模板库覆盖"上线自动化"主模板加三个常用小件。主模板就是 2.8 的四段式发布脚本——预检、发布(软链接切换)、健康检查轮询、失败自动回滚,它是割接窗口里"人只做决策不做操作"的落点,这里不重复展开,面试手写按 2.8 默写。三个小件:服务启停脚本——case 分支 start/stop/status:start 先 pgrep 防重复启动再 nohup 拉起,stop 用 kill -15 等 5 秒再补 -9(数据库类绝不 -9),status 查进程加端口;这套脚本我给每个交付应用都配一份,写进部署手册的"日常运维"一节,客户现场的人照着用。服务存活自愈——crontab 每 2 分钟查端口,不通先 systemctl restart,重启后二次检测仍不通就写 FAILED 日志触发告警,"只尝试一次,失败交给人"是自愈的边界,无限重启反而掩盖故障。备份脚本——mysqldump 全量加滚动清理,参数细节(--single-transaction 不锁表等)见第 3 章。五个手写高频题(TOP IP 统计、备份、日志清理、自愈、批量巡检)全部有模板——我的准备方式是考前每个默写一遍,现场按题选模板改细节。

实操模板:服务启停脚本(可默写)

bash
#!/bin/bash
# appctl.sh —— start|stop|restart|status
APP=/opt/app/current/bin/server.sh
LOG=/var/log/appctl.log
start() { pgrep -f "server.sh" >/dev/null && { echo "already running"; return 0; }
  nohup $APP >> /var/log/server.out 2>&1 & echo "started pid $!"; }
stop()  { local p=$(pgrep -f "server.sh"); [ -n "$p" ] || { echo "not running"; return 0; }
  kill -15 $p; sleep 5; pgrep -f "server.sh" >/dev/null && kill -9 $p; echo "stopped"; }
status(){ pgrep -f "server.sh" >/dev/null && ss -lnt | grep :8080 || echo "down"; }
case "$1" in
  start) start;; stop) stop;; restart) stop; start;; status) status;;
  *) echo "Usage: $0 {start|stop|restart|status}"; exit 1;;
esac

追问链:

  • 迭问1:自愈脚本重启也拉不起来怎么办? → 答:自愈必须有边界:二次检测失败就写 FAILED 并触发告警(mail 或 curl webhook),交给人处理。无限重启会掩盖故障、刷爆日志,把"配置错误"伪装成"随机故障"。这道题真正考的是故障升级意识,不是语法——实施岗尤其在意这个,因为现场没有人能 24 小时盯着。
  • 迭问2:现场手写脚本写不出完整版怎么办? → 答:降级不空白:先复述需求确认输入输出和失败处理(既是确认也是争取构思时间,还展示工程素养);写固定骨架;填最熟的命令;拿不准的参数写注释不硬编;最后主动补一句"生产版会加日志、加 flock、加告警"。一个结构完整、注释清楚、诚实标注疑点的版本,远好过编造参数的"完整"版本——面试官考手写本质是验证有没有真写过。

2.13 Shell 与 Python / Ansible 的边界 【理解】 ​

标准回答:选型我讲两条边界。Shell 和 Python 的分界线三条:脚本超 100 行、if 嵌套超 3 层、要处理 JSON 或调 API——踩到任何一条就换 Python。我的真实分工:批量部署、环境初始化、日志清理这类命令编排用 Shell——一行管道解决的事不值得写程序,而且任何一台服务器开箱能用,这在客户内网环境是巨大优势;要算数据、出固定格式报表、调接口的用 Python——nginx 日志统计脚本、资源采集脚本这类我 Python 也写过(Counter 统计、psutil 采样的水平)。Shell 和 Ansible 的边界在规模:三五台的重复任务用脚本;十台以上、多环境、要长期维护一致性的上 Ansible——它声明式幂等(playbook 描述终态,跑十遍结果一样)、inventory 分组、模块吸收跨发行版差异。我的现状是两个项目的批量工作用 Shell 全流程扛下来了(简历技能第 14 条口径是"熟悉"),同时清楚十倍规模时 Ansible 的价值点——"脚本管机器、平台管状态",讲清边界和演进计划,比吹嘘安全得多。AI 工具对脚本效率的提升也顺带一句:我现在写复杂脚本会先让 AI 出草稿再按 2.7 的四属性逐条审——审的过程反而是我强化规范的机会(详见第 11 章)。

追问链:

  • 迭问1:Ansible 的幂等怎么保证的? → 答:两层:模块层"先检查再执行"——package 模块先判断装没装、service 先查状态,已是目标状态就返回 ok;声明层 playbook 写 state: present 描述终态而非动作序列。对比 Shell 要自己写"存在就跳过"的判断,Ansible 把幂等做进框架——和 K8s"调谐到期望态"是同一个哲学,我能把这两个体系串起来讲。

关联话术:"'Shell 扛日常自动化、Python 做要算数据的活、上规模换 Ansible'——这是我的工具箱定位。面试聊自动化我不把 Ansible 吹成主力,而是讲清我的脚本在什么规模下够用、什么时候该换平台——实施岗很多场景就是三五台到十几台,脚本的零依赖优势反而是最匹配的。"

2.14 本章案例 ​

  • 【真实项目】(银行)数据面节点批量镜像加载脚本:背景是银行内网完全无法访问外部镜像仓库,9 个 BCS 数据面节点需要各加载几十个 arm64 镜像 tar 包,逐台手工既慢又容易漏。任务由我负责把镜像高效准确地铺满全部数据面节点。行动:写 for 循环遍历镜像目录的脚本,case 语句排除 edge-manager 开头的管理面专用镜像(数据面不需要),其余逐个 cat $f | docker load 导入并 echo 进度,$? 判断成败、失败即停。结果:9 个节点一次跑通,BCS 数据面纳管如期完成,脚本和镜像清单一起沉淀进《智能合约引擎安装部署指南》——简历技能第 8 条"批量部署自动化"最实证的一条。(STAR 完整展开见第 12 章)
  • 【真实项目】(移动云)标准化初始化脚本交付:背景是新集群节点环境一致性要求高。任务是把初始化做成可复用资产。行动:按 2.7 四属性(幂等/日志/自检/防重入)实现,先试跑再全量,逐台留摘要档。结果:7 台节点十分钟一台,脚本成为公司外包项目标准底座组件——"脚本交付能力"从个人技能变成公司资产,是我最常讲的成长故事。
  • 【课程项目】MySQL 定时备份全流程:按模板完成"全量 mysqldump --single-transaction + flush-logs 切 binlog 做增量 + find -mtime 滚动清理 + crontab 2:00 调度"闭环,并做恢复演练。价值是把备份理论变成肌肉记忆,面试聊备份每个参数都有实操支撑。
  • 【经典坑】crontab 里的 % 转义:备份文件名带日期 date +%F 直接写在 crontab 命令行,任务"日志显示触发了却没产物"。定位:cron 日志确认执行了,脚本输出发现命令在 % 处被截断——% 在 crontab 里是换行特殊字符。解决:转义 \%,更彻底的是封装进脚本、crontab 只写脚本绝对路径。从此我的铁律:"crontab 只调脚本,不写长命令"——写进了交付文档的注意事项。

章末自测清单(快问快答) ​

  1. $#、$?、$! 分别是什么? → 参数个数、上条命令退出状态、最近后台进程 PID。
  2. 遍历参数为什么用 "$@"? → 每个参数独立,带空格的路径不被拆碎。
  3. 批量改 15 台配置的 sed 主力写法? → sed -ri 's/^#?(Port ).*/\1 2222/'(反向引用兼容注释行)+ -i.bak。
  4. uniq 前为什么必须 sort? → uniq 只合并相邻行,不排序计数错误。
  5. 批量 ssh 两个必配参数? → -o ConnectTimeout=5、-o StrictHostKeyChecking=no。
  6. 清单驱动的脚本好处? → 清单即文档,加节点改一行,配置与逻辑分离。
  7. 幂等脚本怎么实现? → 覆盖写不追加、先判断再写、mkdir -p、enable --now;验证靠连跑两遍 diff。
  8. set -euo pipefail 三个选项? → 出错即停、未定义变量报错、管道任一环失败整体失败。
  9. 发布脚本四段结构? → 预检、发布(软链接切换)、健康检查轮询、失败自动回滚。
  10. 回滚为什么用软链接? → 改名级秒级完成,旧版本目录原封未动,确定性高。
  11. 健康检查三层? → 进程层 ss 查端口、应用层 curl 健康接口、业务层抽核心接口。
  12. crontab 五段顺序和 % 的坑? → 分时日月周;% 是换行特殊字符必须转义或封装进脚本。
  13. cron 任务排查两步? → 查 /var/log/cron 有没有触发;触发了没结果查脚本日志。
  14. flock 的作用? → 防重复执行,上一轮没跑完新触发自动退出。
  15. Shell 换 Python 的三条线? → 超百行、嵌套三层、碰 JSON/API。
  16. 自愈脚本的边界? → 重启一次二次检测失败就告警交人,不无限重启。