RoadRunner:PHP 不是為了死而生的,還是 Golang 來拯救

RoadRunner:PHP 不是為了死而生的,還是 Golang 來拯救

你好,哈布爾! 我們在 Badoo 很活躍 致力於 PHP 效能,因為我們用這種語言有一個相當大的系統,而效能問題就是省錢的問題。 十多年前,我們為此創建了 PHP-FPM,它最初是 PHP 的一組補丁,後來成為官方發行版的一部分。

近年來,PHP 取得了長足的進步:垃圾收集器得到了改進,穩定性水平也得到了提高 - 今天您可以在 PHP 中編寫守護進程和長期腳本而不會出現任何問題。 這使得 Spiral Scout 能夠走得更遠:RoadRunner 與 PHP-FPM 不同,不會在請求之間清理內存,這提供了額外的效能優勢(儘管這種方法使開發過程變得複雜)。 我們目前正在試驗這個工具,但還沒有任何結果可以分享。 為了讓等待他們變得更有趣, 我們正在發布 Spiral Scout 的 RoadRunner 公告的翻譯。

文章中的方法與我們很接近:在解決問題時,我們也最常使用 PHP 和 Go 的組合,獲得兩種語言的好處,而不是放棄一種語言而選擇另一種語言。

享受您的購物之旅!

在過去的十年裡,我們為清單中的公司創建了應用程式 財富500,以及受眾不超過 500 名用戶的企業。 一直以來,我們的工程師主要用PHP開發後端。 但兩年前,有件事不僅對我們產品的性能產生了巨大影響,而且對其可擴展性產生了巨大影響——我們將 Golang (Go) 引入了我們的技術堆疊。

幾乎立刻,我們發現 Go 允許我們建立更大的應用程序,效能提高了 40 倍。 有了它,我們能夠擴展用 PHP 編寫的現有產品,透過結合兩種語言的優點來改進它們。

我們將告訴您 Go 和 PHP 的組合如何幫助解決實際的開發問題,以及它如何變成我們的工具,可以消除與 PHP 垂死模型.

您的日常 PHP 開發環境

在我們討論如何使用 Go 來復興 PHP 垂死的模型之前,讓我們先來看看您的標準 PHP 開發環境。

在大多數情況下,您使用 nginx Web 伺服器和 PHP-FPM 伺服器的組合來執行應用程式。 第一個服務靜態檔案並將特定請求重定向到 PHP-FPM,PHP-FPM 本身執行 PHP 程式碼。 也許您正在使用 Apache 和 mod_php 中不太流行的組合。 但儘管其工作原理略有不同,但原理是相同的。

讓我們來看看 PHP-FPM 如何執行應用程式程式碼。 當請求到達時,PHP-FPM 會初始化 PHP 子程序,並將請求詳細資訊作為其狀態的一部分傳遞(_GET、_POST、_SERVER 等)。

PHP 腳本執行期間狀態無法更改,因此只有一種方法可以獲得一組新的輸入資料:清除進程記憶體並重新初始化。

這種執行模式有許多優點。 你不用太擔心記憶體消耗,所有進程都是完全隔離的,如果其中一個進程死掉了,它會自動重新創建,不會影響其餘進程。 但這種方法在嘗試擴展應用程式時也存在一些缺點。

常規 PHP 環境的缺點與低效率

如果您從事 PHP 專業開發,那麼您知道從哪裡開始新專案 - 透過選擇框架。 它由依賴注入庫、ORM、翻譯和模板組成。 當然,所有使用者輸入都可以方便地放入一個物件(Symfony/HttpFoundation 或 PSR-7)中。 框架很酷!

但凡事都有它的代價。 在任何企業級框架中,要處理簡單的使用者請求或存取資料庫,您將必須載入至少數十個文件,建立大量類別並解析多個配置。 但最糟糕的是,完成每個任務後,您將需要重置所有內容並重新開始:您剛剛啟動的所有程式碼都變得毫無用處,在它的幫助下您將不再處理另一個請求。 把這個告訴任何用其他語言寫的程式設計師,你會看到他臉上的困惑。

