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

oschina發表於2016-03-16

介紹

我們成功的把我們的應用遷移到了php7上面(數百臺機器的叢集),而且執行的很好,據說我們是第二個把如此規模的應用切換到php7的企業,在切換的過程我們發現了一些php7位元組碼快取的bug,慶幸的是這些bug現在已經被修復了,現在我們把這個激動人心的訊息分享給所有的php社群:php7現在已經可以穩定的執行在商用環境上,而且比以前更加節省記憶體,效能也有的很大的提高。

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

下面我會詳細的介紹下我們是如何把應用前移動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引用測試框架(當然,也是開源的),並且在整個過程中節省了上百萬美元。

HHVM的試驗

在切換到PHP7之前,我們曾花了不少時間來尋找優化後端的方法。當然,第一步就是從HHVM下手。在試驗了幾周之後,我們獲得了值得關注的結果:在給框架中的JIT熱身之後,我們看到速度與CPU使用率上升了三倍。

另一方面,HHVM 被證實有一些嚴重的缺點:

  • 部署困難而且慢。在部署過程中,你不得不首先啟動JIT-cache。當機器啟動的時候,它不能負載產品流量,因為所有的事情進行的相當慢。HHVM 團隊同樣不推薦啟動並行請求。順便一提,大量聚類操作在啟動階段並不快速。此外,對於幾百個機器構成的大叢集你必須學習如何分批部署。這樣體系結構和部署過程相當繁瑣,而且很難估算出所需要的時間。對於我們來說,部署應該儘可能簡單快捷。我們的開發者將在同一天提供兩個開發版並且釋出許多補丁。
  • 測試不便。我們非常依賴runkit擴充套件,但是它在HHVM中卻不可用。稍後我們將詳細介紹runkit,但是無需多言,它是一個能讓你幾乎隨心所欲更改變數、類、方法、函式行為的擴充套件。這是通過一個抵達PHP核心的整合來實現的。HHVM引擎僅僅顯示了略微相像的PHP外觀,但是他們各自的核心十分不同。鑑 於擴充套件的特定功能,在HHVM上獨立地實現runkit異常困難,而且我們不得不重寫數萬測試用例以確保HHVM和我們的程式碼正確的工作。這看起來似乎不 值得。公平的說,我們以後在處理所有其他選項時也會遇到同樣的問題,而且我們在遷移到PHP7時仍然要重做許多事情包括擺脫runkit。但是以後會更多。
  • 相容性。主要問題是不完全相容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!框架
以下功能在我們的程式碼裡已經測試過:

  1. override類方法
  2. 覆蓋函式執行結果
  3. 更改全域性常量或類常量的值
  4. 類新增方法

所有這些東西都是用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 資料中心的移動應用後端的使用。

響應時間分佈:

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

RUsage (CPU 時間):

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

記憶體使用:

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

CPU 載入 (%)-移動後臺叢集

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

這一切到位,處理時間減少了一半,從而提高整體響應時間約40%,由於一定量的請求處理時間是花在與資料庫和守護程式通訊。從邏輯上講,我們不希望這部分加快切換到php7。除此之外,由於超執行緒技術,叢集的整體負載下降到50%以下,進一步促進了令人印象深刻的結果。廣義而言,當負載增加超過50%,HT-engines,而不是作為有用的物理引擎開始工作。但這已經是另一篇文章的主題。此外,記憶的使用,這從來沒有一個瓶頸,我們,減少了大約八倍以上!最後,我們節省了機器的數量。換句話說,伺服器的數量可以承受更大的負載,從而降低獲取和維修裝置的費用。在剩餘的聚類結果相似,除雲上的收益是一個更溫和的(大約40%個CPU),由於opcache操作的減少。

來算算我們能節省多少費用呢?大致測算一下,一個Badoo應用伺服器叢集大概包含600多臺伺服器。如果cpu使用率減半,我們可以節省大約300臺伺服器。考慮伺服器的硬體成本和折舊,每臺大約4000美元。總的算下來我們能節省大約100萬美元,另加每年10萬的主機託管費。而且這還沒有計算對服務雲效能的提升帶來的價值,這個結果很令人振奮。

另外,您是否也考慮切換到PHP 7.0版本呢? 我們很希望聽聽您關於此問題的觀點,而且非常願意在下面的評論中回答您的疑問。

Badoo 團隊

相關文章