Badoo 告訴你切換到 PHP7 節省了 100 萬美圓

How Badoo saved one million dollars switching to PHP7

咱們成功的把咱們的應用遷移到了php7上面(數百臺機器的集羣),並且運行的很好,聽說咱們是第二個把如此規模的應用切換到php7的企業,在切換的過程咱們發現了一些php7字節碼緩存的bug,慶幸的是這些bug如今已經被修復了,如今咱們把這個激動人心的消息分享給全部的php社區:php7如今已經能夠穩定的運行在商用環境上,並且比之前更加節省內存,性能也有的很大的提升。
下面我會詳細的介紹下咱們是如何把應用前移動php7的,咱們在這中間遇到的問題及處理狀況,還有最終的結果。但首先讓咱們回頭看看一些更常見的問題:
Web項目的瓶頸在於數據庫持久化這是一個常見的誤解。一個設計良好的系統應該是平衡的:當訪問量增加時,由系統的各個部分分攤這些壓力,一樣的,當達到系統閥值時,系統的全部組件(不只僅包括硬盤數據庫,還有處理器和網絡)共同分攤壓力。基於這個事實,應用集羣的處理能力才應該是最重要的因素。在不少項目中,這種集羣由數以百計甚至數以千計的服務器組成,這是由於花時間去調整集羣的處理能力更加經濟實益(咱們所以節省一百多萬)。
PHP的Web應用,處理器的消耗跟其餘動態高級語言同樣多。可是PHP開發者面對着一個特別的障礙(這讓他們成爲其餘社區惡意攻擊的的受害者):缺乏JIT,至少沒有一個像C/C++語言那樣的可編譯文本的生成器。PHP社區無力在覈心項目框架上去實現一個相似的解決方案更是樹立了一種不良的風氣:主要的開發成員開始整合他們的解決方案,因此HHVM在Facebook上誕生了,KPHP在VKontakte上誕生,還有其餘相似的方案。幸運地是,在2015年,隨着PHP7的正式發佈,PHP要開始"Grow up"啦。雖然仍是沒有JIT,但很難去評定這些改變在"engine"中有多重要。如今,儘管沒有JIT,PHP7能夠跟HHVM相匹敵( Benchmarks from the LightSpeed blog or PHP devs benchmarks)。新的PHP7體系架構將會讓JIT的實現變得簡單。
在Badoo的平臺開發者已經很是關注近些年出現的每一次問題,包括HHVM試點項目,可是咱們仍是決定等待頗有前途的PHP7的到來。如今咱們啓動了已經基於PHP7的Baboo!這是一個史詩般的項目,擁有300多萬行的PHP代碼,而且經歷了60000次的測試。咱們爲了處理這些挑戰,提出了一個新的PHP引用測試框架(固然,也是開源的),而且在整個過程當中節省了上百萬美圓。php

HHVM的試驗

在切換到PHP7以前,咱們曾花了很多時間來尋找優化後端的方法。固然,第一步就是從HHVM下手。在試驗了幾周以後,咱們得到了值得關注的結果:在給框架中的JIT熱身以後,咱們看到速度與CPU使用率上升了三倍。
另外一方面,HHVM 被證明有一些嚴重的缺點:nginx

  • 部署困難並且慢。在部署過程當中,你不得不首先啓動JIT-cache。當機器啓動的時候,它不能負載產品流量,由於全部的事情進行的至關慢。HHVM
    團隊一樣不推薦啓動並行請求。順便一提,大量聚類操做在啓動階段並不快速。此外,對於幾百個機器構成的大集羣你必須學習如何分批部署。這樣體系結構和部署過程至關繁瑣,並且很難估算出所須要的時間。對於咱們來講,部署應該儘量簡單快捷。咱們的開發者將在同一天提供兩個開發版而且釋出許多補丁。git

  • 測試不便。咱們很是依賴runkit擴展,可是它在HHVM中卻不可用。稍後咱們將詳細介紹runkit,可是無需多言,它是一個能讓你幾乎爲所欲爲更改變量、類、方法、函數行爲的擴展。這是經過一個抵達PHP核心的集成來實現的。HHVM引擎僅僅顯示了略微相像的PHP外觀,可是他們各自的核心十分不一樣。鑑
    於擴展的特定功能,在HHVM上獨立地實現runkit異常困難,並且咱們不得不重寫數萬測試用例以確保HHVM和咱們的代碼正確的工做。這看起來彷佛不程序員

