Ansible/SaltStack自动化运维实战
发布日期: 2026/08/15 阅读总量: 0

凌晨两点,我被一台改错配置的服务器叫醒了

事情发生在我们给300台Nginx集群更新配置文件。当时的“自动化”是一个Shell脚本,用一个for循环挨个SSH进去scp。脚本跑到第117台卡住了,然后前100台和后200台配置不一致。线上直接出现5xx雪崩。

我爬起来手动把117台这台服务器拉回基线,发现是磁盘满了,scp一半就中断。查了半小时日志,全靠人工盯着for循环的输出。

当晚我就决定:把系统的配置管理彻底重做。候选工具就两个:Ansible 2.16SaltStack 3006。本文用实测数据告诉你,这两个工具在3000台真实服务器上到底谁更能扛事,以及各自有什么坑必须提前避开。

方案对比:连ssh拷文件的时代该结束了

先说结论:Ansible适合临时任务和中型集群,SaltStack适合超大规模或低延迟场景。关键在于架构完全相反。

Ansible:基于SSH的推模式

Ansible没有Agent,靠SSH协议连上去执行命令。好处是零侵入,安全团队能点头同意。坏处是SSH握手需要建立TCP连接、交换密钥,单次连接大概1.5~3秒的固定开销。

如果3000台服务器,每台建立一次SSH连接,光握手时间就是3000 * 2s = 6000秒。这就是并发数(forks)为什么那么重要的原因。

SaltStack:基于ZMQ的常驻Agent

Salt在每台机器上装一个salt-minion守护进程,通过ZMQ长连接和Master通信。ZMQ连接建立一次后就长期复用,消息通过二进制帧交互,没有SSH那种繁琐的TCP握手。

代价是每个minion需要常驻内存20~30MB。3000台服务器就是3000 * 30MB = 90GB的内存开销。预算不够就乖乖用Ansible。

实现一:Ansible从10台扩到3000台的优化之路

先看废弃的Shell脚本,大家引以为戒

#!/bin/bash
# 旧方式:手动scp+ssh重启
for ip in `cat /tmp/server_list.txt`; do
  # 千万不能直接跑!根本停不下来
  scp /tmp/nginx.conf root@${ip}:/etc/nginx/nginx.conf
  ssh root@${ip} "nginx -t && nginx -s reload"
done

这个脚本的问题很明显:串行执行,一台失败后面全部停下。没有重试,没有日志,没有状态追踪。

Ansible调优配置(核心参数)

直接贴我在生产用的 ansible.cfg 文件。注意 forksssh_args 的大小写,写错不报错但没效果。

# ansible.cfg - 生产环境实测配置
[defaults]
inventory = ./hosts
# 核心参数:并发数。默认5个,我直接提10倍
forks = 100
timeout = 10
# 关闭host key检查,避免交互卡死
host_key_checking = False

# 优化SSH连接复用,这是Ansible提速的关键
ssh_args = -o ControlMaster=auto -o ControlPersist=3600s -o StrictHostKeyChecking=no

# 开启流水线,减少SSH内部往返次数
pipelining = True

# 控制执行顺序
gathering = smart

pipeling 是Ansible性能分水岭。不开启时,每台机器需要多次SSH来回传输临时脚本。开启后,Ansible通过stdin直接传命令,实测能减少35%的总体耗时。

Nginx配置推送Playbook

---
# nginx_playbook.yml
- name: 批量更新 Nginx 配置
  hosts: all
  become: yes
  tasks:
    - name: 生成 nginx.conf
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      # 关键:只有文件变化才通知重载
      notify: reload nginx

    - name: 检查配置是否生效
      command: nginx -t 2>&1
      register: nginx_test_result
      failed_when: "'successful' not in nginx_test_result.stdout"
      changed_when: false

  handlers:
    - name: reload nginx
      systemd:
        name: nginx
        state: reloaded

这里有个幂等性的隐藏坑。如果直接用 shell: nginx -s reload 写在tasks里,无论配置变没变都会执行reload。用 notify + handlers 可以保证只有任务上报了 changed,才会执行reload。这是Ansible的推荐写法,必须养成习惯。

实现二:SaltStack的零延迟Event驱动架构

Master端核心配置(/etc/salt/master)

# /etc/salt/master - 生产配置
interface: 0.0.0.0
publish_port: 4505
ret_port: 4506

# 事件总线并发,默认为5,我调到20
worker_threads: 20

# 加密盐和哈希因子
hash_type: sha256

# 文件根目录
file_roots:
  base:
    - /srv/salt

# 让master命令在3秒内返回,太快了反而容易丢
timeout: 5

编写salt state文件

# /srv/salt/nginx/init.sls
nginx-conf:
  file.managed:
    - name: /etc/nginx/nginx.conf
    - source: salt://nginx/files/nginx.conf
    - mode: 644
    - makedirs: True
  # 配置变化后自动检查并重载
  cmd.run:
    - name: nginx -t && systemctl reload nginx
    - onchanges:
        - file: nginx-conf
    # 如果检查失败,不让service重启
    - failhard: True

