serial 与 delegate_to:切成批次,把活交出去
一句话总结
零停机部署不是由部署工具替你做好的,而是如何拆分使用 playbook 的问题,其旋钮就是把主机按批次划分的 serial,以及让其他机器去做一部分作业的 delegate_to。
为什么需要它
在默认行为下,Ansible 对所有主机按顺序逐个执行 task。task 1 在所有主机上结束后,再进入 task 2。如果有重启服务的 task,那一刻所有主机会同时下线。如果是二十台的 Web 层,二十台一起挂掉。
所以需要“一次最多几台”这样的限制。但光有限制还不够。如果不事先把要部署的服务器从负载均衡器摘下来,用户请求还会不断进到那里;如果部署之后不确认状态,坏掉的版本就会蔓延到下一批。这三样——拆分、摘除与加回、每批判定——齐备了才能叫零停机。
工作原理
serial 把 play 的主机列表分成批次。
| 写法 | 对三台的结果 |
|---|---|
serial: 2 |
web1、web2 → web3 两批 |
serial: "50%" |
两台 → 一台 |
serial: [1, "100%"] |
先 web1 一台作为金丝雀(Canary),然后其余全部 |
以列表书写就成了金丝雀发布。先在一台上试一下,没问题就把其余的一次性推送。如果最后一个元素比剩下的主机少,就按那个大小重复。
这里有件必须知道的事。使用 serial 时,play 的行为就像每一批都从头重新运行一遍。 运行日志中 PLAY [...] 标题打印的次数等于批次数,就是证据。结果就是三件事变成了以批次为单位。
run_once: true不是整个 play 一次,而是每批一次。- handler 不是在 play 结束时,而是在批次结束时触发。
max_fail_percentage按每批判定。
最后一条最重要。把 serial: 1 和 max_fail_percentage: 0 一起用,就成了“只要有一台失败,就不往下一台走”。第二台失败的话,第三台连开始都不会开始,日志中会打印 NO MORE HOSTS LEFT。防止事故蔓延到三台的装置就是这个。
看起来相似的 any_errors_fatal: true 是另一回事。它是“任何主机出现失败,就当场停下整个 play”,不以比例留余地。如果想在二十台中有一台失败也继续,就用 max_fail_percentage;如果只要有一台失败就必须立刻停下,就用 any_errors_fatal。
delegate_to 把一个 task 放到另一台主机上执行。目标列表保持不变,只是把执行挪了位置。
- name: 로드밸런서에서 뺀다
community.general.haproxy:
state: disabled
host: "{{ inventory_hostname }}"
delegate_to: lb1
这里有一点容易混淆。在被委托的 task 里,inventory_hostname 仍然指向原来的主机。 上面的例子中,inventory_hostname 不是 lb1,而是正在部署的 web1。所以“让负载均衡器把 web1 摘掉”一行就能表达。运行日志中会像 changed: [web1 -> lb1] 这样同时打印两个名称。
如果同时给出 delegate_facts: true,该 task 产生的 fact 会存到委托对象名下,而不是原来的主机。比如在负载均衡器上查询一次的值,可以通过 hostvars['lb1'] 让所有主机一起看到。去掉这个选项,fact 会留在原来的主机上,hostvars['lb1'] 里什么也没有。
并发旋钮也值得了解。forks 是决定控制节点同时处理多少台主机的全局设置,serial 是 play 的批次大小。两者中较小的一个就是实际并发执行数。throttle 是只对一个 task 加的限制,用于“这个 API 每秒最多多少次”这类情形。strategy: free 让主机之间不互相等待,快的主机先结束,但它不保证顺序,不适合零停机部署。
在现场相遇的样子
第一,批次大小是容量计算。 二十台加上 serial: "25%",就是五台同时摘除。如果剩下的十五台扛不住平时流量,这次部署本身就是故障。批次大小不是偏好,而应当是“最多可以摘除几台”的答案。
第二,没有状态确认的批次没有意义。 部署之后如果不用 uri 或 wait_for 确认,坏掉的版本就会原样去往下一批。确认 task 必须失败,max_fail_percentage 才能发挥作用。
第三,摘掉之后一定要加回来。 部署过程中如果 play 失败,最后一个被摘除的服务器就留在了负载均衡器之外。所以恢复 task 要放在 block/always 里,或者至少需要另外一道“现在有没有被摘掉的服务器”的确认流程。
第四,委托对象也必须在 inventory 中。 delegate_to: lb1 只有在 lb1 位于 inventory 中时,才会使用那台主机的连接设置。没有的话,只凭名称尝试连接。负载均衡器、堡垒机、监控服务器这类“不是目标、却要让它做事”的主机,也要写进 inventory,原因就在这里。
下一项实验要做什么
在 inventory 中写入三台 Web 和一台负载均衡器,用 serial 拆分批次,通过记录确认实际运行了几轮。把金丝雀批次做成列表,用产出物数一数 run_once 和 handler 每批运行的情况,在负载均衡器一侧留下用 delegate_to 摘除和加回的记录,用 delegate_facts 看 fact 被写到谁的名下。然后让中间一台在状态确认时失败,确认第三批连开始都没有开始,最后留下一份把摘除、部署、确认、恢复走完一圈的摘要文件。