记得那是我在一家做在线教育平台的公司,当时我们运维团队只有三个人,要管着两百多台服务器。那时候最怕听到的词就是“扩容”。每次市场部搞活动,我们就得手动登录几十台机器去改Nginx配置、部署后端jar包。我那时候写了一个Shell脚本,原本以为能救急,结果因为不同机器的环境变量差异,跑一半卡住了,最后还得一台台手动回滚。那时候我就意识到,靠“脚”跑的脚本,跑不赢业务增长的速度。
后来我们要引入一套自动化工具。当时团队内部吵翻了天,有人提议用SaltStack,有人觉得Chef才是正统。我作为那个背锅侠,决定亲自去测一遍。
先说Chef。我试着用Chef搭建了一套环境,它的理念很先进,基于Ruby DSL,写起来像写代码。但问题也在这,为了配置一个简单的Nginx,我得先学Ruby语法,还得理解Cookbook、Recipe、Resource这些复杂的概念。对于我们这种小团队,学习曲线太陡峭了。而且Chef需要在客户端安装Agent(chef-client),那次我在预发布环境推送一个更新,因为Agent版本不一致,导致一半的节点没响应,排查了半天才发现是Ruby依赖冲突。
再看SaltStack。它的执行速度确实快,基于ZeroMQ的消息队列,响应非常灵敏。我当时很喜欢它的state.sls状态管理。但在实际测试大规模并发时,Salt Master的压力非常大。有一次我们同时给80台机器推送一个安全补丁,salt '*' cmd.run 'yum update' 跑起来后,Master节点的CPU直接飙到了90%以上,甚至出现了消息丢失的情况。而且SaltStack的配置相对复杂,对于刚接手的新人来说,理解Reactor和Engine机制需要很长时间。
最后我选了Ansible。为什么?因为它足够“轻”。我当时在笔记本上只用了十分钟就搭起了一个控制端。它不需要在被管节点上安装任何Agent,只要能SSH通就行。对于我们这种大部分是物理机或者KVM虚拟机的环境来说,这简直是救命稻草。
我拿当时最痛的一个场景举例:给所有Web服务器分发一个新的SSL证书。如果用Shell,我得写个循环,还得处理密码输入。用Ansible,我只需要写一个Playbook:
这套逻辑跑起来非常简单,直接执行 ansible-playbook update_cert.yml。它最大的优势在于幂等性。我那时候担心如果证书没变,会不会报错或者导致Nginx重启影响业务?结果Ansible的copy模块很聪明,它会先计算文件的MD5值,如果目标文件和源文件一致,它就直接跳过,不做任何操作。这和SaltStack或者Chef那种强制覆盖的思路完全不同,它更像是在说:“如果没坏,就别修。”
现在回过头看,Ansible 10.0.0(基于 Core 2.17)依然保持着这种无代理的优雅。相比之下,Chef和SaltStack的Agent维护成本在长期运行中会成倍增加。对于我这种需要快速响应业务、又不想被工具本身绑死的人来说,Ansible是那个最务实的选择。
去年双十一前夕,我们接到了一个任务:把全站两百多个微服务的网关入口从HTTP/1.1切换到HTTP/2。这涉及到近千台Nginx配置文件的修改。当时我看着那堆参差不齐的配置文件,心里就在想,这要是手动改,改到明年也改不完,而且只要有一台改错了,大促期间接口超时,那就是事故。
我决定用Ansible 10.0.0来干这个活。我们当时的环境是混合云,有IDC的物理机,也有阿里云的ECS。Ansible 10.0.0的动态清单(Dynamic Inventory)帮了大忙,我直接用alibaba.cloud.alicloud集合拉取了所有ECS实例,结合本地CMDB的静态清单,生成了一个统一的主机列表。
核心挑战在于Nginx配置模板。我们不能用简单的copy模块覆盖,因为不同业务的proxy_pass地址不一样。我决定用template模块配合Jinja2。
这是当时我写的Playbook片段:
这里有个细节,validate参数非常关键。我要求它在把配置推上去之前,先跑一下nginx -t。如果不加这个,万一模板里有个语法错误,直接把Nginx搞挂了,那千台机器同时报错,场面不敢想。
遇到的那个幂等性陷阱:
当时我自信满满地跑了第一版Playbook,结果发现有30多台机器每次执行都会触发Reload Nginx动作,哪怕配置没变。我检查了半天,发现是因为nginx.conf里有一行配置用了ansible_hostname变量。
在Jinja2模板里,我写了这样一句:
server_name {{ ansible_hostname }}.example.com;
问题来了,Ansible的copy或template模块是基于文件内容(MD5)来判断是否变更的。但ansible_hostname这个变量在不同的机器上值不一样,所以每次渲染出来的内容其实都是不同的。如果我的模板里包含了这种“每台机器都不同”的动态值,而这个动态值又不是通过标准的Facts收集来的固定值,就可能导致幂等性失效。
我的解决办法是,把这种动态值从模板里剥离出来,改成在Playbook里定义变量,或者在模板里用inventory_hostname。后来我重构了模板,把server_name改成了从主机清单的变量里读取,确保只要业务配置没变,MD5就不应该变。
改完之后,再跑Playbook,只有配置真正发生变化的机器才会Reload。大促当天,千台Nginx平滑切换到了HTTP/2,接口的平均响应时间从之前的120ms降到了85ms,这让我在庆功会上睡了个好觉。
那是我们在做混合环境补丁管理的时候,要一次性给超过1200台服务器推送一个紧急的OpenSSL安全补丁。第一次我用默认配置跑,挂在那等了半小时,进度条愣是没动多少。我看着终端里刷屏的SSH连接日志,意识到Ansible默认的“线性执行”模式在这种规模下已经彻底失效了。
默认情况下,Ansible是串行执行的,或者通过-f参数开启少量进程。面对1200台机器,如果只开5个fork,那得跑到猴年马月。但盲目调大fork进程数也不行,我试过把forks调到100,结果我的控制机(一台8核16G的跳板机)直接OOM(内存溢出)重启了。
我花了两天时间研究Ansible 2.17(Ansible 10.0.0的核心)的执行策略,总结出了一套针对大规模并发的调优方案。
首先,我调整了ansible.cfg的配置。这是调优的关键:
具体的优化过程和对比数据:
1. 优化Fact收集
第一次跑的时候,Ansible每连接一台机器都要先跑setup模块收集内存、磁盘等信息,这占了总时间的60%。我分析了一下,我的补丁任务只需要知道操作系统版本,不需要内存大小。于是我在Playbook里加了gather_facts: no,然后手动通过变量判断系统类型。这一招直接省去了每台机器大约8秒的等待时间。1200台机器就是节省了近3分钟。
2. 调整并发策略
我把forks从5调整到了50。为什么是50?因为我的控制机是16核,每开一个fork大约占用20MB内存。50个fork也就是1GB左右,加上系统开销,完全在可控范围内。如果开到200,内存就撑不住了。
3. 异步执行长耗时任务
打补丁这种事,yum update或者apt-get upgrade有时候会因为下载慢卡住。我采用了异步模式。
这是优化后的补丁推送Playbook:
这里我用了 strategy: free。这是Ansible的一个高级特性,默认策略是“线性”的,即所有主机执行完任务A才进入任务B。而free策略允许主机自由执行,哪台跑得快哪台先跑下一个任务。配合async,主机之间互不等待。
数据对比:
| 指标 | 优化前(默认配置) | 优化后(并发+异步+策略) |
| :--- | :--- | :--- |
| 执行总耗时 | 30分12秒 | 3分05秒 |
| 控制机CPU峰值 | 40% (单核跑满) | 75% (多核利用) |
| 内存占用 | 约 500MB | 约 1.2GB |
| 失败重试次数 | 15次(连接超时) | 0次 |
优化后,我从原来的盯着屏幕半小时不敢动,变成了去接杯咖啡回来就跑完了。而且因为开了SSH Pipelining和ControlPersist,SSH连接建立的开销也降到了最低。
这次调优让我明白,Ansible虽然号称无代理,但在大规模场景下,控制端的资源配置和策略选择才是决定效率的关键。不要盲目追求高并发,要根据ansible.cfg里的参数和硬件能力做平衡。
去年我们接手了一个混合云项目,核心业务跑在本地 IDC 的 VMware 虚拟机上,而图片处理和日志分析则放在 AWS 的 EC2 实例里。当时遇到两个棘手的问题:一是 AWS 的实例 IP 是动态的,每次扩缩容都要手动修改 Ansible 的 hosts 文件,运维同学每天花在更新清单上的时间平均超过 30 分钟;二是数据库密码、AWS 的 Access Key 直接写在 Playbook 里,有一次代码提交到 GitLab 差点泄露,吓得我连夜回滚了提交记录。
解决这两个问题,我直接上了 Ansible Vault 和 AWS 动态清单。先说版本,我们生产环境用的是 Ansible Core 2.17(对应 Ansible 10.0.0,2024 年 5 月刚发布的稳定版),这个版本对 AWS 集合的支持更完善,尤其是 amazon.aws 集合的 7.0.0 版本,动态清单的缓存机制优化了不少,之前拉取 50 台 EC2 实例的清单要 12 秒,现在只要 3 秒左右。
我先把所有敏感信息都塞进 Vault。比如 AWS 的认证信息,之前是这么写的(千万别这么干):
现在用 Vault 加密成一个文件 aws_creds.yml:
加密后的文件内容是乱码,只有执行 Playbook 时输入密码才能解密:
然后在 Playbook 里引用这个文件,而不是直接写明文:
为什么这么做? 之前有次实习生把 Playbook 提交到公共 GitLab 仓库,明文密钥被扫描工具报警,我们花了 2 小时才完成密钥轮换。用 Vault 后,即使代码泄露,没有 Vault 密码也拿不到敏感信息,从那以后我们团队规定:所有 *.yml 里的 password、key、token 字段,必须用 Vault 加密。
静态清单的问题是 AWS 实例重启后 IP 会变,比如我们的大促期间自动扩容到 80 台 EC2,手动改 hosts 文件根本来不及。我用 amazon.aws.aws_ec2 动态清单插件,配置文件 inventory.aws_ec2.yml 如下:
执行时直接指定这个清单文件,Ansible 会自动从 AWS 拉取符合条件的实例:
实际效果:之前手动维护清单,大促扩容后平均要 25 分钟才能把新实例加入管控;用动态清单后,新实例启动后 1 分钟内就能被 Ansible 识别(缓存过期后自动更新),我们上次 618 大促扩容 40 台 EC2,全程没手动改过一次清单。
我们把静态清单(本地 IDC)和动态清单(AWS)放在同一个目录,用 inventory 目录管理:
执行 Playbook 时指定整个目录:
这样就能同时管控本地 30 台物理机和 AWS 的 80 台 EC2 实例,去年我们做全量配置更新,原本预计要 4 小时,现在 45 分钟就搞定了——因为 Ansible 的无代理架构,不需要在每台机器上装客户端,SSH 通就能管,省了至少 1 小时的客户端部署时间。
上个月有个线上故障,我到现在还记得清楚:我们的订单系统有 12 台 Nginx 服务器,需要更新 nginx.conf 并重启服务。我写了个 Playbook,本地测试没问题,结果线上执行时,有 3 台服务器的 Nginx 没重启,导致配置不生效,用户访问时出现了 502 错误,持续了 17 分钟,QPS 掉了 40%(从 12000 降到 7200)。
当时的 Playbook 简化版是这样的:
执行后,大部分服务器都重启了,但有 3 台(都是 CentOS 7.6 的老机器)没反应。我第一反应是 copy 模块没执行?但查日志发现 copy 任务显示 changed,说明文件确实更新了。那为什么 Handler 没触发?
我先看了 Ansible 的执行输出,那 3 台机器的 copy 任务状态是 changed,但后面没有 NOTIFIED 的日志(正常触发的会有 NOTIFIED: [restart nginx])。然后我手动在那 3 台机器上跑了 ansible-playbook,加了 -vvv 调试参数,发现一个细节:
copy 模块在检查文件是否变化时,用了 checksum 对比。那 3 台机器的 /etc/nginx/nginx.conf 之前被运维手动改过权限,是 0640,而 Playbook 里写的是 mode: 0644。copy 模块在更新权限时,虽然文件内容没变,但 mode 变了,所以 changed 状态是 yes——但为什么 Handler 没触发?
我又查了 Ansible Core 2.17 的文档,发现 copy 模块的 notify 触发逻辑是:只有当模块的 changed 状态为 true 时才会触发 Handler。但问题出在 copy 模块的 changed 判断里,权限变化是否算 changed?我做了个测试:
第一次执行,copy 模块 changed 为 true,Handler 触发;第二次执行,我把 mode 改成 0640,copy 模块显示 changed 为 true(因为权限变了),但 Handler 还是触发了——那为什么线上那 3 台没触发?
再查那 3 台机器的 Ansible 版本,发现它们用的是 Ansible Core 2.14(之前升级时漏了),而 2.14 版本的 copy 模块有个 bug:当文件内容没变,只有权限/属主变化时,changed 状态虽然为 true,但 notify 不会触发。这个 bug 在 2.16 版本才修复,我们生产环境其他机器已经升到 2.17 了,就这 3 台老机器没升。
我选了两种方案结合:
copy 模块里加 force: yes,强制覆盖文件(即使内容相同),确保 changed 状态正确触发 Handler:`yaml
- name: 复制新配置文件
copy:
src: ./nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: 0644
force: yes # 强制覆盖,避免权限变化导致 changed 状态异常
notify: restart nginx
`
为什么之前没发现? 因为我们测试环境都是 2.17 版本,而那 3 台老机器是之前遗留的,没纳入版本统一管理。这次故障后,我写了个 Ansible Playbook 自动检查所有控制端的版本,低于 2.16 的自动升级,现在全集群 50 个控制端都是 2.17 了。
还有一次,我写了个 Playbook 同时更新 Nginx 和 MySQL 配置,Handler 里先重启 Nginx 再重启 MySQL。结果执行时,Nginx 重启后,MySQL 还没重启,导致应用连接数据库失败,持续了 2 分钟。后来才知道,Handler 的执行顺序是按定义顺序,而不是触发顺序。我把 Handler 顺序改成了先重启 MySQL 再重启 Nginx,问题就解决了:
总结:Handler 触发问题,90% 都是版本 bug 或者 changed 状态判断错误。我现在每次写 Playbook,都会在测试环境用 -vvv 跑一遍,看 NOTIFIED 日志是否正常,避免线上再出这种低级错误。
今年 Q1 我们团队试了 Red Hat 的 Ansible Lightspeed(集成 LLM 的 Playbook 生成工具),还有事件驱动自动化(用 Kafka 联动 Prometheus 实现故障自愈),这两个方向我认为是未来 2-3 年的主流趋势——尤其是我们这种有 200+ 台服务器、每天至少 3 次发布的团队,手动写 Playbook 已经跟不上节奏了。
之前写一个管理 Docker 容器的 Playbook,我要查模块文档、试参数,平均要 1 小时。现在用 Ansible Lightspeed,我直接输入自然语言:“写一个 Playbook,在 Ubuntu 22.04 上安装 Docker,拉取 nginx:latest 镜像,运行容器映射 80 端口,并且设置开机自启”,10 秒就生成了初稿:
实际效果:初稿的准确率大概 70%,比如 docker_container 的 restart_policy 参数我之前经常忘写,AI 会自动加上。现在我写 Playbook 的时间从 1 小时降到 5 分钟,剩下的时间用来优化逻辑(比如加条件判断、错误处理)。不过 AI 生成的代码也有坑,比如有一次它生成的 yum 模块没加 update_cache: yes,导致安装包失败,所以不能完全依赖 AI,必须人工 review。
我们之前的监控告警都是人工处理:Prometheus 告警说某台服务器的 CPU 使用率超过 90%,运维同学收到短信后手动登录服务器查进程,然后重启服务。现在用 Ansible Event-Driven Automation(EDA),联动 Kafka 和 Prometheus,实现自动处理:
架构大概是:Prometheus 发现告警 → 发送到 Kafka 的 alerts 主题 → EDA 规则引擎订阅这个主题 → 触发对应的 Ansible Playbook。
比如 CPU 过高的自愈规则 cpu_high.yml:
restart_high_cpu_process.yml 内容:
实际数据:我们上线这个规则后,CPU 过高的告警处理时间从平均 15 分钟降到 30 秒以内,上个月自动处理了 12 次 CPU 告警,其中有一次是订单系统的 Java 进程内存泄漏导致 CPU 飙高,EDA 自动重启了进程,用户完全没感知到——要是之前人工处理,至少得 5 分钟,QPS 可能掉 30%。
根据 2024 年的社区讨论,Ansible 未来会重点支持边缘计算(比如 IoT 设备),轻量化 Agent 适配低功耗设备,强化离线执行能力。我们团队已经在试边缘场景:工厂的 50 台 IoT 网关,之前用脚本部署,现在用 Ansible 的离线模式(把 Playbook 和依赖打包成 tar 包,传到网关后本地执行),部署时间从 2 小时降到 20 分钟。
还有安全左移,Ansible 10.0.0 已经内置了 CIS 基准合规扫描,Playbook 执行前自动检测风险。比如我写一个修改 /etc/passwd 的 Playbook,执行时会自动报警:“修改系统关键文件,不符合 CIS 基准”,避免误操作。
我的判断:未来 2 年,Ansible 会从“配置管理工具”变成“全场景自动化平台”,AI 生成 Playbook 会成为标配,事件驱动自动化会替代 80% 的人工告警处理。我们现在已经在做 Ansible 与 Crossplane 的融合(社区热门讨论点),用 Crossplane 管理 Kubernetes 原生基础设施,Ansible 负责配置和应用部署,实现多云资源的全生命周期管理。
去年双十一前,我负责给一个金融客户的几百台业务机做内核参数优化。当时我觉得这活儿简单,随手写了一个Playbook去改 sysctl.conf。为了省事,我用了 shell 模块直接 echo 追加配置,没用 lineinfile 或者 template 模块。
结果在灰度执行的时候,因为目标机器环境不一致,有台老机器的配置文件里已经有了重复项。我的脚本没做幂等处理,导致参数重复写入,机器直接网络异常,业务进程卡死。那几秒钟我心跳飙到了一百八,赶紧手动上去修。
后来我学乖了,哪怕再简单的操作,我也强制自己用 Ansible 的模块思维 去写,而不是把它当成一个简单的批量SSH工具。对于这种配置管理,我现在的习惯是:能用 template 就别用 shell,能检查状态就别直接执行。
关于选型,我也想多嘴一句。如果你手里就那么三五台服务器,或者只是偶尔跑个一次性命令,真没必要上 Ansible。直接写个 For 循环配合 SSH 可能更快。Ansible 的价值在于规模化管理和状态维护,当你面对几百台机器,需要保证它们配置一致且随时可回滚时,它才是神器。
最后给个建议:别一上来就对着官方文档死磕所有模块。先拿虚拟机搭个环境,把你平时最烦琐的手动操作写成 Playbook,跑通它。遇到报错别慌,看 Ansible 的返回结果其实挺人性化的,多看看那个 JSON 输出,比瞎猜强得多。