關於 PHP 8.1 的 Fiber RFC

2021-05-14 11:14:31

最新的 PHP 8.1 增加了一個 Fiber 的提案,最近討論的比較多。有不少好事者拿來說事兒,說是 「Fiber 進入核心之後,Swoole 的使用者就大幅減少「

實際上 Fiber 擴充套件進入核心後,由於它是一個非常底層的 API ,並不是直接可以使用的技術,不會對 Swoole 產生影響。真正和 Swoole 競爭的是應該是 Amphp 、ReactPHP 。Fiber 反而對 Swoole 是有好處的,PHP 核心開發者維護了協程切換的全域性狀態列表,Swoole PHPCoroutine 這部分的程式碼實現就變簡單了。另外,其他擴充套件也會注意到協程的存在,使用 C 全域性變數或棧上記憶體時考慮到協程切換的可能性,避免出現 Crash。ext-fiber 合併進來之後,也應標記為 alpha 狀態,一些特殊情況能會引起崩潰,需要比較長的時間去收集解決這些問題。

最近這幾年即便官方連續出了很多個大版本,PHP 還是一直是在走下坡路。有許多 PHP 開發者說是因為 PHP 效能不行,沒有 JIT。於是 PHP8.0 加入了 JIT。還有人說 PHP 沒有協程,所以 PHP8.1 要加入 Fiber。馬上就會有人說 PHP 缺少多執行緒,按照現在這個節奏,可以預見未來有可能 PHP 的多執行緒擴充套件 parallels 也會合併到核心。PHP8 還加入了一個 FFI 模組,甚至可以直接使用 PHP 呼叫 C 庫。

可是真的加入如此多的能力,PHP 就得到很大的改變了嗎?

你們想要的 Fiber 是這樣的:

在這裡插入圖片描述

實際上 PHP 8.1 Fiber 是這樣的:

在這裡插入圖片描述

動態語言中除了 PHP 之外,Python、Ruby、Lua 在很早就有協程支援了,但實際上這些程式語言在協程並行程式設計方面並沒有多出色。真正將協程技術發揚光大的是 Golang ,為什麼 Golang 在協程程式設計方面的如此成功?這是因為它提供了完整的、成體系的一整套技術方案,從語言設計到編譯器、協程排程器、標準庫、偵錯程式,這才是工業級的技術。在多執行緒技術方向,很多程式語言都有多執行緒支援,但真正被廣泛使用、達到工業級水平的多執行緒系統只有 Java 。在 PHP 中真正能達到工業級水平的技術也就是 Apache+mod_php 和 PHP-FPM 。

協程的技術也是一樣,PHP 開發者想要從傳統的 LAMP/LNMP 短生命週期、序列程式設計的模式轉型到 CSP 協程+通道並行程式設計,目前暫時也只有 Swoole 是相對來說最成熟的方案。使用者真正需要的是一種完整的、系統性、成體系、簡單易用、可靠的一整套技術方案。

PHP 8.1 加入 Fiber 我認為是一個倉促的決定。不如系統性地設計一下,從這些7個方面考慮:

  • EventLoop API
  • 協程(對應 ext-fiber)
  • IO 排程器(Socket/FileSystem/ChildProcess/Signal/Timer/Stdout/Stdin)
  • CPU 排程器
  • 現有同步阻塞 IO 擴充套件(redis、curl、php_stream、sockets、mysqli、pdo_mysql
    等)和內建函數(sleep、shell_exec、sleep、gethostbyname 等)如何實現支援協程,變成非同步非阻塞模式
  • 協程通訊(channel)
  • 伺服器:實現 PHP-FPM 協程版,或者提供一個新的協程 HttpServer

事件迴圈

EventLoop 是協程實現中最核心的基礎設施,這裡不是指具體實現,C 層面 select/epoll/poll ,PHP 層面 stream_select 或者 libevent/libuv/event 擴充套件都可以實現,如果 ZendVM 底層提供了 EventLoop,那麼不同的框架、不同的庫可以在同一個 Loop 中,協程排程器也可以構建在此之上。如果沒有統一的 EventLoop 的基礎設施,amphp 、 reactphp 等框架都需要各自實現,意味著你在使用 amphp 的程式時,無法使用 reactphp 實現的任何類庫。

