Ansible 学习总结(11)—— task 并行执行之 forks 与 serial 参数详解

Posted 科技D人生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Ansible 学习总结(11)—— task 并行执行之 forks 与 serial 参数详解相关的知识,希望对你有一定的参考价值。

前言

Ansible 执行 task 的时候,会依据配置文件中指定的 forks 参数、inventory 中指定的主机以及主机组、以及选择的目标主机和定义的 task 数决定执行的方式。比如 inventory 主机清单文件中,记录了 20 台服务器,配置文件中的 forks 参数指定为 5,同时有 2 个 task 需要执行,此时就表示每次最多允许对 5 台服务器进行操作,如果剩下的服务器数不足 5 台,则对剩下的所有服务器执行操作。那么对于多个 task 以及多台服务器执行的时候,按照目标主机的执行方式,通常有两种方式:

  • 广度优先:将每个 task 先按照 forks 约定的参数,在所有服务器上分批并行执行,等这个 task 执行完毕之后,继续按照相同的方式执行后续的 task。
  • 深度优先:serial 指定(通常小于 forks 指定的参数)约定数目的服务器上并行执行完所有的 task 之后继续在下一组服务器上,按照相同的方式执行完所有的 task。

一、forks(广度优先)

依据 forks 参数,决定一次在多少个服务器上并行执行相应的 task,对于包含多个 task 的场景,这种方式会先将 1 个 task 在指定的所有服务器上都执行完成之后,才会执行后续的 task。所以对于一个包含多个 task 的 playbook 来说,此时所有服务器的状态都是不完整的,都是处于中间状态的。但是当这个包含多个 task 的 playbook 执行完成之后,所有执行成功的服务器的状态都是被更新完成的。这种方式适合对服务器的中间状态不敏感的场景,其优点是可以对指定的所有主机执行同步的配置操作;缺点是更新过程中所有服务器都是未完成配置的中间状态。比如下面的场景,有 4 台服务器需要配置(nodeA, nodeB, nodeC, nodeD),playbook 中定义了 2 个 task,forks 指定的参数是 5,而每个 task 执行的耗时为 5 秒。此时,执行过程如下:

  • 第一次执行:由于 forks 数大于服务器数,所以所有的服务器都会被选中,并行执行第一个 task,所以此时执行完成耗时为 5 秒;
  • 第二次执行:同样所有的服务器都会被选中,并且并行执行第二个 task,所以此时执行完成耗时同样为 5 秒。
  • 最终,这个 playbook 在所有服务器上执行完成之后,总的耗时为 10 秒。

示例代码如下:

user@wsg7:1st$ vim test_forks.yml
user@wsg7:1st$ cat test_forks.yml
---
- hosts: c7u6s1,c7u6s2,c7u6s3,c7u6s4
  gather_facts: false

  tasks:
  - name: echo hostname
    command: hostname

  - name: echo datetime
    command: date
user@wsg7:1st$ ansible-playbook -f 5 test_forks.yml

PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] >******************************************************************************************> ******************

TASK [echo hostname]  **************************************************************************************************************************
changed: [c7u6s1]
changed: [c7u6s2]
changed: [c7u6s3]
changed: [c7u6s4]

TASK [echo datetime] >**************************************************************************************************************************
changed: [c7u6s1]
changed: [c7u6s3]
changed: [c7u6s2]
changed: [c7u6s4]

PLAY RECAP ************************************************************************************************************************************
c7u6s1                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
c7u6s2                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
c7u6s3                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
c7u6s4                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

user@wsg7:1st$ 

上述输出结果中,显示每个 task 在所有服务器上并行执行,执行完成之后,才会开始执行后续的 task。上面是 forks 数大于服务器数的情况,对于 forks 数小于服务器数的场景,比如有 4 台服务器,playbook 中同样有 2 个task,forks 数配置为 2,每个 task 的执行所需时间仍然为 5 秒。此时的执行过程如下:

  • 第一次执行:4台服务器中的2台被选中,并行执行第一个task,所以执行完成之后的耗时为5秒;
  • 第二次执行:剩下的2台服务器被选中,执行第一个task,所以此时执行完成之后耗时5秒;
  • 第三次执行:4台服务器中的2台被选中,并行执行第二个task,所以执行完成之后耗时为5秒;
  • 第四次执行:剩下的2台服务器被选中,执行第二个task,所以执行完成耗时为5秒;
  • 最终总的耗时为20秒。