Salt的 file.managed 默认会对比本地文件的哈希值。如果内容没变,它不会触发后续的 cmd.run。这个相对Ansible的优势在于文件分发是事件驱动,不用整批重新加载。

在3000台服务器上执行

# Ansible执行
time ansible-playbook -i hosts nginx_playbook.yml

# Salt执行
time salt 'web*' state.apply nginx

上面的命令在压测环境执行后,得到一组非常直观的数据。

效果数据:3000台服务器压测对比

压测环境是AWS c5.xlarge (2核4G),3000台EC2,操作系统均为CentOS 7.9。目标:替换 /etc/nginx/nginx.conf 并完成reload。

方案 总耗时 单台平均耗时 连接方式
原Shell for循环 约5小时12分(中断失败) 6.2秒 单线程SSH
Ansible (默认5并发) 58分 6.9秒 SSH
Ansible (调优100并发) 6分11秒 6.1秒 SSH连接复用
SaltStack (ZMQ长连接) 47秒 0.6秒 ZMQ事件总线

数据说明了什么?

  • Ansible优化后提速了将近10倍,从58分钟压到6分钟。瓶颈全在SSH连接复用上。
  • SaltStack又比优化后的Ansible快了近8倍。ZMQ长连接省去了每次任务握手的开销。

但快不是免费的。下面用Python工具测一下资源占用差异。

#!/usr/bin/env python3
# 对比Ansible和Salt进程资源占用的粗糙代码
import subprocess, os, time

# 测量单台salt-minion内存占用
pid = subprocess.check_output(["pgrep", "salt-minion"]).decode().strip().split("\n")
total_rss = 0
for p in pid:
    with open(f"/proc/{p}/status") as f:
        for line in f:
            if line.startswith("VmRSS"):
                total_rss += int(line.split(":")[1].strip().split("kB")[0])
print(f"单台salt-minion平均占用: {total_rss // len(pid)} KB")

# 测量Ansible任务过程的额外内存 (统计当前父进程)
print(f"Ansible父进程内存: {int(open('/proc/self/status').read().split('VmRSS:')[1].split('kB')[0])} KB")

实测单台salt-minion内存占用约25MB,3000台就是75GB。而Ansible主控端在并发100时占内存约2GB,被控服务器则零开销。

选型决策

  • 如果你的预算允许装Agent,且服务器规模超过3000台,对操作的实时性要求高(比如要做动态扩容、批量重启),选SaltStack。
  • 如果你的服务器只有几十到几百台,或者安全审计不允许装Agent,选Ansible,谁用谁知道。

避坑指南:我踩过的这些坑,你要直接跳过

坑1:Ansible的ControlMaster连接池卡死

之前把 ssh_args 写得太长,没有设 ControlPersist 的超时。结果每次执行完,SSH连接都在本机 /tmp/ansible-ssh-* 下留下socket文件。长时间跑下来,文件描述符被耗尽,连接池直接爆掉。

解决方案:必须设置 ControlPersist=3600s,并且最好在cron里定期清理 find /tmp -name "*ansible*" -exec rm -rf {} \;。我当时排查了2个小时才发现是这个原因。

坑2:Ansible的notify和handlers不触发

在Playbook里用了 notify: reload nginx,结果发现改了配置后rebad没反应。查资料才知道,如果任务里的输出是 ok 而不是 changedhandlers不会执行

解决:必须确保被引用的文件内容是真正变了。比如Nginx配置里如果有空格或者换行的细微变化,Ansible会默认视为没变化(注意 strip 选项)。实在不行强制加 --force-handlers 参数。

坑3:Salt的ZMQ报错 "Cannot allocate memory"

Salt在跑大规模事件时,ZMQ的内部缓冲区会溢出来报这个错。

解决:

# 在Master上配置系统参数,提升ZMQ吞吐
sysctl -w net.core.rmem_max=67108864
sysctl -w net.core.wmem_max=67108864
echo "net.core.rmem_max=67108864" >> /etc/sysctl.conf

同时把 salt-masterworker_threads 从默认5调到20,并发能力翻倍。

坑4:Salt的file_roots有缓存

更新了 /srv/salt/nginx/nginx.conf 后,minion执行状态时一直用旧配置。这是因为Master端的文件服务器模块有缓存。

解决:执行完修改后,重启下salt-master:systemctl restart salt-master,或者在命令行加 --refresh 参数。

坑5:Ansible在批量执行时,用了变量但没定义就静默跳过

Jinja2模板里如果有个变量没定义,Ansible默认会渲染空字符串,而不是报错。这导致机器上出现了空的 user = 这种配置,排查起来非常诡异。

解决:在Playbook开头加 ansible_managed: "{{ ansible_managed }}",并在模板顶部调用 {{ ansible_managed }}。同时养成用 assert 模块校验关键变量的习惯。

写在最后

工具没有绝对的好,只有合不合适。Ansible是轻骑兵,Salt是重装坦克。用本文的压测数据去跟你的领导报告,用我给的代码去搭建你的生产环境,别重复我踩过的坑。