使用 ControlPersist 和 Pipelining 最大化 Ansible 性能
通过启用 SSH 连接复用(ControlPersist)和流水线化(Pipelining)模块执行,显著提升 Ansible 剧本性能。本指南提供关键见解和实用配置,帮助减少执行时间,尤其适用于大规模环境。学习如何调整 `ansible.cfg` 以实现更快速、更高效的 IT 自动化。
使用 ControlPersist 和 Pipelining 最大化 Ansible 性能
慢速的 Ansible 运行通常让人摸不着头脑,直到你观察网络上的实际活动。一个包含二十个小任务、针对一百台主机的剧本,可能会花费大量时间在打开 SSH 会话、复制临时模块文件、运行 Python、收集输出以及关闭连接上。远程主机上的工作可能只需几毫秒,但连接开销却反复出现。
有两个设置通常能提供帮助:通过 ControlPersist 实现 SSH 连接复用,以及 Ansible 的流水线化(Pipelining)。它们不是魔法开关,也无法修复缓慢的软件包镜像、过载的数据库或执行繁重任务的操作。它们确实能减少可避免的通信开销,而这正是许多小任务剧本浪费时间的地方。
首先,测量当前的痛点
在更改配置之前,先启用计时功能运行一次剧本:
ANSIBLE_CALLBACKS_ENABLED=ansible.posix.profile_tasks ansible-playbook site.yml
如果未安装该回调,可以使用 CI 系统中更简单的内置计时输出,或者用 time 包裹命令。目标不是完美的基准测试,而是获取一个基线,并了解慢速任务是实际工作还是跨多台主机重复的小任务。
一个有用的烟雾测试是在代表性清单上执行 ad-hoc ping:
time ansible all -m ping
运行两次。如果配置连接复用后第二次运行明显更快,则确认 SSH 建立成本是问题的一部分。
ControlPersist 改变了什么
ControlPersist 是 OpenSSH 的一个特性。它会在一段时间内保持一个主 SSH 连接打开,以便后续针对同一主机、用户和端口的 SSH 命令可以复用该连接。Ansible 通常为每个任务使用 SSH,因此连接复用消除了重复的握手过程。
一个实用的项目级 ansible.cfg 如下所示:
[defaults]
forks = 20
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=600s -o ControlPath=~/.ansible/cp/%%h-%%p-%%r
创建具有私有权限的套接字目录:
mkdir -p ~/.ansible/cp
chmod 700 ~/.ansible ~/.ansible/cp
ControlMaster=auto 告诉 SSH 在可用时使用现有主连接,否则创建一个。ControlPersist=600s 在最后一个会话退出后保持主连接十分钟。这个值不是固定的。对于短时间的 CI 任务,几分钟可能就足够了。对于在维护窗口期间反复运行剧本的操作员来说,十或十五分钟可能更合适。
ControlPath 的重要性超出人们的预期。Unix 套接字路径在许多系统上有长度限制。深层工作区路径中的长清单主机名可能会破坏连接复用,并产生令人困惑的错误。将路径保持简短(例如在 ~/.ansible/cp 下)可以避免这类失败。
较新的 Ansible 版本在许多正常配置中默认启用了 SSH 复用,但在项目 ansible.cfg 中显式设置可以使行为更易于审计。使用以下命令检查活动配置:
ansible-config dump --only-changed
流水线化改变了什么
没有流水线化时,Ansible 通常会将模块复制到远程主机上的临时目录,执行它,然后清理。启用流水线化后,Ansible 可以通过 SSH 连接传递模块代码,而不是写入那么多临时文件。这节省了往返时间和远程文件系统的工作。
在同一个文件中启用它:
[ssh_connection]
pipelining = True
或者为一次运行测试它:
ANSIBLE_PIPELINING=True ansible-playbook site.yml
当剧本包含许多小模块时,此设置最为明显:file、lineinfile、template、user、service 和短命令任务。当任务大部分时间用于安装软件包、构建软件、传输大型工件或等待外部服务时,它的影响较小。
sudo requiretty 陷阱
经典的流水线化问题是 sudoers 中的 requiretty。一些较旧的企业 Linux 配置要求 sudo 使用 TTY。流水线化与此要求不兼容,因为 Ansible 试图通过 SSH 非交互式地流式传输工作。
仔细检查 sudoers。不要使用普通编辑器编辑 /etc/sudoers;使用 visudo:
sudo visudo
如果你看到类似这样的全局行:
Defaults requiretty
你可能需要删除它,或者为 Ansible 自动化用户覆盖它:
Defaults:ansible !requiretty
仅当符合组织的安全策略时才进行此更改。在许多现代发行版中,requiretty 默认未启用。
更安全的组合配置
对于许多团队来说,这是一个合理的起点:
[defaults]
forks = 20
gathering = smart
fact_caching = jsonfile
fact_caching_connection = .ansible_facts
fact_caching_timeout = 86400
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=600s -o ControlPath=~/.ansible/cp/%%h-%%p-%%r
pipelining = True
forks 控制并行度。它与性能相关,但并非相同的优化。将 forks 从 5 提高到 20 可能有助于控制节点和网络的处理能力。但提高太多可能会使堡垒主机、软件包仓库或受管节点本身过载。
事实缓存有助于解决另一种缓慢问题。如果你的剧本每次运行都收集事实,并且事实不需要每分钟都新鲜,缓存可以消除重复的设置工作。请谨慎使用;如果你依赖当前的内存、磁盘或接口数据,过时的事实可能会造成混淆。
如何测试而不自欺欺人
在类似生产环境的子集上进行测试。同一子网上的五个空闲测试虚拟机无法告诉你关于堡垒主机后面一百台混合主机的太多信息。
在更改前后运行相同的剧本。如果需要冷比较,请在测试运行之间清除现有的控制套接字:
rm -f ~/.ansible/cp/*
time ansible-playbook site.yml --limit web
然后运行热比较:
time ansible-playbook site.yml --limit web
寻找在小型重复任务上花费更少秒数的情况。同时观察故障模式。如果使用 become 的任务开始失败,请在归咎于流水线化本身之前调查 sudo 配置。
这些设置何时帮助不大
ControlPersist 和流水线化不会使远程工作变快。如果 apt update 等待镜像,连接复用也无济于事。如果服务重启因应用程序断开连接而等待三十秒,流水线化则无关紧要。如果你的剧本向每台主机复制 2 GB 的工件,请专注于工件分发、缓存或本地软件包仓库。
它们也不能替代幂等的剧本设计。无条件运行 shell 命令的剧本仍然会浪费时间。使用可以检测当前状态的模块,在适当的情况下为命令任务添加 creates 或 removes,并在剧本不使用事实时避免收集它们。
实用的方法很简单:使用简短、私有的控制路径启用 ControlPersist;根据你的 sudo 策略测试流水线化;逐步调整 forks;并使用相同的清单和工作负载进行测量。在许多真实的 Ansible 环境中,这些更改将缓慢的运行转变为可容忍的运行,因为它们消除了重复的开销,而不是隐藏它。
堡垒主机和跳板主机
许多清单并不直接连接到受管节点,而是通过堡垒主机。ControlPersist 仍然有帮助,但你需要考虑两个连接:控制节点到堡垒主机,以及堡垒主机到目标。
一个常见的清单变量如下所示:
[private]
app01 ansible_host=10.0.10.11
app02 ansible_host=10.0.10.12
[private:vars]
ansible_user=ansible
ansible_ssh_common_args='-o ProxyJump=bastion.example.com'
如果每个任务都重复建立跳转连接,堡垒主机就会成为开销的一部分。保持控制路径简短和私有,并注意堡垒主机的 SSH 连接限制。一个对十台主机运行良好的剧本,当 forks 提高到五十时,可能会使小型堡垒主机过载。
对于较旧的 SSH 客户端,你可能会看到 ProxyCommand 而不是 ProxyJump:
ansible_ssh_common_args='-o ProxyCommand="ssh -W %h:%p bastion.example.com"'
在归咎于 Ansible 之前,先测试普通的 SSH 连接:
ssh -o ProxyJump=bastion.example.com [email protected] hostname
如果这很慢,Ansible 也会很慢。
流水线化和实际剧本中的 become
关于流水线化不适用于特权升级的旧建议过于宽泛。流水线化可以与 become 一起使用;常见的障碍是 sudo 要求 TTY 或干扰非交互式执行的策略。唯一可靠的答案是使用你的剧本使用的相同 become 设置进行测试。
一个小测试剧本就足够了:
- hosts: all
become: true
gather_facts: false
tasks:
- name: 确认特权命令有效
ansible.builtin.command: id
changed_when: false
在启用流水线化的情况下运行它。如果失败,请检查 sudoers、身份验证提示以及自动化用户是否可以在没有密码提示的情况下运行所需命令。大型自动化运行中的密码提示很脆弱;它们还会使计时测量变得嘈杂。
尊重依赖关系调整 forks
增加 forks 很诱人,因为它能立即产生可见的变化。但它也可能造成新的瓶颈。如果一个剧本同时更新所有主机上的软件包缓存,高 fork 计数可能会惩罚软件包镜像。如果所有主机同时重启并重新连接到同一数据库,数据库将承受冲击。
一种更谨慎的方法更好:
[defaults]
forks = 10
运行剧本。尝试 20。然后 30。观察控制节点 CPU、SSH 进程数、堡垒主机负载、网络饱和、软件包仓库以及正在更改的服务。最快的设置并不总是并行度最高的那个。对于滚动应用程序部署,你可能还需要在剧本中使用 serial 来保护可用性:
- hosts: web
serial: 10
forks 控制 Ansible 一次可以执行多少操作。serial 控制剧本一次应处理多少台主机。它们解决不同的问题。
清理过时的控制套接字
控制套接字通常会自行清理,但笔记本电脑会休眠,CI 任务会被终止,网络路径也会发生变化。如果 SSH 开始报告复用错误,请删除过时的套接字:
rm -f ~/.ansible/cp/*
当没有 Ansible 运行使用这些套接字时,这是安全的。在共享的自动化运行器中,避免将控制套接字放置在共享的可写目录中。每个自动化用户应有自己的私有路径。
清单设计可能抵消性能提升
连接调整有帮助,但清单设计仍然可能拖慢一切。缓慢调用云 API 的动态清单脚本、执行昂贵查找的组变量以及为从未接触的主机收集事实的剧本,都可能在 SSH 启动之前增加延迟。
如果在第一个任务之前运行暂停,请分析清单加载。在插件支持的情况下缓存动态清单。避免在解析时进行昂贵的变量查找。保持主机模式紧凑:
ansible-playbook site.yml --limit web:&prod
该命令针对同时属于 web 和 prod 的主机。针对 all 运行宽泛的剧本,然后使用 when 条件跳过大多数任务,这会浪费时间并使输出更难阅读。
当状态相关时,优先使用更少、更清晰的任务
Ansible 的可读性很重要,但过多的小任务可能会使连接开销更加明显。如果你使用十个单独的 lineinfile 任务在一个配置文件中设置十行相关内容,你将支付十次任务开销,并使回滚推理更加困难。模板可能更清晰、更快:
- name: 渲染应用程序配置
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
mode: '0644'
notify: restart app
这不是支持在 Ansible 中使用巨型 shell 脚本的论点。而是提醒我们在适当的级别建模所需状态。一个配置文件使用一个模板通常比许多微编辑更好。
避免过早的全局更改
尽可能将性能设置放在项目 ansible.cfg 中。更改共享控制节点上的 /etc/ansible/ansible.cfg 可能会让其他团队感到意外。项目级文件使行为随存储库一起传播,并使 CI、笔记本电脑和自动化运行器的配置更接近。
确认 Ansible 正在使用哪个配置文件:
ansible --version
输出包括活动配置路径。这可以捕获一个常见错误:编辑一个 ansible.cfg 而 Ansible 正在读取另一个。
知道何时停止调整 Ansible
如果连接开销不再是运行时间的主要部分,请停止调整 SSH 并关注工作本身。软件包缓存更新、容器拉取、数据库迁移、服务健康检查和云 API 调用通常主导着成熟的剧本。此时,更好的优化可能是本地软件包镜像、工件缓存、更小的部署批次或将一次性配置移出每次部署运行。