• 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 秒。

    示例代码如下:

    1. user@wsg7:1st$ vim test_forks.yml
    2. user@wsg7:1st$ cat test_forks.yml
    3. ---
    4. - hosts: c7u6s1,c7u6s2,c7u6s3,c7u6s4
    5.   gather_facts: false
    6.   tasks:
    7.   - name: echo hostname
    8.     command: hostname
    9.   - name: echo datetime
    10.     command: date
    11. user@wsg7:1st$ ansible-playbook -f 5 test_forks.yml
    12. PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] >******************************************************************************************> ******************
    13. TASK [echo hostname]  **************************************************************************************************************************
    14. changed: [c7u6s1]
    15. changed: [c7u6s2]
    16. changed: [c7u6s3]
    17. changed: [c7u6s4]
    18. TASK [echo datetime] >**************************************************************************************************************************
    19. changed: [c7u6s1]
    20. changed: [c7u6s3]
    21. changed: [c7u6s2]
    22. changed: [c7u6s4]
    23. PLAY RECAP ************************************************************************************************************************************
    24. c7u6s1                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    25. c7u6s2                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    26. c7u6s3                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    27. c7u6s4                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    28. 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秒。

    示例代码如下:

    1. ---
    2. - hosts: c7u6s1,c7u6s2,c7u6s3,c7u6s4
    3.   gather_facts: false
    4.   tasks:
    5.   - name: echo hostname
    6.     shell: hostname; sleep 1
    7.   - name: echo datetime
    8.     shell: date; sleep 1

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

    1. user@wsg7:1st$ ansible-playbook -f 2 -v test_forks.yml
    2. Using /etc/ansible/ansible.cfg as config file
    3. PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] ************************************************************************************************************
    4. TASK [echo hostname] **************************************************************************************************************************
    5. 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"]}
    6. 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"]}
    7. 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"]}
    8. 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"]}
    9. TASK [echo datetime] **************************************************************************************************************************
    10. 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"]}
    11. 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"]}
    12. 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"]}
    13. 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秒。

    示例代码如下:

    1. ---
    2. - hosts: c7u6s1,c7u6s2,c7u6s3,c7u6s4
    3.   gather_facts: false
    4.   serial: 2
    5.   tasks:
    6.   - name: echo hostname
    7.     shell: hostname; sleep 1
    8.   - name: echo datetime
    9.     shell: date; sleep 1

    执行过程如下:

    1. user@wsg7:1st$ ansible-playbook -f 5 test_serial.yml
    2. PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] ************************************************************************************************************
    3. TASK [echo hostname] **************************************************************************************************************************
    4. changed: [c7u6s2]
    5. changed: [c7u6s1]
    6. TASK [echo datetime] **************************************************************************************************************************
    7. changed: [c7u6s1]
    8. changed: [c7u6s2]
    9. PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] ************************************************************************************************************
    10. TASK [echo hostname] **************************************************************************************************************************
    11. changed: [c7u6s3]
    12. changed: [c7u6s4]
    13. TASK [echo datetime] **************************************************************************************************************************
    14. changed: [c7u6s3]
    15. changed: [c7u6s4]
    16. PLAY RECAP ************************************************************************************************************************************
    17. c7u6s1                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    18. c7u6s2                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    19. c7u6s3                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    20. c7u6s4                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    21. user@wsg7:1st$

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

    1. ---
    2. - hosts: c7u6s1,c7u6s2,c7u6s3,c7u6s4
    3.   gather_facts: false
    4.   serial: 4
    5.   tasks:
    6.   - name: echo hostname
    7.     shell: hostname; sleep 1
    8.   - name: echo datetime
    9.     shell: date; sleep 1

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

    1. user@wsg7:1st$ ansible-playbook -f 2 test_serial.yml
    2. PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] ************************************************************************************************************
    3. TASK [echo hostname] **************************************************************************************************************************
    4. changed: [c7u6s1]
    5. changed: [c7u6s2]
    6. changed: [c7u6s3]
    7. changed: [c7u6s4]
    8. TASK [echo datetime] **************************************************************************************************************************
    9. changed: [c7u6s1]
    10. changed: [c7u6s2]
    11. changed: [c7u6s3]
    12. changed: [c7u6s4]
    13. PLAY RECAP ************************************************************************************************************************************
    14. c7u6s1                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    15. c7u6s2                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    16. c7u6s3                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    17. 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关 键字除了可以指定为数字之外,还可以指定为百分数,表示单次并行执行的主机数占指定的总主机数的百分比。此时的定义形式如下:

    1. ---
    2. - name: test play
    3.   hosts: webservers
    4.   serial: "30%"
    5.   tasks:
    6.   - name: task1
    7.     command: hostname
    8.   - name: task2
    9.     command: datetime

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

    1. ---
    2. - name: test play
    3.   hosts: webservers
    4.   serial:
    5.     - 1
    6.     - 5
    7.     - 10
    8.   tasks:
    9.   - name: task1
    10.     command: hostname
    11.   - name: task2
    12.     command: date

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

    1. ---
    2. - name: test play
    3.   hosts: webservers
    4.   serial:
    5.     - 1
    6.     - "50%"
    7.   tasks:
    8.   - name: task1
    9.     command: hostname
    10.   - name: task2
    11.     command: date

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

    1. ---
    2. - hosts: c7u6s1,c7u6s2,c7u6s3,c7u6s4
    3.   gather_facts: false
    4.   serial:
    5.   - 1
    6.   - 50%
    7.   tasks:
    8.   - name: echo hostname
    9.     shell: hostname; sleep 1
    10.   - name: echo datetime
    11.     shell: date; sleep 1

    上述 playbook 的执行过程如下:

    1. user@wsg7:1st$ ansible-playbook -f 5 test_serial.yml
    2. PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] ************************************************************************************************************
    3. TASK [echo hostname] **************************************************************************************************************************
    4. changed: [c7u6s1]
    5. TASK [echo datetime] **************************************************************************************************************************
    6. changed: [c7u6s1]
    7. PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] ************************************************************************************************************
    8. TASK [echo hostname] **************************************************************************************************************************
    9. changed: [c7u6s2]
    10. changed: [c7u6s3]
    11. TASK [echo datetime] **************************************************************************************************************************
    12. changed: [c7u6s3]
    13. changed: [c7u6s2]
    14. PLAY [c7u6s1,c7u6s2,c7u6s3,c7u6s4] ************************************************************************************************************
    15. TASK [echo hostname] **************************************************************************************************************************
    16. changed: [c7u6s4]
    17. TASK [echo datetime] **************************************************************************************************************************
    18. changed: [c7u6s4]
    19. PLAY RECAP ************************************************************************************************************************************
    20. c7u6s1                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    21. c7u6s2                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    22. c7u6s3                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    23. c7u6s4                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

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

  • 相关阅读:
    ts 分发
    2-Java进阶知识总结-7-UDP-TCP
    seatunnel-web-1.0.0运行时候报错
    Java版本spring cloud + spring boot企业电子招投标系统源代码
    JavaWeb—系统架构
    考研二战失败找工作心路历程
    笔试强训第24天--(年终奖 + 迷宫问题)
    第十三章 枚举类型与泛型总结
    Vue第一讲
    面向OLAP的列式存储DBMS-12-[ClickHouse]的管理与运维
  • 原文地址:https://blog.csdn.net/u012562943/article/details/126111128