值得。公平的說,咱們之後在處理全部其餘選項時也會遇到一樣的問題,並且咱們在遷移到PHP7時仍然要重作許多事情包括擺脫runkit。可是之後會更多。github

  • 兼容性。主要問題是不徹底兼容PHP5.5(參考此處)
    ,而且不兼容現有的擴展(許多PHP5.5的)。這些全部的不兼容性致使了這個項目的明顯缺點: HHVM正則表達式

不是被大社區開發的,相反只是Facebook的一個分支。在這種狀況下公司很容易不參考社區就修改內部規則和標準,並且大量的代碼包含其中。換句話說,
他們關起門來利用本身的資源解決了問題。所以,爲了解決類似的問題,一個公司須要有Facebook同樣的資源不只投入最初的實現一樣要投入後續支持。這
個提議不只有風險並且可能開銷很大,因此咱們決定拒絕它。數據庫

  • 潛力。儘管Facebook是一個大公司並且擁有無數頂尖程序員,咱們仍然懷疑他們的HHVM開發者比整個PHP社區更強。咱們猜測PHP的相似於HHVM的東西會很快出現,而前者將慢慢淡出咱們的視野。後端

讓咱們耐心等待PHP7。
切換到新版本的PHP7解釋器是一個重要和艱難的過程,咱們準備創建一個精確的計劃。這個計劃包括三個階段:數組

  • 修改PHP構建/部署的基礎設施和爲大量的擴展調整現有的code緩存

  • 改變基礎設施和測試環境

  • 修改PHP應用程序的代碼。

咱們稍後會給出這些這些階段的細節。

引擎和擴展的變化

在Badoo中, 咱們有積極的支持和更新的PHP分支,咱們在PHP7正式版release以前咱們就已經開始切換到php7了. 因此咱們不得不在咱們的代碼樹常常整合(reBase)PHP7上游的代碼,以便它來更新每一個候選發佈版。咱們天天在工做中所用的補丁和自定義的code都須要在兩個版本之間進行移植。
下載和構建依賴庫、擴展程序、還包括PHP 5.5和7.0的構建這些過程都是自動化的完成的。這不只簡化了咱們目前的工做,也預示着將來:在版本7.1出來時, 也許這一切(解析引擎和擴展等等)都已經準備到位了;
如上所述,咱們將注意力轉向擴展。咱們提供超過70種擴展,已經比基於咱們產品改寫的開源產品的半數還要多。
爲了儘快可以切換到它們,咱們已經決定開始同時進展兩件事情。第一個是逐一重寫各個關鍵擴展,包括blitz模板引擎,共享內存/APCu中的數據緩存,pinba數據分析採集器,以及其餘內部服務的自定義擴展(總的來講,咱們已經經過本身的力量完成大概20種擴展的重寫了)。
第二個是積極的清理僅僅在架構中那些非關鍵部分使用的擴展,讓整個架構更加簡潔。咱們已經迅速清理了11種擴展,都是那些無足輕重的!
另外,咱們也同那些維護主要開放擴展的做者,一塊兒積極地討論PHP7的兼容性(特別感謝xdebug的開發者Derick Rethans)。
咱們遲點將進入更詳細的關於移植PHP7擴展的技術細節。
開發者已經對PHP7中的內部API作了大量修改,意味着咱們能夠修改大量的擴展代碼了。
下面是幾個最重要的變動:

  • zval * -> zval。在早期的版本中,zval一直爲新變量來分配內存,可是如今引入了棧。

  • char * ->
    Zend_string。PHP7的引擎使用了更先進的字符串緩存機制。理由是,當字符串與自身的長度同時存儲時,新的引擎能夠將普通字符串完整的轉換爲zend-string格式。

  • 數組API的改變。zend_string做爲key來使用,同時基於雙向鏈表的數組實現方法也被替代爲普通的數組,須要強調的是,數組佔用一個大的文件塊,而不是不少小的空間。

