Skip to content

案例:批量运维脚本 ​

现象 ​

需要在 20 台机器上统一修改 client_max_body_size。手工 ssh 逐台改,改完发现漏了两台,一周后大文件上传在这两台机器上 413。

定位过程 ​

不是技术问题,是流程问题。手工操作的问题:

  • 20 台 × 3 条命令 = 60 次操作,漏一次没人发现。
  • 终端输出刷屏,看不出哪台失败了。
  • 改完没有验证,不知道是否生效。
  • 没有记录,下次不知道改过哪些。

处置 ​

方案一:shell + xargs 并行(轻量) ​

bash
#!/usr/bin/env bash
# /opt/ops/update-nginx-size.sh
set -uo pipefail

HOSTS_FILE="${1:?usage: $0 hosts.txt}"
OLD='client_max_body_size 1m;'
NEW='client_max_body_size 20m;'

run_one() {
  local h="$1"
  ssh -o ConnectTimeout=5 -o BatchMode=yes "$h" bash -s <<EOF
set -e
CONF=\$(nginx -T 2>/dev/null | awk '/# configuration file/{print \$NF}' | grep -v 'nginx.conf$' | head -1)
grep -q '$NEW' "\$CONF" && { echo "SKIP $h"; exit 0; }
sed -i 's/^\(\s*\)$OLD/\1$NEW/' "\$CONF"
nginx -t
nginx -s reload
echo "OK $h"
EOF
}

export -f run_one
cat "$HOSTS_FILE" | xargs -n1 -P8 -I{} bash -c 'run_one "$@"' _ {} 2>&1 | tee /tmp/nginx-update.log

echo "---- summary ----"
grep -c '^OK'   /tmp/nginx-update.log || true
grep -c '^SKIP' /tmp/nginx-update.log || true
grep -E 'FAIL|denied|Connection' /tmp/nginx-update.log || echo "no failures"
bash
cat > /tmp/hosts.txt <<'EOF'
10.0.0.41
10.0.0.42
10.0.0.43
...
EOF

./update-nginx-size.sh /tmp/hosts.txt
text
skipped: nginx reload
OK 10.0.0.41
OK 10.0.0.42
OK 10.0.0.43
...

---- summary ----
20
0
no failures

关键设计:

设计作用
set -e + nginx -t配置错了不会 reload
grep -q 幂等判断重复执行不会重复改
BatchMode=yes免密码失败时快速报错,不卡住
-P8并行,20 台几十秒完成
tee 日志有完整记录可查
汇总统计一眼看到成功/失败数量

批量执行的三个必须

  1. 先 dry-run:把 sed -i 换成 sed 's/.../.../p' 只看不写。
  2. 先单台验证:抽一台跑通再全量。
  3. 必须幂等:重复执行结果一致,否则重试会改坏。

方案二:Ansible(有规模后) ​

yaml
# update-nginx.yml
- hosts: web
  become: yes
  serial: 5
  max_fail_percentage: 0

  tasks:
    - name: ensure client_max_body_size
      lineinfile:
        path: /etc/nginx/nginx.conf
        regexp: '^\s*client_max_body_size'
        line: '    client_max_body_size 20m;'
      notify: reload nginx

    - name: verify config
      command: nginx -t
      changed_when: false

  handlers:
    - name: reload nginx
      service:
        name: nginx
        state: reloaded
bash
ansible -i inventory.ini web -m ping
ansible-playbook -i inventory.ini update-nginx.yml --check    # dry-run
ansible-playbook -i inventory.ini update-nginx.yml
text
PLAY [web] ****************************************************************

TASK [ensure client_max_body_size] ****************************************
changed: [10.0.0.41]
changed: [10.0.0.42]
...

PLAY RECAP ****************************************************************
10.0.0.41  : ok=3 changed=1 unreachable=0 failed=0 skipped=0
10.0.0.42  : ok=3 changed=1 unreachable=0 failed=0 skipped=0

Ansible 的优势:

  • serial: 5 分批,出问题最多影响 5 台。
  • --check 天然支持 dry-run。
  • 每个模块幂等。
  • 有 PLAY RECAP 明确列出失败主机。

方案三:配置管理(长期) ​

配置文件纳入 Git,用 Ansible/Chef/Puppet/Salt 保证最终状态一致。人工改服务器视为"漂移",被自动纠正。

验证 ​

不管用哪种方式,必须验证:

bash
cat /tmp/hosts.txt | xargs -n1 -P8 -I{} sh -c \
  'echo -n "{} "; ssh -o BatchMode=yes {} "nginx -T 2>/dev/null | grep -c \"client_max_body_size 20m\""'
text
10.0.0.41 1
10.0.0.42 1
10.0.0.43 1
...

每台都是 1 才算全部生效。有 0 或连接错误的,就是漏掉的。

复盘 ​

问题根因改进
漏了两台手工逐台操作脚本遍历 + 汇总
没发现漏了没有验证步骤改完统一下发验证命令
不知道改了什么没有日志tee 到文件
无法回滚没备份改前 cp -a 留备份
bash
# 改前备份
ssh "$h" "cp -a /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$(date +%F-%H%M)"

批量操作的最低标准

哪怕只用 shell,也要做到:

  1. 主机清单在文件里,不靠脑子记。
  2. 每台的输出带主机名前缀。
  3. 最后有成功/失败汇总。
  4. 有完整日志。

这四条能挡掉 90% 的"漏了一台"事故。