PHP 工程師花了數年時間尋找解決這個問題的方法,使用巧妙的延遲加載技術、微框架、優化庫、快取等。但最終,您仍然必須重置整個應用程式並一次又一次地重新開始。 (譯者註:這個問題將隨著 預緊 在 PHP 7.4 中)

PHP 與 Go 能否承受多個請求?

可以編寫持續時間超過幾分鐘(長達數小時或數天)的 PHP 腳本:例如,cron 任務、CSV 解析器、佇列破壞程式。 它們都根據相同的場景工作:檢索任務,執行它,然後等待下一個任務。 程式碼駐留在記憶體中,節省了寶貴的毫秒時間,因為載入框架和應用程式需要許多額外的步驟。

但開發長期存在的腳本並不那麼容易。 任何錯誤都會完全殺死進程,診斷記憶體洩漏會讓你發瘋,而且你不能再使用 F5 偵錯。

隨著 PHP 7 的發布,這種情況得到了改善:出現了可靠的垃圾收集器,處理錯誤變得更加容易,現在可以保護核心擴充免受洩漏。 確實,工程師仍然需要小心記憶體並注意程式碼中的狀態問題(有一種語言我們不必擔心這些事情嗎?)。 然而,在 PHP 7 中,等待我們的驚喜就更少了。

是否有可能採用使用長期 PHP 腳本的模型,使其適應更瑣碎的任務,例如處理 HTTP 請求,從而消除為每個請求從頭開始加載所有內容的需要?

為了解決這個問題,我們首先需要實作一個伺服器應用程序,它可以接受 HTTP 請求並將它們一一轉發給 PHP Worker,而不必每次都殺死它。

我們知道我們可以用純 PHP (PHP-PM) 或使用 C 擴充 (Swoole) 來寫 Web 伺服器。 儘管每種方法都有自己的優點,但這兩種選擇都不適合我們——我們想要更多的東西。 我們需要的不僅僅是一個 Web 伺服器 - 我們希望得到一個解決方案,可以使我們擺脫與 PHP 的「硬啟動」相關的問題,同時可以輕鬆地針對特定應用程式進行調整和擴展。 也就是說,我們需要一個應用程式伺服器。

Go 可以幫忙解決這個問題嗎? 我們知道它可以,因為語言將應用程式編譯成單一二進位; 它是跨平台的; 使用自己的、非常優雅的平行處理模型(並發)和函式庫來處理 HTTP; 最後,我們將可以使用數千個開源程式庫和整合。

結合兩種程式語言的困難

第一步是確定兩個或多個應用程式如何相互通訊。

例如,使用 很棒的圖書館 Alex Palaestras 可以實現 PHP 和 Go 進程之間的記憶體共享(類似於 Apache 中的 mod_php)。 但這個函式庫的一些特性限制了它解決我們問題的用途。

我們決定使用另一種更常見的方法:透過套接字/管道在進程之間建立互動。 這種方法在過去的幾十年中已經證明了其可靠性,並且在作業系統層面得到了很好的優化。

首先,我們創建了一個簡單的二進位協議,用於在進程之間交換資料並處理傳輸錯誤。 從最簡單的形式來看,這種類型的協議類似於 網址 с 固定大小的資料包頭 (在我們的例子中為 17 位元組),其中包含有關資料包類型、資料大小和用於檢查資料完整性的二進位遮罩的資訊。

在 PHP 端我們使用 打包功能,而在 Go 方面 - 一個庫 編碼/二進位.

在我們看來,一個協議是不夠的 - 所以我們添加了調用的能力 直接從 PHP 轉到服務 net/rpc。 這對我們後來的開發幫助很大,因為我們可以輕鬆地將 Go 庫整合到 PHP 應用程式中。 例如,可以在我們的其他開源產品中看到這項工作的結果 戈里奇.

將任務指派給多個 PHP Worker

