跳到主要内容

批量运维实战与排障

前面几篇把语法和模块攒齐了,这篇落地:三个直接能抄的实战 playbook(批量巡检、滚动更新、ansible-pull),加上性能调优参数、排障三板斧和一份常见故障清单。这是系列的收官篇,查问题的时候直接从「排障三板斧」那节翻起。

环境说明

文中示例基于 ansible-core 2.16/2.17,控制节点为 macOS 或 Linux,受管节点为 Ubuntu 22.04。所有 playbook 默认清单已配好分组(web、app、db 等),become 提权已在 ansible.cfg 或清单里配置。

实战一:批量巡检​

巡检的需求很朴素:定时把所有机器的运行时长、磁盘、内存摸一遍,输出一张报表。Ansible 做这个比写 shell 循环舒服——连接、并发、错误处理都现成。

- name: 批量巡检
hosts: all
gather_facts: true

tasks:
- name: 获取运行时长
ansible.builtin.command: uptime -p
register: uptime_result
changed_when: false

- name: 获取根分区使用情况
ansible.builtin.command: df -h /
register: df_result
changed_when: false

- name: 获取内存使用情况
ansible.builtin.command: free -m
register: free_result
changed_when: false

- name: 解析根分区使用率(去掉 % 号)
ansible.builtin.set_fact:
disk_usage: "{{ df_result.stdout_lines[1].split()[4] }}"

- name: 解析已用内存(MB)
ansible.builtin.set_fact:
mem_used_mb: "{{ free_result.stdout_lines[1].split()[2] }}"

- name: 输出巡检报表
ansible.builtin.debug:
msg: >-
{{ inventory_hostname }}
| 运行 {{ uptime_result.stdout | replace('up ', '') }}
| 根分区 {{ disk_usage }}
| 内存 {{ mem_used_mb }}MB / {{ ansible_facts['memtotal_mb'] }}MB

执行:

ansible-playbook playbooks/inspect.yml
TASK [输出巡检报表] *******************************************
ok: [web1] => {
"msg": "web1 | 运行 3 weeks, 2 days | 根分区 42% | 内存 3102MB / 7939MB"
}
ok: [db1] => {
"msg": "db1 | 运行 5 hours, 12 minutes | 根分区 71% | 内存 6544MB / 16014MB"
}

几个细节说明:

  • command + register 把输出存下来,set_fact 把字符串解析成干净的变量,最后 debug 统一输出——这是「采集-解析-展示」的标准三段式。
  • changed_when: false 告诉 Ansible 这些命令只是读数据,不算变更,输出里就不会出现误导性的黄色 changed。
  • df -h / 第二行按空格切分后第 5 列是使用率;free -m 第二行第 3 列是已用内存。列号对不上时先在单机上手动跑一遍确认。

定时跑用 cron 就行,输出追加到日志文件留档:

crontab -e
# 每天早上 9 点巡检一次
0 9 * * * cd /opt/ansible && /usr/bin/ansible-playbook playbooks/inspect.yml >> /var/log/ansible-inspect.log 2>&1
巡检报表再进一步

想推送到飞书/钉钉/企业微信,最后加一个 ansible.builtin.uri 任务把报表 POST 到 webhook 即可;想留存历史对比,把结果写进控制节点的 CSV(ansible.builtin.lineinfile + delegate_to: localhost)。基础版够用时别急着加。

实战二:滚动更新​

一次更新几十台跑同一服务的机器,全量一把梭等于主动制造故障窗口。滚动更新的核心是一批一批来,每批更新前先摘流量、更新后确认健康再放回去,任何一批出问题立即中止。

关键参数四个:

参数作用常用值
serial每批处理多少台,支持百分比1(逐台)或 30%
max_fail_percentage失败机器占比超过该值就中止整个 play0(零容忍)
any_errors_fatal任一任务报错立即终止,不等本批跑完true
pre_tasks / post_tasks每批正式任务前 / 后执行,天然适合做摘流量和恢复见骨架

完整骨架:

