回到:Ansible系列文章
各位讀者,請您:由於Ansible使用Jinja2模板,它的模板語法{% raw %} {{}} {% endraw %}和{% raw %} {%%} {% endraw %}和博客系統的模板使用的符號一樣,在渲染時會產生衝突,儘管我盡我努力地花了大量時間做了調整,但無法保證已經全部都調整。因此,如果各位閱讀時發現一些明顯的詭異的錯誤(比如像這樣的空的
行內代碼),請一定要回復我修正這些渲染錯誤。
11.Ansible你快點:Ansible執行過程分析、異步、效率優化
Ansible雖然方便,但有個"爲人詬病"的問題:任務執行速度太慢了,在有大量任務、大量循環任務時,其速度之慢真的是會讓人等到崩潰的。
Ansible官方給了一些優化選項供用戶選擇,還可以去網上尋找優化Ansible相關的插件。但在調優Ansible之前,應當先去理解Ansible的執行流程,如此才能知道爲什麼速度慢、要如何調優以及調優後會造成什麼後果。此外,還應學會測量任務的執行速度。
此外,本文還會回顧部分Ansible執行策略,更詳細的執行策略說明,可複習第十章的"理解Ansible執行策略"部分。
11.1 測量任務執行速度:profile_tasks插件
Ansible官方提供了幾個可用於計時的回調插件:
- (1).
profile_tasks
:該回調插件用於計時每個任務的執行時長 - (2).
profile_roles
插件用於計時每個Role的執行時長 - (3).
timer
插件用於計時每個play執行時長
要使用這些插件,需要在ansible.cfg配置文件中的callback_whitelist
中加入各插件。如下:
[defaults]
callback_whitelist = profile_tasks
# callback_whitelist = profile_tasks, profile_roles, timer
上面我只開啓了profile_tasks
插件。
這些回調插件會將對應的計時信息輸出,通過觀察這些計時信息,便可以知道任務執行消耗了多長時間,並多次比對計時信息,從而可確定哪種方式更高效。
然後執行幾個任務看看輸出結果,如下playbook文件內容:
---
- name: test for timer
hosts: timer
gather_facts: no
tasks:
- name: only one debug
debug:
var: inventory_hostname
- name: shell
shell:
cp /etc/fstab /tmp/
loop: "{{ range(0, 100)|list }}"
- name: scp
copy:
src: /etc/hosts
dest: /tmp/
loop: "{{ range(0, 100)|list }}"
其中timer主機組有三個節點,所以整個playbook中,每個節點執行201次任務,總共執行603次任務。以下是開啓profile_tasks
後在屏幕中輸出的計時信息:
$ ansible-playbook -i timer.host timer.yml
...................
......省略輸出......
...................
=========================================
scp ------------------------------------ 57.96s
shell ---------------------------------- 42.78s
only one debug ------------------------- 0.07s
從結果中可看到,3個節點的debug任務總共花費0.07秒,3個節點的shell任務總共300次任務花費42.78秒,3個節點的scp任務總共300次任務花費57.96秒。
11.2 Ansible執行流程分析
ansible命令或ansible-playbook命令加上-vvv
選項,會輸出很多調試信息,包括建立的連接、發送的文件等等。
例如,下面是Ansible 2.9默認配置中執行每單個任務都涉及到的步驟,其中我省略了大量信息以便各位能夠看懂關鍵步驟。各位可自行加上-vvv
去執行一個任務並觀察輸出信息,同時可與我所做的註釋做比較。
需注意:不同版本的Ansible爲每個任務建立的連接數量不同,Ansible 2.9爲每個任務建立7次ssh連接。有的資料或書籍中介紹時說只建立二次、三次、四次ssh連接都是有可能的,版本不同確實是有區別的。
# 1.第一個連接:獲取用戶家目錄,此處爲/root
<node1> ESTABLISH SSH CONNECTION FOR USER: None
<node1> SSH: EXEC ssh -vvv ....... '/bin/sh -c '"'"'echo ~ && sleep 0'"'"''
<node1> (0, '/root\n', ......)
# 2.第二個連接:在家目錄下創建臨時目錄,臨時目錄由配置文件中remote_tmp指令控制
<node1> ESTABLISH SSH CONNECTION FOR USER: None
<node1> SSH: EXEC ssh -vvv ...... '/bin/sh -c '"'"'( umask 77 && mkdir -p "` echo /root/.ansible/tmp/ansible-tmp-1575542743.85-116022411851390 `" && ...... `" ) && sleep 0'"'"''
# 3.第三個連接:探測目標節點的平臺和python解釋器的版本信息
<node1> Attempting python interpreter discovery
<node1> ESTABLISH SSH CONNECTION FOR USER: None
<node1> SSH: EXEC ssh -vvv ......
# 4.第四個連接:將要執行的模塊相關的代碼和參數放到本地臨時文件中,並使用sftp將任務文件傳輸到被控節點的臨時文件中
<node1> ESTABLISH SSH CONNECTION FOR USER: None
<node1> SSH: EXEC ssh -vvv ......
Using module file /usr/lib/python2.7/site-packages/ansible/modules/system/ping.py
......
<node1> SSH: EXEC sftp ......
<node1> (0, 'sftp> put /root/.ansible/tmp/ansible-local-78628na2FKL/tmpaE1RbJ /root/.ansible/tmp/ansible-tmp-1575542743.85-116022411851390/AnsiballZ_ping.py\n', ......
# 5.第五個連接:對目標節點上的任務文件授以執行權限
<node1> ESTABLISH SSH CONNECTION FOR USER: None
<node1> SSH: EXEC ssh -vvv ...... '/bin/sh -c '"'"'chmod u+x /root/.ansible/tmp/ansible-tmp-1575542743.85-116022411851390/ /root/.ansible/tmp/ansible-tmp-1575542743.85-116022411851390/AnsiballZ_ping.py && sleep 0'"'"''
......
# 6.第六個連接:執行目標節點上的任務
<node1> ESTABLISH SSH CONNECTION FOR USER: None
<node1> SSH: EXEC ssh -vvv ...... '/bin/sh -c '"'"'/usr/bin/python /root/.ansible/tmp/ansible-tmp-1575542743.85-116022411851390/AnsiballZ_ping.py && sleep 0'"'"''
<node1> (0, '\r\n{"invocation": {"module_args": {"data": "pong"}}, "ping": "pong"}\r\n',
......
# 7.第七個連接:刪除目標節點上的臨時目錄
<node1> ESTABLISH SSH CONNECTION FOR USER: None
<node1> SSH: EXEC ssh -vvv ...... '/bin/sh -c '"'"'rm -f -r /root/.ansible/tmp/ansible-tmp-1575542743.85-116022411851390/ > /dev/null 2>&1 && sleep 0'"'"''
......
總結一下Ansible爲每單個任務建立7次ssh連接所作的事情:
- (1).第一個連接:獲取遠程主機時行目標用戶的家目錄,此處爲/root
- (2).第二個連接:在遠程家目錄下創建臨時目錄,臨時目錄可由ansible.cfg中
remote_tmp
指令控制 - (3).第三個連接:探測目標節點的平臺和python解釋器的版本信息
- (4).第四個連接:將待執行模塊的相關代碼和參數放到本地臨時文件中,並使用sftp將任務文件傳輸到被控節點的臨時文件中
- (5).第五個連接:對目標節點上的任務文件授以執行權限
- (6).第六個連接:執行目標節點上的任務
- (7).第七個連接:刪除目標節點上的臨時目錄,並將執行結果返回給Ansible端
從單個任務的執行流程跳出來,更全局一點,那麼整個執行流程(默認配置下)大致如下(不考慮inventory階段或執行完任務後的回調階段,只考慮執行的任務流程):
- (1).進入第一個play,挑選forks=5設置的5個節點
- (2).每個節點執行第一個任務,每個節點都會建立7次ssh連接
- (3).每個節點執行第二個任務,每個節點都再次建立7次ssh連接
- (4).按照相同邏輯執行該play中其它任務...
- (5).所有節點執行完該play中的所有任務後,進入下一個play
- (6).按照上面的流程執行完所有play中的所有任務
以上便是整個執行流程,各位大概也看出來了,Ansible在建立ssh連接方面上實在是"不遺餘力",可能是因爲Ansible官方團隊太愛ssh了……開玩笑的啦……。
11.3 回顧Ansible的執行策略
使用forks、serial、strategy等指令可以改變Ansible的執行策略。
默認情況下forks=5,這表明在某一時刻最多隻有5個執行任務的工作進程(還有一個主進程),也即最多隻能挑選5個節點同時執行任務。
serail
是play級別的指令,用於指定幾個節點作爲一批去執行該play,該play執行完後才讓下一批節點執行該play中的任務。如果不指定serial,則默認的行爲等價於將所有節點當作一批。
strategy
指令用於指定節點執行任務時的策略,其側重點在於節點而在於任務,默認情況下其策略爲linear
,表示某個節點先執行完一個任務後等待其餘所有節點都執行完該任務,才統一進入下一個任務。另一種策略是free
策略,表示某節點執行完一個任務後不等待其它節點,而是毫不停留的繼續執行該play中的剩餘任務,直到該play執行完成,才釋放節點槽位讓其它未執行任務的節點開始執行任務。
前面的文章已經詳細介紹過Ansible的執行策略,所以此處僅作簡單回顧,如有所遺忘,請複習前面的文章。
11.4 加大forks的值
11.5 修改執行策略
默認情況下Ansible會讓所有節點(或者serial指定的數量)執行完同一個任務後才讓它們進入下一個任務,這體現了各節點的公平性和實時性:每個節點都能儘早執行到任務。這其實和操作系統的進程調度是類似的概念,只不過相對於操作系統的調度系統來說,Ansible的調度策略實在是太簡陋了。
假設forks設置的比較大,可以一次性讓足夠多的節點併發執行任務,那麼同時設置任務的執行策略爲strategy=free
便能讓這些執行任務的節點徹底放飛自我。只是剩餘的一部分節點可能會比較悲劇,它們處於調度不公平的一方。但是從整體來說,先讓大部分節點快速完成任務是值得的。
但是要注意,有些場景下要小心使用free策略,特別是節點依賴時。比如,某些節點運行服務A,另一些節點運行服務B,而服務B是依賴於服務A的,那麼必須不能讓運行B服務的節點先執行,對於有節點依賴關係的任務,爲了健壯性,一般會定義好等待條件,但是出現等待有可能就意味着浪費。
11.6 使Ansible異步執行任務
默認情況下,Ansible按照同步執行的方式執行每個任務。即對每個任務來說,都需要等待目標節點執行完該任務後回饋給Ansible端的報告,然後Ansible才認爲該節點上的該任務已經執行完成,纔會考慮下一步驟,比如free策略下該節點繼續執行下一個任務,或者等待其它節點完成該任務,等等。
11.6.1 async和poll指令
Ansible允許在task級別(且只支持task級別)指定該task是否以異步模式(即放入後臺)執行,即將該異步任務放入後臺。例如:
- name: it is an async task
copy:
src:
dest:
async: 200
poll: 2
- name: a sync task
copy:
src:
dest:
其中async
指令表示該任務將以異步的模式執行。async指令的值200表示,如果該後臺任務200秒還未完成,則認爲該任務失敗。poll
指令表示該任務丟入後臺後,Ansible每隔多久去檢查一次異步任務是否已成功、是否報錯等,只有檢查到已完成後才認爲該異步任務執行完成,纔會進入下一個任務。
如此看來,似乎這個異步執行模式並非想象中那樣真正的異步:將一個任務放入後臺執行,立即進入下一個任務。而且這裏的異步似乎會減慢任務的執行流程。比如後臺任務在第3秒完成,也必須等到第4秒檢查的時候才認爲執行完成。
如果poll指令的值大於0,這確實不是真正的異步,每個工作進程必須等待放入後臺的任務執行完成纔會進入下一個任務,換句話說,儘管使用了async異步指令,也仍然會阻塞在該異步任務上。這會減慢任務的執行速度,但此時執行該異步任務的Ansible工作進程會放棄CPU,使得CPU可以執行其它進程(對於Ansible控制節點來說,這算哪門子優點?)。
但如果poll指令的值爲0,將會以真正的異步模式執行任務,表示Ansible工作進程不檢查後臺任務的執行狀況,而是直接執行下一個任務。
不管poll指令的值是否大於0,只要使用了異步,那麼強烈建議將forks指令的值設置的足夠大。比如能夠一次性讓所有節點都開始異步執行某任務,這樣的話,無論poll的值是否大於0,都能提升效率。
此外,也可以在ansible命令中使用-B N
選項指定async功能,N爲超時時長,-P N
選項指定poll功能,N爲檢查後臺任務狀況的時間間隔。
例如:
$ ansible inventory_file -B200 -P 0 -m yum -a 'name=dos2unix' -o -f 20
11.6.2 等待異步任務
無論是編程語言還是如Ansible一般的工具,只要提供異步執行模式,都必不可少的需要提供一個等待異步任務執行完成的功能(注:同步執行模式不需要等待,因爲同步執行模式本就是從上到下依次執行的)。
例如下面的任務執行流程:
T1(async) --> T2(sync) --> T3(sync) --> T4(wait T1) --> T5
T1是一個異步任務,放入後臺執行時立即去執行T2和T3,但是T5這個任務比較特殊,它依賴於T1任務的執行成功,於是在T5任務之前插入一個等待T1異步任務執行完成的等待任務,只要T1沒有完成,就會一直阻塞在T4上,那麼自然不會執行到T5,如果T1完成了,T4便等待完成,於是可以執行T5。
Ansible中想要等待異步任務需要藉助於async_status
模塊,該模塊接受一個後臺任務的job id作爲參數,然後獲取該後臺任務的狀態並返回。
該模塊返回的狀態信息包含以下幾項屬性:
- (1).ansible_job_id:異步任務的job id
- (2).finished:表示所等待的異步任務是否已執行完成,值爲1表示完成,0表示未完成
- (3).started:表示所等待的異步任務是否已開始執行,值爲1表示已開始,0表示未開始
例如,下面是官方提供的一個典型的異步等待示例:
- name: Asynchronous yum task
yum:
name: nginx
state: present
async: 1000
poll: 0
register: yum_sleeper
- name: Wait for asynchronous job to end
async_status:
jid: '{{ yum_sleeper.ansible_job_id }}'
register: job_result
until: job_result.finished
retries: 30
此示例中,異步任務註冊了一個變量yum_sleeper
,該變量中包含一個ansible_job_id
的屬性。將該屬性交給async_status
模塊的jid選項,該模塊便可以獲取該異步任務的狀態,並將狀態註冊到變量job_result
中,結合until
指令不斷等待job_result.finished
事件發生,即表示異步任務執行完成。
同時等待多個異步任務也是常見的需求:只有所有想要等待的任務全都完成了才繼續向下執行。Ansible中可以對async_status
模塊使用loop循環來完成該功能。
例如:
---
- name: test for timer
hosts: timer
gather_facts: no
tasks:
- name: async task 1
shell: sleep 5
async: 200
poll: 0
register: async_task1
- name: async task 2
shell: sleep 10
async: 200
poll: 0
register: async_task2
- name: waiting for all async task
async_status:
jid: "{{item}}"
register: job_result
until: job_result.finished
loop:
- "{{ async_task1.ansible_job_id }}"
- "{{ async_task2.ansible_job_id }}"
- name: after waiting
debug:
msg: "after waiting"
11.6.3 何時使用異步任務
有時候合理應用異步任務能大幅提升Ansible的執行效率,但也並非所有場景都能夠使用異步任務。
總結來說,以下一些場景可能使用到Ansible的異步特性:
11.7 開啓ssh長連接
Ansible對ssh的依賴性非常強,優化ssh連接在一定程度上也是在優化Ansible。
其中一項優化是開啓ssh的長連接,即長時間保持連接狀態。開啓長連接後,在ssh連接過期前會一直保持ssh連接已建立的狀態,使得下次和目標節點建立ssh連接時將直接使用該連接。相當於對ssh連接進行了緩存。
要開啓ssh長連接,要求Ansible端的openssh版本高於或等於5.6。使用ssh -V
可以查看版本號。然後設置ansible使用ssh連接被控端的連接參數,此處修改/etc/ansible/ansible.cfg,在此文件中啓動下面的連接選項,其中ControlPersist=5d
是控制ssh連接會話保持時長爲5天。
ssh_args = -C -o ControlMaster=auto -o ControlPersist=5d
除此之外直接設置/etc/ssh/ssh_config
(不是sshd_config,因爲ssh命令是客戶端命令)中對應的長連接選項也是可以的。
以後只要有了一次ssh連接,就會將連接保留下來,例如:執行一次Ansible的ad-hoc操作,會建立ssh連接。
ansible centos -m ping
查看netstat,發現ssh進程的會話一直是established狀態(爲了排版,我略了前面3個字段)。
$ netstat -tnalp
Local Address Foreign Address State PID/Program name
0.0.0.0:22 0.0.0.0:* LISTEN 1143/sshd
127.0.0.1:25 0.0.0.0:* LISTEN 2265/master
192.168.200.26:58474 192.168.200.59:22 ESTABLISHED 31947/ssh: /root/.a
192.168.200.26:22 192.168.200.1:8189 ESTABLISHED 29869/sshd: root@pt
192.168.200.26:37718 192.168.200.64:22 ESTABLISHED 31961/ssh: /root/.a
192.168.200.26:38894 192.168.200.60:22 ESTABLISHED 31952/ssh: /root/.a
192.168.200.26:48659 192.168.200.61:22 ESTABLISHED 31949/ssh: /root/.a
192.168.200.26:33546 192.168.200.65:22 ESTABLISHED 31992/ssh: /root/.a
192.168.200.26:54824 192.168.200.63:22 ESTABLISHED 31958/ssh: /root/.a
:::22 :::* LISTEN 1143/sshd
::1:25 :::* LISTEN 2265/master
同時,會在當前用戶家目錄的.ansible/cp
目錄下生成一些socket文件,每個ssh連接會話一個文件。
$ ls -l ~/.ansible/cp/
total 0
srw------- 1 root root 0 Jun 3 18:26 5c4a6dce87
srw------- 1 root root 0 Jun 3 18:26 bca3850113
srw------- 1 root root 0 Jun 3 18:26 c89359d711
srw------- 1 root root 0 Jun 3 18:26 cd829456ec
srw------- 1 root root 0 Jun 3 18:26 edb7051c84
srw------- 1 root root 0 Jun 3 18:26 fe17ac7eed
這些socket文件的存放路徑由ansible.cfg文件中的control_path_dir
指令決定。
11.7.1 開啓ssh長連接後的注意事項
開啓ssh長連接固然會將連接緩存下來以避免頻繁建立ssh連接,但也因此帶來了一個問題:只要目標節點sshd監聽地址和端口未變,那麼只要ssh長連接未過期,客戶端(比如Ansible)總能使用已緩存的連接和目標節點通信。
這是什麼意思呢?比如A節點上的ssh開啓了長連接(注:長連接是ssh的特性,不是Ansible的特性,所以ssh自身也可以設置),當A通過ssh第一次連接到root@B
節點後,A會緩存到B節點的ssh連接,如果此時B節點目標用戶root修改了密碼,A節點藉助緩存下來的ssh長連接仍然能夠連接到root@B
節點。
對於Ansible來說,開啓長連接後可能會帶來一些問題,比如緩存了某節點的ssh連接後,又修改了Inventory中該節點的ssh連接變量,但這些連接變量在ssh長連接過期之前將不會生效。對於沒有注意到這一現象的人來說,這樣的問題是非常難以排查的,因爲很難想到ssh長連接這方面。
11.8 開啓Pipelining
從前面對Ansible執行任務的流程中可以發現,Ansible執行每個任務時都會在本地將模塊(通常是Python腳本程序)和相關參數打包後通過sftp發送到目標節點上,然後執行目標節點上的臨時腳本文件。這些行爲還帶來了副作用,比如多建立了幾個ssh連接來創建臨時目錄、刪除目錄等。
Ansible現在也支持使用ssh的pipelining特性(注意,仍然是ssh的特性),當Ansible中開啓了Pipelining後,一個任務的所有動作都在一個ssh會話中完成,也會省去sftp到遠端的過程,它會直接將要執行任務涉及到的指令(比如python語句)通過遠程shell的方式發送到目標節點的標準輸入(stdin)中,然後在目標節點執行這些代碼。
如果不理解這個過程,可以理解下面這個更直觀的ssh命令:
$ echo 'hostname -I' | ssh [email protected] 'bash'
192.168.200.48
上面的命令中,ssh連接到192.168.200.48,同時ssh命令會讀取標準輸入中的hostname -I
並將其寫入到遠程主機上的標準輸入供bash命令讀取,於是bash命令執行讀取到的數據。所以,相當於是在遠程主機上執行了echo "hostname -I" | bash
。
類似的,當Ansible開啓Pipelining特性後,會將任務相關的指令(通常是Python語句)通過ssh發送到目標節點的標準輸入中,然後python解釋器程序讀取指令並執行。相當於:
$ echo 'print("hello world")' | ssh [email protected] 'python'
也相當於在遠程主機上執行了:
$ echo 'print("hello world")' | python
既然任務相關的指令已經發送到目標的標準輸入,那自然就不需要再通過傳輸文件的方式將任務傳輸到目標節點再執行,這顯然也減少了一大堆副作用而建立的ssh連接。事實上,當Ansible開啓了Pipelining特性後,提升的效率是巨大的。
Ansible開啓Pipelining的方式是在配置文件(如ansible.cfg)中設置pipelining=true
,默認是false,即默認Pipelining是禁用狀態。
$ grep '^pipelining' /etc/ansible/ansible.cfg
pipelining = True
11.8.1 開啓Pipelining後的注意事項
但是要注意,如果在Ansible中使用sudo相關行爲時,需要在被控節點的/etc/sudoers中禁用"requiretty"。
例如,對於下面的play:
---
- name: test for timer
hosts: timer
gather_facts: no
become: yes
become_user: root
become_method: sudo
tasks:
- shell: sleep 1
不禁用requiretty
將報錯:
Pseudo-terminal will not be allocated because stdin is not
a terminal.\r\nsudo: sorry, you must have a tty to run sudo
可以通過visudo編輯配置文件,註釋該選項來禁用requiretty。
$ grep requiretty /etc/sudoers
Defaults requiretty # 註釋此行表示禁用
之所以要設置/etc/sudoers中的requiretty,是因爲ssh遠程執行命令時,它的環境是非登錄式非交互式shell,默認不會分配tty,沒有tty,ssh的sudo就無法關閉密碼回顯(使用"-tt"選項強制SSH分配tty)。所以出於安全考慮,/etc/sudoers中默認是開啓requiretty的,它要求只有擁有tty的用戶才能使用sudo,也就是說ssh連接過去不允許執行sudo。
但是,修改設置/etc/sudoers的操作是在被控節點上進行的(或者ansible連接過去修改),其實在ansible端也可以解決sudo的問題,只需在ansible的ssh參數上加上"-tt"選項即可(注:經測試,Ansible2.9中使用-tt會阻塞,-t或—ttt不阻塞但仍然失敗,但我肯定,以前的版本是可以這麼做的,所以相關方案仍然留在此處)。
$ grep 'ssh_args' /etc/ansible/ansible.cfg
ssh_args = -C -o ControlMaster=auto -o ControlPersist=1d -tt
# 或者在命令行中指定
$ ansible-playbook --ssh-extra-args="-tt"" xxxxxx
11.8.2 開啓Pipelining後的執行流程
開啓Pipelining後,再來看下執行單個任務時的執行流程(爲排版也爲讓各位能一眼看懂,我省略了一些信息):
TASK [test task] *******************************************
task path: /root/ansible/timer.yml:6
# 第一個SSH連接,用於探測目標節點上支持的Python版本
<192.168.200.48> Attempting python interpreter discovery
<192.168.200.48> ESTABLISH SSH CONNECTION FOR USER: None
<192.168.200.48> SSH: EXEC ssh -C -o ......
<192.168.200.48> (0, b'
PLATFORM\nLinux\nFOUND\n
/usr/bin/python\n
/usr/bin/python3.6\n
/usr/bin/python2.7\n
/usr/bin/python3\n
/usr/bin/python\n
ENDFOUND\n', b'')
# 第二個SSH連接用於探測目標節點操作系統的信息
<192.168.200.48> ESTABLISH SSH CONNECTION FOR USER: None
<192.168.200.48> SSH: EXEC ssh -C -o ......
<192.168.200.48> (0, b'{"osrelease_content":
"NAME=\\"CentOS Linux\\"\\n
VERSION=\\"7 (Core)\\"\\n
ID=\\"centos\\"\\n
ID_LIKE=\\"rhel fedora\\"\\n
VERSION_ID=\\"7\\"\\n
......b'')
# 準備執行任務,加載任務使用的模板文件,且發現開啓了Pipelining
Using module file ......ansible/modules/commands/command.py
Pipelining is enabled.
# 第三個SSH連接用於執行任務
<192.168.200.48> ESTABLISH SSH CONNECTION FOR USER: None
<192.168.200.48> SSH: EXEC ssh -C ......
'/bin/sh -c '"'"'/usr/bin/python && sleep 0'"'"''
<192.168.200.48> (目標節點執行任務返回的結果)
從執行流程上已經看到,開啓Pipelining後,除了建立兩個必要的SSH連接探測Python版本(老版本的Ansible不支持多Python版本自動探測功能)和操作系統信息外,執行任務相關的SSH連接只有一個。
11.8.3 開啓和不開啓Pipelining的效率比較
下面是開啓和不開啓Pipelining時執行本文開頭的計時任務,3個節點總共603個任務。
# 開啓Pipelining之前
$ ansible-playbook -i timer.host timer.yml
...................
......省略輸出......
...................
=========================================
scp ------------------------------------ 57.96s
shell ---------------------------------- 42.78s
only one debug ------------------------- 0.07s
# 開啓Pipelining後
$ ansible-playbook -i timer.host timer.yml
...................
......省略輸出......
...................
=========================================
scp ------------------------------------ 39.99s
shell ---------------------------------- 20.29s
only one debug ------------------------- 0.07s
可見,使用了Pipelining後,總時間幾乎是對半減,效率提升不可謂不高。
11.9 修改facts收集行爲
Ansible默認會收集所有節點的所有facts信息,而收集facts信息是非常慢的。
如果能夠確保play中使用不到facts中的信息,則可以gather_facts: no
關閉收集功能。
如果只想要facts中的一部分信息,那麼在收集時可以指定只收集這一部分信息,其它的不要收集。例如只想收集網卡相關信息可以設置gather_subset=!all,!any,network
,這樣可以減少收集的數據量,從而提升效率。
最後,還可以將facts緩存下來。關於facts緩存,在前面"迴歸Ansible並進階"的章節中已經詳細介紹過。所以此處略過。
11.10 Shell層次上的優化:將任務分開執行
在前面"利用Role部署LNMP"的章節最後,我提到過這種優化手段。這裏再簡單回顧一下。
在LNMP的示例中,分別爲nginx和php和MySQL都單獨定義了自己的Role,它們分別在三批節點上執行。爲了統籌這些Role,一般會定義一個匯聚了所有的Role的playbook文件,稱爲入口playbook,比如稱爲main.yml或site.yml。
但是,把這些Role聚集到單個playbook文件中後就必然會產生前後順序關係。比如執行nginx Role的時候,PHP Role和MySQL Role對應的節點都在空閒。這是一種很低效的執行方式。
這樣一來,分別執行這三個Role的三批節點就可以同時開始執行任務了。
而且,如果某個Role依賴於另一個Role,可以協調它們的順序並取消被依賴Role的後臺執行方式。比如php Role依賴於mysql Role時(只是假設),可以將mysql.yml以非後臺的方式放在php Role的前面執行。
$ ansible-playbook nginx.yml >/tmp/nginx.log &
$ ansible-playbook mysql.yml >/tmp/mysql.log # 被依賴,所以不後臺
$ ansible-playbook php.yml >/tmp/php.log
當然,更推薦也更健壯的方式是在php Role中定義等待MySQL Role的任務。
再者,還可以寫一個簡單的Shell腳本,在Shell腳本中加入一些判斷邏輯來決定如何執行這些Role,這樣實現的邏輯可能比Ansible本身還要更豐富。
11.11 第三方策略插件:Mitogen for Ansible
Ansible的執行策略(即由strategy指令指定的值)是以插件方式提供的。Ansible官方目前提供了四種策略插件:
- (1).linear
- (2).free
- (3).host-pinned
- (4).debug
作爲使用Ansible的用戶來說,可以去編寫自己的策略插件或網上尋找相關的策略插件。有一款備受青睞的策略插件名爲Mitogen for Ansible。
官方介紹和文檔:Mitogen for Ansible--https://mitogen.networkgenomics.com/ansible_detailed.html。
使用該插件後,將盡可能地儘早執行任務,注意是儘早而不是儘快,它所作的工作不可能會影響一個已編碼完成的Ansible模塊的執行速度,換句話說,Mitogen提升的是從某個任務開始到該任務對應模塊開始執行之間的速度。按照Mitogen官方描述,該插件可以將Ansible的執行速度提升到1.25倍-7倍。
不管怎麼官方怎麼說,自己測試一下是最好的。
首先下載並解壓:
$ wget 'https://networkgenomics.com/try/mitogen-0.2.9.tar.gz'
$ mkdir -p ~/.ansible/plugins
$ tar xf mitogen-0.2.9.tar.gz -C ~/.ansible/plugins/
然後在ansible.cfg中設置使用該策略插件,並指定該策略插件提供的策略。
[defaults]
strategy_plugins = ~/.ansible/plugins/mitogen-0.2.9/ansible_mitogen/plugins/strategy
strategy = mitogen_linear
mitogen插件提供了三種策略:
$ ls -1 ~/.ansible/plugins/mitogen-0.2.9/ansible_mitogen/plugins/strategy/
__init__.py
mitogen_free.py
mitogen_host_pinned.py
mitogen_linear.py
mitogen.py
其中:
- (1).
mitogen_linear
對應於Ansible自身的linear策略 - (2).
mitogen_free
對應於Ansible自身的free策略 - (3).
mitogen_host_pinned
對應於Ansible自身的host_pinned策略
然後就可以開始計時測試了,仍然是本文開頭的計時任務。目前Ansible已開啓Pipelining特性,觀察使用mitogen_linear的效率和原生linear的效率相比如何:
# 開啓Pipelining但未使用mitogen插件
$ ansible-playbook -i timer.host timer.yml
...................
......省略輸出......
...................
=========================================
scp ------------------------------------ 39.99s
shell ---------------------------------- 20.29s
only one debug ------------------------- 0.07s
# 開啓Pipelining且使用mitogen插件
$ ansible-playbook -i timer.host timer.yml
...................
......省略輸出......
...................
=========================================
shell -------------------------------- 8.02s
scp ---------------------------------- 3.37s
only one debug ----------------------- 0.33s
從結果可見,效率的提升非常明顯。
使用mitogen時,有些配置可能和Ansible原生衝突,或需要做額外配置。比如:
- (1).原生Ansible允許使用forks設置最大併發節點數量,但mitogen默認固定最多32個連接,需要修改環境變量
MITOGEN_POOL_SIZE
的值來設置最大併發量。 - (2).mitogen的sudo處理行爲和Ansible不一樣,所以可能需要單獨在目標節點的sudoer配置中加入對應用戶的配置。比如
your_ssh_username = (ALL) NOPASSWD:/usr/bin/python -c*
。
使用mitogen後,不少Ansible的內部操作會發生變化,mitogen自然會以相同的結果不同的效率來完成目標,所以作爲使用者,不用關心這些內部的事。
最後,如果要使用mitogen,建議閱讀一次官方手冊,瞭解使用mitogen時的一些注意事項。