全部這些均可以從根本上減小小型內存分配的數量,結果是,提升PHP引擎2%的速度。
咱們可以注意到,全部這些修改都至少須要改變全部的擴展(即便不是徹底重寫)。雖然咱們能夠依賴內置擴展的做者進行必要的修改,咱們也固然有責任本身修改他們,雖然工做量很大。因爲內部API的修改,使得只修改一些代碼段變得簡單。
不幸的是,引入使代碼執行速度提高的垃圾回收機制讓引擎變得更加複雜而且變得更加難以定位問題。涉及到OpCache的問題。在緩存刷新期間,當可用於別的進程的已緩存的文件字節碼在此時損壞,就會致使崩潰。這就是它從外部看起來的樣子(zend_string):使用方法名或者常量忽然崩潰而且垃圾就會出現。
鑑於咱們使用了大量的內部擴展,其中許多處理都是專門針對字符串的,咱們懷疑這個問題與如何使用字符串在內部擴展有關。咱們寫了大量的測試,並進行了大量的實驗,但沒有獲得咱們預期的結果。最後,咱們從PHP引擎開發人員 Dmitri Stogov 那裏尋求了幫助。
他的第一個問題是「你有沒有清除緩存?」咱們解釋說,事實上,咱們每一次都在清除緩存。在這一點上,咱們意識到這個問題並不在咱們這裏,而是opcache。咱們很快就轉載了這一案例,這有助於咱們在幾天內回覆並解決這個問題。在7.0.4版本,這個修復沒有出來,就不可能使php7進入穩定產品。

更改測試基礎設施

