Ansible执行task的时候,会依据配置文件中指定的forks参数、inventory中指定的主机以及主机组、以及选择的目标主机和定义的task数决定执行的方式。
比如inventory主机清单文件中,记录了20台服务器,配置文件中的forks参数指定为5,同时有2个task需要执行,此时就表示每次最多允许对5台服务器进行操作,如果剩下的服务器数不足5台,则对剩下的所有服务器执行操作。
那么,对于多个task,以及多台服务器执行的时候,按照目标主机的执行方式,通常有2种方式:广度优先和深度优先。
所谓的广度优先,是将每个task先按照forks
约定的参数,在所有服务器上分批并行执行,等这个task执行完毕之后,继续按照相同的方式执行后续的task。
而所谓的深度优先,是在serial
参数指定的参数(通常小于forks指定的参数)约定数目的服务器上并行执行完所有的task之后,才会继续在下一组服务器上,按照相同的方式执行完所有的task。
1. Ansible执行task的方式:广度优先
依据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。
2. Ansible执行task的方式:深度优先
而对于深度优先的执行方式,则是在指定数目的服务器上执行完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的值作为并行运行的服务器数量的依据。
2.1. 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数大于剩余的服务器数,所以直接将剩余的服务器全部执行。
3. References
[1]. Difference between Forks and Serial in Ansible
[2]. Controlling playbook execution: strategies and more