示例代码如下:

---
- hosts: c7u6s1,c7u6s2,c7u6s3,c7u6s4
  gather_facts: false

  tasks:
  - name: echo hostname
    shell: hostname; sleep 1

  - name: echo datetime
    shell: date; sleep 1

执行上述的 palybook,具体如下:

user@wsg7:1st$ ansible-playbook -f 2 -v test_forks.yml
Using /etc/ansible/ansible.cfg as config file

PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] ************************************************************************************************************

TASK [echo hostname] **************************************************************************************************************************
changed: [c7u6s1] => "ansible_facts": "discovered_interpreter_python": "/usr/bin/python", "changed": true, "cmd": "hostname; sleep 1", "delta": "0:00:01.006387", "end": "2022-06-28 17:10:30.556334", "msg": "", "rc": 0, "start": "2022-06-28 17:10:29.549947", "stderr": "", "stderr_lines": [], "stdout": "c7u6s1", "stdout_lines": ["c7u6s1"]
changed: [c7u6s2] => "ansible_facts": "discovered_interpreter_python": "/usr/bin/python", "changed": true, "cmd": "hostname; sleep 1", "delta": "0:00:01.003715", "end": "2022-06-28 17:10:30.557691", "msg": "", "rc": 0, "start": "2022-06-28 17:10:29.553976", "stderr": "", "stderr_lines": [], "stdout": "c7u6s2", "stdout_lines": ["c7u6s2"]
changed: [c7u6s3] => "ansible_facts": "discovered_interpreter_python": "/usr/bin/python", "changed": true, "cmd": "hostname; sleep 1", "delta": "0:00:01.004623", "end": "2022-06-28 17:10:31.800163", "msg": "", "rc": 0, "start": "2022-06-28 17:10:30.795540", "stderr": "", "stderr_lines": [], "stdout": "c7u6s3", "stdout_lines": ["c7u6s3"]
changed: [c7u6s4] => "ansible_facts": "discovered_interpreter_python": "/usr/bin/python", "changed": true, "cmd": "hostname; sleep 1", "delta": "0:00:01.004561", "end": "2022-06-28 17:10:31.811011", "msg": "", "rc": 0, "start": "2022-06-28 17:10:30.806450", "stderr": "", "stderr_lines": [], "stdout": "c7u6s4", "stdout_lines": ["c7u6s4"]

TASK [echo datetime] **************************************************************************************************************************
changed: [c7u6s1] => "changed": true, "cmd": "date; sleep 1", "delta": "0:00:01.005884", "end": "2022-06-28 17:10:33.050719", "msg": "", "rc": 0, "start": "2022-06-28 17:10:32.044835", "stderr": "", "stderr_lines": [], "stdout": "Tue Jun 28 17:10:32 CST 2022", "stdout_lines": ["Tue Jun 28 17:10:32 CST 2022"]
changed: [c7u6s2] => "changed": true, "cmd": "date; sleep 1", "delta": "0:00:01.004152", "end": "2022-06-28 17:10:33.051957", "msg": "", "rc": 0, "start": "2022-06-28 17:10:32.047805", "stderr": "", "stderr_lines": [], "stdout": "Tue Jun 28 17:10:32 CST 2022", "stdout_lines": ["Tue Jun 28 17:10:32 CST 2022"]
changed: [c7u6s3] => "changed": true, "cmd": "date; sleep 1", "delta": "0:00:01.004385", "end": "2022-06-28 17:10:34.231918", "msg": "", "rc": 0, "start": "2022-06-28 17:10:33.227533", "stderr": "", "stderr_lines": [], "stdout": "Tue Jun 28 17:10:33 CST 2022", "stdout_lines": ["Tue Jun 28 17:10:33 CST 2022"]
changed: [c7u6s4] => "changed": true, "cmd": "date; sleep 1", "delta": "0:00:01.004384", "end": "2022-06-28 17:10:34.236657", "msg": "", "rc": 0, "start": "2022-06-28 17:10:33.232273", "stderr": "", "stderr_lines": [], "stdout": "Tue Jun 28 17:10:33 CST 2022", "stdout_lines": ["Tue Jun 28 17:10:33 CST 2022"]

