三次性能優化經歷
來自: http://www.raychase.net/3658
最近在做一些性能優化工作,回想起工作這些年來,參與過的三次集中性能優化,每次都得折騰少則一個月,多則半年。這些內容既是不同視角、不同思路的比較,也是挺有趣的工作經歷。
Portal的性能優化
這已經是大概五年前了,搞了接近半年的Portal性能優化,后來某些內容總結在這篇文章里面。既然是Portal,性能優化上就有它的特點。比如說:
Portal的性能優化需要從前端和后端兩個角度去思考問題,先考慮客戶端和服務端之間的交互模型,然后再在客戶端和服務端單獨考慮分而治之。這個其實和設計的思路是一樣的,交互問題需要首先考慮,定義好交互的報文形式(比如某JSON的具體形式)以后,包括用戶觸發什么行為引發什么樣的數據訪問,這些需要首先明確,這樣才能對大概的請求模型了然于心。最怕的是那些請求亂七八糟的Portal要做優化,因為業務復雜,然后接口還沒有統一,有的地方返回頁面片段,有的地方返回一個大頁面,有的地方返回一堆腳本,有的地方用JSONP,有的地方有返回純數據格式。互相之間還有許多重復,這種亂七八糟的客戶端和服務端之間的交互,簡直就沒有設計,不同人開發就不同樣,做起優化來簡直就是噩夢。
在思考view這一層的時候,首先要給它分區,如果是簡單的頁面,就要給它分類、分塊。目的只有一個,抽象出動態變化的部分和靜態渲染的部分。有的前端本身解耦做得比較好的,數據和模板已經拆分得很清楚的話,模板是靜態的,數據就要分析,哪些是動態的,哪些是相對靜態的,可容許的不一致時間有多長。動態靜態的劃分主要是為了view層面的緩存。對于Portal來說,緩存是非常討巧的,頁面組件劃分做得好的,性能優化才有余地。
前端的性能優化大家都會記得這流傳甚廣的 幾十條“rules” ,而后端就沒有這樣的統一原則可以拿來指導了。前面講了緩存,但是緩存的引入也會引發一系列的問題。最常見的是關于數據過期造成的不一致性問題,但是其實還有一些其他的問題。比如說,如果某些原因導致緩存全部失效(例如掉電、服務重啟),這個時候壓力一下子全部落到后端部件上去了(比如數據庫上),那么實際的性能測試要保證這種情況下依然可以保證正常服務的提供,流控正常,網站不要掛掉。
Service的性能優化
后來工作需要維護過一個幾十臺機器集群的service,接受get請求,從數據庫里面查數據以某種xml形式返回給用戶。這種請求處理的并發數、TPS和latency都非常關鍵,給數據庫壓力很大,而相對地,計算邏輯就比較簡單。
當時想了幾種優化的辦法。
第一種是中心化的緩存,使用Memcached,主要是考慮總的請求重復率可能在50%左右,但是如果在單臺機器上做緩存,這個cache hit比率是非常低的,但是使用一臺中心緩存服務器可以提高緩存命中率。但是另一方面,如果一旦引入中心緩存,又會帶來很多新問題,比如這樣的緩存讀寫開銷就不能忽略了,對于緩存沒有命中的case,性能反而是下降的;比如一旦這個中心緩存服務器掛掉怎么辦,一定要設置非常短的cache訪問超時機制;還有,寫緩存的操作,完全做成NoReply形式的,像發送UDP報文一樣,只管請求,不管結果,從而盡量保證總的latency。我把Memcached調優的一些總結放在了這里。
第二種思路是把計算部分放到客戶端去,讓service變得很薄,以期望減小CPU的使用。不過這一點上效果也一般,主要是因為瓶頸畢竟主要還是在數據庫查詢上面。基于這一點,后來還出現了一種思路,就是異步算好這些用戶可能需要的結果,等到需要的時候直接來取就好。
最后,為了減小對關系數據庫的壓力,增加擴展性,把數據源挪到了DynamoDB這個NoSQL數據庫上面。
Spark的性能優化
近期則是做了一些Spark性能優化的工作。其實性能優化的核心問題是不變的,考慮的最主要的幾個因素也是不變的,無非CPU、內存、網絡、鎖(本質是并行度)等等。
系統地分析和改進Spark性能問題的時候,大概有這么幾個幾件事:
測試常規數據集在不同不同instance type,不同memory、不同executor number下面的性能表現,開始是單個EMR任務,后來則是整個pipeline,評估性能條件下選擇的理想性價比環境。比如R3.8的內存是要比C3.8多好幾倍,但是價格是1.5倍,這個時候則需要評估如果合理設置executor number,再看性能能達到什么程度,比如如果整個pipeline的計算時間能夠縮短好幾倍,那么R3.8也是不錯的選擇。當然,還有一種思路是OOM的風險(因此通常不會配置一大堆executor,把CPU使用率榨取到窮盡),而OOM在測試中不是必現的,因此不能光看pipeline的時間。另外,在評估執行狀態的時候,指標不要單獨地看。比如有時看到CPU使用率上去了,這其實不一定是計算資源利用率提高的表現,還有可能是內存太小,大量本來安安穩穩在內存里面就可以算完的東西被迫溢出到磁盤上,讀寫一增加CPU使用率自然就上去了。
第二個是需要結合代碼和測試的結果去修正代碼具體代碼寫得不好的地方。例如對于partition的取值,default.parallelism的計算公式,有一些api不合理的使用,還有明明可以并行化的地方,卻沒有做到等等。這里面有一些調優的hints我放在了這篇文章里面。總的來說,在最基本的問題修正以后,可以分析整個pipeline里面最慢的幾個task,做單獨和額外的優化。
第三個則是需要測試異常大的數據量,主要是遇到一些特殊的情形,例如Q4(第四季度)業務量暴漲的時候,這種情形要在測試中覆蓋到。現在剛升級到Spark 1.5的版本,Spark 1.6我還沒有開始使用,但是我知道它是支持動態的executor數量的自動調整(dynamicAllocation.enabled)的,否則給task設置的executor數量還是需要根據input的大小代碼里面計算調整的。
這些內容我的初衷是想寫一些作為系統的或者補充的內容,從而和以前寫的與這三次性能調優單獨記錄的文章想區別,但是目前思路還理得不很清楚。以后隨著思考的深入再慢慢修正。
</div>