這篇文章主要介紹了Swoole4.4協程搶佔式調度器詳解,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
前言
Swoole
內核團隊開設的專欄,會逐漸投入精力寫文章介紹Swoole
的開發歷程,實現原理,應用實踐等,大家可以更好的交流,共同學習,建設PHP
生態。
協程調度
去年Swoole
推出了4.0
版本後,完整的支持PHP
協程,我們可以基於協程實現CSP
編程,身邊的開發者驚呼,原來PHP代碼還可以這樣寫。Swoole
的協程默認是基於IO調度,程序中有阻塞會自動讓出當前協程,協程的各種優勢我們不在這裏展開討論。如果是IO
密集型的場景,可以表現得很不錯。但是對於CPU
密集型的場景,會導致一些協程因爲得不到CPU
時間片被餓死。
搶佔式調度
我們在今年年初就計劃實現Swoole
的搶佔式調度,以滿足實現有些場景下的不均衡調度帶來的問題。我們中間經歷了幾個版本,在這裏和大家分享一下開發過程中的動機和解決辦法。
我們目的是爲了均衡調度每個協程的CPU
時間,比如協程3需要比較長的執行時間,我們必須把協程3的CPU
時間主動中斷,而不依賴IO
事件,使得每個協程得到平均的執行時間。
起初,我們的想法是可以從PHP
的循環中自動檢測執行實踐,若達到限制,可以自動讓出當前協程。因爲畢竟很少有人一馬平川的寫出佔用很多CPU
的代碼,大都通過循環條件來控制。我們hook
循環指令,每次執行循環指令的時候,都來檢查協程的執行時間,我們很欣喜的得到了最初的版本。但是這樣做比較hack,而且opcode
經過opcache
優化後,情況會變得有些複雜。
後來我們使用PHP
的ticks
機制,也就是在PHP
代碼編譯期間,注入ticks
指令,可以執行相應的函數,我們可以在這些函數中檢測處理協程的時間,達到搶佔式的效果,但是這裏有一個問題,PHP
的declare(ticks=N)
語法,只對當前腳本範圍有效,也就是說項目稍微大點,require
或者include
進來的腳本,並不會自動注入ticks
指令,這樣Swoole
開發者幾乎是無法接受的。我們也試圖給PHP
官方提一個PR,可以在擴展層設置一個全局默認的ticks
,但是官方不願意採納我們的提交,因爲官方覺得這個功能對性能損耗比較大,而且有可能在PHP8
移除這個功能。其實經過實測這個性能損耗並不大,而且我們已經在生產環境驗證,並取得了顯著的效果,即可以讓出某些CPU
密集的邏輯部分,使得服務整個相應時間更加均衡。
下圖是我們生產環境一個RPC接口的調用端統計數據對比,客戶端等待超時時間爲2s,超時則統計爲錯誤。
左邊一側是沒有搶佔式調度,右側是開了搶佔式調度,可以發現,左側總是會有偶爾超時情況,而經過優化之後,沒有一個超時的請求,請求響應時間非常平滑,提升了服務的穩定性。
可以從上圖看出,由於搶佔式調度的加入,去除了請求耗時高的毛刺,使得平均請求時間變得更加平滑,穩定。
想要做搶佔式調度,對於PHP
來說,有兩個途徑
- 單線程的
PHP
的執行流,通過執行指令做文章,可以在PHP
執行流程中注入邏輯,以檢查執行時間,再加上Swoole
的協程能力,可以在不同的協程中切換,以達到搶佔CPU
的目的。 - 考慮開線程,負責檢查當前執行協程執行時間。
經過以上辦法的嘗試,注入指令的路數基本是無法得到官方的支持,我們只能另謀出路,多開一個線程,只負責檢查當前協程。具體的做法是,利用PHP-7.1.0
引入的VM interrupt
機制,默認每隔5ms檢查一下當前協程是否達到最大執行時間,默認爲10ms,如果超過,則讓出當前協程,達到被其他協程搶佔的目的。
示例代碼
需要Swoole 4.4
或更高版本
<?php Co::set(['enable_preemptive_scheduler' => 1]); $start = microtime(1); echo "start\n"; $flag = 1; go(function () use (&$flag) { echo "coro 1 start to loop\n"; $i = 0; for (;;) { if (!$flag) { break; } $i++; } echo "coro 1 can exit\n"; }); $end = microtime(1); $msec = ($end - $start) * 1000; echo "use time $msec\n"; go(function () use (&$flag) { echo "coro 2 set flag = false\n"; $flag = false; }); echo "end\n";
執行結果
start coro 1 start to loop use time 11.121988296509 coro 2 set flag = false end coro 1 can exit
可以發現,代碼邏輯可以從第一個協程的死循環中自動yield
出來,執行第二個協程,如果沒有這個特性,第二個協程永遠不會被執行,導致被餓死。而這樣做,第二個協程可以順利被執行,最後執行結束後,第一個協程也會接着繼續往下執行。達到我們的第二個協程主動搶佔第一個協程CPU
的效果。
這個特性在生產環境非常有用,尤其是對於實時系統或者響應時間比較敏感的場景。
最後
感謝大家對 Swoole 的長期支持和關注。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持神馬文庫。