Node.js、Golang、Swoole 底層都有一個全域性的 EventLoop,所有 IO 行為都會被註冊到 EventLoop 中,事件觸發後執行 callback 或者排程協程。

阻塞 IO 函數

PHP 提供的很多 IO 操作函數都是阻塞的,如果在協程中發生阻塞,就會導致並行失效。退化成和普通 PHP-FPM 一樣的序列模式。協程實現中必須要考慮到如何解決這個問題。

Amphp 和 ReactPHP 目前(2021年)採用的實現方式,是使用 PHP 程式碼實現基於協程的非同步非阻塞 IO 版本,在 2018 年之前 Swoole 也是採用這個模式。這樣做最大的問題是,

  • 成本太高,無法複用 PHP 生態,重複造輪子,需要重新實現 Redis、MySQL、CURL、Http2、WebSocket、Kafka等大量網路 IO 庫
  • 品質不高,不像同步阻塞的版本經過大規模驗證
  • 相容性差,如果使用者使用了一個第三方庫,其中包含了阻塞 IO 的使用者端呼叫,就前功盡棄了
  • 學習成本高,使用者需要學習一套全新的 API ,這對 PHP 開發者非常不友好
  • PHP 實現的版本可能還會存在效能問題,在 Swoole 中由於是使用 C 實現的不存在這一點

所以 Swoole 在 4.1 版本(2018年)開始採用了全新的實現方式,會 Hook 掉 PHP 擴充套件中的函數指標,通過很少的工作量就徹底解決了這個問題。PHP 開發者直接使用同步阻塞使用者端的 API 即可,底層會自動替換為非阻塞的協程版本。比如下面的程式碼

在這裡插入圖片描述

Swoole 會替換 PHP 內建的 sleep 和 file_get_contents 函數,變成協程版本,上面的程式就變成了完全並行的了,對使用者來說是無感知的。

CPU 排程器

由於 PHP 是動態解釋執行的程式語言,在實現協程 CPU 排程器方面比 Golang 有優勢。Golang 需要在編譯器內做很多工作,控制單個協程佔用的 CPU 時間,避免幾個協程耗盡 CPU 資源。PHP 可以在 VM 層面直接實現中斷,精準控制每個協程最大可執行的時間。

Golang 使用 GPM 模型解決了這個問題,如果一部分協程持續佔用 CPU ,排程器會建立更多 Thread 執行新的任務,退化為作業系統排程。PHP 由於不支援多執行緒,暫時無法實現 GPM 模型,目前 Swoole 所採作用的 VM 中斷排程實現是最優解

在 Swoole 的實現中,底層建立了一箇中斷執行緒,每 5ms 會產生一箇中斷訊號,在中斷函數中判斷當前協程執行的時長,如果超過了規定的 10ms 最大執行時間,會自動讓出 CPU 切換至其他可執行的協程。

以上程式會交替執行,每個協程最大執行時間不超過 10ms

我認為正確的方式

建立多個 RFC ,把這些問題討論清楚,在 PHP9 版本中提供完整的協程方案實現。不求做到 Golang 的程度,至少要能達到生產可用。這樣 PHP 才會有大的改變。不過這可能就真要取代 Swoole 了 [哭笑]。

再介紹一下 Swoole 現在做到什麼程度:

  • 完整的協程+通道實現
  • 提供了 CPU 排程器,即使是密集計算的程式,也可以使用協程,排程器會按照10ms時間片切換協程
  • 支援絕大部分 PHP 的常用擴充套件和內建函數,LAMP 時代的程式碼可以不用修改直接 copy
    到協程裡執行,而且是非同步非阻塞的方式,是真正的並行,我認為這才是黑科技,PHP 協程方案的關鍵技術
  • curl 擴充套件也可以協程化,包括 curl 和 curl_multi,guzzle 可以直接用,騰訊雲、阿里雲的 PHP SDK
    可以直接在協程中使用
  • 提供了 PGSQL 協程實現,基於 pgsql 官方 C 庫的非同步 API 實現
  • 提供了 ZooKeeper 協程實現,是基於官方的 C 庫 插入了 Hook 程式碼實現
  • Kafka 協程庫的實現
  • 支援協程的新一代偵錯程式:yasd
  • 支援 PHP7.2-8.0 所有版本,ext-fiber 只支援 8.0 以上版本

