Swoole 4.4 協程搶佔式調度器詳解

前言

Swoole內核團隊開設的專欄,會逐漸投入精力寫文章介紹Swoole的開發歷程,實現原理,應用實踐等,你們能夠更好的交流,共同窗習,建設PHP生態。php

協程調度

去年Swoole推出了4.0版本後,完整的支持PHP協程,咱們能夠基於協程實現CSP編程,身邊的開發者驚呼,原來PHP代碼還能夠這樣寫。Swoole的協程默認是基於IO調度,程序中有阻塞會自動讓出當前協程,協程的各類優點咱們不在這裏展開討論。若是是IO密集型的場景,能夠表現得很不錯。可是對於CPU密集型的場景,會致使一些協程由於得不到CPU時間片被餓死。git

搶佔式調度

咱們在今年年初就計劃實現Swoole的搶佔式調度,以知足實現有些場景下的不均衡調度帶來的問題。咱們中間經歷了幾個版本,在這裏和你們分享一下開發過程當中的動機和解決辦法。github

圖片描述

咱們目的是爲了均衡調度每一個協程的CPU時間,好比協程3須要比較長的執行時間,咱們必須把協程3的CPU時間主動中斷,而不依賴IO事件,使得每一個協程獲得平均的執行時間。shell

起初,咱們的想法是能夠從PHP的循環中自動檢測執行實踐,若達到限制,能夠自動讓出當前協程。由於畢竟不多有人一馬平川的寫出佔用不少CPU的代碼,大都經過循環條件來控制。咱們hook循環指令,每次執行循環指令的時候,都來檢查協程的執行時間,咱們很欣喜的獲得了最初的版本。可是這樣作比較hack,並且opcode通過opcache優化後,狀況會變得有些複雜。編程

後來咱們使用PHPticks機制,也就是在PHP代碼編譯期間,注入ticks指令,能夠執行相應的函數,咱們能夠在這些函數中檢測處理協程的時間,達到搶佔式的效果,可是這裏有一個問題,PHPdeclare(ticks=N)語法,只對當前腳本範圍有效,也就是說項目稍微大點,require或者include進來的腳本,並不會自動注入ticks指令,這樣Swoole開發者幾乎是沒法接受的。咱們也試圖給PHP官方提一個PR,能夠在擴展層設置一個全局默認的ticks,可是官方不肯意採納咱們的提交,由於官方以爲這個功能對性能損耗比較大,並且有可能在PHP8移除這個功能。其實通過實測這個性能損耗並不大,並且咱們已經在生產環境驗證,並取得了顯著的效果,便可以讓出某些CPU密集的邏輯部分,使得服務整個相應時間更加均衡。
下圖是咱們生產環境一個RPC接口的調用端統計數據對比,客戶端等待超時時間爲2s,超時則統計爲錯誤。
圖片描述
左邊一側是沒有搶佔式調度,右側是開了搶佔式調度,能夠發現,左側老是會有偶爾超時狀況,而通過優化以後,沒有一個超時的請求,請求響應時間很是平滑,提高了服務的穩定性。
圖片描述
能夠從上圖看出,因爲搶佔式調度的加入,去除了請求耗時高的毛刺,使得平均請求時間變得更加平滑,穩定。swoole

想要作搶佔式調度,對於PHP來講,有兩個途徑函數

  1. 單線程的PHP的執行流,經過執行指令作文章,能夠在PHP執行流程中注入邏輯,以檢查執行時間,再加上Swoole的協程能力,能夠在不一樣的協程中切換,以達到搶佔CPU的目的。
  2. 考慮開線程,負責檢查當前執行協程執行時間。

通過以上辦法的嘗試,注入指令的路數基本是沒法獲得官方的支持,咱們只能另謀出路,多開一個線程,只負責檢查當前協程。具體的作法是,利用PHP-7.1.0引入的VM interrupt機制,默認每隔5ms檢查一下當前協程是否達到最大執行時間,默認爲10ms,若是超過,則讓出當前協程,達到被其餘協程搶佔的目的。oop

示例代碼

須要 Swoole 4.4或更高版本
<?php
ini_set("swoole.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 的長期支持和關注。

相關文章
相關標籤/搜索