背景前端
新浪微博在2016年Q2季度公佈月活躍用戶(MAU)較上年同期增加33%,至2.82億;日活躍用戶(DAU)較上年同期增加36%,至1.26億,總註冊用戶達8億多。PC主站做爲重要的流量入口,承載部分用戶訪問和流量落地,其中咱們提供的部分服務(如:頭條文章)承擔全網全部流量。linux
隨着業務的增加,系統壓力也在不斷的增長。峯值時,服務器Hits達10W+,CPU使用率也達到了80%,遠超報警閾值。另外,當前機房的機架已趨於飽和,遇到突發事件,只能對非核心業務進行下降,挪用這些業務的服務器來進行臨時擴容,這種方案只能算是一種臨時方案,不能知足長久的業務增加需求。再加上一年一度的三節(聖誕、元旦、春節),系統需預留必定的冗餘來應對,因此當前系統面臨的問題很是嚴峻,解決系統壓力的問題也迫在眉急。服務器
面對當前的問題,咱們內部也給出兩套解決方案同步進行。架構
- 方案一:申請新機房,資源統一配置,實現彈性擴容。
- 方案二:對系統進行優化,對性能作進一步提高。
針對方案一,經過搭建與新機房之間的專線與之打通,高峯時,運用內部自研的混合雲DCP平臺,對全部資源進行調度管理,實現了真正意義上的彈性擴容。目前該方案已經在部分業務灰度運行,隨時能對重點業務進行小流量測試。框架
針對方案二,系統層面,以前作過屢次大範圍的優化,好比:ide
- 將Apache升級至Nginx
- 應用框架升級至Yaf
- CPU計算密集型的邏輯擴展化
- 棄用smarty
- 並行化調用
優化效果很是明顯,若是再從系統層面進行優化,性能可提高的空間很是有限。好在業界傳出了兩大福音,分別爲HHVM和PHP7。函數
方案選型性能
在PHP7還未正式發佈時,咱們也研究過HHVM(HipHop Virtual Machine),關於HHVM更多細節,這裏就再也不贅述,可參考官方說明。下面對它提高性能的方式進行一個簡單的介紹。單元測試
默認狀況下,Zend引擎先將PHP源碼編譯爲opcode,而後Zend解析引擎逐條執行。這裏的opcode碼,能夠理解成C語言級的函數。而HHVM提高性能方式爲替代Zend引擎將PHP代碼轉換成中間字節碼(HHVM本身的中間字節碼,一般稱爲中間語言),而後在運行時經過即時(JIT)編譯器將這些字節碼轉換成x64的機器碼,相似於Java的JVM。測試
HHVM爲了達到最佳優化效果,須要將PHP的變量類型固定下來,而不是讓編譯器去猜想。Facebook的工程師們就定義一種Hack寫法,進而來達到編譯器優化的目的,寫法相似以下:
<?hh class point { public float $x, $y; function __construct(float $x, float $y) { $this->x = $x; $this->y = $y; } }
經過前期的調研,若是使用HHVM解析器來優化現有業務代碼,爲了達到最佳的性能提高,必須對代碼進行大量修改。另外,服務部署也比較複雜,有必定的維護成本,綜合評估後,該方案咱們也就再也不考慮。
固然,PHP7的開發進展咱們也一直在關注,經過官方測試數據以及內部本身測試,性能提高很是明顯。
使人興奮的是,在去年年末(2015年12月04日),官方終於正式發佈了PHP7,而且對原生的代碼幾乎能夠作到徹底兼容,性能方面與PHP5比較能提高達一倍左右,和HHVM相比已是不相上下。
不管從優化成本、風險控制,仍是從性能提高上來看,選擇PHP7無疑是咱們的最佳方案。
系統現狀以及升級風險
微博PC主站從2009年8月13日發佈初版開始,前後經歷了6個大的版本,系統架構也隨着需求的變化進行過屢次重大調整。截止目前,系統部分架構以下。
從系統結構層面來看,系統分應用業務層、應用服務層,系統所依賴基礎數據由平臺服務層提供。
從服務部署層面來看,業務主要部署在三大服務集羣,分別爲Home池、Page池以及應用服務池。
爲了提高系統性能,咱們自研了一些PHP擴展,因爲PHP5和PHP7底層差異太大,大部分Zend API接口都進行了調整,全部擴展都須要修改。
因此,將PHP5環境升級至PHP7過程當中,主要面臨以下風險:
- 使用了自研的PHP擴展,目前這些擴展只有PHP5版本,將這些擴展升級至PHP7,風險較大。
- PHP5與PHP7語法在某種程度上,多少仍是存在一些兼容性的問題。因爲涉及主站代碼量龐大,業務邏輯分支複雜,不少測試範圍僅僅經過人工測試是很難觸達的,也將面臨不少未知的風險。
- 軟件新版本的發佈,都會面臨着一些未知的風險和版本缺陷。這些問題,是否能快速獲得解決。
- 涉及服務池和項目較多,基礎組件的升級對業務範圍影響較大,升級期間出現的問題、定位會比較複雜。
對微博這種數億用戶級別的系統的基礎組件進行升級,影響範圍將很是之大,一旦某個環節考慮不周全,頗有可能會出現比較嚴重的責任事故。
PHP7升級實踐
1. 擴展升級
一些經常使用的擴展,在發佈PHP7時,社區已經作了相應升級,如:Memcached、PHPRedis等。另外,微博使用的Yaf、Yar系列擴展,因爲鳥哥(laruence)的支持,很早就全面支持了PHP7。對於這部分擴展,須要詳細的測試以及現網灰度來進行保障。
PHP7中,不少經常使用的API接口都作了改變,例如HashTable API等。對於自研的PHP擴展,須要作升級,好比咱們有個核心擴展,升級涉及到代碼量達1500行左右。
新升級的擴展,剛開始也面臨着各式各樣的問題,咱們主要經過官方給出的建議以及測試流程來保證其穩定可靠。
官方建議
- 在PHP7下編譯你的擴展,編譯錯誤與警告會告訴你絕大部分須要修改的地方。
- 在DEBUG模式下編譯與調試你的擴展,在run-time你能夠經過斷言捕捉一些錯誤。你還能夠看到內存泄露的狀況。
測試流程
- 首先經過擴展所提供的單元測試來保證擴展功能的正確性。
- 其次經過大量的壓力測試來驗證其穩定性。
- 而後再經過業務代碼的自動化測試來保證業務功能的可用性。
- 最後再經過現網流量灰度來確保最終的穩定可靠。
總體升級過程當中,涉及到的修改比較多,如下只簡單列舉出一些參數變動的函數。
(1)addassocstringl參數4個改成了3個。
//PHP5 add_assoc_stringl(parray, key, value, value_len); //PHP7 add_assoc_stringl(parray, key, value);
(2)addnextindex_stringl 參數從3個改成了2個。
//PHP5 add_assoc_stringl(parray, key, value, value_len); //PHP7 add_assoc_stringl(parray, key, value);
(3)RETURN_STRINGL 參數從3個改成了2個。
//PHP5 RETURN_STRINGL(value, length,dup); //PHP7 RETURN_STRINGL(value, length);
(4)變量聲明從堆上分配,改成棧上分配。
//PHP5 zval* sarray_l; ALLOC_INIT_ZVAL(sarray_l); array_init(sarray_l); //PHP7 zval sarray_l; array_init(&sarray_l);
(5)zendhashgetcurrentkey_ex參數從6個改成4個。
//PHP5 ZEND_API int ZEND_FASTCALL zend_hash_get_current_key_ex ( HashTable* ht, char** str_index, uint* str_length, ulong* num_index, zend_bool duplicate, HashPosition* pos); //PHP7 ZEND_API int ZEND_FASTCALL zend_hash_get_current_key_ex( const HashTable *ht, zend_string **str_index, zend_ulong *num_index, HashPosition *pos);
更詳細的說明,可參考官方PHP7擴展遷移文檔:https://wiki.PHP.net/PHPng-upgrading。
2. PHP代碼升級
總體來說,PHP7向前的兼容性正如官方所描述那樣,能作到99%向前兼容,不須要作太多修改,但在總體遷移過程當中,仍是須要作一些兼容處理。
另外,在灰度期間,代碼將同時運行於PHP5.4和PHP7環境,現網灰度前,咱們首先對全部代碼進行了兼容性修改,以便同一套代碼能同時兼容兩套環境,而後再按計劃對相關服務進行現網灰度。
同時,對於PHP7的新特性,升級期間,也強調不容許被使用,不然代碼與低版本環境的兼容性會存在問題。
接下來簡單介紹下升級PHP7代碼過程當中,須要注意的地方。
(1)不少致命錯誤以及可恢復的致命錯誤,都被轉換爲異常來處理,這些異常繼承自Error類,此類實現了 Throwable 接口。對未定義的函數進行調用,PHP5和PHP7環境下,都會出現致命錯誤。
undefine_function();
錯誤提示:
PHP Fatal error: Call to undefined function undefine_function() in /tmp/test.PHP on line 4
在PHP7環境下,這些致命的錯誤被轉換爲異常來處理,能夠經過異常來進行捕獲。
try { undefine_function(); } catch (Throwable $e) { echo $e; }
提示:
Error: Call to undefined function undefine_function() in /tmp/test.PHP:5 Stack trace: #0 {main}
(2)被0除,PHP 7 以前,被0除會致使一條 E_WARNING 並返回 false 。一個數字運算返回一個布爾值是沒有意義的,PHP 7 會返回以下的 float 值之一。
- +INF
- -INF
- NAN
以下:
var_dump(42/0); // float(INF) + E_WARNING var_dump(-42/0); // float(-INF) + E_WARNING var_dump(0/0); // float(NAN) + E_WARNING
當使用取模運算符( % )的時候,PHP7會拋出一個 DivisionByZeroError 異常,PHP7以前,則拋出的是警告。
echo 42 % 0;
PHP5輸出:
PHP Warning: Division by zero in /tmp/test.PHP on line 4
PHP7輸出:
PHP Fatal error: Uncaught DivisionByZeroError: Modulo by zero in /tmp/test.PHP:4 Stack trace: # 0 {main} thrown in /tmp/test.PHP on line 4
PHP7環境下,能夠捕獲該異常:
try { echo 42 % 0; } catch (DivisionByZeroError $e) { echo $e->getMessage(); }
輸出:
Modulo by zero
(3)pregreplace() 函數再也不支持 "\e" (PREGREPLACEEVAL). 使用 pregreplace_callback() 替代。
$content = preg_replace("/#([^#]+)#/ies", "strip_tags('#\\1#')", $content);
PHP7:
$content = preg_replace_callback("/#([^#]+)#/is", "self::strip_str_tags", $content); public static function strip_str_tags($matches){ return "#".strip_tags($matches[1]).'#'; }
(4)以靜態方式調用非靜態方法。
class foo { function bar() { echo 'I am not static!'; } } foo::bar();
以上代碼PHP7會輸出:
PHP Deprecated: Non-static method foo::bar() should not be called statically in /tmp/test.PHP on line 10 I am not static!
(5)E_STRICT 警告級別變動。
原有的 ESTRICT 警告都被遷移到其餘級別。 ESTRICT 常量會被保留,因此調用 errorreporting(EALL|E_STRICT) 不會引起錯誤。
關於代碼兼容PHP7,基本上是對代碼的規範要求更嚴謹。之前寫的不規範的地方,解析引擎只是輸出NOTICE或者WARNING進行提示,不影響對代碼上下文的執行,而到了PHP7,頗有可能會直接拋出異常,中斷上下文的執行。
如:對0取模運行時,PHP7以前,解析引擎只拋出警告進行提示,但到了PHP7則會拋出一個DivisionByZeroError異常,會中斷整個流程的執行。
對於警告級別的變動,在升級灰度期間,必定要關注相關NOTICE或WARNING報錯。PHP7以前的一個NOTICE或者WARNING到了PHP7,一些報警級變成致命錯誤或者拋出異常,一旦沒有對相關代碼進行優化處理,邏輯被觸發,業務系統很容易由於拋出的異常沒處理而致使系統掛掉。
以上只列舉了PHP7部分新特性,也是咱們在遷移代碼時重點關注的一些點,更多細節可參考官方文檔http://PHP.net/manual/zh/migration70.PHP。
3. 研發流程變動
一個需求的開發到上線,首先咱們會經過統一的開發環境來完成功能開發,其次通過內網測試、仿真測試,這兩個環境測試經過後基本保證了數據邏輯與功能方面沒有問題。而後合併至主幹分支,並將代碼部署至預發環境,再通過一輪簡單迴歸,確保合併代碼沒有問題。最後將代碼發佈至生產環境。
爲了確保新編寫的代碼能在兩套環境(未灰度的PHP5.4環境以及灰度中的PHP7環境)中正常運行,代碼在上線前,也須要在兩套環境中分別進行測試,以達到徹底兼容。
因此,在灰度期間,對每一個環節的運行環境除了現有的PHP5.4環境外,咱們還分別提供了一套PHP7環境,每一個階段的測試中,兩套環境都須要進行驗證。
4. 灰度方案
以前有過簡單的介紹,系統部署在三大服務池,分別爲Home池、Page池以及應用服務池。
在準備好安裝包後,先是在每一個服務池分別部署了一臺前端機來灰度。運行一段時間後,期間經過錯誤日誌發現了很多問題,也有用戶投訴過來的問題,在問題都基本解決的狀況下,逐漸將各服務池的機器池增長至多臺。
通過前期的灰度測試,主要的問題獲得基本解決。接下是對應用服務池進行灰度,陸續又發現了很多問題。先後大概經歷了一個月左右,完成了應用服務池的升級。而後再分別對Home池以及Page池進行灰度,通過漫長灰度,最終完成了PC主站全網PHP7的升級。
雖然不少問題基本上在測試或者灰度期間獲得瞭解決,但依然有些問題是全量上線後一段時間才暴露出來,業務流程太多,不少邏輯須要必定條件才能被觸發。爲此BUG都要第一時間同步給PHP7升級項目組,對於升級PHP引發的問題,要求必須第一時間解決。
5. 優化方案
(1)啓用Zend Opcache,啓用Opcache很是簡單, 在PHP.ini配置文件中加入:
zend_extension=opcache.so opcache.enable=1 opcache.enable_cli=1"
(2)使用GCC4.8以上的編譯器來編譯安裝包,只有GCC4.8以上編譯出的PHP纔會開啓Global Register for opline and execute_data支持。
(3)開啓HugePage支持,首先在系統中開啓HugePages, 而後開啓Opcache的hugecodepages。
關於HugePage
操做系統默認的內存是以4KB分頁的,而虛擬地址和內存地址須要轉換, 而這個轉換要查表,CPU爲了加速這個查表過程會內建TLB(Translation Lookaside Buffer)。 顯然,若是虛擬頁越小,表裏的條目數也就越多,而TLB大小是有限的,條目數越多TLB的Cache Miss也就會越高, 因此若是咱們能啓用大內存頁就能間接下降這個TLB Cache Miss。
PHP7與HugePage
PHP7開啓HugePage支持後,會把自身的text段, 以及內存分配中的huge都採用大內存頁來保存, 減小TLB miss, 從而提升性能。相關實現可參考Opcache實現中的accel_move_code_to_huge_pages()函數。
# 開啓方法
以CentOS 6.5爲例, 經過命令:
sudo sysctl vm.nr_hugepages=128
分配128個預留的大頁內存。
$ cat /proc/meminfo | grep Huge AnonHugePages: 444416 kB HugePages_Total: 128 HugePages_Free: 128 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB
而後在PHP.ini中加入
opcache.huge_code_pages=1
6. 關於負載太高,系統CPU使用佔比太高的問題
當咱們升級完第一個服務池時,感受整個升級過程仍是比較順利,當灰度Page池,低峯時一切正常,但到了流量高峯,系統CPU佔用很是高,如圖:
系統CPU的使用遠超用戶程序CPU的使用,正常狀況下,系統CPU與用戶程序CPU佔比應該在1/3左右。但咱們的實際狀況則是,系統CPU是用戶CPU的2~3倍,很不正常。
對比了一下兩個服務池的流量,發現Page池的流量正常比Home池高很多,在升級Home池時,沒發現該問題,主要緣由是流量沒有達到必定級別,因此未觸發該問題。當單機流量超過必定閾值,系統CPU的使用會出現一個直線的上升,此時系統性能會嚴重降低。
這個問題其實困擾了咱們有一段時間,經過各類搜索資料,均未發現任何升級PHP7會引發系統CPU太高的線索。但咱們發現了另一個比較重要的線索,不少軟件官方文檔裏很是明確的提出了能夠經過關閉Transparent HugePages(透明大頁)來解決系統負載太高的問題。後來咱們也嘗試對其進行了關閉,通過幾天的觀察,該問題獲得解決,如圖:
什麼是Transparent HugePages(透明大頁)
簡單的講,對於內存佔用較大的程序,能夠經過開啓HugePage來提高系統性能。但這裏會有個要求,就是在編寫程序時,代碼裏須要顯示的對HugePage進行支持。
而紅帽企業版Linux爲了減小程序開發的複雜性,並對HugePage進行支持,部署了Transparent HugePages。Transparent HugePages是一個使管理Huge Pages自動化的抽象層,實現方案爲操做系統後臺有一個叫作khugepaged的進程,它會一直掃描全部進程佔用的內存,在可能的狀況下會把4kPage交換爲Huge Pages。
爲何Transparent HugePages(透明大頁)對系統的性能會產生影響
在khugepaged進行掃描進程佔用內存,並將4kPage交換爲Huge Pages的這個過程當中,對於操做的內存的各類分配活動都須要各類內存鎖,直接影響程序的內存訪問性能。而且,這個過程對於應用是透明的,在應用層面不可控制,對於專門爲4k page優化的程序來講,可能會形成隨機的性能降低現象。
怎麼關閉Transparent HugePages(透明大頁)
(1)查看是否啓用透明大頁。
[root@venus153 ~]# cat /sys/kernel/mm/transparent_hugepage/enabled [always] madvise never
使用命令查看時,若是輸出結果爲[always]表示透明大頁啓用了,[never]表示透明大頁禁用。
(2)關閉透明大頁。
echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defrag
(3)啓用透明大頁。
echo always > /sys/kernel/mm/transparent_hugepage/enabled echo always > /sys/kernel/mm/transparent_hugepage/defrag
(4)設置開機關閉。
修改/etc/rc.local文件,添加以下行:
if test -f /sys/kernel/mm/redhat_transparent_hugepage/enabled; then echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defrag fi
升級效果
因爲主站的業務比較複雜,項目較多,涉及服務池達多個,每一個服務池所承擔業務與流量也不同,因此咱們在對不一樣的服務池進行灰度升級,遇到的問題也不盡相同,致使總體升級先後達半年之久。慶幸的是,遇到的問題,最終都被解決掉了。最讓人興奮的是升級效果很是好,基本與官方一致,也爲公司節省了很多成本。
如下簡單地給你們展現下此次PHP7升級的成果。
(1)PHP5與PHP7環境下,分別對咱們的某個核心接口進行壓測(壓測數據由QA團隊提供),相關數據以下:
一樣接口,分別在兩個不現的環境中進行測試,平均TPS從95提高到220,提高達130%。
(2)升級先後,單機CPU使用率對好比下。
升級先後,1小時流量狀況變化:
升級先後,1小時CPU使用率變化:
升級先後,在流量變化不大的狀況下,CPU使用率從45%降至25%,CPU使用率下降44.44%。
(3)某服務集羣升級先後,同一時間段1小時CPU使用對好比下。
PHP5環境下,集羣近1小時CPU使用變化:
PHP7環境下,集羣近1小時CPU使用變化:
升級先後,CPU變化對比:
升級先後,同一時段,集羣CPU平均使用率從51.6%下降至22.9%,使用率下降56.88%。
以上只簡單從三個維度列舉了一些數據。爲了讓升級效果更加客觀,咱們實際的評估維度更多,如內存使用、接口響應時間佔比等。最終綜合得出的結論爲,經過本次升級,PC主站總體性能提高在48.82%,效果很是好。團隊今年的職能KPI就算是提早完成了。
總結
總體升級從準備到最終PC主站全網升級完成,時間跨度達半年之久,不管是擴展編寫、準備安裝腳本、PHP代碼升級仍是全網灰度,期間一直會出現各式各樣的問題。最終在團隊的共同努力下,這些問題都完全獲得瞭解決。
一直以來,對社區的付出深懷敬畏之心,也是由於他們對PHP語言性能極限的追求,才能讓你們的業務坐享數倍性能的提高。同時,也讓咱們更加相信,PHP必定會是一門愈來愈好的語言。
免費提供最新Linux技術教程書籍,爲開源技術愛好者努力作得更多更好:http://www.linuxprobe.com/