影響服務器性能的因素有哪些?又該如何提高系統的性能?

影響性能的因素

你想要提升性能,首先肯定要知道哪些因素對於系統性能的影響最大,然後再針對這些具體的因素想辦法做優化,是不是這個邏輯?

那麼,哪些因素對性能有影響呢?在回答這個問題之前,我們先定義一下“性能”,服務設備不同對性能的定義也是不一樣的,例如 CPU 主要看主頻、磁盤主要看 IOPS(Input/Output Operations Per Second,即每秒進行讀寫操作的次數)。

而今天我們討論的主要是系統服務端性能,一般用 QPS(Query Per Second,每秒請求數)來衡量,還有一個影響和 QPS 也息息相關,那就是響應時間(Response Time,RT),它可以理解為服務器處理響應的耗時。

正常情況下響應時間(RT)越短,一秒鐘處理的請求數(QPS)自然也就會越多,這在單線程處理的情況下看起來是線性的關係,即我們只要把每個請求的響應時間降到最低,那麼性能就會最高。

但是你可能想到響應時間總有一個極限,不可能無限下降,所以又出現了另外一個維度,即通過多線程,來處理請求。這樣理論上就變成了“總 QPS =(1000ms / 響應時間)× 線程數量”,這樣性能就和兩個因素相關了,一個是一次響應的服務端耗時,一個是處理請求的線程數。

接下來,我們一起看看這個兩個因素到底會造成什麼樣的影響。

首先,我們先來看看響應時間和 QPS 有啥關係。

對於大部分的 Web 系統而言,響應時間一般都是由 CPU 執行時間和線程等待時間(比如 RPC、IO 等待、Sleep、Wait 等)組成,即服務器在處理一個請求時,一部分是 CPU 本身在做運算,還有一部分是在各種等待。

理解了服務器處理請求的邏輯,估計你會說為什麼我們不去減少這種等待時間。很遺憾,根據我們實際的測試發現,減少線程等待時間對提升性能的影響沒有我們想象得那麼大,它並不是線性的提升關係,這點在很多代理服務器(Proxy)上可以做驗證。

如果代理服務器本身沒有 CPU 消耗,我們在每次給代理服務器代理的請求加個延時,即增加響應時間,但是這對代理服務器本身的吞吐量並沒有多大的影響,因為代理服務器本身的資源並沒有被消耗,可以通過增加代理服務器的處理線程數,來彌補響應時間對代理服務器的 QPS 的影響。

其實,真正對性能有影響的是 CPU 的執行時間。這也很好理解,因為 CPU 的執行真正消耗了服務器的資源。經過實際的測試,如果減少 CPU 一半的執行時間,就可以增加一倍的 QPS。

也就是說,我們應該致力於減少 CPU 的執行時間。

其次,我們再來看看線程數對 QPS 的影響。

單看“總 QPS”的計算公式,你會覺得線程數越多 QPS 也就會越高,但這會一直正確嗎?顯然不是,線程數不是越多越好,因為線程本身也消耗資源,也受到其他因素的制約。例如,線程越多系統的線程切換成本就會越高,而且每個線程也都會耗費一定內存。

那麼,設置什麼樣的線程數最合理呢?其實很多多線程的場景都有一個默認配置,即“線程數 = 2 * CPU 核數 + 1”。除去這個配置,還有一個根據最佳實踐得出來的公式:

線程數 = [(線程等待時間 + 線程 CPU 時間) / 線程 CPU 時間] × CPU 數量

當然,最好的辦法是通過性能測試來發現最佳的線程數。

換句話說,要提升性能我們就要減少 CPU 的執行時間,另外就是要設置一個合理的併發線程數,通過這兩方面來顯著提升服務器的性能。

現在,你知道了如何來快速提升性能,那接下來你估計會問,我應該怎麼發現系統哪裡最消耗 CPU 資源呢?

如何發現瓶頸

就服務器而言,會出現瓶頸的地方有很多,例如 CPU、內存、磁盤以及網絡等都可能會導致瓶頸。此外,不同的系統對瓶頸的關注度也不一樣,例如對緩存系統而言,制約它的是內存,而對存儲型系統來說 I/O 更容易是瓶頸。