- name: 滚动更新 app 服务
hosts: app
serial: 1
max_fail_percentage: 0
any_errors_fatal: true

pre_tasks:
- name: 从负载均衡摘除本节点
ansible.builtin.uri:
url: "http://lb.internal/api/backends/app/servers/{{ inventory_hostname }}/disable"
method: POST
status_code: 200

- name: 等待存量连接排空
ansible.builtin.pause:
seconds: 10

tasks:
- name: 停止应用
ansible.builtin.systemd:
name: myapp
state: stopped

- name: 下发新版本
ansible.builtin.get_url:
url: "https://artifacts.internal/myapp/{{ app_version }}/myapp"
dest: /usr/local/bin/myapp
mode: '0755'
checksum: "sha256:{{ app_sha256 }}"

- name: 启动应用
ansible.builtin.systemd:
name: myapp
state: started

- name: 等待健康检查通过
ansible.builtin.uri:
url: http://127.0.0.1:8080/health
status_code: 200
register: health
retries: 5
delay: 3
until: health.status == 200

post_tasks:
- name: 重新加入负载均衡
ansible.builtin.uri:
url: "http://lb.internal/api/backends/app/servers/{{ inventory_hostname }}/enable"
method: POST
status_code: 200

骨架里值得照抄的模式:

  • 摘流量在 pre_tasks,恢复在 post_tasks。Ansible 对每批主机的执行顺序是 pre_tasks → tasks → post_tasks,不用自己控制编排。
  • 健康检查用 retries + delay + until,服务启动慢不会误判失败。健康检查不过 → 任务失败 → any_errors_fatal 触发 → 整个 play 中止,坏版本就停在第一台机器上。
  • LB 操作换成你实际的形态:HAProxy 用 socat 打 socket,Nginx 反向代理一般是改 upstream 文件再 reload,云上 SLB 调厂商 API。
serial 和失败中止的配合

serial 只控制每批数量,不控制失败后怎么办。不配 max_fail_percentage 时默认允许全部失败——一台坏了还会继续滚下一台,只是把失败的机器踢出后续批次。滚动更新场景务必显式设 max_fail_percentage: 0 或一个你能接受的小数值。

实战三:ansible-pull 反向拉模式​

前面所有内容都是 push 模式:控制节点通过 SSH 连到受管节点执行。ansible-pull 反过来——每台受管节点自己定时从 git 仓库拉 playbook,在本地执行。

push(默认模式)pull(ansible-pull)
发起方控制节点主动连受管节点主动拉
连接要求控制节点能 SSH 到所有节点节点只要能访问 git 仓库
适用场景机房内、云 VPC 内的服务器边缘设备、NAT 后面的节点、数量极大的 fleet
变更时机执行 playbook 的时刻节点下次拉取的时刻(不精确)

适用判断:节点在门店、现场、客户内网,你根本连不进去;或者节点几千上万台,push 模式的 SSH 扇出成为瓶颈。这两种情况用 pull。

部署一句话:在受管节点上装 ansible-core,然后用 cron 或 systemd timer 定时执行:

# 从 git 拉 main 分支的 site.yml 并在本地执行
ansible-pull -U https://github.com/org/ansible-playbooks.git -C main site.yml
*/15 * * * * /usr/bin/ansible-pull -U https://github.com/org/ansible-playbooks.git -C main site.yml >> /var/log/ansible-pull.log 2>&1
pull 模式的两个注意点

一是节点拉取用的是 git 凭证(deploy key 或 token),分发凭证本身又是一道运维题,一般靠首次装机时注入。二是变更生效有最长一个拉取周期的延迟,紧急变更别指望它,紧急情况还是得能 push 进去或者人工上机。

性能调优​

机器一多,Ansible 的默认参数就开始拖后腿。按见效程度排序:

forks:并发数​

默认只有 5,意味着 100 台机器要分 20 批。控制节点性能够就直接拉大:

[defaults]
forks = 20