實現了互動機制後,我們開始思考如何最有效率地將任務轉移到PHP進程。 當任務到來時,應用程式伺服器必須選擇一個空閒的worker來完成它。 如果一個工作進程/進程因錯誤而終止或“死亡”,我們會刪除它並建立一個新的來替換它。 如果工作人員/進程已成功完成,我們會將其返回到可用於執行任務的工作人員池中。

RoadRunner:PHP 不是為了死而生的,還是 Golang 來拯救

為了儲存我們使用的活躍工人池 緩衝通道,為了從池中刪除意外「死亡」的工作人員,我們添加了一種追蹤錯誤和工作人員狀態的機制。

結果,我們收到了一個能夠處理任何以二進位形式提出的請求的工作 PHP 伺服器。

為了讓我們的應用程式能夠充當 Web 伺服器,我們必須選擇可靠的 PHP 標準來表示任何傳入的 HTTP 請求。 在我們的例子中,我們只是 轉換 來自 Go to format 的 net/http 請求 PSR-7因此它與當今大多數可用的 PHP 框架相容。

由於 PSR-7 被認為是不可變的(有些人會說技術上並非如此),因此開發人員必須編寫從根本上不將請求視為全域實體的應用程式。 這非常符合長壽命 PHP 進程的概念。 我們的最終實作(尚未命名)如下所示:

RoadRunner:PHP 不是為了死而生的,還是 Golang 來拯救

介紹 RoadRunner - 高效能PHP應用伺服器

我們的第一個測試任務是 API 後端,它會定期遇到意外的請求突發(比平常頻繁得多)。 儘管 nginx 在大多數情況下已經足夠,但我們經常遇到 502 錯誤,因為我們無法足夠快地平衡系統以滿足預期的負載增加。

為了取代此解決方案,我們於 2018 年初部署了第一個 PHP/Go 應用程式伺服器。 我們立刻就得到了令人難以置信的效果! 我們不僅徹底擺脫了 502 錯誤,而且還能夠將伺服器數量減少三分之二,為工程師和產品經理節省了大量資金和頭痛的事情。

到年中,我們已經完善了我們的解決方案,在 MIT 許可下將其發佈在 GitHub 上,並將其命名為 道路測設,從而強調其令人難以置信的速度和效率。

RoadRunner 如何改進您的開發堆疊

應用 道路測設 允許我們在 Go 端使用中間件 net/http 在請求到達 PHP 之前執行 JWT 驗證,以及在 Prometheus 中處理 WebSocket 和全域狀態聚合。

由於內建的 RPC,您可以為 PHP 打開任何 Go 庫的 API,而無需編寫擴充包裝器。 更重要的是,RoadRunner 可用於部署新的非 HTTP 伺服器。 範例包括在 PHP 中啟動處理程序 AWS Lambda,創建可靠的隊列破壞者,甚至添加 遠程過程調用 到我們的應用程式。

在PHP 和Go 社群的幫助下,我們提高了解決方案的穩定性,在某些測試中將應用程式效能提高了多達40 倍,改進了調試工具,實現了與Symfony 框架的集成,並增加了對HTTPS、HTTP/ 2、插件和PSR-17。

結論

有些人仍然持有過時的觀點,認為 PHP 是一種緩慢、笨重的語言,只適合編寫 WordPress 外掛。 這些人甚至可能會說PHP有一個限制:當應用程式變得足夠大時,你必須選擇更「成熟」的語言並重寫多年來累積的程式碼庫。

對於這一切,我想回答:再想想。 我們相信只有您才能對 PHP 設定任何限制。 你可以花一輩子的時間從一種語言跳到另一種語言,試圖找到最適合你需求的語言,或者你可以開始將語言視為工具。 像 PHP 這樣的語言的明顯缺點實際上可能是其成功的原因。 如果你將它與另一種語言(例如 Go)結合起來,你可以創造比僅限於一種語言更強大的產品。

在使用 Go 和 PHP 的組合之後,我們可以說我們喜歡它們。 我們不打算犧牲其中之一,而是尋找從雙堆疊中獲得更多價值的方法。

UPD:我們歡迎 RoadRunner 的創作者和原始文章的合著者 - 拉克西斯

來源: www.habr.com

添加評論