架構視角的效能最佳化

架構師修行手冊發表於2023-03-27


來源:一個資料人的自留地

作者介紹

@知乎:鄒志全

專注營銷系統提效

自動化營銷精細運營的長期實踐者

做有效編碼

“資料人創作者聯盟”成員。


00

前言


首先我們的系統通常是非常複雜的。無論你的系統是一個單體應用;還是做了n多解耦、分層、拆分的工作,單元邏輯足夠簡單的分散式應用;但是對於一個功能視角來看,仍然非常複雜,反而分散式環境下問題要比單體應用還要複雜一個量級。


本文要說的就是:如何在複雜的系統下進行最佳化,讓我們的硬體投入划得來,讓我們的系統保障可靠的同時無比的絲滑。


效能是一個很籠統的詞兒,很多時候直接效能最佳化三板斧,只是誤打誤撞的在解決問題,我們需要一個完整的方法,對於效能問題進行鑑別、分析、從而解決。本文要探討的就是這部分“方法論”,讓效能最佳化的ROI最大化。

架構視角的效能最佳化


01

什麼叫效能好?


要分析效能問題,首先我們得有一個鑑別標準,比如:tps承載量、qps承載量、延時、平均響應時間、每秒IO數量(IOPS)、負載、飽和度等等,甚至有些公司內會有一些內部約定的指標,不同的場景下可能直接使用的指標是不同的(這樣做是合理的,更簡單且直接的描述效能),但是如果要把效能最佳化的理論吃透,更從容的解決未知問題,需要找到效能描述最本質的東西。


個人認為,對於不同場景進行分類,描述我們系統最恰當的最原始的指標就是吞吐量(對於容量做考量)和響應時間(對於時間做考量),我們要最佳化的就是這兩點。


tps、qps、IOPS這些一定程度上就是在側面描述吞吐,而平均響應時間、延時同樣是吞吐量的重要影響部分,對於大部分場景,我們追求的往往是吞吐量。而某些場景會最大化追求延時降級,而非吞吐量。


看到這裡可能會好奇,吞吐和響應時間不是有對應關係嗎,為啥是兩個指標?


由於單位資源的限定,即時相同的功能,減少響應時間就是在增大吞吐,而某些情況多犧牲部分資源消耗減少響應時間卻又減少了吞吐(比如極多步併發處理),圍繞微批處理思路增大延時卻又增大了吞吐(比如trigger buffer),具體場景具體分析哈,總之要麼高吞吐、要麼低延遲,更多的時候是在取折中。


架構視角的效能最佳化


話題拉回來,我們要知道什麼是效能好,才能評判系統,才能進行最佳化,才能具體執行。總體來說:


以吞吐、延時為基準


    1: 效能問題的最佳化是自上而下的,首先確定系統整體的效能目標及評判標準;然後整體評判

    2: 其次效能問題一定是可拆分的,若干的劣化共同導致了“差”;

    3: 最佳化的每個子環節應該有自身的目標,而不是對於整體目標的簡單拆分。根據子目標進行分佈評判,有的追求低延時、有的追求高吞吐、有的要兼顧。

    4: 子目標以對整體賦能為標準,避免區域性最優解


如何鑑別和判斷:我們拿到吞吐量、響應時間的資料之後,有N多的呈現方式。比如說平均值、標準方法、百分位數、中位數。


對於效能問題來講平均值參考意義不大,只能大致呈現系統水準,僅在系統整體效能差的情況下有幫助,而大多時候的效能分析,通常更關注極端百分位或者異常值,因為這些bad case往往代表了效能的潛在風險及劣化趨勢。


02

如何最佳化


知道好壞,接下來談如何最佳化。要談最佳化,首先避免犯錯,即使不懂成體系的最佳化理論,常見的坑肯定是熟知的,比如說慢SQL、for-each call、大量重複運算、各種手工暴力解法。這裡不過多贅述了,大家對這些常見的效能坑一定很熟悉。


效能最佳化的思路


我們的應用程式執行是分若干層級的,從應用程式(內部又可以分為業務程式、中介軟體程式、庫函式),到系統庫,再到系統呼叫、最後到核心、再到硬體。


就最佳化角度而言,越靠近工作執行的地方效能最佳化帶來的收益就越大,直白點就是越靠近我們業務邏輯的最佳化,越有效。按理說越底層的東西應該對系統產生的影響越大,那為什麼這麼說呢?

架構視角的效能最佳化



就影響程度而言,越靠近工作執行的地方,所能對系統產生的影響就越大。比如最佳化10條sql的底層儲存,"10條sql"-> "1條sql" -> "無同步sql"差異。就實現角度和專業角度而言,越靠近具體工作執行的地方,實現較蠢的可能性越高。


而這些可以彙總為:對於系統而言會有“技術選型”、“不合理使用問題”等問題,就經驗和機率來講,99%的問題是因為這部分,如果把所有的效能問題進行彙總,然後看具體分佈圖,不難發現,幾乎都分佈在上層(應用層)。說幾個問題大家感受一下,慢sql(應用層)、mysql檢索複雜度過高需要最佳化(中介軟體)、磁碟IO操作不合理(系統呼叫)、磁碟太慢(硬體層)等問題發生的機率和分佈。


總體來說,果斷承認“99%的問題是因為我一時犯蠢或者不太聰明”,才能開始逐漸正視效能問題和啟動效能最佳化工作。所以整體的效能最佳化,應該是自頂而下的,由我及外的。


“慢”在哪裡?


效能問題要求一定是可觀測的、可量化的,但是我們的系統不一定是可觀測的,或者無法具體量化。這就導致面臨效能低下時,就跟面對一個黑盒盲區一樣,只知道差,卻束手無策。


這時就需要對於我們的系統進行剖析,系統必定由n多部分構成(比如各種子服務,各種function,各種中介軟體)除系統構成外,我們更應該知道系統工作時的構成(每條鏈路的執行檢視 -- 實時執行狀態),然後分析哪些地方出了問題。


首先我們對於我們的系統進行層層剖析:

架構視角的效能最佳化



尤其是對於做工程的或者做業務的,我們的整體架構大多是分散式或者微服務架構,如果進行層級、呼叫鏈路的拆分通常是如上圖所示。

我們要做的就是對於系統有一個更加全面的認知,最起碼要回答如下問題,可以不用非常細節,至少要知道一個概貌:


    部署環境&硬體設施是什麼樣子

    由哪些服務構成

    用了哪些中介軟體(&版本 &介面卡 &本地最佳化 &調參)

    怎麼用的中介軟體(比如redis做快取,怎麼用的,鏈路如何)

    IO鏈路是什麼樣子(&對應的協議 &網路處理模型 &放大倍數)

    呼叫鏈路是同步還是非同步&有沒有做批處理

    怎麼用的中介軟體

    每個server功能是什麼樣子


在有了整體的概貌之後,就可以對系統進行一定的剖析了,看每一步或者關鍵步驟是否是支援我們觀測的,並且觀測手段是可以自動化監控,還是巡檢機制,還是case by case。再然後就能可以根據目標對於這些步驟或者說環節進行量化分析。(最好自動化哈)至此大機率就知道我們的系統具體慢在了哪裡。

03

對影響因素有個大體的感知


先說響應時間,首先我們要對速度有一個基礎的概念,資料沒那麼絕對,但是差不多就是這個數量級先硬體相關:

一個CPU週期:0.3ns

L1、L2、L3快取訪問:0.9ns、2.8ns、12.9ns

記憶體訪問:120ns

固態硬碟:150us

機械硬碟:1ms

一次同機房呼叫:1ms

一次跨城網路傳輸:30ms

一次跨專線地域傳輸:100 - 200ms


再包裝下看日常的操作耗時:

一條sql call(索引合理,資料量正常):1-2ms

一次RPC呼叫:1-10ms

一次redis操作(無大key、熱key):1-2ms

一次kafka send(帶buffer):1ms以內

一次http請求:5ms


對於CPU的消耗程度,如果把CPU看作是穩定的(相對有點粗暴哈),消耗程度通常可以用CPU佔用時間來表述(onCPU、offCPU)。比如完成一次md5需要多少時鐘、執行磁碟IO需要多少時鐘、一次網路IO需要多少時鐘等等。


而這個東西具體的表象其實就是cpu使用率,我們不難可以看到各個程式、各個現成的佔用程度,如果再掛一下火焰圖不難看出哪些操作對CPU消耗更高及對應占比。

架構視角的效能最佳化


各個響應時間和CPU消耗程度是對吞吐和響應時間最本質的影響,要進行效能最佳化就是對於CPU佔用和響應時間(處理延時)進行最佳化,各種方法減少CPU動作、減少延時等待,應用層的最佳化就是奔著這些目的去的。


最佳化延時

應用層面 關於延時我們該如何進行最佳化呢?整體來看的話:減少操作、減少等待。說幾個常見的策略:


能否減少操作,可以對於一次請求、一次任務的執行步驟進行梳理,分一下類,核心工作是哪些,哪些工作是可以從核心鏈路摘除的。


能否有複雜度更低的動作,比如檢索演算法是否可以最佳化、序列化演算法是否可以最佳化、是否有更高效的資料結構和演算法。


能否打時間差,前置處理或後置處理,比如初始化動作前置(不光是物件初始化,業務動作也行,可以提前計算、提前開戶等),後置比如非同步化處理,發獎券之類的


能否快取&複用,請求處理的結果、中間過程、或者socket連結是否可以直接複用,各種池化技術。


能否並行,多步並行處理,減少整體耗時,衡量下並行帶來的收益,是否比實現並行的代價高。


能否非阻塞併發處理,減少等待,避免大量的等待操作,業務處理的劣根性大多源於此。


能否提前失敗,對於易失敗請求,提前進行失敗性檢查,避免正常資源的佔用,引起其他請求等待。


能否繼續最佳化中介軟體操作,是否有更高效的中介軟體選型,操作上是否有更多的最佳化空間。


能否無”鎖“化處理,鎖是導致等待的核心因素之一,但這裡是泛指可能讓我們執行緒發生阻塞的情況,儘可能的減少同步鏈路中的等待機制可以讓我們的響應時間更容易得到保證。


以餘額支付系統互動資訊流程舉個例子(省了很多步驟,粗略描述):

架構視角的效能最佳化

嘗試最佳化一下

架構視角的效能最佳化


系統層面(假設包含虛擬機器),只是舉幾個例子

垃圾回收中斷是否影響過大,poll還是epoll、水平觸發還是邊緣觸發,fsync還是flush,執行緒的CPU親和,hardware 是否有特異性最佳化,SSD還是機械硬碟,設配新舊程度


最佳化吞吐

接下來看下吞吐的相關最佳化,吞吐量指的是現有系統在單位時間內能夠處理的請求或者任務數量。而對於吞吐的最佳化通常有兩點:


1: 單位資源內能夠具備更大的吞吐量,提高系統資源利用率。

2: 保證吞吐量沒有瓶頸,一定程度上,可以無限橫向擴充套件。


先看第一個問題,如何讓單位資源內承載更多的吞吐。


影響吞吐的因素有很多,比如前面剛剛提到的響應時間,關於響應時間和吞吐量的關係前面已經描述過了,大部分場景下適當的減少延時對於吞吐的影響是線性增長的,如何降低響應時間參照前一段的方案(大部分是正向的),唯一需要注意的是,在有限的資源下,過度的最佳化是會帶來吞吐下降的,要做的是儘可能在不帶來CPU增長的情況降低響應時間。


常用的技巧技巧批處理,比如多次IO是否可以進行合併,批處理一下,減少多次連結開銷,這樣對整體吞吐帶來的威力是很大的。對同步批處理,一定程度上也算是對於響應時間的調整,對於整體平均響應時間是在下降的,但是對於頭部請求響應時間是被劣化的。


但通常的使用往往是非同步化處理 + 批處理,IO 緩衝區、buffer triger、insert buffer、kafka send buffer 都是這個思路。帶來的吞吐和響應時間提升通常是炸裂的。


接下來看第二個問題,解決吞吐瓶頸。


當完成單位資源的吞吐最佳化之後,按理說有訴求就水平擴容即可,但是實時並非如此,經常會出現一個“熱點”成為系統的吞吐瓶頸,尤其是在OLTP系統中,熱點會以各種各樣的方式出現,但是通常都和狀態強相關,比如說全域性庫存、熱點使用者、熱點機器等等。


如果要最佳化,這裡就不得不提CAP理論了,在資料多節點分佈的情況下,為了保障一致性,系統整體的可用性必然會受到影響,可用性下降,最大的表現其實就是有效吞吐的下降。突破瓶頸,持續放大吞吐,那就適當的犧牲一下資料的一致性吧。

架構視角的效能最佳化

要做的是把熱點資料進行割裂,然後犧牲一致性,也就是把“熱點”進行sharding,讓熱點也能進行進行水平擴容,然後在這些短暫的不一致之上打補丁,儘快達成最終一致。


04

負載對於效能的影響


劣化現象


除了我們程式碼的實現、系統的構成會對系統有一定的影響,瀕臨負載極限的時候不管是吞吐量、響應時間都會受到一定程度的影響。


對吞吐而言,在機器資源一定的情況下,並不是隨負載一直線性變化的,到達一定的臨界值後如果持續增加負載,吞吐通常會由線性增長進入緩慢增長的,如果再持續的增加負載,會出現一定的劣化現象(比之前的吞吐量還會降低),這在JVM等虛擬機器之上執行的應用,或者在池化排隊機制的加持下愈發嚴重和明顯。

架構視角的效能最佳化

對響應時間而言,在機器資源一定的情況下,也不會隨始終保持耗時不變,同樣在到達負載臨界值時,會發成程度不一的劣化現象,有的響應時間增長迅速,有的則呈慢速下降。

架構視角的效能最佳化

劣化的原因


這是為什麼呢,因為在負載到達一定閾值時會觸發程度不一的系統資源搶佔問題,比如大量的排隊等待處理,此時上下文切換會更加的頻繁,並且對於某些JVM等虛擬機器之上的執行的應用還會帶來一定程度上的記憶體清理(分配和回收)及維護代價進而導致競爭更加激烈,也就導致了效能被進一步劣化。


這也是為什麼很多中介軟體會限制最大連結數、最大執行緒數、最大排序序列的原因,而對於我們業務應用程式而言也是一樣的,在進行池化處理排隊機制時一定要明確合適拒絕。


並且我們應該對於我們的的服務加以保護,以此保證負載不會出現這種劣化現象,比如說限流、限併發,比如說tomcat把引數調整的合理一些,讓負載,比如說CPU最大不超過60%、70%


這裡有個大坑 - 百分之90的問題是因為這個


除了到達負載臨界值時會出現這個問題,在“驟發的流量”面前這個問題也非常的凸顯,因為大量的初始化工作(連結建立、執行緒池擴容、client初始化等等),因為卡頓問題或者延遲變高等問題導致排隊激化,同時機器負載也會驟然上升,導致負載臨界問題或者時臨界值提前到來。日常看監控,突發流量時的尖刺,90%的原因都是這個。


這一點一定要額外注意,大流量前充分預熱、保持快取熱度、高效能場景放棄懶載入,極端情況下適當擴大活躍執行緒數、連結保活都是十分重要的。

架構視角的效能最佳化

日常的負載要保持多少

日常應用的負載通常由伺服器資源的瓶頸負載所決定,CPU爭搶會劣化,記憶體爭搶發生換頁情況更糟糕。那我們日常的負載到底應該在多少合適,這裡以CPU負載為例來進行推導:


通常經驗值是60%或者70%,在對穩定性要求極高的場景可能會控制的更低一些,這個值是怎麼得來的?

架構視角的效能最佳化

首先CPU負載超過90%就會有機率發生劣化現象,CPU並非你想象的那麼穩定效能方面會有抖動的;


其次即使隔離部署,作業系統之上執行了不止你一個應用程式的程式,可能會有一些重操作的client,比如log拉取,如果log還在客戶端做了grep、awk等處理那可能影響更重,這部分的buffer要留出來;


其次我們的程式的CPU消耗並不是線性不變的,仔細看看監控會發現忽高忽低,這部分buffer要留出來;


我們很難保證下游不發生抖動,如果下游抖動發生了一定程度的抖動,但還未超時,一定機率會導致主調server發生排隊,進而可能發生雪崩效應,放大問題,這時候產生的劣化波動也是要能承受的,同樣的也需要一定的buffer。


同樣的,我們也很難保證主調方不會發生合理程度內的激增現象,驟然的CPU波動也是合理的,這部分buffer要留出來。


幹掉這些buffer,通常經驗值是60%上下浮動,如果單請求消耗資源極多並且可用性要求極高那可能多留點buffer,否則就少點,然後對應的平穩時對應CPU負載所能承載的負載就是我們應該設定的日常負載,再由此推出限流值、動態擴縮標準等等


熱門問題 - 語言的差異到底有多大


語言是個有趣的問題,也是一個爭論已久的問題,這裡進行簡單的討論哈,通常我們的變成語言會分為編譯型語言和解釋型語言,還有些語言既有直譯器,又有編譯器,不用糾結概念,就從解釋執行和編譯執行的角度看問題即可。

架構視角的效能最佳化

編譯型比如說C/C++,編譯過的程式碼總體來說是高效能的,因為在CPU執行執行時不需要額外的一層對映,並且機器程式碼總是原始程式碼對映的很緊密,當然啦,很大程度取決於編譯器的編譯最佳化。


解釋型如說Java(純解釋執行的話,拋開JIT不談),需要額外的一層解釋工作,會增加不少的開銷,通常不會被期待有很強大的效能。


對於直譯器和編譯器共存的語言Java(帶上JIT了),效能好壞主要看JIT的最佳化力度,想觀測的話可以開啟JIT日誌,看下你的程式碼到底哪些進行最徹底的最佳化(JIT 最佳化層級是4),然後看哪些操作會導致退最佳化的發生,JIT的決策器也非常的關鍵。但是這種體系下,效能分析會相對費勁一些,JIT的最佳化策略(決策、編譯)很難說對所有開發者都是白盒的,並且在真正執行的時候,和我們程式碼的對映已經幾乎匹配不上了(還好Java的效能分析工具集夠全)。


架構視角的效能最佳化


除此之外,語言的垃圾回收機制雖然帶來的很多開發的便捷性,但是對於效能要求極高的場景,垃圾回收會帶來額外的影響,比如說程式的停頓,記憶體中物件的掃描、索引、複製工作。


05

效能最佳化的時機和判斷


效能最佳化的時機


什麼時機進行效能最佳化,常見的有“系統建立時”、“發生改變時”、“問題暴露時”、“擺爛-重新寫”,這個業界並沒有一個標準的答案,個人的處理思路是:在系統建立時滿足近1年半左右的效能訴求就足夠了,剩下的交給將來。


系統設計的初期肯定會進行系統的定位分析及對應的邊界劃分,此時系統應該具備哪些功能、應該被哪些場景所使用就已經確定了,我們要做的就是對這部分內容進行預判,這應該是屬於架構設計很核心的一部分,並且在設計的過程中,如果預判有50%以上場景會發生效能要求的突變,那麼我們就應該給系統留足擴充套件性。


架構設計思路中有種思想叫做“演進式架構”,這裡推崇的就是這個觀點。


但需要額外注意的是,我們可以不做過多的效能最佳化設計,但是要做足效能最佳化分析的設計,正如文章最開始所提到的,我們要保證系統是可觀測、可量化的。


上面講的是設計側的最佳化問題,在系統發生變更時,如果透過分析工具和監測工具發現有效能劣化的現象,是一定要進行最佳化動作的,放任效能問題不管一定會阻礙我們系統的程式,技術債早晚堆成山。


減少區域性最優解


區域性最優解是一個數學求解的最最佳化問題,我們設計實現的摸索,也可以粗暴的看作是一個最優的求解問題,那麼理論上也是會存在區域性最優解問題的。


實踐過程也確實如此,相信大家都經歷過大規模的重構。因為效能最佳化方案選擇絕對不止一條路,再有拆分到子方案岔路就變的更多了,在選擇的過程中很容易發生區域性最優解問題。

架構視角的效能最佳化


就整體方案選擇而言

在進行系統設計的時候,因為當前的系統設計整體方案不合理,在一個相對偏差的方向上進行了極致的最佳化,導致效能看起是得到了解決,並且段時間內沒有問題,但是在後期的發展過程中很容易導致效能最佳化停止不前。所以進行效能問題設計的時候,應該更多的比較各個方案的差異性,以及可發展性。多去探索未知的未知,才能讓我們的方案更加合理。


就方案拆分而言

架構設計應該是一個“一鍋出”的過程,一定要有一個上層指導,不要求每個子模組要用何種方案,但是一定要有明確的上層指導目標,這樣在選擇時就避免方向上的偏差,不至於每個子模組都是自己域內最合理的方案,但拼湊在一起確發現壓根解決不了問題。


效能分析工具

首先基礎架構一定提供了足夠全的分析工具,所在的公司也肯定有人在搞定針對基礎架構的探測分析及業務系統分析的基礎支援,找到對應的人,然後問他們。

要是進行學習或者文件檢索,提供一點關鍵詞,火焰圖、探針、最佳化分析、診斷工具、perf、profiling、Trace、netstat、iostat、vmstat、jstack、top、slabtop、ps

這種資料應該一搜一大片。


05

寫在最後


整體算是一點自己對於效能問題分析及設計最佳化相關的方法論總結,文章裡面並沒有涉及過多的細節方案,核心原因是想“以漁”。另外市面上大多的直接方案,導致小朋友們習慣性的三板斧,個人覺著這種導向性不算良好,應該更清楚為什麼這麼做,才能更好的解決問題吧。





來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70027824/viewspace-2941862/,如需轉載,請註明出處,否則將追究法律責任。

相關文章