从上述的输出时间中,可以看出,每个 task 都是分 2 次执行的,因为指定了 forks 数为 2,实际主机数为 4 台。上述就是所谓的广度优先,即先在所有主机上执行 1 个 task,直至所有的主机都执行完成之后,采用相同的方式执行后续的 task。

三、serial(深度优先)

而对于深度优先的执行方式,则是在指定数目的服务器上执行完 playbook 的所有 task 之后,才会继续在剩余的其他主机上执行这个 playbook 中定义的 task。这是通过在 playbook 中指定 serial 关键字实现的,所以其是在 forks 参数的基础上,进一步进行约定,从而实现指定数目的服务器执行完成 playbook 之后,才会在其他服务器上执行的操作。这种方式类似于滚动更新。比如,此时仍然为 4 台服务器,forks 仍然设置为 5,然后在 playbook 中增加 serial 关键字,并将其值设置为 2,playbook 中仍然有 2 个 task,且每个 task 执行耗费时间为5秒。此时的执行过程如下:

  • 第一次执行:从4台服务器中挑选两台,并行执行第一个task,直至第一个task执行完成,此时耗费时间为5秒;
  • 第二次执行:在这两台服务器上,继续并行执行第二个task,直至第二个task执行完成,此时耗费时间为5秒;
  • 第三次执行:在剩下的2台服务器上,并行执行第一个task,直至第一个task执行完成,此时耗费时间为5秒;
  • 第四次执行:继续在剩下的2台服务器上,并行执行第二个task,直至diergetask执行完成,此时耗时为5秒;
  • 至此,4台服务器上都已经执行完成了,总耗时为20秒。

示例代码如下:

---
- hosts: c7u6s1,c7u6s2,c7u6s3,c7u6s4
  gather_facts: false
  serial: 2

  tasks:
  - name: echo hostname
    shell: hostname; sleep 1

  - name: echo datetime
    shell: date; sleep 1

执行过程如下:

user@wsg7:1st$ ansible-playbook -f 5 test_serial.yml

PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] ************************************************************************************************************

TASK [echo hostname] **************************************************************************************************************************
changed: [c7u6s2]
changed: [c7u6s1]

TASK [echo datetime] **************************************************************************************************************************
changed: [c7u6s1]
changed: [c7u6s2]

PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] ************************************************************************************************************

TASK [echo hostname] **************************************************************************************************************************
changed: [c7u6s3]
changed: [c7u6s4]

TASK [echo datetime] **************************************************************************************************************************
changed: [c7u6s3]
changed: [c7u6s4]

PLAY RECAP ************************************************************************************************************************************
c7u6s1                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
c7u6s2                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
c7u6s3                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
c7u6s4                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

user@wsg7:1st$

上述输出结果,也验证了执行过程,即在 serial 指定数目的服务器上执行完 playbook 中定义的所有 task 之后,才会继续在后续的其他服务器上执行这个 playbook。如果 playbook 中的 serial 值比 forks 的值大,此时以 serial 为准,执行并行操作。修改后的 yaml 文件具体如下所示:

---
- hosts: c7u6s1,c7u6s2,c7u6s3,c7u6s4
  gather_facts: false
  serial: 4

  tasks:
  - name: echo hostname
    shell: hostname; sleep 1

  - name: echo datetime
    shell: date; sleep 1

执行上述 playbook 的时候,指定 forks 值为 2,此时 serial 的值比 forks 值大。此时的执行效果如下:

user@wsg7:1st$ ansible-playbook -f 2 test_serial.yml

PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] ************************************************************************************************************

TASK [echo hostname] **************************************************************************************************************************
changed: [c7u6s1]
changed: [c7u6s2]
changed: [c7u6s3]
changed: [c7u6s4]

TASK [echo datetime] **************************************************************************************************************************
changed: [c7u6s1]
changed: [c7u6s2]
changed: [c7u6s3]
changed: [c7u6s4]

PLAY RECAP ************************************************************************************************************************************
c7u6s1                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
c7u6s2                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
c7u6s3                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
c7u6s4                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