PHP 作為一個社群驅動的開源專案,背後沒有商業公司支援,沒有 Golang、Java、Node.js(v8) 這樣充足的研發資源投入,需要依賴全世界各地的貢獻者提交程式碼,在產品化方面還是做的不夠好。國內有一些人一直在 diss Swoole 有商業公司,但正是因為有商業收入,才保證了我們在 Swoole 開源專案研發上的連續性,在產品化方面也會做的更好。

我對 Fiber 的擔憂

Fiber 整合到 PHP 中之後,會有很多 PHP 的框架或者類庫建立自己的協程方案,由於 PHP 只提供了 Coroutine Context 的實現,其他幾個方面並沒有提供,在 PHP 生態中將會出現很多流派的 EventLoop、AsyncIO 、NetworkClient 多種多樣的實現。就拿 sleep 函數來說,現在 Amphp 和 ReactPHP 分別叫做 amp\delay 和 react\sleep 。

沒有統一的標準,意味著社群的高度分裂。一旦確定了方向,技術的發展演進是非常快的,多樣性是一件好事,但也會帶來更多新問題,再想統一是很困難的一件事情。即便是 Symfony、Laravel 這樣處於頂端的 PHP 框架,也不具備能夠 100% 覆蓋整個 PHP 生態的能力,這將走向失控。

基於 Swoole/Swow 的方案,實際上依舊是 PHP 原先的生態,大家使用的依然是最熟悉的那些 PHP 函數和庫,非同步程式設計和同步阻塞 IO 程式設計的生態是一致的。比如在 Swoole 協程中可以直接使用阿里雲、騰訊雲、AWS 提供的 SDK,Fiber 生態下情況就會比較複雜。

Swow 是一個從 Swoole 專案中剝離與協程無關特性,使用 Swoole 協程設計方案的全新實現,與 php-src 保持一致使用了 C 語言實現,目前正在準備 RFC 提案,貢獻到 PHP 核心中

Swoole 與 Fiber 的差別

Fiber 只是協程 Context 管理的一種實現,更像是 Generator 的升級版
Swoole 是完整的協程 Runtime & Framework,更像是 Golang

Swoole 是否會使用 ext-fiber ?

暫時不會。有兩方面的原因:1. Swoole 的實現是雙層協程設計,底層是 C 協程,上層是 PHP 協程。而 Fiber 的實現耦合在一起的。Swoole 是核心協程化設計,在 core 層面對協程操作進行了封裝,外層只需要呼叫 API 即可,不需要關心發生阻塞 IO 時協程如何切換,PHP 層實際上只是 wrapper ,2. Fiber 是以擴充套件方式加入 PHP 核心的,並不是 ZendAPI,地位等同於 curl/mysql 等擴充套件庫。在 PHP 中擴充套件依賴管理做的很糟糕。處理不好容易出現找不到 符號(symbol not found),而 Swoole 同樣也是 PHP 的一個擴充套件,它與 ext-fiber 是平級關係,協程是 Swoole 的核心部分,不太好依賴另外一個庫的實現。

當然 Swoole 會對齊 ext-fiber 在 PHP 協程切換部分的程式碼,保證一致性。由於 PHP 暫時還未提供 EventLoop 的基礎設施,Swoole 擴充套件提供的功能和其他基於 ext-fiber 擴充套件實現的 PHP 協程類庫,不在同一個 Loop 中,也無法實現共存。

建立 C 協程
圖

建立 PHP 協程
圖

在 PHP 程式碼中建立協程
圖

我們也會持續關注 ext-fiber ,在未來某個節點,ext-fiber 足夠成熟穩定,並且遷移到 ZendAPI 中時,Swoole 也會考慮使用 ext-fiber

點關注,不迷路

好了各位,以上就是這篇文章的全部內容了,能看到這裡的人呀,都是人才。之前說過,PHP方面的技術點很多,也是因為太多了,實在是寫不過來,寫過來了大家也不會看的太多,所以我這裡把它整理成了PDF和檔案,如果有需要的可以點這裡進階PHP月薪30k>>>架構師成長路線【視訊、面試檔案免費獲取】在這裡插入圖片描述
在這裡插入圖片描述