或者命令行临时指定 ansible-playbook -f 20 site.yml。上限看控制节点的 CPU、内存和 SSH 服务端 MaxStartups 限制,生产环境 20~50 是常见区间。

SSH pipelining​

默认 Ansible 每执行一个模块都要「打包模块 → SCP 上传 → 执行 → 清理」走一轮 SSH 往返。pipelining 把模块代码直接通过 SSH 标准输入喂过去,省掉文件传输环节,任务量大时提速明显:

[ssh_connection]
pipelining = True
老系统 sudo 的 requiretty 冲突

开启 pipelining 后如果遇到 sudo 报 sorry, you must have a tty to run sudo,是受管端 sudoers 里 Defaults requiretty 导致的。Ubuntu 22.04 默认没有这项配置,只有老系统(CentOS 6/7 时代)需要处理——在 sudoers 里去掉或按用户豁免。

ControlPersist 连接复用​

SSH 建连本身要花几百毫秒,复用长连接能省下每次任务的握手开销。新版 ansible-core 默认已开启,确认一下配置即可:

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

按需收集 facts​

不需要系统信息的 play 直接关掉:

- name: 只是下发一个配置文件
hosts: web
gather_facts: false
tasks: [...]

全局折中方案写在 ansible.cfg:gathering = smart 避免重复收集,再配 fact_caching = jsonfile 跨运行缓存:

