不先测量就动手,改的永远是你早就知道的那一处
一句话总结
playbook 慢,原因通常是五种之一,修法各不相同——所以要用 profile_tasks 先测量,然后再选旋钮。而且每个旋钮都有代价。
为什么需要它
“部署要花 40 分钟”这样的报告,哪个团队都会收到。但这 40 分钟里混着性质完全不同的时间。
- 为了在 200 台目标主机上收集全部 fact 花掉的 5 分钟
- 每个 task 来回几次 SSH 累积起来的时间
- 因为
forks默认值是 5,把 200 台分成 40 批来运行所花的时间 - 一台主机上要跑 10 分钟的软件包安装,其余 199 台站着等的时间
- 其实只是远端服务本身很慢
如果不测量就开始修,人们会去修自己熟悉的地方。通常是调高 forks 就结束。但如果瓶颈是 fact 收集,调高 forks 几乎不会变快,反而只会增加控制节点的内存和目标主机的负载。测量不是性能优化工作的热身,而是工作本身的第一步。
工作原理
先测量——profile_tasks
Ansible 的 callback 是接收运行过程中发生的事并生成输出的插件。开启 ansible.posix.profile_tasks,会打印每个 task 的耗时,并在运行结束时再给出一份按耗时排序的摘要。不需要安装任何东西,一行配置即可。
[defaults]
callbacks_enabled = ansible.posix.profile_tasks
摘要是以 task 为单位的。所以它能告诉我们“哪个 task”,却不会告诉我们“那个 task 里的什么”。接下来就是把下面的旋钮一个个试过去。
旋钮一——要收集多少 fact
gather_facts: true 相当于在 play 的最前面悄悄插入一个 setup task。这个 task 会把目标主机的 CPU、内存、磁盘、网络接口、挂载列表全部抓回来。返回上百个 fact,实际用到的通常只有 ansible_distribution 一个。
选择的办法有两种。
- 用
gather_facts: false关掉,只在确实需要的 play 中直接调用ansible.builtin.setup。 - 用
gather_subset缩小范围。min只取发行版和主机名之类,network只取接口。也可以用!all,!min,network这样减去再加上的写法。
旋钮二——fact 缓存,以及虚假 fact
开启 fact_caching,一旦收集过的 fact 就会存到文件或 Redis 中,下次运行时重复使用。jsonfile 缓存会为每台主机留下一个 JSON 文件,可以直接打开查看,用来学习也很合适。与 gathering = smart 搭配,就成了“缓存里有就不再询问”。
这里有本模块最重要的代价。不再询问,意味着目标主机变了,Ansible 也不知道。 昨天存进缓存的磁盘容量、内核版本、本地 fact,今天原样信任并据此判断。如果条件语句在看 fact,那个条件就是靠昨天的事实分出分支的。实际工作中的防御有三种——把缓存有效时间设得比部署周期更短、在部署流水线的第一步用 --flush-cache 清空、以及不要用缓存的 fact 做有风险的判断。
旋钮三——SSH 往返与 pipelining
关闭 pipelining 时,Ansible 会为每个 task 把模块文件复制到目标主机、执行、再删除。SSH 往返好几次。开启后,把模块通过远端 Python 的标准输入送进去,把往返减少到一次。通常这是最便宜、收益最大的一项。
默认关闭是有原因的。如果目标主机的 sudoers 中开着 requiretty,pipelining 就无法工作。所以 Ansible 把它设成了“能用的地方再开”。这项设置放在 [ssh_connection] 一节中,是否真的生效,要用 ansible-config dump --only-changed -t all 来看——没有 -t all,连接插件的设置是看不到的。
旋钮四——forks 与 strategy
两者是不同的轴。
| 决定什么 | 默认值 | |
|---|---|---|
forks |
同时连接的目标数 | 5 |
strategy |
各主机在每个 task 上是否互相等待 | linear |
forks 是 5 而目标有 200 台时,Ansible 会分成 40 批运行。调高会变快,但控制节点的 CPU 和内存,以及打开的 SSH 连接数也会随之上升。
strategy: linear 要求所有主机完成一个 task 才能进入下一个。free 则是每台主机按自己的速度一路跑到底。在一台慢主机拖住整体的情况下,free 大幅取胜。不过 free 会打破主机之间的顺序约定。 像“先在所有 Web 服务器上摘除,再部署”这样的 playbook,在 free 下含义就崩塌了。与 serial 或 run_once 混用时尤其要小心。
旋钮五——异步
async 和 poll 是一对。poll 不为 0 时,Ansible 会在原地等待;为 0 时只拿到作业编号就直接去下一个 task。之后用 async_status 收取。
- name: Start the long job
ansible.builtin.command: /opt/bin/reindex
async: 1800
poll: 0
register: job
async 的值是允许那项作业运行的最长时间。设得太短,本来没问题的作业会被中途切断。这种方式适合“耗时长但彼此独立的作业”,如果用在需要立刻得到结果的作业上,只会让 playbook 变复杂。
在现场相遇的样子
第一,最大的收益通常来自 fact。 目标越多,fact 收集线性增加,而其中大部分用不上。一行 gather_facts: false 往往比把 forks 翻倍收益更大。
第二,一定要确认设置是否生效。 ansible.cfg 可能有多个位置,而只有一个会被使用。 改完之后在另一个目录运行,结果什么也没变,就说“没效果啊”,这种事很常见。ansible-config dump --only-changed 的 CONFIG_FILE 行会显示使用的是哪个位置。
第三,区分测量时间的位置和判定的位置。 在仿真环境或共享 runner 上,同一个 playbook 的运行时间会波动到两倍。所以证明“变快了”时,留下的不是一次运行的时间,而是结构已经改变的证据——配置转储、缓存文件、callback 留下的测量文件之类。
第四,把读取测量结果的习惯做成工具。 用眼睛扫一遍摘要,与提取前几名并留存记录是不同的。记录积累起来,就能说出“这周变慢的 task”,从那时起,性能才成为被管理的东西。
参考文档
- playbook 策略与 forks:https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_strategies.html
- 异步作业与轮询:https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_async.html
- setup 模块与 gather_subset:https://docs.ansible.com/ansible/latest/collections/ansible/builtin/setup_module.html
- 缓存插件:https://docs.ansible.com/ansible/latest/plugins/cache.html
- ssh 连接插件(pipelining):https://docs.ansible.com/ansible/latest/collections/ansible/builtin/ssh_connection.html
下一项实验要做什么
先用 profile_tasks 留下基线,再把旋钮一个个拧过去。把收集全部 fact 的结果和只收集最小集合的结果并排保存,数一数缺了什么;开启 jsonfile 缓存后,修改目标主机的本地 fact,亲自确认缓存会原样返回旧值,再用 --flush-cache 解除。开启 pipelining,用配置转储确认它真的生效,应用 forks 和 strategy: free,把长作业用 poll: 0 扔出去,之后用 async_status 收回。最后亲手做一个小工具,从测量结果中提取最慢的 task。