新浪微博PHP7升級實踐

新浪微博PHP7升級實踐新浪微博PHP7升級實踐

背景前端

新浪微博在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更多細節,這裏就再也不贅述,可參考官方說明。下面對它提高性能的方式進行一個簡單的介紹。單元測試

新浪微博PHP7升級實踐新浪微博PHP7升級實踐

默認狀況下,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升級實踐新浪微博PHP7升級實踐

固然,PHP7的開發進展咱們也一直在關注,經過官方測試數據以及內部本身測試,性能提高很是明顯。

使人興奮的是,在去年年末(2015年12月04日),官方終於正式發佈了PHP7,而且對原生的代碼幾乎能夠作到徹底兼容,性能方面與PHP5比較能提高達一倍左右,和HHVM相比已是不相上下。

新浪微博PHP7升級實踐新浪微博PHP7升級實踐

不管從優化成本、風險控制,仍是從性能提高上來看,選擇PHP7無疑是咱們的最佳方案。

系統現狀以及升級風險

微博PC主站從2009年8月13日發佈初版開始,前後經歷了6個大的版本,系統架構也隨着需求的變化進行過屢次重大調整。截止目前,系統部分架構以下。

新浪微博PHP7升級實踐新浪微博PHP7升級實踐

從系統結構層面來看,系統分應用業務層、應用服務層,系統所依賴基礎數據由平臺服務層提供。

從服務部署層面來看,業務主要部署在三大服務集羣,分別爲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你能夠經過斷言捕捉一些錯誤。你還能夠看到內存泄露的狀況。

測試流程

  • 首先經過擴展所提供的單元測試來保證擴展功能的正確性。

新浪微博PHP7升級實踐新浪微博PHP7升級實踐

  • 其次經過大量的壓力測試來驗證其穩定性。
  • 而後再經過業務代碼的自動化測試來保證業務功能的可用性。

新浪微博PHP7升級實踐新浪微博PHP7升級實踐

  • 最後再經過現網流量灰度來確保最終的穩定可靠。

總體升級過程當中,涉及到的修改比較多,如下只簡單列舉出一些參數變動的函數。

(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升級實踐新浪微博PHP7升級實踐

關於代碼兼容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()函數。

新浪微博PHP7升級實踐新浪微博PHP7升級實踐

# 開啓方法
以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佔用很是高,如圖:
新浪微博PHP7升級實踐新浪微博PHP7升級實踐

系統CPU的使用遠超用戶程序CPU的使用,正常狀況下,系統CPU與用戶程序CPU佔比應該在1/3左右。但咱們的實際狀況則是,系統CPU是用戶CPU的2~3倍,很不正常。

對比了一下兩個服務池的流量,發現Page池的流量正常比Home池高很多,在升級Home池時,沒發現該問題,主要緣由是流量沒有達到必定級別,因此未觸發該問題。當單機流量超過必定閾值,系統CPU的使用會出現一個直線的上升,此時系統性能會嚴重降低。

這個問題其實困擾了咱們有一段時間,經過各類搜索資料,均未發現任何升級PHP7會引發系統CPU太高的線索。但咱們發現了另一個比較重要的線索,不少軟件官方文檔裏很是明確的提出了能夠經過關閉Transparent HugePages(透明大頁)來解決系統負載太高的問題。後來咱們也嘗試對其進行了關閉,通過幾天的觀察,該問題獲得解決,如圖:

新浪微博PHP7升級實踐新浪微博PHP7升級實踐

什麼是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團隊提供),相關數據以下:

新浪微博PHP7升級實踐新浪微博PHP7升級實踐

一樣接口,分別在兩個不現的環境中進行測試,平均TPS從95提高到220,提高達130%。

(2)升級先後,單機CPU使用率對好比下。

升級先後,1小時流量狀況變化:

新浪微博PHP7升級實踐新浪微博PHP7升級實踐

升級先後,1小時CPU使用率變化:

新浪微博PHP7升級實踐新浪微博PHP7升級實踐

升級先後,在流量變化不大的狀況下,CPU使用率從45%降至25%,CPU使用率下降44.44%。

(3)某服務集羣升級先後,同一時間段1小時CPU使用對好比下。

PHP5環境下,集羣近1小時CPU使用變化:

新浪微博PHP7升級實踐新浪微博PHP7升級實踐

PHP7環境下,集羣近1小時CPU使用變化:

新浪微博PHP7升級實踐新浪微博PHP7升級實踐

升級先後,CPU變化對比:

新浪微博PHP7升級實踐新浪微博PHP7升級實踐

升級先後,同一時段,集羣CPU平均使用率從51.6%下降至22.9%,使用率下降56.88%。

以上只簡單從三個維度列舉了一些數據。爲了讓升級效果更加客觀,咱們實際的評估維度更多,如內存使用、接口響應時間佔比等。最終綜合得出的結論爲,經過本次升級,PC主站總體性能提高在48.82%,效果很是好。團隊今年的職能KPI就算是提早完成了。

總結

總體升級從準備到最終PC主站全網升級完成,時間跨度達半年之久,不管是擴展編寫、準備安裝腳本、PHP代碼升級仍是全網灰度,期間一直會出現各式各樣的問題。最終在團隊的共同努力下,這些問題都完全獲得瞭解決。

一直以來,對社區的付出深懷敬畏之心,也是由於他們對PHP語言性能極限的追求,才能讓你們的業務坐享數倍性能的提高。同時,也讓咱們更加相信,PHP必定會是一門愈來愈好的語言。

免費提供最新Linux技術教程書籍,爲開源技術愛好者努力作得更多更好:http://www.linuxprobe.com/

相關文章
相關標籤/搜索