之前做過的一次最佳化實踐,最近翻出來看看,有些通用的最佳化手段還是可以複用的。系統跑得時間長了,總會出現這樣那樣的問題和瓶頸,有了問題不可怕,我們有“打虎”的傢伙事兒--無非就是定位問題->分析問題->提出解決方案->實踐->結果反饋->總結再最佳化。
問題描述:系統採用 PHP5 + Zend framework 開發,在資料規模和訪問量增加後(千萬級),出現了後台apache伺服器負載過高的現象,在訪問高峰時段(比如每天下班到晚上10點這一段時間,特別是周五),機器CPU負載會飆升到170多,CPU負載過高造成處理請求也相應的變慢,所以亟需解決這個問題。
問題分析:通過連續幾天的觀察和分析,當CPU使用率達到100%時,其中系統CPU使用率佔據了很大的比例,使用者CPU使用率倒不是很高,另外前端 haproxy 和 squid cache的cpu負載很低,memcached和squid的hit ratio一般都能達到60%左右。
分析backend的access-log,發現相當大一部分請求的User-Agent是搜尋爬蟲;
同時,在 apache 上配置了xdebug,在空閑時段對主要的頁面的測量了一組效能資料,通過使用kcachegrind對測得的資料進行分析(如何配置xdebug,可以用soso搜一下),發現:
效能資料不夠穩定,同樣的請求之間測試資料會相差比較大
慢的點比較分散
memcached的訪問大部分的情況都比較慢(100ms以上)
解決方案通過上述初步的分析,對現有的程式逐步作了一系列調整。
首先考慮到的是是否可以想辦法增加前端squid cache的Hit ratio,從而減少穿透squid到達後端apache的請求數。
考慮到相當一部分請求來源於Crawler,而之前squid cache只會對設定了language cookie的請求作cache,而來自Crawler的請求都沒有cookie資訊。於是想到把來自Crawler的請求都預設為language為zh_CN的,然後修改haproxy的配置,把User-Agent為常見的Crawler的請求都轉交給squid cache.
修改php代碼,把一些頁面的緩衝時間設定得更長一些
經過如上兩個步驟,到達apache的請求確實減少了一些,但是這個對cpu負載過高的問題協助很少,於是另尋它法。
其次,根據使用 xdebug profiling的結果來看,和memcached的互動耗時比較長,於是想是否可以想辦法讓memcached能更快地響應請求,從而使得每一次請求能更快完成,從而使並發降低。
通過程式碼分析,發現線上memcached使用的是poll(),而memcached的串連數在繁忙的時候保持在1000左右,memcached的CPU使用率在 30% 左右。很顯然,poll()方式在處理如此多的並發串連時是很低效的。於是重新編譯memcached,使其使用epoll()的方式來處理請求,替換為epoll之後,memcached的cpu usage從 30%左右降低到 3% 左右,10倍之多!
另外,memcached的hit ratio不是特別高,而且被換出的item數也比較高,於是想到對cache的內容作partition.原本打算做 manually partition,後來發現php的最新的memcache擴充就能支援根據cache的key自動作partition,而且能在不修改程式碼(需要修改設定檔:-))的情況下增加新的memcached執行個體。於是升級每一個apache的php memcache擴充,然後再設定檔中增加了一台新的memcached。到此完成memcached的內容partition。修改之後的效果比較顯著,頁面的載入時間比修改前縮短了很多。
經過這兩步的調整,memcached的效率比以前高了,但是apache的負載仍然居高不下,沒轍,再想其它辦法!
進一步深入分析前面說到主要系統CPU佔用很高,要找原因只能深入核心了:) 從現在開始了我們的strace之旅。套用一句Nike的廣告詞:Just strace it!
在高峰時段對 httpd 進程進行了strace,方法不外乎如下這些
strace -p PID -c 得出 summary
strace -p PID -o output.log 寫入檔案,慢慢研究
strace -p PID -e trace=file 只看 filesystem 操作相關的 syscalls
strace -p PID -elstat64,stat64,open,getcwd 只跟蹤這些 syscalls
…
從上述strace分析得到如下結論:
lstat64,stat64,open等 syscalls實在是多啊
上述 syscalls 佔用時間確實不少! 60%以上的時間都被它們搶了, orz
絕大多數 syscall 是失敗的,真是屢敗屢戰啊
有了上述資料,我們就找到了問題的方向了,那就是找這些毫無意義的系統調用是怎麼來的。
經過分析,這些是php要載入某一個類時,會去 include_path 中定義的一系列目錄中搜尋該類對應的檔案,挨個目錄這麼試過去,直到找到為止。嗯,這種方式顯然是比較低效的,有沒有更好的方式來完成這個事情呢?答案是肯定的,有!而且還有不止一種方法!
調用require_once()時,參數寫絕對路徑(開始Guys write Zend Framework就不懂這個道理;後來才有更新))
使用 __autoload()對class進行 lazy loading,也就是說真正需要的時候才去載入,而不是不管三七二十一把可能用到的類檔案都require_once了。
問題是找到了,但是要解決這個問題還面臨著另一個問題。開發中代碼都注意用絕對路徑了,唯一可以改進的地方是改為 lazy loading,但是 Zend Framework中大量的require_once採用相對路徑,這個就是導致問題——這裡我說的問題是本文我們談論的CPU負載過高的問題——的根本原因。
OK,既然問題找到了,動手解決。寫個指令碼自動產生 Class -> File Path 對應關係,產生代碼中所有類和Zend Framework中所有類的對應關係檔案。把代碼中和Zend Framework庫中所有的 require_once 都注釋掉。然後進行詳細的測試,然後上線。結果令人吃驚,負載降到了 3 以內!!問題解決。
總結:
寫代碼的人都知道,可能出問題的地方總會出問題,任何問題都會有個原因(哪怕暫時沒有找到),從根上解決才是王道,解決什麼問題不重要,希望大家能學習這個解決的思路,善於利用工具。ok,這個case就這樣了。
http://www.bkjia.com/PHPjc/478024.htmlwww.bkjia.comtruehttp://www.bkjia.com/PHPjc/478024.htmlTechArticle之前做過的一次最佳化實踐,最近翻出來看看,有些通用的最佳化手段還是可以複用的。系統跑得時間長了,總會出現這樣那樣的問題和瓶頸,...