咱們爲咱們在Badoo上作測試感到特別驕傲。咱們部署服務器的PHP代碼到產品環境,天天兩次,每次部署包含20-50份任務量(咱們使用功能分支Git和自動化緊JIRA集成版本)。鑑於這種時間表和任務量,咱們沒有辦法不選擇自動測試。目前,咱們大約有6萬個單元測試,約50%的覆蓋率,其運行在雲上,平均2-3分鐘(參見咱們的文章瞭解更多)。除了單元測試,咱們使用更高級別的自動測試,集成和系統測試,併爲網頁作了Selenium測試,爲手機客戶端作了Calabash測試。做爲一個總體,這使咱們可以迅速達成與結論有關的代碼,每一個具體版本的質量,並應用相應的解決方案。
切換到新版本的解釋器是一個充滿潛在問題的重大變化,因此全部測試工做都是極其重要的。爲了弄清咱們到底作了什麼,以及咱們如何設法作到這一點,讓咱們來看看近幾年測試開發在Badoo上是如何演變的。
一般,當咱們開始考慮實施產品測試(或在某些狀況下,已經開始實施的話)時,在測試過程當中咱們會發現他們的代碼「並無達到測試階段」。出於這個緣由,在大多數狀況下,開發者在寫代碼時要牢記,代碼的可測試性是很重要的。架構師應容許用單元測試去取代調用和外部依賴對象,以便代碼測試能與外部環境相隔離。固然,毫無疑問這是一個備受憎恨的要求,不少程序員認爲寫「可測試性」的代碼是徹底不可接受的。他們認爲,這些限制徹底不顧「優秀代碼」的標準並且一般不會取得成功。你能想象到,大量不按規則編寫的代碼,致使測試爲了等「一個更好的時機」被延遲,或者經過運行小型測試來知足而且在測試結果被推遲,或實驗者爲了使本身運行的小測試可以經過,只作了可以經過的那部分(也就是指測試沒有產生預期的結果)。
我並非說咱們公司是一個例外,從一開始,咱們的項目也未執行測試。由於依然有幾行代碼在生產過程當中正常運做,帶來效益,因此正如文獻中建議的,若是隻是爲了運行測試重寫代碼將是一件愚蠢的事情。那將佔用太長的時間,花費太多。
幸運的是咱們有一個很棒的工具來解決「未測試代碼」的大問題——runkit。當腳本在運行時,這個 PHP 擴展容許你對方法、類及函數進行增、刪、改的操做。此工具還有不少其它的功能但咱們這裏用不到它們。從 2005 年到 2008 年這個工具由 Sara Goleman(就任於 Facebook,有趣的是他在作 HHVM 方向的工做)開發和支持了多年。從 2008 年至今則由 Dmitri Zenovich (帶領 Begun 和 Mail.ru 的測試部門)進行維護。咱們也對這個項目作了些許貢獻。
同時,runkit 是一個很是危險的擴展,它容許你在使用它的腳本在運行的時候對常量、函數及類進行修改。就像是一個容許你在飛行中重建飛機的工具。runkit 有直達 PHP 「心臟」的權力,一個小錯誤或缺陷就能讓一切毀掉,致使 PHP 失敗或者你要用不少時間來查找內存泄漏或作一些底層的調試。儘管如此,這個工具對於咱們的測試仍是必要的:不須要作大的重構來完成項目測試只能在程序運行的時候改變代碼來實現。
可是在切換到PHP7的時候發現runkit帶來了很大麻煩,由於它並不支持新的版本。咱們固然也能夠在新版本中添加支持,可是從長遠考慮,這看起來並非最可靠的解決途徑。所以咱們選擇了其餘方法。
最適合的方法之一就是從runkit遷移到uopz。後者也是PHP的擴展,有着(與runkit)相似的功能性,於2014年正式推出。我在Wamba的同事建議使用uopz,它將有很好的速度體驗。順便說一下uopz的維護者就是Joe Watkins(First Beat Media公司,英國)。不幸的是咱們遷移到uopz的測試程序不管怎樣都沒法成功運行。在某些地方總會發生致命的錯誤,出如今段錯誤中。咱們提交了一些報告,但很遺憾他們並無動做(e.g.https://github.com/krakjoe/uopz/issues/18)。爲了解決這種困境而重寫測試程序的付出將會很是高昂,即便重寫了也很容易再次暴露出問題。
鑑於咱們不得不重寫大量的代碼,並且還要依賴於runkit和uopz這種不知道有沒有問題的項目。很明顯,咱們有告終論:咱們應該重寫咱們的代碼,並且要儘量獨立。咱們也承諾將盡一切可能來避免從此發生相似的問題,即便咱們最終切換到HHVM或任何相似的產品。最終咱們作出來了本身的框架。
咱們的系統名爲「SoftMocks」,「soft」意思是純php實現,未使用擴展。該項目目前是一個開源的php庫。 SoftMocks不跟PHP引擎綁定,它是在運行中動態重寫代碼,功能相似於Go語言的AOP!框架。

如下功能在咱們的代碼裏已經測試過:

  • override類方法

  • 覆蓋函數執行結果

  • 更改全局常量或類常量的值

  • 類新增方法
    全部這些東西都是用runkit實現的。動態修改代碼使項目臨時變動有了可能性。

咱們沒有更多篇幅來討論關於SoftMocks的細節,但咱們計劃寫一篇關於這個主題的文章。 這裏咱們給出一些關鍵點:

  • 經過重寫中間函數來適配原有的用戶代碼。所以全部的包含操做將自動被中間函數重寫。

  • 在每個用戶定義的方法內都增長了是否有重寫的檢查。若是存在重寫,相應的重寫代碼就會被執行。
    原來直接函數調用的方式將被經過中間函數調用的方式所替換;這樣內嵌函數和用戶自定義函數都能被執行到。

  • 對中間函數的動態調用將覆蓋代碼中變量的訪問權限

SoftMocks 能夠和 Nikita Popov's 的 PHP-Parser 配合: 這個庫不是很快(解析速度大概比token_get_all 慢15倍),但他的接口讓你繞過語法解析樹,而且包含了一個方便的API 用來處理不肯定的語法結構。
如今讓咱們回到本文主題:切換到PHP 7.0版本。 當咱們經過SoftMocks把整個項切換過來後,咱們依然有1000多個測試須要手動處理。你能夠說這還不算太差的結果,和咱們在開始時提到的60000個測試相比的話。 和runkit相比,測試速度沒有降低,因此SoftMocks並無性能問題。 爲了公平起見,咱們認爲uopz 明顯的快不少。
儘管PHP7包含了許多新功能,可是仍然存在一些與老版本兼容的問題。首要的解決辦法是閱讀官方的移植文檔,以後咱們會立刻明白若是不去修改現有代碼,咱們將會面對的不只僅是在生產環境中遇到致命的未知錯誤而且因爲升級後代碼的改變,咱們沒法在日誌中查找到任何信息。這將會致使程序沒法正常運行。
Badoo中有許多PHP代碼倉庫,其中最大的有超過2百萬行代碼。此外,咱們還使用PHP實現了不少功能,從網站業務邏輯到手機應用後段再到集成測試和代碼部署。就目前來講,咱們的狀況很複雜,畢竟Badoo有很長的歷史,咱們使用它已經快十年了,最不幸的是仍然有采用PHP4的環境在運行。在Badoo中,咱們不推薦用‘just stare at it long enough’的方式來發現問題。一套所謂的'Brazilian'系統將代碼部署在生產環境,你須要等待直到它發生錯誤,這很容易引起大面積用戶在使用中遇到業務上的錯誤,使其不明緣由。綜上所訴,咱們開始尋找一種方法能自動發現不兼容的地方。
最初,咱們試圖用IDE的,這是開發者中很受歡迎,但不幸的是,他們要麼不支持PHP7的語法和特徵,要麼沒有函數能夠在代碼中找到全部的明顯的危險的地方,發現全部明顯危險的地方。進行了一些研究(如谷歌搜索)後,咱們決定嘗試php7mar工具,它是用PHP實現一個靜態代碼分析儀。這PHP7工具使用起來很是簡單,很快工程,併爲您提供了一個文本文件。固然,它不是萬能的; 找特別是精心隱藏的問題點。儘管如此,該實用程序幫助咱們剷除約 90%的問題,大大加快和簡化了準備 PHP7 的代碼的過程。

對咱們來講,最常遇到的和潛在危險的問題是如下內容:

  • 在func_get_arg()以及func_get_args的行爲變化()。在PHP的第5版本中,這些功能中的傳輸的時刻返回參數值,但在七個版本發生這種狀況的時刻時func_get_args()被調用。換句話說,若是函數內func_get_args前參數變量的變化()被調用,則該代碼的行爲能夠由五個版本不一樣。一樣的事情發生時,應用程序的業務邏輯壞了,但並無什麼在日誌中。

  • 間接訪問對象變量,屬性和方法。並再次,危險在於,該行爲能夠更改「靜默」。對於那些尋找更多的信息,版本間的差別進行了詳細的描述在這裏。

  • 使用保留類名。在PHP7,能夠再也不使用布爾,整型,浮點,字符串,空,真假類名稱。,是的,咱們有一個空的類。它的缺席實際上使事情變得更容易,但由於它經常致使錯誤。

  • 使用引用許多潛在的問題的foreach結構被發現了。因爲咱們試圖早不改變迭代數組中的foreach或雖在其內部指針數,幾乎全部的人都表如今版本5和7相同。

剩餘的不兼容性的狀況下也不多遇到了 (像 'e' 修飾符在正則表達式),或他們固定的一個簡單的替換 (例如,如今全部構造函數應該被命名爲 __construct()。類名稱不容許使用)。
可是,咱們即便在開始修復代碼以前,咱們很擔憂,一些開發商作一些必要的兼容性變化,其餘人會繼續寫不符合 PHP7 的代碼。爲了解決這一問題,咱們把 pre-receive 鉤在已更改的文件 (換句話說,確保語法匹配 PHP7) 上執行 php7-l 在每個 git 存儲庫中。這並不能保證不會有任何兼容性問題,但它不會清除主機問題。在其餘狀況下,開發人員只是不得不變得更加專一。除此以外,咱們開始在 PHP7 上運行的測試整個集並與 PHP5 的結果進行了比較。
此外,開發者不容許使用任何PHP7的新功能,例如,咱們沒有禁止老版本的預接收鉤子 php5 -l。這容許咱們讓代碼兼容PHP5和PHP7。爲何這個很重要?由於除了php代碼的問題以外,還有PHP7極其自身擴展的一些潛在的問題(這些均可以證明)。而且不幸的是,不是全部的問題均可以在測試環境中重現出來;有一些咱們只在產品的大負載時才見過。

實踐出真知

很明顯咱們須要一種簡單快速的方法在任何數量以及類型的服務器上切換php版本。要啓用的話,全部指向CLI-interpreter的代碼路徑都替換成了 /local/php,相應的,是/local/php5或者/local/php7。這樣的話,要在服務器上改變php版本,須要改變連接(爲cli腳本操做設置原子操做是很重要的),中止php5-fpm,而後啓動php7-fpm。在Nginx中,咱們使用不一樣的端口爲php-fpm和啓動php5-fpm,php7-fom設置兩個不一樣的upstream,但咱們不喜歡複雜的nginx配置。
在執行完以上的清單後,咱們接着在預發佈環境運行Selenium 測試,這個階段暴露更多咱們早期沒注意到的問題。這些問題涉及到PHP代碼(好比,咱們再也不使用過時全局變量$HTTP_RAW_POST_DATA,取而代之是 file_get_contents(「php://input」))以及擴展(這裏存在各類不一樣類型的段錯誤)。
修復完早期發現的問題和重寫單元測試(這個過程當中咱們也發現若干隱藏在解析器的BUG好比這裏)後,進入到咱們稱爲「隔離」發佈階段。這個階段咱們在必定數量的服務器上運行新版PHP。一開始咱們在每一個主要PHP集羣(Web後臺,移動APP後臺,雲平臺)上只啓動一個服務,而後在沒有錯誤出現狀況下,一點一點增長服務數量。雲平臺是第一個徹底切換到PHP7的大集羣,由於這個集羣沒有php-fpm需求。 fpm 集羣必須等到咱們找到或者Dmitri Stogov修復了OpCache問題。以後,咱們也會將fpm集羣切換到PHP7。
如今看下結果,簡單的說,他們是很是出色的。在這裏,你能看到響應時間圖,包括內存消耗和咱們的最大的集羣(包括263服務器)的處理器的使用狀況,以及在 Prague 數據中心的移動應用後端的使用。
響應時間分佈:
圖片描述
RUsage (CPU 時間):
圖片描述
內存使用:
圖片描述
CPU 加載 (%)-移動後臺集羣
圖片描述這一切到位,處理時間減小了一半,從而提升總體響應時間約40%,因爲必定量的請求處理時間是花在與數據庫和守護進程通訊。從邏輯上講,咱們不但願這部分加快切換到php7。除此以外,因爲超線程技術,集羣的總體負載降低到50%如下,進一步促進了使人印象深入的結果。廣義而言,當負載增長超過50%,HT-engines,而不是做爲有用的物理引擎開始工做。但這已是另外一篇文章的主題。此外,記憶的使用,這歷來沒有一個瓶頸,咱們,減小了大約八倍以上!最後,咱們節省了機器的數量。換句話說,服務器的數量能夠承受更大的負載,從而下降獲取和維修設備的費用。在剩餘的聚類結果類似,除雲上的收益是一個更溫和的(大約40%個CPU),因爲opcache操做的減小。來算算咱們能節省多少費用呢?大體測算一下,一個Badoo應用服務器集羣大概包含600多臺服務器。若是cpu使用率減半,咱們能夠節省大約300臺服務器。考慮服務器的硬件成本和折舊,每臺大約4000美圓。總的算下來咱們能節省大約100萬美圓,另加每一年10萬的主機託管費。並且這尚未計算對服務雲性能的提高帶來的價值,這個結果很使人振奮。 另外,您是否也考慮切換到PHP 7.0版本呢? 咱們很但願聽聽您關於此問題的觀點,並且很是願意在下面的評論中回答您的疑問。

相關文章
相關標籤/搜索