需求:併發檢測1000臺web伺服器狀態(或者併發為1000臺web伺服器分發檔案等)如何用shell實現?
方案一:(這應該是大多數人都第一時間想到的方法吧)
思路:一個for迴圈1000次,順序執行1000次任務。
實現:
#!/bin/bash start_time=`date +%s` #定義指令碼執行的開始時間 for ((i=1;i<=1000;i++)) do sleep 1 #sleep 1用來模仿執行一條命令需要花費的時間(可以用真實命令來代替) echo 'success'$i; done stop_time=`date +%s` #定義指令碼執行的結束時間 echo "TIME:`expr $stop_time - $start_time`"
執行結果:
[root@iZ94yyzmpgvZ ~]# . test.sh success1 success2 success3 success4 success5 success6 success7 ........此處省略 success999 success1000 TIME:1000
程式碼解析以及問題:
一個for迴圈1000次相當於需要處理1000個任務,迴圈體用sleep 1代表執行一條命令需要的時間,用success$i來標示每條任務.
這樣寫的問題是,1000條命令都是順序執行的,完全是阻塞時的執行,假如每條命令的執行時間是1秒的話,那麼1000條命令的執行時間是1000秒,效率相當低,而我的要求是併發檢測1000臺web的存活,如果採用這種順序的方式,那麼假如我有1000臺web,這時候第900臺機器掛掉了,檢測到這臺機器狀態所需要的時間就是900s!
所以,問題的關鍵集中在一點:如何併發
方案二:
思路:一個for迴圈1000次,迴圈體裡面的每個任務都放入後臺執行(在命令後面加&符號代表後臺執行)。
實現:
#!/bin/bash start=`date +%s` #定義指令碼執行的開始時間 for ((i=1;i<=1000;i++)) do { sleep 1 #sleep 1用來模仿執行一條命令需要花費的時間(可以用真實命令來代替) echo 'success'$i; }& #用{}把迴圈體括起來,後加一個&符號,代表每次迴圈都把命令放入後臺執行 #一旦放入後臺,就意味著{}裡面的命令交給作業系統的一個執行緒處理了 #迴圈了1000次,就有1000個&把任務放入後臺,作業系統會併發1000個執行緒來處理 #這些任務 done wait #wait命令的意思是,等待(wait命令)上面的命令(放入後臺的)都執行完畢了再 #往下執行。 #在這裡寫wait是因為,一條命令一旦被放入後臺後,這條任務就交給了作業系統 #shell指令碼會繼續往下執行(也就是說:shell指令碼里面一旦碰到&符號就只管把它 #前面的命令放入後臺就算完成任務了,具體執行交給作業系統去做,指令碼會繼續 #往下執行),所以要在這個位置加上wait命令,等待作業系統執行完所有後臺命令 end=`date +%s` #定義指令碼執行的結束時間 echo "TIME:`expr $end - $start`"
執行結果:
[root@iZ94yyzmpgvZ /]# . test1.sh ...... [989] Done { sleep 1; echo 'success'$i; } [990] Done { sleep 1; echo 'success'$i; } success992 [991] Done { sleep 1; echo 'success'$i; } [992] Done { sleep 1; echo 'success'$i; } success993 [993] Done { sleep 1; echo 'success'$i; } success994 success995 [994] Done { sleep 1; echo 'success'$i; } success996 [995] Done { sleep 1; echo 'success'$i; } [996] Done { sleep 1; echo 'success'$i; } success997 success998 [997] Done { sleep 1; echo 'success'$i; } success999 [998] Done { sleep 1; echo 'success'$i; } [999]- Done { sleep 1; echo 'success'$i; } success1000 [1000]+ Done { sleep 1; echo 'success'$i; } TIME:2
程式碼解析以及問題:
shell中實現併發,就是把迴圈體的命令用&符號放入後臺執行,1000個任務就會併發1000個執行緒,執行時間2s,比起方案一的1000s,已經非常快了。
可以看到輸出結果success4 ...success3完全都是無序的,因為大家都是後臺執行的,這時候就是cpu隨機執行了,所以並沒有什麼順序
這樣寫確實可以實現併發,然後,大家可以想象一下,1000個任務就要併發1000個執行緒,這樣對作業系統造成的壓力非常大,它會隨著併發任務數的增多,作業系統處理速度會變慢甚至出現其他不穩定因素,就好比你在對nginx調優後,你認為你的nginx理論上最大可以支援1w併發了,實際上呢,你的系統會隨著高併發壓力會不斷攀升,處理速度會越來越慢(你以為你扛著500斤的東西你還能跑的跟原來一樣快嗎)
方案三:
思路:基於方案二,使用linux管道檔案特性製作佇列,控制執行緒數目
知識儲備:
一.管道檔案
1:無名管道(ps aux | grep nginx)
2:有名管道(mkfifo /tmp/fd1)
有名管道特性:
1.cat /tmp/fd1(如果管道內容為空,則阻塞)
實驗:
2.echo "test" > /tmp/fd1(如果沒有讀管道的操作,則阻塞)
總結:
利用有名管道的上述特性就可以實現一個佇列控制了
你可以這樣想:一個女士公共廁所總共就10個蹲位,這個蹲位就是佇列長度,女廁 所門口放著10把藥匙,要想上廁所必須拿一把藥匙,上完廁所後歸 還藥匙,下一個人就可以拿藥匙進去上廁所了,這樣同時來了1千
位美女上廁所,那前十個人搶到藥匙進去上廁所了,後面的990人 需要等一個人出來歸還藥匙才可以拿到藥匙進去上廁所,這樣10把藥匙就實現了控制1000人上廁所的任務(os中稱之為訊號量)
二.檔案描述符
1.管道具有存一個讀一個,讀完一個就少一個,沒有則阻塞,放回的可以重複取,這正是佇列特性,但是問題是當往管道檔案裡面放入一段內容,沒人取則會阻塞,這樣你永遠也沒辦法往管道里面同時放入10段內容(想當與10把藥匙),解決這個問題的關鍵就是檔案描述符了。
2. mkfifo /tmp/fd1
建立有名管道檔案exec 3<>/tmp/fd1,建立檔案描述符3關聯管道檔案,這時候3這個檔案描述符就擁有了管道的所有特性,還具有一個管道不具有的特性:無限存不阻塞,無限取不阻塞,而不用關心管道內是否為空,也不用關心是否有內容寫入引用檔案描述符: &3可以執行n次echo >&3 往管道里放入n把鑰匙
exec命令用法:http://blog.sina.com.cn/s/blog_7099ca0b0100nby8.html
實現:
#!/bin/bash start_time=`date +%s` #定義指令碼執行的開始時間 [ -e /tmp/fd1 ] || mkfifo /tmp/fd1 #建立有名管道 exec 3<>/tmp/fd1 #建立檔案描述符,以可讀(<)可寫(>)的方式關聯管道檔案,這時候檔案描述符3就有了有名管道檔案的所有特性 rm -rf /tmp/fd1 #關聯後的檔案描述符擁有管道檔案的所有特性,所以這時候管道檔案可以刪除,我們留下檔案描述符來用就可以了 for ((i=1;i<=10;i++)) do echo >&3 #&3代表引用檔案描述符3,這條命令代表往管道里面放入了一個"令牌" done for ((i=1;i<=1000;i++)) do read -u3 #代表從管道中讀取一個令牌 { sleep 1 #sleep 1用來模仿執行一條命令需要花費的時間(可以用真實命令來代替) echo 'success'$i echo >&3 #代表我這一次命令執行到最後,把令牌放回管道 }& done wait stop_time=`date +%s` #定義指令碼執行的結束時間 echo "TIME:`expr $stop_time - $start_time`" exec 3<&- #關閉檔案描述符的讀 exec 3>&- #關閉檔案描述符的寫
執行結果:
[root@iZ94yyzmpgvZ /]# . test2.sh success4 success6 success7 success8 success9 success5 ...... success935 success941 success942 ...... success992 [992] Done { sleep 1; echo 'success'$i; echo 1>&3; } success993 [993] Done { sleep 1; echo 'success'$i; echo 1>&3; } success994 [994] Done { sleep 1; echo 'success'$i; echo 1>&3; } success998 success999 success1000 success997 success995 success996 [995] Done { sleep 1; echo 'success'$i; echo 1>&3; } TIME:101
程式碼解析以及問題:
兩個for迴圈,第一個for迴圈10次,相當於在女士公共廁所門口放了10把鑰匙,第二個for
迴圈1000次,相當於1000個人來上廁所,read -u3相當於取走一把藥匙,{}裡面最後一行程式碼echo >&3相當於上完廁所送還藥匙。
這樣就實現了10把藥匙控制1000個任務的執行,執行時間為101s,肯定不如方案二快,但是比方案一已經快很多了,這就是佇列控制同一時間只有最多10個執行緒的併發,既提高了效率,又實現了併發控制。
注意:建立一個檔案描述符exec 3<>/tmp/fd1 不能有空格,代表檔案描述符3有可讀(<)可寫(>)許可權,注意,開啟的時候可以寫在一起,關閉的時候必須分開關,exec 3<&-關閉讀,exec 3>&-關閉寫