可以看出,上述的执行过程,是 4 个服务器并行执行,并行数量并不受 forks 的数量限制。从上述的两种方式中可以看出,forks 和 serial 都可以决定并行执行的服务器数量,当没有在 playbook 中指定 serial 的时候,则以 forks 的值作为并行运行的服务器数量依据;当在 playbook 中指定了 serial 且同时配置了 forks 的时候,则以 serial 的值作为并行运行的服务器数量的依据。

serial 的其他用法

serial关 键字除了可以指定为数字之外,还可以指定为百分数,表示单次并行执行的主机数占指定的总主机数的百分比。此时的定义形式如下:

---
- name: test play
  hosts: webservers
  serial: "30%"

  tasks:
  - name: task1
    command: hostname
  - name: task2
    command: datetime

上述输出表示,每次从指定的所有主机中选择 30% 执行。还可以将 serial 的值设置为列表,每个列表项表示并行执行的服务器数量。具体如下:

---
- name: test play
  hosts: webservers
  serial:
    - 1
    - 5
    - 10
  tasks:
  - name: task1
    command: hostname
  - name: task2
    command: date

上述代码的含义,是第一次执行的时候,从指定的所有服务器中挑选 1 台,执行 playbook 中的所有 task,第二次执行的时候,从指定的所有服务器中选中 5 台执行 playbook 中的所有 task,第三次执行,从指定的所有服务器中选中 10 台执行 playbook 中的所有 task,此时如果还有未执行过的服务器,则按照 forks 定义的数量并行执行。在 serial 的列表中,还可以将数字与百分数混合使用,具体如下:

---
- name: test play
  hosts: webservers
  serial:
    - 1
    - "50%"
  tasks:
  - name: task1
    command: hostname
  - name: task2
    command: date

上述代码的含义,是从指定的服务器中选择 1 台执行 playbook,当执行完成之后,从剩余主机中选择全部主机总数的 50% 的主机执行 playbook,此时如果还有未执行过的指定主机,则按照 forks 的指定参数,并行执行。代码如下:

---
- hosts: c7u6s1,c7u6s2,c7u6s3,c7u6s4
  gather_facts: false
  serial:
  - 1
  - 50%

  tasks:
  - name: echo hostname
    shell: hostname; sleep 1

  - name: echo datetime
    shell: date; sleep 1

上述 playbook 的执行过程如下:

user@wsg7:1st$ ansible-playbook -f 5 test_serial.yml

PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] ************************************************************************************************************

TASK [echo hostname] **************************************************************************************************************************
changed: [c7u6s1]

TASK [echo datetime] **************************************************************************************************************************
changed: [c7u6s1]

PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] ************************************************************************************************************

TASK [echo hostname] **************************************************************************************************************************
changed: [c7u6s2]
changed: [c7u6s3]

TASK [echo datetime] **************************************************************************************************************************
changed: [c7u6s3]
changed: [c7u6s2]

PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] ************************************************************************************************************

TASK [echo hostname] **************************************************************************************************************************
changed: [c7u6s4]

TASK [echo datetime] **************************************************************************************************************************
changed: [c7u6s4]

PLAY RECAP ************************************************************************************************************************************
c7u6s1                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
c7u6s2                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
c7u6s3                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
c7u6s4                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

从上述输出中可以看出,先挑选出一台服务器执行 playbook,执行完成之后,从剩余的服务器中选择总数的 50% 的服务器(总数 4 台,50% 即为 2 台)继续执行 playbook,此时还有主机没有被执行完,且此时的 forks 数大于剩余的服务器数,所以直接将剩余的服务器全部执行。

以上是关于Ansible 学习总结(11)—— task 并行执行之 forks 与 serial 参数详解的主要内容,如果未能解决你的问题,请参考以下文章

Ansible 学习总结—— Ansible playbook 入门详解

Ansible 学习总结—— Ansible playbook 入门详解

Ansible 学习总结—— Ansible 循环条件判断触发器处理失败等任务控制使用总结

Ansible 学习总结—— Ansible 循环条件判断触发器处理失败等任务控制使用总结

ansible学习笔记4-playbooks之task

Ansible之playbook的使用总结