這我們定位的場景是秒殺,它的瓶頸更多地發生在 CPU 上。

那麼,如何發現 CPU 的瓶頸呢?其實有很多 CPU 診斷工具可以發現 CPU 的消耗,最常用的就是 JProfiler 和 Yourkit 這兩個工具,它們可以列出整個請求中每個函數的 CPU 執行時間,可以發現哪個函數消耗的 CPU 時間最多,以便你有針對性地做優化。

當然還有一些辦法也可以近似地統計 CPU 的耗時,例如通過 jstack 定時地打印調用棧,如果某些函數調用頻繁或者耗時較多,那麼那些函數就會多次出現在系統調用棧裡,這樣相當於採樣的方式也能夠發現耗時較多的函數。

雖說秒殺系統的瓶頸大部分在 CPU,但這並不表示其他方面就一定不出現瓶頸。例如,如果海量請求湧過來,你的頁面又比較大,那麼網絡就有可能出現瓶頸。

怎樣簡單地判斷 CPU 是不是瓶頸呢?一個辦法就是看當 QPS 達到極限時,你的服務器的 CPU 使用率是不是超過了 95%,如果沒有超過,那麼表示 CPU 還有提升的空間,要麼是有鎖限制,要麼是有過多的本地 I/O 等待發生。

現在你知道了優化哪些因素,又發現了瓶頸,那麼接下來就要關注如何優化了。

如何優化系統

對 Java 系統來說,可以優化的地方很多,這裡我重點說一下比較有效的幾種手段,供你參考,它們是:減少編碼、減少序列化、Java 極致優化、併發讀優化。接下來,我們分別來看一下。

減少編碼

Java 的編碼運行比較慢,這是 Java 的一大硬傷。在很多場景下,只要涉及字符串的操作(如輸入輸出操作、I/O 操作)都比較耗 CPU 資源,不管它是磁盤 I/O 還是網絡 I/O,因為都需要將字符轉換成字節,而這個轉換必須編碼。

每個字符的編碼都需要查表,而這種查表的操作非常耗資源,所以減少字符到字節或者相反的轉換、減少字符編碼會非常有成效。減少編碼就可以大大提升性能。

那麼如何才能減少編碼呢?例如,網頁輸出是可以直接進行流輸出的,即用 resp.getOutputStream() 函數寫數據,把一些靜態的數據提前轉化成字節,等到真正往外寫的時候再直接用 OutputStream() 函數寫,就可以減少靜態數據的編碼轉換。

“Velocity 優化實踐”就是基於把靜態的字符串提前編碼成字節並緩存,然後直接輸出字節內容到頁面,從而大大減少編碼的性能消耗的,網頁輸出的性能比沒有提前進行字符到字節轉換時提升了 30% 左右。

減少序列化

序列化也是 Java 性能的一大天敵,減少 Java 中的序列化操作也能大大提升性能。又因為序列化往往是和編碼同時發生的,所以減少序列化也就減少了編碼。

序列化大部分是在 RPC 中發生的,因此避免或者減少 RPC 就可以減少序列化,當然當前的序列化協議也已經做了很多優化來提升性能。有一種新的方案,就是可以將多個關聯性比較強的應用進行“合併部署”,而減少不同應用之間的 RPC 也可以減少序列化的消耗。

所謂“合併部署”,就是把兩個原本在不同機器上的不同應用合併部署到一臺機器上,當然不僅僅是部署在一臺機器上,還要在同一個 Tomcat 容器中,且不能走本機的 Socket,這樣才能避免序列化的產生。

另外針對秒殺場景,我們還可以做得更極致一些,接下來我們來看第 3 點:Java 極致優化。

Java 極致優化

Java 和通用的 Web 服務器(如 Nginx 或 Apache 服務器)相比,在處理大併發的 HTTP 請求時要弱一點,所以一般我們都會對大流量的 Web 系統做靜態化改造,讓大部分請求和數據直接在 Nginx 服務器或者 Web 代理服務器(如 Varnish、Squid 等)上直接返回(這樣可以減少數據的序列化與反序列化),而 Java 層只需處理少量數據的動態請求。針對這些請求,我們可以使用以下手段進行優化:

