先量清楚剧本为什么慢,再动手改
目标
先用 callback 测量 playbook 为什么慢,然后依次拧动五个旋钮:fact 收集、缓存、pipelining、并发执行、异步。最后亲手做一个小工具,从测量结果中提取该修的地方。
为什么重要
“playbook 很慢”这样的报告总会收到,但里面混着性质不同的原因。在一百台目标主机上因为收集全部 fact 而慢,与每个 task 来回三次 SSH 而慢,与一台在做十分钟的作业、其余九十九台站着等而慢,修法完全不同。所以顺序很重要——先测量,再修。 不测量就去修,修的就是自己熟悉的地方,而熟悉的地方通常不是瓶颈。而且每个旋钮都有代价。缓存很快,却会制造虚假 fact;free 策略很快,却会打破主机之间的顺序约定。本实验把每个旋钮都拧一遍,连同它的代价一起看。
步骤
- 在
/root/ansperf/ansible.cfg中把默认 inventory 设为./inventory/hosts.ini,并开启ansible.posix.profile_taskscallback。在/root/ansperf/inventory/hosts.ini中写入web组的web1(work_sec 1)、web2(2)和db组的db1(3)(三者都是ansible_host=127.0.0.1ansible_port=2222,[all:vars]的ansible_user是root)。/root/ansperf/slow.yml要收集全部 fact,并由分别休眠 3 秒、2 秒、1 秒的三个 task,以及写入/root/ansperf/out/baseline.marker的一个 task 构成。把运行结果保存到/root/ansperf/out/profile_baseline.txt。 - 创建
/root/ansperf/subsets.yml——在 play 级别用gather_facts: false关闭自动收集,对web1调用ansible.builtin.setup两次。一次不带参数收集全部,一次用gather_subset: [min]只收集最小集合。分别用register接收,把各自那个 task 返回的 fact 以 JSON 形式保存到/root/ansperf/out/facts_all.json和/root/ansperf/out/facts_min.json,并运行。 - 在
/root/ansperf/ansible.cfg中加上gathering = smart、fact_caching = ansible.builtin.jsonfile、fact_caching_connection = ./factcache、fact_caching_timeout = 3600。然后创建由目标主机自己提供的本地 fact——在/etc/ansible/facts.d/lab.fact中写入[app]节和release=1.0.0。/root/ansperf/release.yml是把web1的ansible_local.lab.app.release写入变量outfile所指文件的 playbook。清空缓存目录后运行这个 playbook,留下/root/ansperf/out/rel1.txt。 - 上一步结束的现在,缓存里是
1.0.0。只把/etc/ansible/facts.d/lab.fact的release升为2.0.0。原样再运行一次 playbook,留下/root/ansperf/out/rel2_cached.txt,接着加上清空缓存的选项再运行,留下/root/ansperf/out/rel3_fresh.txt。亲自确认三个文件的值是如何分开的。 - 在
/root/ansperf/ansible.cfg中加上[ssh_connection]节并开启pipelining = True。然后把包含所有与默认值不同的设置的输出(ansible-config dump --only-changed -t all)保存到/root/ansperf/out/config.txt,并在保持 pipelining 开启的状态下确认对web1的 ad-hoc ping 能成功。 - 在
/root/ansperf/ansible.cfg的[defaults]中加上forks = 10。创建/root/ansperf/free.yml——strategy: free,不收集 fact,由两个 task 构成。第一个休眠该主机的work_sec秒,第二个向out/free-<호스트이름>.txt写入一行<호스트이름> <work_sec>(占位符均为主机名)。运行输出保存到/root/ansperf/out/free.txt。 - 创建
/root/ansperf/async.yml——在web1上把一个 8 秒的作业用async: 120poll: 0扔出去,并用register接收。然后在下一个 task 中写入/root/ansperf/out/meanwhile.txt(意思是这期间还有别的事可做),用ansible.builtin.async_status等到结束,再把结果以 JSON 形式保存到/root/ansperf/out/async.json,并运行。 - 创建
/root/ansperf/slowest.sh——从作为第一个参数收到的测量输出文件中读取 callback 摘要,按耗时从长到短,每行输出一条<초>s <태스크이름>(占位符依次为秒数、task 名称)。输出几行由第二个参数决定,默认值是 3。文件不存在时向标准错误报告并以非 0 值结束。把这个工具用于第 1 步的/root/ansperf/out/profile_baseline.txt,把结果保存到/root/ansperf/out/slowest.txt。
参考
- 这个环境是在仿真中运行的,运行时间波动很大。所以本实验的判定靠的是留下的证据,而不是时间——配置转储、缓存文件、callback 留下的测量文件、异步作业文件。
- sshd 运行在 127.0.0.1 的 2222 端口上,inventory 中的三台主机都连到那里。
- 设置是否真的生效,始终用
ansible-config dump --only-changed确认。要看到连接插件的设置,就加上-t all。 - 常见错误:改了
ansible.cfg却在另一个目录运行,那个文件没有被读取。设置文件只使用一个,使用了哪个会显示在CONFIG_FILE行中。 - 常见错误:开启缓存后目标主机变了,却仍按旧值判断。缓存的意思是“不再询问”。
- 常见错误:把
async值设得比作业实际耗时短,把本来没问题的作业切断。 - Playbook 策略 · 异步作业 · setup 模块与 gather_subset · 缓存插件 · ssh 连接插件 · 配置参考
修之前先测量
在 /root/ansperf/ansible.cfg 中把默认 inventory 设为 ./inventory/hosts.ini,并开启 ansible.posix.profile_tasks callback。在 /root/ansperf/inventory/hosts.ini 中写入 web 组的 web1(work_sec 1)、web2(2)和 db 组的 db1(3)(三者都是 ansible_host=127.0.0.1 ansible_port=2222,[all:vars] 的 ansible_user 是 root)。/root/ansperf/slow.yml 要收集全部 fact,并由分别休眠 3 秒、2 秒、1 秒的三个 task,以及写入 /root/ansperf/out/baseline.marker 的一个 task 构成。把运行结果保存到 /root/ansperf/out/profile_baseline.txt。
callback 是设置而不是安装——把名称写进 callbacks_enabled 就开启了。ansible.posix collection 已经在这个镜像里了。这个 callback 会打印每个 task 的耗时,并在运行结束时再按耗时顺序打印一遍摘要。这一步的价值不是优化,而是基线。不测量就去修,没有人能说出什么变好了。
数一数 fact 收集带回了什么
创建 /root/ansperf/subsets.yml——在 play 级别用 gather_facts: false 关闭自动收集,对 web1 调用 ansible.builtin.setup 两次。一次不带参数收集全部,一次用 gather_subset: [min] 只收集最小集合。分别用 register 接收,把各自那个 task 返回的 fact 以 JSON 形式保存到 /root/ansperf/out/facts_all.json 和 /root/ansperf/out/facts_min.json,并运行。
gather_facts: true 相当于在 play 的最前面悄悄插入一个 setup task。关掉它、在需要的位置自己调用,就由自己来决定什么时候、收集多少。用 register 接收 setup task 时,只有那次执行返回的 fact 会放进 <이름>.ansible_facts(占位符为变量名)——不会与已经附在主机上的 fact 混在一起,便于比较。用来漂亮地保存 JSON 的 filter 是 to_nice_json。缺了什么,用 jq 'keys' 看。
把 fact 缓存到文件,跳过第二次运行
在 /root/ansperf/ansible.cfg 中加上 gathering = smart、fact_caching = ansible.builtin.jsonfile、fact_caching_connection = ./factcache、fact_caching_timeout = 3600。然后创建由目标主机自己提供的本地 fact——在 /etc/ansible/facts.d/lab.fact 中写入 [app] 节和 release=1.0.0。/root/ansperf/release.yml 是把 web1 的 ansible_local.lab.app.release 写入变量 outfile 所指文件的 playbook。清空缓存目录后运行这个 playbook,留下 /root/ansperf/out/rel1.txt。
gathering 为 smart 的意思是“缓存里有就不再收集”。它介于没有缓存的 implicit 和不使用缓存的 explicit 之间。jsonfile 缓存会为每台主机留下一个 JSON 文件,所以可以亲自打开看里面有什么。/etc/ansible/facts.d/*.fact 会被 setup 模块自动读取,放到 ansible_local.<파일이름>(占位符为文件名)之下——如果是 INI 形式,节名会原样成为一层。
缓存造成的虚假 fact
上一步结束的现在,缓存里是 1.0.0。只把 /etc/ansible/facts.d/lab.fact 的 release 升为 2.0.0。原样再运行一次 playbook,留下 /root/ansperf/out/rel2_cached.txt,接着加上清空缓存的选项再运行,留下 /root/ansperf/out/rel3_fresh.txt。亲自确认三个文件的值是如何分开的。
缓存的意思是“不再询问”,所以目标主机变了,Ansible 也不知道。 这就是开启缓存时随之而来的风险。ansible-playbook 有一个选项,可以当场丢弃缓存并重新收集(在 --help 中按 cache 搜索)。实际工作中,要么把缓存有效时间设得比部署周期短,要么在部署流水线的第一步清空缓存。
减少 SSH 往返,并把改动过的设置留在文件里
在 /root/ansperf/ansible.cfg 中加上 [ssh_connection] 节并开启 pipelining = True。然后把包含所有与默认值不同的设置的输出(ansible-config dump --only-changed -t all)保存到 /root/ansperf/out/config.txt,并在保持 pipelining 开启的状态下确认对 web1 的 ad-hoc ping 能成功。
关闭 pipelining 时,Ansible 会为每个 task 把模块文件复制到目标主机、执行、再删除——SSH 往返好几次。开启后,把模块通过远端 Python 的标准输入送进去,把往返减少到一次。通常这是最便宜、收益最大的一项。不过如果目标主机的 sudoers 中开着 requiretty,就无法开启——所以默认值是关闭。这项设置是否真的生效,用 ansible-config dump --only-changed -t all 确认。没有 -t all,连接插件的设置是看不到的。
同时连接几台,是否让它们互相等待
在 /root/ansperf/ansible.cfg 的 [defaults] 中加上 forks = 10。创建 /root/ansperf/free.yml——strategy: free,不收集 fact,由两个 task 构成。第一个休眠该主机的 work_sec 秒,第二个向 out/free-<호스트이름>.txt 写入一行 <호스트이름> <work_sec>(占位符均为主机名)。运行输出保存到 /root/ansperf/out/free.txt。
forks 是 Ansible 同时连接的目标数。默认值 5 在几十台服务器的 inventory 上马上就会成为瓶颈。strategy 是另一条轴——默认的 linear 要求所有主机完成一个 task 才能进入下一个,free 则让每台主机按自己的速度一路跑到底。free 并不总是好的。在主机之间有顺序约定的 playbook(先摘除后加回)中,这种约定会被打破。所以与 serial 或 run_once 一起使用时要特别小心。
把长作业扔出去,之后再收回
创建 /root/ansperf/async.yml——在 web1 上把一个 8 秒的作业用 async: 120 poll: 0 扔出去,并用 register 接收。然后在下一个 task 中写入 /root/ansperf/out/meanwhile.txt(意思是这期间还有别的事可做),用 ansible.builtin.async_status 等到结束,再把结果以 JSON 形式保存到 /root/ansperf/out/async.json,并运行。
poll 不为 0 时,Ansible 会在原地等待。设为 0,就只拿到作业编号并直接去下一个 task——这就是“扔出去”。async 的值是允许那项作业运行的最长时间。设得短,本来没问题的作业会被中途切断。收回的是 async_status,要传入拿到的 ansible_job_id。它可能不会一次就已结束,所以用 until 再多问几次。这种方式适合“耗时长但彼此独立的作业”。用在需要立刻得到结果的作业上,只会变复杂。
从测量结果中提取该修的地方的工具
创建 /root/ansperf/slowest.sh——从作为第一个参数收到的测量输出文件中读取 callback 摘要,按耗时从长到短,每行输出一条 <초>s <태스크이름>(占位符依次为秒数、task 名称)。输出几行由第二个参数决定,默认值是 3。文件不存在时向标准错误报告并以非 0 值结束。把这个工具用于第 1 步的 /root/ansperf/out/profile_baseline.txt,把结果保存到 /root/ansperf/out/slowest.txt。
摘要从由等号构成的分隔线之后开始,每行的形式是 <이름> ----- <초>s(占位符依次为名称、秒数)。分隔线之前混有每个 task 打印的时间行,所以只能从遇到分隔线之后才开始读。以秒为单位的值总是打印到小数点后第二位,所以原样搬过来就行。排序用 sort -rn,名称里有空格,所以把秒数与名称之间用制表符分开,后面处理起来更容易。这个工具做的事很小,但位置很重要——人们在优化时最常犯的失误,就是不测量而去修自己熟悉的地方。