[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = ./facts_cache
fact_caching_timeout = 3600

async + poll:长任务不卡连接​

全量备份、大版本升级这种一跑半小时的任务,同步执行容易把 SSH 连接熬断。async 让任务在受管端后台跑,poll: 0 立即返回,之后再查状态:

- name: 执行全量备份(后台跑,最长容忍 1 小时)
ansible.builtin.command: /opt/scripts/full-backup.sh
async: 3600
poll: 0
register: backup_job

- name: 等待备份完成
ansible.builtin.async_status:
jid: "{{ backup_job.ansible_job_id }}"
register: job_result
until: job_result.finished
retries: 60
delay: 60

排障三板斧​

playbook 跑挂了别慌,三板斧按顺序上,九成问题能定位。

第一板斧:逐级加 verbose。 -v 显示任务结果详情,-vv 多带任务输入参数,-vvv 显示连接过程,-vvvv 连 SSH 的完整命令行都打印出来:

ansible-playbook site.yml -v      # 先看任务级报错
ansible-playbook site.yml -vvv # 怀疑连接问题时上
ansible-playbook site.yml -vvvv # SSH 参数级别的疑难杂症

第二板斧:--check --diff 干跑。 不改任何东西,只预报会发生什么:

ansible-playbook site.yml --check --diff

--check 演练、--diff 显示文件内容差异。注意有些模块不支持 check 模式会被跳过,command/shell 在 check 模式下默认不执行。

第三板斧:ansible-config dump 看生效配置。 Ansible 配置来源多(ansible.cfg、环境变量、命令行),怀疑配置没生效时直接看最终值:

ansible-config dump | grep -i forks
DEFAULT_FORKS(/opt/ansible/ansible.cfg) = 20

输出里带括号的来源说明,和默认值不同的项还会标颜色——配置没生效一眼就能看出来是不是文件路径写错了。

常见故障清单​

SSH 连接失败(UNREACHABLE)​

报错长这样:"msg": "Failed to connect to the host via ssh: ..."。按这个顺序查:

  1. 手动 SSH 试一次:ssh user@host,Ansible 能连的前提是系统 SSH 本来就能连。
  2. 密钥问题:私钥权限是不是 600;公钥有没有 ssh-copy-id 到目标机;用的是不是清单里 ansible_user 指定的用户。
  3. known_hosts:首次连接交互式确认 host key,Ansible 里会卡住或失败。要么先手动连一次录入,要么 ansible.cfg 里设 host_key_checking = False(内网环境可以接受,公网慎重)。
  4. 端口和防火墙:非 22 端口在清单里写 ansible_port=2222;安全组 / iptables 放行控制节点来源 IP。

快速验证连通性用 ping 模块(走的是完整 SSH + Python 链路,比系统 ping 更接近真实):

ansible web -m ansible.builtin.ping

权限与 sudo 报错​

两类典型报错:

  • Missing sudo password:become 需要密码但没给。命令行加 --ask-become-pass(或缩写 -K)交互输入,或者清单里配 ansible_become_password(配了就上 vault 加密)。
  • Permission denied 类任务报错:任务本身需要 root 而 play 没开提权。给 play 或单个任务加 become: true。

生产环境的干净做法是受管端 sudoers 给运维账号配 NOPASSWD,Ansible 侧就不用管密码了。

受管端缺 Python​

Ansible 模块靠受管端的 Python 执行。Ubuntu 22.04 自带 python3 没问题,但极简镜像、网络设备、刚装的裸系统可能没有,表现为所有模块都报 module interpreter not found 之类错误。两个解法:

先用 ansible.builtin.raw 模块(不依赖 Python,直接发 SSH 命令)把 Python 装上:

ansible web -m ansible.builtin.raw -a "apt-get update && apt-get install -y python3" --become

再一个是消除解释器发现的告警:ansible-core 2.16/2.17 默认会做 auto interpreter discovery 并打 warning,在 ansible.cfg 里固定下来:

[defaults]
interpreter_python = /usr/bin/python3

模块参数报错的阅读方式​

模块报错的输出看着唬人,其实固定看两个字段就够:

fatal: [web1]: FAILED! => {
"changed": false,
"msg": "Unsupported parameters for (ansible.builtin.apt) module: update_chche. Supported parameters include: ...",
"invocation": {
"module_args": {
"name": "nginx",
"update_chche": true
}
}
}
  • msg:一句话说清错在哪,十有八九是参数名拼错、参数值类型不对、或远端缺依赖。
  • invocation.module_args:Ansible 实际收到的参数。怀疑变量没渲染对、模板传错值时看它——这里显示的是渲染后的最终值。

参数名拼错这种低级错误,配合 -vv 在任务执行前就能看到完整参数,干跑时就能发现。

最佳实践汇总​

系列写到这儿,把散在各篇的要点收拢成一份清单,新项目立项时对着过一遍:

  • 幂等优先:能用模块就不用 command/shell;非用不可就补 changed_when、creates 等条件,保证重复执行不出幺蛾子。
  • 小步提交:playbook 进 git,一次改动只干一件事,commit message 写清变更意图,出事能精确回滚。
  • 先干跑再真跑:重要变更 --check --diff 预演;大集群变更先 --limit 一台或 serial: 1 小范围验证。
  • 敏感信息一律 vault:密码、密钥、token 加密后再入库,.vault_pass 进 .gitignore 且权限 600。
  • 版本要 pin:控制节点的 ansible-core 版本、collection 依赖(requirements.yml)都锁定,团队所有人跑的是同一套。
  • 模块名写 FQCN:ansible.builtin.copy 而不是 copy,避免同名 collection 模块撞车,也让读者知道模块来自哪里。
  • 清单和变量分层:主机分组清晰,公共变量进 group_vars/all,环境差异进 group_vars/<env>,机器特有进 host_vars。
  • tags 标记大 playbook:ansible-playbook site.yml --tags nginx 能只跑局部,救火时不用等全量。
  • handler 收口变更动作:重启、reload 这类动作统一走 handler + notify,配置文件没变就不重启。

小结​

本篇是系列的最后一篇,三个实战模式(巡检报表、滚动更新、ansible-pull)覆盖了批量运维里最高频的场景,调优参数和故障清单留给出问题时翻。回看整个系列八篇的脉络:从安装配置、清单管理起步,到 ad-hoc 命令和 playbook 语法,再到变量、循环、条件、模板与 vault 这些「把 playbook 写活」的能力,最后落到本篇的「跑得好、挂了会修」。Ansible 的学习曲线到这里就算走完了,剩下的不是新知识,而是在自己的基础设施上反复练:先把一台机器管明白,再把一百台管成一台的感觉。