直接使用 Servlet 處理請求。避免使用傳統的 MVC 框架,這樣可以繞過一大堆複雜且用處不大的處理邏輯,節省 1ms 時間(具體取決於你對 MVC 框架的依賴程度)。

直接輸出流數據。使用 resp.getOutputStream() 而不是 resp.getWriter() 函數,可以省掉一些不變字符數據的編碼,從而提升性能;數據輸出時推薦使用 JSON 而不是模板引擎(一般都是解釋執行)來輸出頁面。

4. 併發讀優化

也許會覺得這個問題很容易解決,無非就是放到 Tair 緩存裡面。集中式緩存為了保證命中率一般都會採用一致性 Hash,所以同一個 key 會落到同一臺機器上。雖然單臺緩存機器也能支撐 30w/s 的請求,但還是遠不足以應對像“大秒”這種級別的熱點商品。那麼,該如何徹底解決單點的瓶頸呢?

答案是採用應用層的 LocalCache,即在秒殺系統的單機上緩存商品相關的數據。

那麼,又如何緩存(Cache)數據呢?你需要劃分成動態數據和靜態數據分別進行處理:

像商品中的“標題”和“描述”這些本身不變的數據,會在秒殺開始之前全量推送到秒殺機器上,並一直緩存到秒殺結束;

像庫存這類動態數據,會採用“被動失效”的方式緩存一定時間(一般是數秒),失效後再去緩存拉取最新的數據。

你可能還會有疑問:像庫存這種頻繁更新的數據,一旦數據不一致,會不會導致超賣?

這就要用到前面介紹的讀數據的分層校驗原則了,讀的場景可以允許一定的髒數據,因為這裡的誤判只會導致少量原本無庫存的下單請求被誤認為有庫存,可以等到真正寫數據時再保證最終的一致性,通過在數據的高可用性和一致性之間的平衡,來解決高併發的數據讀取問題。

總結一下

性能優化的過程首先要從發現短板開始,除了今天介紹的一些優化措施外,你還可以在減少數據、數據分級(動靜分離),以及減少中間環節、增加預處理等這些環節上做優化。

首先是“發現短板”,比如考慮以下因素的一些限制:光速(光速:C = 30 萬千米 / 秒;光纖:V = C/1.5=20 萬千米 / 秒,即數據傳輸是有物理距離的限制的)、網速(2017 年 11 月知名測速網站 Ookla 發佈報告,全國平均上網帶寬達到 61.24 Mbps,千兆帶寬下 10KB 數據的極限 QPS 為 1.25 萬 QPS=1000Mbps/8/10KB)、網絡結構(交換機 / 網卡的限制)、TCP/IP、虛擬機(內存 /CPU/IO 等資源的限制)和應用本身的一些瓶頸等。

其次是減少數據。事實上,有兩個地方特別影響性能,一是服務端在處理數據時不可避免地存在字符到字節的相互轉化,二是 HTTP 請求時要做 Gzip 壓縮,還有網絡傳輸的耗時,這些都和數據大小密切相關。

再次,就是數據分級,也就是要保證首屏為先、重要信息為先,次要信息則異步加載,以這種方式提升用戶獲取數據的體驗。

最後就是要減少中間環節,減少字符到字節的轉換,增加預處理(提前做字符到字節的轉換)去掉不需要的操作。

此外,要做好優化,你還需要做好應用基線,比如性能基線(何時性能突然下降)、成本基線(去年雙 11 用了多少臺機器)、鏈路基線(我們的系統發生了哪些變化),你可以通過這些基線持續關注系統的性能,做到在代碼上提升編碼質量,在業務上改掉不合理的調用,在架構和調用鏈路上不斷的改進。

最後,歡迎你在留言區評論,你也可以說說在實際工作中,關於性能提升還有哪些更好的思路或者方案。

影響服務器性能的因素有哪些?又該如何提高系統的性能?


分享到:


相關文章: