03 | 系統設計目標(一):如何提升系統性能?

提到互聯網系統設計,你可能聽到最多的詞兒就是“三高”,也就是“高併發”“高性能”“高可用”,它們是互聯網系統架構設計永恆的主題。在前兩節課中,我帶你瞭解了高併發系統設計的含義,意義以及分層設計原則,接下來,我想帶你整體瞭解一下高併發系統設計的目標,然後在此基礎上,進入我們今天的話題:如何提升系統的性能?

高併發系統設計的三大目標:高性能、高可用、可擴展

高併發,是指運用設計手段讓系統能夠處理更多的用戶併發請求,也就是承擔更大的流量。它是一切架構設計的背景和前提,脫離了它去談性能和可用性是沒有意義的。很顯然嘛,你在每秒一次請求和每秒一萬次請求,兩種不同的場景下,分別做到毫秒級響應時間和五個九(99.999%)的可用性,無論是設計難度還是方案的複雜度,都不是一個級別的。

而性能和可用性,是我們實現高併發系統設計必須考慮的因素。

性能反映了系統的使用體驗,想象一下,同樣承擔每秒一萬次請求的兩個系統,一個響應時間是毫秒級,一個響應時間在秒級別,它們帶給用戶的體驗肯定是不同的。

可用性則表示系統可以正常服務用戶的時間。我們再類比一下,還是兩個承擔每秒一萬次的系統,一個可以做到全年不停機、無故障,一個隔三差五宕機維護,如果你是用戶,你會選擇使用哪一個系統呢?答案不言而喻。

另一個耳熟能詳的名詞叫“可擴展性”,它同樣是高併發系統設計需要考慮的因素。爲什麼呢?我來舉一個具體的例子。

流量分爲平時流量和峯值流量兩種,峯值流量可能會是平時流量的幾倍甚至幾十倍,在應對峯值流量的時候,我們通常需要在架構和方案上做更多的準備。這就是淘寶會花費大半年的時間準備雙十一,也是在面對“明星離婚”等熱點事件時,看起來無懈可擊的微博系統還是會出現服務不可用的原因。而易於擴展的系統能在短時間內迅速完成擴容,更加平穩地承擔峯值流量。

高性能、高可用和可擴展,是我們在做高併發系統設計時追求的三個目標,我會用三節課的時間,帶你瞭解在高併發大流量下如何設計高性能、高可用和易於擴展的系統。

瞭解完這些內容之後,我們正式進入今天的話題:如何提升系統的性能?

性能優化原則

“天下武功,唯快不破”。性能是系統設計成功與否的關鍵,實現高性能也是對程序員個人能力的挑戰。不過在瞭解實現高性能的方法之前,我們先明確一下性能優化的原則。

首先,性能優化一定不能盲目,一定是問題導向的。脫離了問題,盲目地提早優化會增加系統的複雜度,浪費開發人員的時間,也因爲某些優化可能會對業務上有些折中的考慮,所以也會損傷業務。

其次,性能優化也遵循“八二原則”,即你可以用 20% 的精力解決 80% 的性能問題。所以我們在優化過程中一定要抓住主要矛盾,優先優化主要的性能瓶頸點。

再次,性能優化也要有數據支撐。在優化過程中,你要時刻了解你的優化讓響應時間減少了多少,提升了多少的吞吐量。

最後,性能優化的過程是持續的。高併發的系統通常是業務邏輯相對複雜的系統,那麼在這類系統中出現的性能問題通常也會有多方面的原因。因此,我們在做性能優化的時候要明確目標,比方說,支撐每秒 1 萬次請求的吞吐量下響應時間在 10ms,那麼我們就需要持續不斷地尋找性能瓶頸,制定優化方案,直到達到目標爲止。

在以上四個原則的指引下,掌握常見性能問題的排查方式和優化手段,就一定能讓你在設計高併發系統時更加遊刃有餘。

性能的度量指標

性能優化的第三點原則中提到,對於性能我們需要有度量的標準,有了數據才能明確目前存在的性能問題,也能夠用數據來評估性能優化的效果。所以明確性能的度量指標十分重要。

一般來說,度量性能的指標是系統接口的響應時間,但是單次的響應時間是沒有意義的,你需要知道一段時間的性能情況是什麼樣的。所以,我們需要收集這段時間的響應時間數據,然後依據一些統計方法計算出特徵值,這些特徵值就能夠代表這段時間的性能情況。我們常見的特徵值有以下幾類。

平均值

顧名思義,平均值是把這段時間所有請求的響應時間數據相加,再除以總請求數。平均值可以在一定程度上反應這段時間的性能,但它敏感度比較差,如果這段時間有少量慢請求時,在平均值上並不能如實地反應。

舉個例子,假設我們在 30s 內有 10000 次請求,每次請求的響應時間都是 1ms,那麼這段時間響應時間平均值也是 1ms。這時,當其中 100 次請求的響應時間變成了 100ms,那麼整體的響應時間是 (100 * 100 + 9900 * 1) / 10000 = 1.99ms。你看,雖然從平均值上來看僅僅增加了不到 1ms,但是實際情況是有 1% 的請求(100/10000)的響應時間已經增加了 100 倍。所以,平均值對於度量性能來說只能作爲一個參考。

最大值

這個更好理解,就是這段時間內所有請求響應時間最長的值,但它的問題又在於過於敏感了。

還拿上面的例子來說,如果 10000 次請求中只有一次請求的響應時間達到 100ms,那麼這段時間請求的響應耗時的最大值就是 100ms,性能損耗爲原先的百分之一,這種說法明顯是不準確的。

分位值

分位值有很多種,比如 90 分位、95 分位、75 分位。以 90 分位爲例,我們把這段時間請求的響應時間從小到大排序,假如一共有 100 個請求,那麼排在第 90 位的響應時間就是 90 分位值。分位值排除了偶發極慢請求對於數據的影響,能夠很好地反應這段時間的性能情況,分位值越大,對於慢請求的影響就越敏感。

在我來看,分位值是最適合作爲時間段內,響應時間統計值來使用的,在實際工作中也應用最多。除此之外,平均值也可以作爲一個參考值來使用。

我在上面提到,脫離了併發來談性能是沒有意義的,我們通常使用吞吐量或者響應時間來度量併發和流量,使用吞吐量的情況會更多一些。但是你要知道,這兩個指標是呈倒數關係的。

這很好理解,響應時間 1s 時,吞吐量是每秒 1 次,響應時間縮短到 10ms,那麼吞吐量就上升到每秒 100 次。所以,一般我們度量性能時都會同時兼顧吞吐量和響應時間,比如我們設立性能優化的目標時通常會這樣表述:在每秒 1 萬次的請求量下,響應時間 99 分位值在 10ms 以下。

那麼,響應時間究竟控制在多長時間比較合適呢?這個不能一概而論。

從用戶使用體驗的角度來看,200ms 是第一個分界點:接口的響應時間在 200ms 之內,用戶是感覺不到延遲的,就像是瞬時發生的一樣。而 1s 是另外一個分界點:接口的響應時間在 1s 之內時,雖然用戶可以感受到一些延遲,但卻是可以接受的,超過 1s 之後用戶就會有明顯等待的感覺,等待時間越長,用戶的使用體驗就越差。所以,健康系統的 99 分位值的響應時間通常需要控制在 200ms 之內,而不超過 1s 的請求佔比要在 99.99% 以上。

現在你瞭解了性能的度量指標,那我們再來看一看,隨着併發的增長我們實現高性能的思路是怎樣的。

高併發下的性能優化

假如說,你現在有一個系統,這個系統中處理核心只有一個,執行的任務的響應時間都在 10ms,它的吞吐量是在每秒 100 次。那麼我們如何來優化性能從而提高系統的併發能力呢?主要有兩種思路:一種是提高系統的處理核心數,另一種是減少單次任務的響應時間。

1. 提高系統的處理核心數

提高系統的處理核心數就是增加系統的並行處理能力,這個思路是優化性能最簡單的途徑。拿上一個例子來說,你可以把系統的處理核心數增加爲兩個,並且增加一個進程,讓這兩個進程跑在不同的核心上。這樣從理論上,你係統的吞吐量可以增加一倍。當然了,在這種情況下,吞吐量和響應時間就不是倒數關係了,而是:

吞吐量 = 併發進程數 / 響應時間

計算機領域的阿姆達爾定律(Amdahl’s law)是吉恩·阿姆達爾在 1967 年提出的。它描述了併發進程數與響應時間之間的關係,含義是在固定負載下,並行計算的加速比,也就是並行化之後效率提升情況,可以用下面公式來表示:

(Ws + Wp) / (Ws + Wp/s)

其中,Ws 表示任務中的串行計算量,Wp 表示任務中的並行計算量,s 表示並行進程數。從這個公式我們可以推導出另外一個公式:

1/(1-p+p/s)

其中,s 還是表示並行進程數,p 表示任務中並行部分的佔比。當 p 爲 1 時,也就是完全並行時,加速比與並行進程數相等;當 p 爲 0 時,即完全串行時,加速比爲 1,也就是說完全無加速;當 s 趨近於無窮大的時候,加速比就等於 1/(1-p),你可以看到它完全和 p 成正比。特別是,當 p 爲 1 時,加速比趨近於無窮大。

以上公式的推導過程有些複雜,你只需要記住結論就好了。

我們似乎找到了解決問題的銀彈,是不是無限制地增加處理核心數就能無限制地提升性能,從而提升系統處理高併發的能力呢?很遺憾,隨着併發進程數的增加,並行的任務對於系統資源的爭搶也會愈發嚴重。在某一個臨界點上繼續增加併發進程數,反而會造成系統性能的下降,這就是性能測試中的拐點模型。

從圖中你可以發現,併發用戶數處於輕壓力區時,響應時間平穩,吞吐量和併發用戶數線性相關。而當併發用戶數處於重壓力區時,系統資源利用率到達極限,吞吐量開始有下降的趨勢,響應時間也會略有上升。這個時候,再對系統增加壓力,系統就進入拐點區,處於超負荷狀態,吞吐量下降,響應時間大幅度上升。

所以我們在評估系統性能時通常需要做壓力測試,目的就是找到系統的“拐點”,從而知道系統的承載能力,也便於找到系統的瓶頸,持續優化系統性能。

說完了提升並行能力,我們再看看優化性能的另一種方式:減少單次任務響應時間。

2. 減少單次任務響應時間

想要減少任務的響應時間,首先要看你的系統是 CPU 密集型還是 IO 密集型的,因爲不同類型的系統性能優化方式不盡相同。

CPU 密集型系統中,需要處理大量的 CPU 運算,那麼選用更高效的算法或者減少運算次數就是這類系統重要的優化手段。比方說,如果系統的主要任務是計算 Hash 值,那麼這時選用更高性能的 Hash 算法就可以大大提升系統的性能。發現這類問題的主要方式,是通過一些 Profile 工具來找到消耗 CPU 時間最多的方法或者模塊,比如 Linux 的 perf、eBPF 等。

IO 密集型系統指的是系統的大部分操作是在等待 IO 完成,這裏 IO 指的是磁盤 IO 和網絡 IO。我們熟知的系統大部分都屬於 IO 密集型,比如數據庫系統、緩存系統、Web 系統。這類系統的性能瓶頸可能出在系統內部,也可能是依賴的其他系統,而發現這類性能瓶頸的手段主要有兩類。

第一類是採用工具,Linux 的工具集很豐富,完全可以滿足你的優化需要,比如網絡協議棧、網卡、磁盤、文件系統、內存,等等。這些工具的用法很多,你可以在排查問題的過程中逐漸積累。除此之外呢,一些開發語言還有針對語言特性的分析工具,比如說 Java 語言就有其專屬的內存分析工具。

另外一類手段就是可以通過監控來發現性能問題。在監控中我們可以對任務的每一個步驟做分時的統計,從而找到任務的哪一步消耗了更多的時間。這一部分在演進篇中會有專門的介紹,這裏就不再展開了。

那麼找到了系統的瓶頸點,我們要如何優化呢?優化方案會隨着問題的不同而不同。比方說,如果是數據庫訪問慢,那麼就要看是不是有鎖表的情況、是不是有全表掃描、索引加的是否合適、是否有 JOIN 操作、需不需要加緩存,等等;如果是網絡的問題,就要看網絡的參數是否有優化的空間,抓包來看是否有大量的超時重傳,網卡是否有大量丟包等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章