效能分析方法論
2018-10-19
有個專案 POC,效能達不到要求。一個小朋友抓了一下火焰圖,感覺 parser 佔的挺高,就吭哧吭哧要優化 parser。這做事方式,沒有講究方法論。
用資料說話,拒絕先入為主
首先沒有分析系統的瓶頸。影響系統整體效能的因素很多,瓶頸有可能在 CPU,當然也有可能是網路,磁碟,甚至排程器,鎖等等等。抓火焰圖看的只是 CPU。CPU 是不是打滿了,瓶頸是不是在 CPU,這是首先要確認的。
其次沒有用資料說話,parse 佔用的 CPU 高,並不能代表 parse 消耗的時間最長,是系統瓶頸。就算 parse 是耗時,那它耗時究竟是多長呢?跟其它步驟相比呢,比如和 optimize,execute 相比呢,這需要量化。做事情吶,一定要拒絕先入為主,要用資料說話,不能夠 "我覺得 parser 佔 CPU,所以要優化它"。
假設系統處理一個任務要經過三個主要步驟,耗時比較分別佔 10% 20% 70%,將第一個步驟優化 30%,那對系統整體的提升也只有 3%。而如果我們找到那個瓶頸的 70%,將它提升 30%,對系統整體的提升卻有 21%。一定要先抓住主要矛盾,先定位瓶頸。
培養資料敏感性
有時候做壓測,就覺得結果不如自己想象中好,總覺得應該哪裡還能有提升,但是又不知道如何定位到瓶頸。這是缺乏資料敏感性,說白了是平時太懶得思考和分析,沒養成好習慣。
假設我們得到的是 8000 QPS,80 併發連線。那麼大腦馬上應該反應,平均每個連線上面,是每秒處理 100 個請求。所以,平均處理一個請求需要 10ms。 如果我們有監控,是不是應該去看看,80%95% 99% 999% 這些時間分別是多少,資料是否能對上。80 跟平均應該是相差不多的。
假設我們看到,80 跟 99 差得特別多,比如 80 在幾百 us,而 99 到了幾十 ms,我們是不是應該立刻思考哪些因素影響響應時間的分佈情況,考慮長尾問題。 會不會統計的請求型別不同,大部分請求型別都很快,而特定幾類很慢,將所以將整體 99 響應時間拖長了?或者拿我們自己的資料庫監控來說,是不是應該馬上看一下,有沒有事務衝突重試,看下鎖的監控是什麼樣子的?
假設我們算出平均處理一個請求需要 10ms,並且我們知道系統各階段會經歷什麼。還是拿我們的系統舉例,一個 SQL 請求進到資料庫以後,要做 parse 生成 AST,然後 optimize 生成執行計劃,接著 execute執行它。另外我們的事務模型需要取一個全域性唯一時鐘 tso。那麼我們就可以算一算,各步驟分別佔用的耗時,加起來跟整個請求處理時間是否吻合。
parse 正常情況下落在 100 us,optimize 也是 幾百 us,execute 對簡單點查就等於走一次網路時間,tso 也是一次網路時間,我們在同資料中心內,一次網路也就 500 us,小於 1ms。 取 tso 跟 parse + optimize 是並行的,parse + optimize 正常小於 tso,制約因素會落在 tso,那麼經過分析,點查的理論處理時間應該在兩次網路請求,2ms。
如果跟理論算的不一樣,就應該看監控定位問題。是不是應該想到tso 的問題。也要修正自己的理論,比如 SQL 特別複雜,那 parse 時間會不會升高嚴重,或者 parse + optimize 超過了 tso 時間成為制約因素?這些都是需要資料敏感性的。怕就怕,沒有資料敏感性,多快叫快?多慢叫慢?沒有分析方法,拿到監控也是大腦一片空白。
資料敏感性一定要建立起來。有一個 jeff dean 的 what are the numbers that every computer engineer should know,網上可以搜到,推薦每個程式員都應該瞭解一下:
Latency Comparison Numbers (~2012) ---------------------------------- L1 cache reference0.5 ns Branch mispredict5ns L2 cache reference7ns14x L1 cache Mutex lock/unlock25ns Main memory reference100ns20x L2 cache, 200x L1 cache Compress 1K bytes with Zippy3,000ns3 us Send 1K bytes over 1 Gbps network10,000ns10 us Read 4K randomly from SSD*150,000ns150 us~1GB/sec SSD Read 1 MB sequentially from memory250,000ns250 us Round trip within same datacenter500,000ns500 us Read 1 MB sequentially from SSD*1,000,000ns1,000 us1 ms~1GB/sec SSD, 4X memory Disk seek10,000,000ns10,000 us10 ms20x datacenter roundtrip Read 1 MB sequentially from disk20,000,000ns20,000 us20 ms80x memory, 20X SSD Send packet CA->Netherlands->CA150,000,000ns150,000 us150 ms
另外,我更推薦自己實驗,自己平時總結資料,自己動手得到的資料,印象更加深刻!平時多積累。比如說當我看到這個ofollow,noindex" target="_blank">repo ,就知道這個程式設計師的資料敏感性肯定挺好,那基本素質絕對不會差。
先假設再求證
觀察到效能問題後,在分析時應該是先假設,再求證的。注意要排除掉外界干擾因素,比如說以前遇到過同機器上部署了其它業務,週期性打滿 CPU的 。
曾經有一次使用過某個 SB 的 bench 工具,可以控制固定流量的負載去觀察系統表現。結果發現不太對。把 top 重新整理時間調短後,觀察 CPU 使用很不穩定。就懷疑 bench 程式有問題。 果然,它是這樣控制固定的 QPS 的:在每一秒的前期瘋狂的製造很高的併發請求,然後就等待直到下一秒。比如生成 1000 QPS 負載,它實際全部落在每秒的幾分之一秒內,完全不均勻。到了系統那邊,就變成了一下暴發流量,一下又空了,效能隨之波動。根本不是在測試固定 QPS 下的效果。
以前聊過 Go 的排程導致的問題,同樣是假設網路問題,假設 runtime 有問題,再一步一步分析,驗證。 最近在 tso 問題上面,又有一個新的發現。某系統每小時週期性的跑一些分析型任務,然後觀察到 tso 的獲取時間會週期性變長。對應機器配置特別高,所以跑週期任務時,負載其實也並不高,不是之前遇到的排程問題。 另外我們觀察到,分析任務跑的時候,記憶體會從平時 300M 以內,上升到 6~7G,記憶體的上漲波動,跟 tso 的時間波動能正好對得上。
好啦,只聊方法論,基於這個觀察,可以做一個假設,記憶體佔用影響到 GC 時間,然後 GC 時間影響到 tso 對應的 goroutine,進而影響了延遲。Go 語言的 GC 的 stop the world 時間是很短的,但是注意到,即使不用 stop the world,掃描某個 goroutine 的時候,還是需要掛起這個 goroutine 的,所以它是 stop the goroutine 的。如果整個系統依賴於這個核心的 goroutine,那系統整體延遲還是受影響的。如果求證怎麼做?可以模擬類似的場景,然後看記憶體使用高和記憶體使用低時,對比 trace 的區別。
個人經驗,在幾個用 Go 語言做系統裡面,我觀察到機器配置較高的時候,開多個 Go 程序比單個大 Go 程序效能好。最後部署都是前面掛 proxy 起多個程序,而不是單臺機器上只部署一個大的程序的。那怎麼分析這個現象呢? 可以假設,排程那一層的損耗並不是隨著負載 O(n) 的事情,當負載 N 越大,其實效能損失並不線性增加。就類似為什麼排序使用二分的時候可以獲得更好的效能。比如說,程式有單點,有全域性的 lock,這些隨著執行緒 CPU 核數增加時,並不會 scalable。另外,在系統排程的這一層,單程序和多程序也不同。多程序相對於單程序,是否會更多的從系統那邊獲取被排程機會。驗證起來並不太好做。
先估算再實現
效能是應該在系統設計階段就估算的。我們公司有一個架構師(前金山快盤之父),做過一次分享,他的方法論我很認同。他有一個特點,不計較一城一池的得失,而強調巨集觀把控。 系統有 CPU,網路,磁碟等等很多資源,他強調的是,如何合理的 把各項資源吃滿,只要這一個點做到位了,系統整體的效能不會差到哪去。
他喜歡把終端視窗切成好多,放到一個螢幕裡,然後同時監控 CPU,網路,磁碟等等。如果一會 CPU 一個波峰其它很低,一會兒又磁碟吃滿了 CPU 空著,這種系統整體的吞吐就不太好。 反之,如果整體曲線是各項相對平滑,那說明這個系統合理地把各項資源都利用上了,這就是一個設計得好的系統。
舉個例子,對一個下載類的任務,從一處下載資料,然後將資料壓縮,最後要將結果存檔。這是典型的分階段任務,不同階段資源瓶頸不同。下載需要網路,壓縮主要是耗 CPU,而存檔需要磁碟 IO。 如果才能達到更好的吞吐呢?假設序列的做,系統就會出現典型的先卡在網路,再卡在 CPU,最後卡在磁碟。如果我們將任務劃分成小塊,然後流水線的做這些事情,就可以把各階段的等待時間消除掉。 提升吞吐,就那幾個關鍵字:並行,批量,流式。但這裡面的門道卻很多,任務切分按多大?切大切小分別有什麼問題。不同機器效能不一致,有特定的就比其它慢怎麼搞?資料亂序到達如何處理,重傳的冪等性。如何確定各級流水線,分別該分配多少執行緒呢?扯遠了。各級流水線生產者消費者之間,用訊息佇列串起來,最終的結果,肯定只有一個訊息佇列塞滿,其它都空著。塞滿的那一個,就對應整個系統的瓶頸所在。
估算,提前要做 benchmark,這個例子裡面,網路傳輸的速度多少,寫盤速度多少,然後各種壓縮演算法的資料處理速度是多少 M/s,這些做到心裡有數。按照他的方法,在設計階段,整體系統大概的吞吐量,就應該能估算出來。 程式碼寫得挫沒關係,不計較一城一池的得失,各個環節,哪一步有不符合預期,再去做細節優化,慢慢調整,最終整個系統的效能目標就是可控的。
最後,我發現在系統性能這一塊,是區分新手老手的一個很好的試金石。因為系統的效能分析的東西,基本是靠經驗積累上來的。看校招,看新人,這些地方就很明顯。