怎麼做好Java效能優化

得物技術發表於2021-12-22

文:蘇木

0. 開篇

效能優化是一個很複雜的工作,且充滿了不確定性。它不像Java業務程式碼,可以一次編寫到處執行(write once, run anywhere),往往一些我們可能並不能察覺的變化,就會帶來驚喜/驚嚇。能夠全面的瞭解並評估我們所負責應用的效能,我認為是提升技術確定性和技術感知能力的非常有效的手段。本文儘可能簡短的總結我自己在效能優化上面的一些體會和經驗,從實踐的角度出發儘量避免過於囉嗦和生硬,但相關的知識實在太多,受限於個人經驗和技術深度,不足之外還請大家補充。
第1部分是偏背景類知識的介紹,有這方面知識的同學可以直接跳過。

1. 瞭解執行環境

大多數的程式語言(尤其是Java)做了非常多的事情來幫助我們不用太瞭解硬體也能很容易的寫出正確工作的程式碼,但你如果要全面瞭解效能,卻需要具備不少的從硬體、作業系統到軟體層面的知識。

1.1 伺服器

目前我們大量使用Intel 64位架構的Xeon處理器,除此之外還會有AMD x64處理器、ARM伺服器處理器(如:華為鯤鵬、阿里倚天)、未來還會有RISC-V架構的處理器、以及一些專用FPGA晶片等等。我們這裡主要聊聊目前我們大量使用的阿里雲ECS使用的Intel 8269CY處理器。

1.1.1 處理器

Intel Xeon Platinum 8269CY,阿里雲使用的這一款處理器是阿里定製款,並不能在Intel的官方手冊中查詢到,不過我們可以通過下方Intel處理器的命名規則瞭解到不少的資訊,它是一款這樣的處理器:主頻2.5GHz(睿頻3.2GHz、最大睿頻3.8GHz),26核心52執行緒(具備超執行緒技術),6通道DDR4-2933記憶體,最大配置記憶體1T,Cascade Lake微架構,48通道PCI-E 3.0,14nm光刻工藝,205W TDP。它是這一代至強處理器中效能比較強的型號了,最大支援8路部署。

具備動態自動超頻的能力將能夠短時間提升效能,同時在少數核心忙碌的時候還可以讓它們保持長時間的自動超頻,這會嚴重的影響我們對應用效能的評估(少量測試時效能很好,大規模測試時下降很厲害)。

最大配置記憶體1TB,代表處理器具備48bit的VA(虛擬地址),也就是通常需要四級頁表(下一代具備57bit VA的處理器已經在設計中了,通常需要五級頁表),過深的頁表顯然是極大的影響記憶體訪問的效能以及佔用記憶體(頁表也是儲存在記憶體中的)的,所以Intel設計了大頁(2MB、1GB大頁)機制,以減少過深的頁錶帶來的影響。6通道2933MHz的記憶體匯流排代表它具備總計約137GB/s(記憶體匯流排是64bit位寬)的寬頻,不過需要記住他們是高度並行設計的。
image.png
這個微處理的CPU核架構如下圖所示,採用8發射亂序架構, 32KB指令+32KB資料 L1 Cache,1MB L2 Cache。發射單元是CPU內部真正的計算單元,它的多少是CPU效能的關鍵因素。8個發射單元中有4個單元都可以進行基本整數運算(ALU單元),只有2個可以進行整數乘除和浮點運算,所以對於大量浮點運算的場景並行效率會偏低。1個CPU核對應的2個HT(這裡指超執行緒技術虛擬出的硬體執行緒)是共享8個發射單元的,所以這兩個HT之間將會有非常大的相互影響(這也會導致作業系統內CPU的使用率不再是線性值,具體請查閱相關資料),L1、L2 Cache同樣也是共享的,所以也會相互影響。

Intel Xeon處理器在分支預測上面花了很多功夫,所以在較多分支程式碼(通常就是if else這類程式碼)時效能往往也能做的很好,比大多數的ARM架構做的都要好。Java常用的指標壓縮技術也受益於x86架構靈活的定址能力(如:mov eax, ecx * 8 + 8),可以一條指令完成,同時也不會帶來效能的損失,但是這在ARM、RISC-V等RISC(精簡指令集架構,Reduced Instruction Set Computing)處理器上就不適用了。

從可靠渠道瞭解道,下一代架構(Sunny Cove)將大幅度的進行架構優化,升級為10發射,同時L1 Cache將資料部分增加到48KB,這代表接下來的處理器將更加側重於提升SIMD(單指令多運算元,Signle Instruction Multiple Data)等的資料計算效能。
image.png
一顆8269CY內部有26個CPU核,採用如下的拓撲結構進行連線。這一代處理器最多有28個CPU核,8269CY遮蔽掉了2個核心,以降低對產品良率的要求(節約成本)。可以看到總計35.75MB的L3 Cache(圖中為LLC: Last Level Cache)被分為了13塊(圖中是14塊,遮蔽2個核心的同時也遮蔽了與之匹配的L3 Cache),每塊2.75MB,並不是簡單意義理解上的是一大塊統一的區域。6通道的記憶體控制器也分佈在左右兩側,與實際主機板上記憶體插槽的位置關係是對應的。這些資訊都告訴我們,這是一顆並行能力非常強的多核心處理器。
image.png

1.1.2 伺服器

阿里雲通常都是採用雙Intel處理器的2U機型(基於散熱、密度、價效比等等的考慮),基本都是2個NUMA(非一致性記憶體訪問,Non Uniform Memory Access)節點。具體到Intel 8269CY,代表一臺伺服器具備52個物理核心,104個硬體執行緒,通常阿里雲會稱之為104核。NUMA技術的出現是硬體工程師的妥協(他們實在沒有能力做到在多CPU的情況下還能實現訪問任何地址的效能一致性),所以做的不好也會嚴重的降低效能,大多數情況下虛擬機器/容器排程團隊要做的是將NUMA開啟,同時將一個虛擬機器/容器部署到同一個NUMA節點上。

這幾年AMD的發展很好,它的多核架構與Intel有很大的不同,不久的將來阿里雲將會部署不少採用AMD處理器的機型。AMD處理器的NUMA節點將會更多,而且拓撲關係也會更復雜,阿里自研的倚天(採用ARM架構)就更復雜了。這意味著虛擬機器/容器排程團隊夠得忙了。

多數情況下伺服器大都採用CPU:記憶體為1:2或1:4的配置,即配置雙Intel 8269CY的物理機,通常都會配備192GB或384GB的記憶體。如果虛擬機器/容器需要的記憶體:CPU過大的情況下,將很難實現記憶體在CPU對應的NUMA節點上就近分配了,也就是說效能就不能得到保證。

由於2U機型的物理高度是1U機型的2倍,所以有更多的空間放下更多的SSD盤、高效能PCI-E裝置等。不過雲廠商肯定是不願意直接將物理機賣給使用者(畢竟他們已經不再是以前的託管物理機公司)的,再怎麼也得在上面架一層,也就是做成ECS再賣給客戶,這樣諸如熱遷移、高可用等功能才能實現。前述的"架一層"是通過虛擬化技術來實現的。

1.1.3 虛擬化技術

一臺物理機效能很強大,通常我們只需要裡面的一小塊,但我們又希望不要感知到其他人在共享這臺物理機,所以催生了虛擬化技術(簡單來說就是讓可以讓一個CPU工作起來就像多個CPU並行執行,從而使得在一臺伺服器內可以同時執行多個作業系統)。早期的虛擬機器技術是通過軟體實現的,老牌廠商如VMWare,但是效能犧牲的有點多,硬體廠商也看好虛擬機器技術的前景,所以便有了硬體虛擬化技術。

各廠商的實現並不相同,但差異不是很大,好在有專門的虛擬化處理模組去相容就可以了。Intel的虛擬化技術叫Intel VT(Virtualization Technology),它包括VT-x(處理器的虛擬化支援)、VT-d(直接I/O訪問的虛擬化)、VT-c(網路連線的虛擬化),以及在網路效能上的SR-IOV技術(Single Root I/O Virtualization)。

這裡面一個很重要的事情是,原本我們訪問記憶體的一層轉換(線性地址->實體地址)會變成二層轉換(VM內線性地址->Host線性地址->實體地址),這會引入更多的記憶體開銷以及頁表的轉換工作。所以大多數雲廠商會在Host作業系統上開啟大頁(Linux 作業系統通常是使用透明大頁技術),以減少記憶體相關的虛擬化開銷。

伺服器對網路效能的要求是很高的,現在的網路硬體都支援網路卡多佇列技術,通常情況下需要將VM中的網路中斷分散給不同的CPU核來處理,以避免單核轉發帶來的效能瓶頸。

Host作業系統需要管理它上面的一個或多個VM(虛擬機器,Virtual Machine),以及前述提及的處理網路卡中斷,這會帶來一定的CPU消耗。阿里雲上一代機型,伺服器總計是96核(即96個硬體執行緒HT,實際是48個物理核),但最多隻能分配出88核,需要保留8個核(相當於物理機CPU減少8.3%)給Host作業系統使用,同時由於I/O相關的虛擬化開銷,整機效能會下降超過10%。
阿里云為了最大限度的降低虛擬化的開銷,研發了牛逼的“彈性裸金屬伺服器 - 神龍”,號稱不但不會因為虛擬化降低效能,反而會提升部分效能(主要是網路轉發)。

1.1.4 神龍伺服器

為了避免虛擬化對效能的影響,阿里雲(類似還有亞馬遜等雲廠商的類似方案)研發了神龍伺服器。簡單來說就是設計了神龍MOC卡,將大部分虛擬機器管理工作、網路中斷處理等從CPU offload到MOC卡進行處理。神龍MOC卡是一塊PCI-E 3.0裝置,其內有專門設計的用於網路處理的FPGA晶片,以及2顆低功耗的x86處理器(據傳是Intel Atom),最大限度的接手Host作業系統的虛擬化管理工作。通過這樣的設計,在網路轉發效能上甚至能做到10倍於裸物理機,做到了當之無愧的裸金屬。104核的物理機可以直接虛擬出一臺104核的超大ECS,再也不用保留幾個核心給Host作業系統使用了。

1.2 VPC

VPC(虛擬專有云,Virtual Private Cloud),大多數雲上的使用者都希望自己的網路與其它的客戶隔離,就像自建機房一樣,這裡面最重要的是網路虛擬化技術,目前阿里雲採用的是VxLAN協議,它底層採用UDP協議進行資料傳輸,整體資料包結構如下圖所示。VxLAN在VxLAN幀頭中引入了類似VLAN ID的網路標識,稱為VxLAN網路標識VNI(VxLAN Network ID),由24位元組成,理論上可支援多達16M的VxLAN段,從而滿足了大規模不同網路之間的標識、隔離需求。這一層的引入將會使原始的網路包增加50 Bytes的固定長度的頭。當然,還需要與之匹配的交換機、路由器、閘道器等等。
image.png

1.3 容器技術

虛擬化技術的極致優化雖然已經極大解決了VM層虛擬化的額外開銷問題,但VM作業系統層的開銷是無法避免的,同時如今的Java應用大多都可以做到單程式部署,VM作業系統這一層的開銷顯得有一些浪費(當前,它換來了極強的隔離性和安全性)。容器技術構建於作業系統的支援,目前主要使用Linux作業系統,容器最有名的是Docker,它是基於Linux 的 cgroup 技術構建的。VM的體驗實在是太好了,所以容器的終極目標就是具備VM的體驗的同時還沒有VM作業系統層的開銷。在容器裡,執行top、free等命令時,我們只希望看到容器檢視,同時網路也是容器檢視,不得不排查問題需要抓包時可以僅抓容器網路的包。

目前阿里雲ECS 16GB記憶體的機型,實際上作業系統內看到的可用記憶體只有15GB,32GB的機型則只有30.75GB。容器沒有這個問題,因為實際上在容器上執行的任務僅僅是作業系統上的一個或多個程式而已。由於容器的這種邏輯隔離特性,所以不同企業的應用基本上是不太可能部署到同一個作業系統上的(即同一臺ECS)。
容器最影響效能的點是容器是否超賣、是否綁核、核分配的策略等等,以及前述的眾多知識點都會對效能有不小的影響。

通常企業核心應用都會要求綁核(即容器的多個vCPU的分佈位置是確定以及專用的,同時還得考慮Intel HT、AMD CCD/CCX、NUMA等問題),這樣效能的確定性才可以得到保證。

在Docker與VM之間,其實還存在別的更均衡的容器技術,大多數公司稱之為安全容器,它採用了硬體虛擬化來實現強隔離,但並不需要一個很重的VM作業系統(比如 Linux),取而代之的是一個非常輕的微核心(它僅支援實現容器所必須的部分核心功能,同時大多數工作會轉發給Host作業系統處理)。這個技術是雲廠商很想要的,這是他們售賣可靠FaaS(功能即服務,Function as a Service)的基礎。

2. 獲取效能資料

進行效能優化前,我們需要做的是收集到足夠、準確以及有代表性的效能資料,分析效能瓶頸,然後才能進行有效的優化。
評估一個應用的效能無疑是一件非常複雜的事,大多數情況下一個應用會有很多個介面,且同一個接品會因為入參的不同或者內部業務邏輯的不同帶來非常大的執行邏輯的變化,所以我們得首先想清楚,我們到底是要優化什麼業務場景下的效能(對於訂單來說,也許就是下單)。

在效能測試用例跑起來後,怎麼樣拿到我們想要的真實的效能資料就很關鍵了,因為觀測者效應的存在(指“觀測”這種行為對被觀測物件造成一定影響的效應,它在生活中極其常見),獲取效能資料的同時也會對被測應用產生或多或少的影響,所以我們需要深入的瞭解我們所使用的效能資料獲取工具的工作原理。

具體到Java上(其它語言也基本是類似的),我們想知道一個應用到底在做什麼,主要有兩種手段:

  1. Instrumentation(程式碼嵌入):指的是可以用獨立於應用程式之外的代理(Agent)程式來監測執行在JVM上的應用程式,包括但不限於獲取JVM執行時狀態,替換和修改類定義等。通俗點理解就是在函式的執行前後插程式碼,統計函式執行的耗時。瞭解基本原理後,我們大概會知道,這種方式對效能的影響是比較大的,函式越簡短執行的次數越多影響也會越大,不同它的好處也是顯而易見的:可以統計出函式的執行次數以及不漏過任何一個細節。這種方式一般用於應用早期的優化分析。
  2. Sampling(取樣):採用固定的頻率打斷程式的執行,然後拉取各執行緒的執行棧進行統計分析。取樣頻率的大小決定了觀測結果的最小粒度和誤差,一些執行次數較多的小函式可能會被統計的偏多,一些執行次數較少的小函式可能不會被統計到。主流的作業系統都會從核心層進行支援,所以這種方式對應用的效能影響相對較少(具體多少和取樣頻率強相關)。
    在效能資料裡,時間也是一個非常重要的指標,主要有兩類:
  3. CPU Time(CPU時間):佔用的CPU時間片的總和。這個時間主要用來分析高CPU消耗。
  4. Wall Time(牆上時間):真實流逝的時間。除了CPU消耗,還有資源等待的時間等等,這個時間主要用來分析rt(響應時間,Response time)。
    效能資料獲取方式+時間指標一共有四種組合方式,每一種都有它們的最適用的場景。不過需要記住,Java應用通常都需要至少5分鐘(這是一個經驗值,通常Server模式的JVM需要在方法執行5000~10000次後才會進行JIT編譯)的大流量持續測試才能使應用的效能達到穩定狀態,所以除非你要分析的是應用正在預熱時的效能,否則你需要等待5分鐘以上再開始收集效能資料。
    Linux系統上面也有不少非常好用的效能監控工具,如下圖:
    image.png

2.1 構造效能測試用例

通常我們都會分析一個或多個典型業務場景的效能,而不僅僅是某一個或多個API介面。比如對於雙11大促,我們要分析的是導購、交易、發優惠券等業務場景的效能。
好的效能測試用例的需要能夠反映典型的使用者和系統行為(並不是所有的。我們無法做到100%反映真實使用者場景,只能逐漸接近它),比如一次下單平均購買多少個商品(實際的用例裡會細分為:購買一個商品的佔比多少,二個商品的佔比多少,等等)、熱點售買商品的數量與成交佔比、使用者數、商品數、熱點庫存分佈、買家人均有多少張券等等。像淘寶的雙11的壓測用例,像這樣關鍵的引數會多達200多個。
實際執行時,我們期望測試用例是可以穩定持續的執行的(比如不會跑30分鐘下單發現庫存沒了,優惠券也沒了),快取的命中率、DB流量等等的外部依賴也可以達到一個穩定狀態,在秒級時間(一般不需要更細了)粒度上應用的效能也是穩定的(即請求的計算複雜度在時間粒度上是均勻分佈的,不會一會高一會低的來回抖動)。
為了配合效能測試用例的執行,有的時候還需要應用系統做一些相應的改造。比如對於會使用到快取的場景來說,剛開始命中率肯定是不高的,但跑了一會兒過後就會慢慢的變為100%,這顯然不是通常真實的情況,所以可能會配合寫一些邏輯,來讓快取命中率一直維持在某個特定值上。
綜上,一套好的效能測試用例是開展後續工作所必不可少的,值得我們在它上面花時間。

2.2 真實的測試環境

保持測試環境與其實環境的一致性是極其重要的,但是往往也是很難做到的,所以大多數網際網路公司的全鏈路壓測方案都是直接使用線上環境來做效能測試。如果我們無法做到使用線上環境來做效能測試,那麼就需要花上不少的精力來仔細對比我們所使用的環境與線上環境的差異,確保我們知道哪一些效能資料是可以值得相信的。
直接使用線上環境來做效能測試也並不是那麼簡單,這需要我們有一套整體解決方案來讓壓測流量與真實流量進行區分,一般都是在流量中加一個壓測標進行全鏈路的透傳。同時基本上所有的基礎元件都需要進行改造來支援壓測,主要有:

  1. DB:為業務表建立對應的壓測表來儲存壓測資料。不使用增加欄位做邏輯隔離的原因是容易把它們與正式資料搞混,同時也不便於單獨清理壓測資料。不使用新建壓測庫的原因是:一方面它違背了我們使用線上環境做效能壓測的基本考慮,另一方面也會導致應用端多了一倍的資料庫連線。
  2. 快取:為快取Key增加特殊的字首,如__yt_。快取大多數沒有表的概念,看起來就是一個巨大的Map儲存一樣,所以除了加固定字首並沒有太好的辦法。不過為了減少壓測資料的儲存成本,通常需要:1) 在快取client包中做一些處理來減少壓測資料的快取過期時間;2)快取控制檯提供專門清理壓測資料的功能。
  3. 訊息:在傳送、消費時透傳壓測標。儘量做到不需要業務團隊的開發同學感知,在訊息的內部結構中增加是否是壓測資料的標記,不需要業務團隊申請新的壓測專用的Topic之類。
  4. RPC:透傳壓測標。當然,HTTP、DUBBO等具體的RPC介面透傳的方案會是不同的。
  5. 快取、資料庫 client包:根據壓測標做請求的路由。這需要配合前面提到的具體快取、DB的具體實現方案。
  6. 非同步執行緒池:透傳壓測標。為了減少支援壓測的改造代價,通常都會使用ThreadLocal來儲存壓測標,所以當使用到非同步執行緒池的時候,需要記得帶上它。
  7. 應用內快取:做好壓測資料與正式資料的隔離。如果壓測資料的主鍵或者其它的唯一識別符號可以讓我們顯著的讓它與正式資料區分開來,也許不用做太多,否則我們也許需要考慮要麼再new一套快取、要麼為壓測資料加上一個什麼特別的字首。
  8. 自建任務:透傳壓測標。需要我們自行做一些與前述提到的訊息元件類似的事情,畢竟任務和訊息從技術上來說是很像很像的。
  9. 二方、三方介面:具體分析與解決。需要看二方、三方介面是否支援壓測,如果支援那麼很好,我們按照對方期望的方式進行引數的傳遞即可,如果不支援,那麼我們需要想一些別的奇技婬巧(比如開設一個壓測專用的商戶、賬戶之類)了。
    為了降低效能測試對使用者的影響,通常都會選擇流量低峰時進行,一般都是半夜。當然,如果我們能有一套相對獨立的折中方案,比如使用小得物環境、在支援單元化的系統中使用部分單元等等,就可以做到在任何時候進行效能測試,實現效能測試的常態化。

2.3 JProfiler的使用

JProfiler是一款非常成熟的產品,很貴很好用,它是專門為Java應用的效能分析所準備的,而且是跨平臺的產品,是我經常使用的工具。它的大體的架構如下圖所示,Linux agent加上Windows UI是最推薦的使用方式,它不但同時支援Instrumentation & Sampling,CPU Time & Wall Time的選項,而且還擁有非常易用的圖形介面。
image.png
分析時,我們只需要將其agent包上傳到應用中的某個目錄中(如:/opt/jprofiler11.1.2),然後新增JVM的啟動選項來載入它,我通常都這樣配置:

-agentpath:/opt/jprofiler11.1.2/bin/linux-x64/libjprofilerti.so=port=8849

接下來我們重啟應用,這裡的修改就會生效了。使用這個配置,Java程式在開始啟動時需要等待JProfiler UI的連線才會繼續啟動,這樣我們可以進行應用啟動時效能的分析了。
JProfiler的功能很多,就不一一介紹了,大家可以閱讀其官方文件。採集的效能資料還可以儲存為*.jps檔案,方便後續的分析與交流。其典型的分析介面如下圖所示:
image.png
JProfiler的一些缺點:
1)需要在Java應用啟動載入agent(當然它也有啟動後attach的方式,但是有不少的限制),不太便於短時間的分析一些緊急的效能問題;
2)對Java應用的效能影響偏大。使用取樣的方式來採集效能資料開銷肯定會低很多,但還是沒有接下來要介紹的perf做的更好。

2.4 perf的使用

perf是Linux上面當之無愧的效能分析工具的一哥,這一點需要特別強調一下。不但可以用來分析Linux使用者態應用的效能,甚至還常用來分析核心的效能。它的模組結構如下圖所示:
image.png

想像一下這樣的場景,如果我們換了一家雲廠商,或者雲廠商的伺服器硬體(主要就是CPU了)有了更新迭代,我們想知道具體效能變化的原因,有什麼辦法嗎?perf就能很好的勝任這個工作。

CPU的設計者為了幫助我們分析應用執行時的效能,專門設計了相關的硬體電路,PMU(效能監控單元,Performance Monitor Unit)就是這其中最重要的部分。簡單來說裡面包含了很多效能計數器(圖中的PMCs,Performance Monitor Counters),perf可以讀取這些資料。不僅如此,核心層面還提供了很多軟體級別的計數器,perf同樣可以讀取它們。一些和CPU架構相關的關鍵指標,可以瞭解一下:

  1. IPC(每週期執行指令條數,Instruction per cycle):基於功耗/效能的考慮,大多數伺服器處理器的頻率都在2.5~2.8GHz的範圍,這代表同一時間片內的週期數是差異不大的,所以單個週期能夠執行的指令條數越多說明我們的應用優化的越好。過多的跳轉指令(即if else這類程式碼)、浮點計算、記憶體隨機訪問等操作顯然是非常影響IPC的。有的人比較喜歡說CPI(Cycle per instruction),它是IPC的倒數。
  2. LLC Cache Miss(最後一級快取丟失):偏記憶體型的應用需要關注這個指標,過大的話代表我們沒有利用好處理器或作業系統的快取預載入機制。
  3. Branch Misses(預測錯誤的分支指令數):這個值過高代表了我們的分支類程式碼設計的不夠友好,應該做一些調整儘量滿足處理器的分支預測演算法的期望。如果我們的分支邏輯依賴於資料的話,做一些資料的調整一樣可以提高效能(比如這個經典案例:資料有100萬個元素,值在0-255之間,需要統計值小於128的元素個數。提前對陣列排序再進行for迴圈判斷會執行的更快)。

因為perf是為Linux上的原生應用準備的,所以直接使用它分析Java應用程式的話,它只會把Java程式當成一個普通的C++程式來看待,不能顯示出Java的呼叫棧和符號資訊。好訊息是perf-map-agent外掛專案解決了這個問題,這個外掛可以匯出Java的符號資訊並幫助perf進行Java執行緒的棧回溯,這樣我們就可以使用perf來分析Java應用程式的效能了。執行 perf top -p <Java程式Id> 後,就可以看到perf顯示的實時效能統計資訊了,如下圖:

image.png

perf僅支援取樣 + CPU Time的工作模式,不過它的效能非常好,進行普通的Java效能分析任務時通常只會引入5%以內的額外開銷。使用環境變數PERF_RECORD_FREQ來設定取樣頻率,推薦值是999。不過如你所見,它是標準的Linux命令列式的互動行為,不是那麼方便。同時雖然他是可以把效能資料錄製為檔案供後續繼續分析的,但要記得同時儲存Java程式的符號檔案,不然你就無法檢視Java的呼叫棧資訊了。雖然限制不少,但是perf卻是最適合用來即時分析線上效能問題的工具,不需要任何前期的準備,隨時可用,同時對線上效能的影響也很小,可以很快的找到效能瓶頸點。在安裝好perf(需要sudo許可權)以及perf-map-agent外掛後,通常使用如下的命令來開啟它:

export PERF_RECORD_SECONDS=5 && export PERF_RECORD_FREQ=999
./perf-java-report-stack <Java程式pid>

重點需要介紹的資訊就是這麼多,實踐過程中需要用好perf的話需要再查閱相關的一些文件。

2.5 核心態與使用者態

對作業系統有了解的同學會經常聽到這兩個詞,也都知道經常在核心態與使用者態之間互動是非常影響效能的。從執行層面來說,它是處理器的設計者設計出來構建如今穩定的作業系統的基礎。有了它,使用者態(x86上面是ring3)程式無法執行特權指令與訪問核心記憶體。大多數時候為了安全,核心也不能把某部分核心記憶體直接對映到使用者態上,所以在進行核心呼叫時,需要先將那部分引數寫入到特定的傳參位置,然後核心再從這裡把它想要的內容複製走,所以多會一次記憶體的複製開銷。你看到了,核心為了安全,總是小心翼翼的面對每一次的請求。

在Linux上,TCP協議支援是在核心態實現的,曾經這有很多充分的理由,但核心上的更新迭代速度肯定是慢於如今網際網路行業的要求的,所以QUIC(Quick UDP Internet Connection,谷歌制定的一種基於UDP的低時延的網際網路傳輸層協議)誕生了。如今主流的發展思路是能不用核心就不用核心,儘量都在使用者態實現一切。
有一個例外,就是搶佔式的執行緒排程在使用者態做不到,因為實現它需要的定時時鐘中斷只能在核心態設定和處理。協程技術一直是重IO型Java應用減少核心排程開銷的極好的技術,但是很遺憾它需要執行執行緒主動讓出剩餘時間片,不然與核心執行緒關聯的多個使用者態執行緒就可能會餓死。阿里巴巴的Dragonwell版JVM還嘗試了動態調整策略(即使用者態執行緒不與固定的核心態執行緒關聯,在需要時可以切換),不過由於前述的時鐘中斷的限制,也不能工作的很好。

包括如今的虛擬化技術,尤其是SR-IOV技術,只需要核心參與介面分配/回收的工作,中間的通訊部分完全是在使用者態完成的,不需要核心參與。所以,如果你發現你的應用程式在核心態上耗費了太多的時間,需要想一想是否可以讓它們在使用者態完成。

2.6 JVM關鍵指標

JVM的指標很多,但有幾個關鍵的指標需要大家經常關注。

  1. GC次數與時間:包括Young GC、Full GC、Concurrent GC等等,Young GC頻率過高往往代表過多臨時物件的產生。
  2. Java堆大小:包括整個Java堆的大小(由Xmx、Xms兩個引數控制),年輕代、老年代分別的大小。不同時指定Xms和Xmx很可能會讓你的Java程式一直使用很小的堆空間,過大的老年代空間大多數時候也意味著記憶體的浪費(多分配一些給年輕代將顯著降低Young GC頻率)。
  3. 執行緒數:通常我們採用的都是4C8G(4Core vCPU,8GB記憶體)、8C16G的機型,分配出上千個執行緒大多數時候都是錯誤的。
  4. Metaspace大小和使用率:不要讓JVM動態的擴充套件元空間的大小,儘量通過設定MetaspaceSize、MaxMetaspaceSize讓它固定住。我們需要知道我們的應用到底需要多少元空間,過多的元空間佔用以及過快的增長都意味著我們可能錯誤的使用了動態代理或指令碼語言。
  5. CodeCache大小和使用率:同樣的,不要讓JVM動態的擴充套件程式碼快取的大小,儘量通過設定InitialCodeCacheSize、ReservedCodeCacheSize讓它固定住。我們可以通過它的變化來發現最近是不是又引入了新的類庫。
  6. 堆外記憶體大小:限制最大堆外記憶體的大小,計算好JVM各塊記憶體的大小,不要給作業系統觸發OOM Killer的機會。

2.7 瞭解JIT

位元組碼的解釋執行肯定是相當慢的,Java之所以這麼流行和他擁有高效能的JIT(即時,Just in time)編譯器也有很大的關係。但編譯過程本身也是相當消耗效能的,且由於Java的動態特性,也很難做到像C/C++這樣的程式語言提前編譯為native code再執行,這導致Java應用的啟動是相當慢的(大多數都需要3分鐘以上,Windows作業系統的啟動都不需要這麼久),而且同一個應用的多臺機器之間並不能共享JIT的經驗(這顯然是極大的浪費)。

我們使用的JVM都採用分層編譯的策略,根據優化的程度不同從低到高分別是C1、C2、C3、C4,C4是最快的。JIT編譯器會收集不少執行時的資料,來指導它的編譯策略,核心假設是可以逐步收集資訊、僅編譯熱點方法和路徑。但是這個假設並不總是對的,比如對於雙11大促的場景來說,我們的流量是到點突然垂直增加的,以及部分程式碼分支在某個時間點之前並不會執行(比如某種優惠要零點過後才會生效可用)。

2.7.1 編譯

極熱的函式通常JIT編譯器會函式進行內聯(inlining)優化,就相當於直接把程式碼抄寫到呼叫它的地方來減少一次函式呼叫的開銷,但是如果函式體過大的話(具體要看JVM的實現,通常是幾百位元組)將不能內聯,這也是為什麼程式設計規範裡面通常都會說不要將一個函式寫的過大的原因。

JVM並不會對所有執行過的方法都進行JIT優化,通常需要5000~10000次的執行後才進行,而且它還僅僅編譯那些曾經執行過的分支(以減少編譯所需要的時間和Code Cache的佔用,優化CPU的執行效能)。所以在寫程式碼的時候,if程式碼後面緊跟的程式碼塊最好是較大概率會執行到的,同時儘量讓程式碼執行流比較固定。

阿里巴巴的Dragonwell版JVM新增了一些功能,可以讓JVM在執行時記錄編譯了哪些方法,再把它們寫入檔案中(還可以分發給應用叢集中別的機器),下次JVM啟動時可以利用這部分資訊,在第一次執行這些方法時就觸發JIT編譯,而不是在執行上千次以後,這會極大的提升應用啟動的速度以及啟動時CPU的消耗。不過動態AOP(Aspect Oriented Programming)程式碼以及lambda程式碼將不能享受這個紅利,因為它們執行時實際生成的函式名都是形如MethodAccessor$1586 這類以數字結尾的不穩定的名稱,這次是1586,下一次就不知道什麼了。

2.7.2 退優化

JIT編譯器的激進優化並不總是對的,如果它發現目前需要的執行流在以前的編譯中被省略了的話,它就會進行退優化,即重新提交該方法的編譯請求。在新的編譯請求完成之前,該方法很大可能是進行解釋執行(如果存在還未丟棄的低階編譯程式碼,比如C1,那麼就會執行C1的程式碼),加上編譯執行緒的開銷,這會導致短時間內應用效能的下降。在雙11大促這種場景下,也就是零點的高峰時刻,由於退優化的發生,導致應用的效能比壓測時有相當顯著的降低。

阿里巴巴的Dragonwell版JVM在這一塊也提供了一些選項,可以在JIT編譯時去除一些激進優化,以防止退優化的發生。當然,這會導致應用的效能有微弱的下降。

2.8 真實案例

在效能優化的實踐過程中,有一句話需要反覆深刻的理解:通過資料反映一切,而不是聽說或者經驗。

下面列舉一個在重構專案中進行優化的應用的效能比較資料,以展示如何利用我們前面說到的知識。這個應用是偏末端的應用,下游基本不再依賴其它應用。特別說明,【此案例非得物案例】,也不對應任何一個真實的案例。

應用容器:8C32G,Intel 8269CY處理器,8個處理器繫結到4個物理核的8個HT上。

老應用:12.177.126.52,12.177.126.141
新應用:12.177.128.150, 12.177.128.28
測試介面與流量
確認訂單優惠渲染 單機@1012QPS
下單確認優惠 單機@330QPS
下單核銷優惠 單機@416QPS

時間:2021-05-03

基礎資料

專案\資料老應用(收集時間 21:00)新應用(收集時間 21:15)
CPU43.44%(user:36.78%)45.04%(user:40.52%)
RT: 確認訂單優惠渲染5.2ms6.1ms
RT: 下單確認優惠7.5ms4.7ms
RT: 下單核銷優惠1.0ms1.3ms
GC頻次18.525
NetIn:11.4M Out:13.7MIn:7.3M Out:12.4M
Thread669(Daemon:554)789(Daemon:664)
每分鐘Java Exception51327324

快取訪問

操作\QPS老應用新應用用途
GET:935503825商品快取
GET:8604848券規則快取
GET:75811651365賣家快取
GET:811581280賣家全域性規則快取
PREFIX_GETS:6881425活動索引(新應用廢棄)
GET:6882282活動索引(新應用廢棄)
GET:1002121店鋪免息優惠快取
PREFIX_GETS:10073店鋪免息優惠快取
GET:41008店鋪某類優惠快取
GET:1008623942580卡券領取關係快取
GET:770100SKU優惠快取
PREFIX_GETS:77092SKU優惠快取
GET:882121商品限購快取

DB訪問

庫表\QPS老應用新應用用途
QUERY promotion_detail865優惠活動
KV_GET promotion_detail898
QUERY promotion_detail_sku21 SKU優惠活動
QUERY buyer_coupon190160優惠券
KV_GET buyer_coupon--56
UPDATE buyer_coupon5560優惠券
2.8.1 初步發現
  1. 通過外部依賴的差異可以發現,新老應用的程式碼邏輯會有不同,需要繼續深入評估差異是什麼。通常在做收集效能資料的同時,我們需要有一個簡單的分析和判斷,首先確保業務邏輯的正確性,不然效能資料就沒有多少意義。
  2. Java Exception是很消耗效能的,主要消耗在收集異常棧資訊,新老應用較大的異常數區別需要找到原因並解決。
  3. 新應用的介面“下單確認優惠”RT有過於明顯的下降,其它介面都是提升的,說明很可能存在執行路徑上較大的差別,也需要深入的分析。

3. 開始做效能優化

和獲取效能資料需要從底層逐步瞭解到上層業務不同,做效能優化卻是從上層業務開始,逐步推進到底層,即從高到低進行分層優化。越高層的優化往往難度更低,而且收益還越大,只是需要與業務的深度結合。越低層的優化往往難度比較大,很難獲得較大的收益(畢竟一堆技術精英一直在做著呢),但是通用性比較好,往往可以適用於多類業務場景。接下來分別聊一聊每一層可以思考的一些方向和實際的例子。

3.1 優化的目的與原則

在聊具體的優化措施之前,我們先聊一聊為什麼要做效能優化。多數情況下,效能優化的目的都是為了成本、效率與穩定。達到同樣的業務效果,使用更少的資源,或者帶來更好的使用者體驗(通常是指頁面的響應更快)。不怎麼考慮成本的技術方案往往沒有太多的挑戰,對於電商平臺來說,我們常常用單訂單成本來衡量機器成本,比如淘寶這個值可能在0.17元左右。業務發展的早期往往並不是那麼在意成本,反而更加看重效率,等到逐步成熟起來過後,會慢慢的開始重視成本,通俗的講就是開始比的是有沒有,然後比的是好不好。所以在不同的時期,我們進行效能優化的目的和方向會有所側重。

網際網路行業是一個快速發展的行業,研發效率對業務的健康發展是至關重要的,在進行優化的過程中,我們在技術方案的選擇上需要兼顧研發效率的提升(至少不能損害過多),給人一種“它本來應該就是這樣”的感覺,而不是做一些明顯無法長期持續、後期維護成本過高的設計。好的優化方案就像藝術品一樣,每一個看到的人都會為之讚歎。

3.2 業務

大家為什麼會在雙11的零點開始上各大電商網站買東西?春節前大家為什麼都在上午10點搶火車票?等等,其實都是業務上的設計。準備一大波機器資源使用2個月就為了雙11峰值的那幾分鐘,實際上是極大的成本浪費,所以為了不那麼浪費,淘寶的雙11預售付尾款的時間通常都放在凌晨1點。12306早幾年是每臨進春節必掛,因為想要回家的遊子實在是太多,所以後面慢慢按照車次將售賣時間打散,參考其公告:

自今年1月8日起,為避免大量旅客在網際網路排隊購票,把原來的8點、10點、12點、15點四個時間節點放票改為15個節點放票,即:8點-18點,其間每小時或每半小時均有部分新票起售。

這些策略都可以極大的降低系統的峰值流量,同時對於使用者使用體驗來說基本是無感的,諸如此類的許多優化是我們最開始就要去思考的(不過請記住永遠要把業務效果放在第一位,和業務講業務,而不是和業務講技術)。

3.3 系統架構

諸如商品詳情頁動靜分離(靜態頁面與動態頁面分開不同系統訪問),使用者介面層(即HTTP/S層)與後端(Java層)合併部署等等,都是架構優化的成功典範。業務架構師往往會將系統設計為很多層,但是在執行時,他們往往可以部署在一塊兒,以減少跨程式、跨機器、跨地域通訊。

淘寶的單元化架構在效能上來看也是一個很好的設計,一個交易請求幾乎所有的處理都可以封閉在單元內完成,減少了很多跨地域的網路長傳頻寬需求。

富客戶端方案對於像商品資訊、使用者資訊等基礎資料來說也是很好的方案,畢竟大多數情況下它們都是訪問Redis等快取,多一次到服務端的RPC請求總是顯得很多餘,當然,後續需要升級資料結構的時候則需要做更多的工作。

關於架構的討論是永恆的話題,同時不同的公司有不同的背景,實際進行優化時也需要根據實際情況來取捨。

3.4 呼叫鏈路

在分散式系統裡不可避免需要依賴很多下游服務才能完成業務動作,怎麼依賴、依賴什麼介面、依賴多少次則是需要深入思考的問題。藉助呼叫鏈檢視工具(在得物,這個工具應該是dependency),我們可以仔細分析每一個業務請求,然後去思考它是不是最優的方式。舉一個我聽說過的例子(特別說明,【此案例非得物案例】):

背景:營銷團隊接到了一個拉新的需求,它會在公司週年慶的10:00形成爆點,預計會產生最高30萬的UV,然後只需要點選活動頁的參與按鈕(預估轉化率是75%),就會彈出一個組團頁,讓使用者邀請他的好友參與組團,每多邀請一個朋友,在團內的使用者都可以享受更多的優惠折扣。

營銷團隊為組團頁提供了一個新的後臺介面,最開始這個介面需要完成這些事:
image.png

在“為使用者建立一個新團”這一步,會同時將新團的資訊持久化到資料庫中,按照業務的轉化率預估,這會有30W*75%=22.5W QPS的峰值流量,基本上我們需要10個左右的資料庫例項才能支撐這麼高的併發寫入。但這是必要的嗎?顯然不是,我們都知道這類需要使用者轉發的活動的轉化率是有多麼的低(大多數不到8%),同時對於那些沒人蔘與的團,將它們的資訊儲存在資料庫中也是意義不大的。最後的優化方案是:

1)在“為使用者建立一個新團”時,僅將團資訊寫入Redis快取中;
2)在使用者邀請的朋友同意參團時,再將團資訊持久化到資料庫中。新的設計,僅僅需要1.8W QPS的資料庫併發寫入量,使用原有的單個資料庫例項就可以支撐,在業務效果上也沒有任何區別。

除了上述的例子,鏈路優化還有非常多的方法,比如多次呼叫的合併、僅在必要時才呼叫(類似COW [Copy on write]思想)等等,需要大家結合具體的場景去分析設計。

3.5 應用程式碼

應用程式碼的優化往往是我們最熱衷和擅長的,畢竟業務與系統架構的優化往往需要架構師出馬。JProfiler或者perf的剖析(Profiling)資料是非常有用的參考,任何不基於實際執行資料的猜測往往會讓我們誤入歧途,接下來我們需要做的大多數時候都是“找熱點 -> 優化” ,然後“找熱點 -> 優化”,然後一直迴圈。找熱點不是那麼難,難在準確的分析程式碼的邏輯然後判斷到底它應該消耗多少資源(通常都是CPU),然後制定優化方案來達到目標,這需要相當多的優化經驗。
從我做過的效能優化來總結,大概主要的問題都發生在這些地方:

  1. 和字串過不去:非常多的程式碼喜歡將多個Java變數使用StringBuilder拼接起來(那些連StringBuilder都不會用,只會使用 + 的傢伙就更讓人頭疼了),然後再找準時機spilt成多個String,然後再轉換成別的型別(Long等)。好吧,下次使用StringBuilder時記得指定初始的容量大小。
  2. 日誌滿天飛:管它有用沒有,反正打了是不會錯的,畢竟誰都經歷過沒有日誌時排查問題的痛苦。怎麼說呢,列印有用的、有效的日誌是程式設計師的必修課。
  3. 喜愛Exception:不知道是不是某些Java的追隨者吹過頭了,說什麼Java的Exception和C/C++的錯誤碼一樣高效。然而事實並不是這樣的,Exception進行呼叫棧的回溯是相當消耗效能的,尤其是還需要將它們列印在日誌中的時候,會更加糟糕。
  4. 容器的深拷貝:List、HashMap等是大家非常喜歡的Java容器,但Java語言並沒有好的機制阻止別人修改它,所以大家常常深拷貝一個新的出來,反正也就是一句程式碼的事兒。
  5. 對JSON情有獨鍾:將物件序列化為JSON string,將JSON string反序列化為物件。前者主要用來打日誌,後者主要用來讀配置。JSON是挺好,只是請別用的到處都是(還把每個屬性的名字都取的老長)。
  6. 重複重複再重複:一個請求裡查詢同樣的商品3次,查詢同樣的使用者2次,查詢同樣的快取5次,都是常有的事,也許多查詢幾次一致性更好吧 :(。還有一些基本不會變的配置,也會放到快取中,每次使用的時候都會從快取中讀出來,反序列化,然後再使用,嗯,挺重的。
  7. 多執行緒的樂趣:不會寫多執行緒程式的開發不是好開發,所以大家都喜歡new執行緒池,然後非同步套非同步。在流量很低的時候,看起來多執行緒的確解決了問題(比如RT的確變小了),但是流量上來過後,問題反而惡化了(畢竟我們主流的機器都是8核的)。
    再重申一遍,在這一步,找到問題並不是太困難,找到好的優化方案卻是很困難和充滿考驗的。

3.6 快取

大多數的快取都是Key、Value的結構,選擇緊湊的Key、Value以及高效的序列化、反序列化演算法是重中之重(二進位制序列化協議比文字序列化協議快的太多了)。還有的快取是Prefix、Key、Value的結構,主要的區別是誰來決定實際的資料路由到哪臺伺服器進行處理。單條快取不能太大,基本上大於64KB就需要小心了,因為它總是由某一臺實際的伺服器在處理,很容易將出口寬頻或計算效能打滿。

3.7 DB

資料庫比起快取來說他能抗的流量就低太多了,基本上是差一個數量級。SQL通訊協議雖然很易用,但實際上是非常低效的通訊協議。關於DB的優化,通常都是從減少寫入量、減少讀取量、減少互動次數、進行批處理等等方面著手。DB的優化是一門複雜的學問,很難用一篇文章說清楚,這裡僅舉一些我認為比較有代表性的例子:

  1. 使用MultiQuery減少網路互動:MySQL等資料庫都支援將多條SQL語言寫到一起,一起傳送給DB伺服器,這會將多次網路互動減少為一次。
  2. 使用BatchInsert代替多次insert:這個很常見。
  3. 使用KV協議取代SQL:阿里雲資料庫團隊在資料庫伺服器上面外掛了一個KV引擎,可以直接讀取InnoDB引擎中的資料,bypass掉了資料庫的資料層,使得基於唯一鍵的查詢可以比使用SQL快10倍。
  4. 與業務結合:淘寶下單時可以同時使用多達10個紅包,這意味著一次下單需要傳送至多10次update SQL。假設一次下單使用了N個紅包,基於對業務行為的分析,會發現前N-1個都是全額使用的,最後一個可能會部分使用。對於使用完的紅包,我們可以使用一條SQL就完成更新。
update red_envelop set balance = 0 where id in (...);
  1. 熱點優化:庫存的熱點問題是每個電商平臺都面臨的問題,使用資料庫來扣減庫存肯定是可靠性最高的方案,但是基本上都很難突破500tps的瓶頸。阿里雲資料庫團隊設計了新的SQL hint,配合上第1條說的MultiQuery技術,與資料庫進行一次互動就可以完成庫存的扣減。同時加上資料庫核心的針對性優化,已經可以實現8W tps的熱點扣減能力。下表中的commit_on_success用來表明,如果update執行成功就立即提交,這樣可以讓庫存熱點行的鎖佔用時間降到最低。target_affect_row(1)以及rollback_on_fail用來限制當庫存售罄時(即inv_count - 1 >= 0不成立)update執行失敗並回滾整個事務(即前面插入的庫存流水作廢)。
insert 庫存扣減流水;
update /* commit_on_success rollback_on_fail target_affect_row(1) */ inventory 
set inv_count = inv_count - 1 
where inv_id = 11222 and inv_count - 1 >= 0;

3.8 執行環境

我們的程式碼是執行在某個環境中的,這個環境有很多知識是我們需要了解的,如果上面所有的優化完成後還不能滿足要求,那麼我們也不得不向下深入。這可能是一個困難的過程,但也會是一個有趣的過程,因為你終於有了和各領域的大佬們交流討論的機會。

3.8.1 中介軟體

目前大多數中介軟體的程式碼是和我們的業務程式碼執行在一起的,比如監控採集、訊息client、RPC client、配置推送 client、DB連線元件等等。如果你發現這些元件的效能問題,那麼可以大膽的提出來,不要害怕傷害到誰 :)。
我遇到過這樣的一些場景:

  1. 應用偶爾會大量的發生ygc:排查到的原因是,在我們依賴的服務發生地址列表變化(比如發生了重啟、掉線、擴容等場景)時,RPC client會接收到大量的推送,然後解析這些推送的資訊,然後再更新一大堆記憶體結構。提出的優化建議是:

    1)地址推送從全量推送改變為增量推送;
    2)地址列表從掛接到服務介面維度更改為掛接到應用維度。
  2. DB連線元件過多的字串拼接:DB連線元件需要進行SQL的解析來計算分庫分表等資訊,但實現上面不夠優雅,拼接的字串過多了,導致執行SQL時記憶體消耗過多。
3.8.2 容器

關於容器技術本身大多數時候我們做不了什麼,往往就是儘量採用最新的技術(比如使用阿里雲的神龍伺服器什麼的),不過在梆核(即容器排程)方面往往可以做不少事。我們的應用和誰執行在一起、相互之間有資源爭搶嗎、有沒有跨NUMA排程、有沒有資源超賣等等問題需要我們關注(當然,這需要容器團隊提供相應的檢視工具)。這裡主要有兩個需要考慮的點:

  1. 是不是支援離線上混部:線上任務要求實時響應,而離線任務的執行又需要耗費非常多的機器。在雙11大促這樣的場景,把離線機器借過來用幾個小時就可以減少相應的線上機器採購,能省下很多錢。
  2. 基於業務的排程:把高消耗的應用和低消耗的應用部署在一起,同時如果雙方的峰值時刻還不完全相同,那就太美妙啦。
3.8.3 JVM

為了解決重IO型應用執行緒過多的問題開發了協程。
為了解決Java容器過多小物件的問題(如HashMap的K, V都只能是包裝型別)開發了值容器。

為了解決Java堆過大時GC時間過長的問題(當然還有覺得Java的記憶體管理不夠靈活的原因)開發了GCIH(GC Invisible Heap,淘寶雙11期間部分熱點優惠活動的資料都是存在GCIH當中的)。

為了解決Java啟動時的效能問題(即程式碼要跑好幾千次才進行JIT,而且每次啟動都還要重複這個過程)開發了啟動Hint功能。

為了解決業務峰值時刻JIT退優化的問題(即平時不使用的程式碼執行路徑在業務峰值時候需要使用,比如0點才生效的優惠)開發了JIT編譯激進優化去除選項。

雖然目前JVM的實現就是你知道的那樣,但是並不代表這樣做就一直是合理的。

3.8.4 作業系統

基本上我們都是使用Linux作業系統,新版本的核心通常會帶來一些新功能和效能的提升,同時作業系統還需要為支撐容器(即Docker等)做不少事情。對Host作業系統來說,開啟透明大頁、配置好網路卡中斷CPU打散、配置好NUMA、配置好NTP(Network Time Protocol)服務、配置好時鐘源(不然clock_gettime可能會很慢)等等都是必要的。還有就是需要做好各種資源的隔離,比如CPU隔離(高優先順序任務優先排程、LLC隔離、超執行緒技術隔離等)、記憶體隔離(記憶體寬頻、記憶體回收隔離避免全域性記憶體回收)、網路隔離(網路寬頻、資料包金銀銅等級劃分)、檔案IO隔離(檔案IO寬頻的上限與下限、特定檔案操作限制)等等。

大多數核心級別的優化都不是我們能做的,但我們需要知道關鍵的一些影響效能的核心引數,並能夠理解大多數核心機制的工作原理。

3.9 硬體

通常我們都是使用Intel的x86架構的CPU,比如我們正在使用的Intel 8269CY,不過它的單顆售價得賣到4萬多塊人民幣,卻只有區區26C52T(26核52執行緒)。相比之下,AMD的EPYC 7763的規格就比較牛逼了(64C128T,256MB三級快取,8通道 DDR4 3200MHz記憶體,擁有204GB/s的超高記憶體寬頻),但卻只要3萬多一顆。當然,用AMD 2021年的產品和Intel 2019的產品對比並不是太公平,主要Intel 2021的新品Intel Xeon Platinum 8368Q處理器並不爭氣,僅僅只是提升到了38C76T而已(雖然和自家的上一代產品相比已經大幅提升了近50%)。

除了x86處理器,ARM 64位處理器也在向服務端產品發力,而且這個產業鏈還可以實現全國產化。華為2019年初發布的鯤鵬920-6426處理器,採用7nm工藝,具備64個CPU核,主頻2.6GHz。雖然單核效能上其只有Intel 8269CY的近2/3,但是其CPU核數卻要多上一倍還多,加上其售價親民,同樣計算能力的情況下CPU部分的成本會下降近一半(當然計算整個物理機成本的話其實下降有限)。

2020年雙11開始,淘寶在江蘇南通部署了支撐1萬筆/s交易的國產化機房,正是採用了鯤鵬920-6426處理器,同時在2021年雙11,更是用上了阿里雲自主研發的倚天710處理器(也是採用ARM 64位架構)。在未來,更是有可能基於RISC-V架構設計自己的處理器。這些事實都在說明,在處理器的選擇上,我們還是有不少空間的。
除了採用通用處理器,在一些特殊的計算領域,我們還可以採用專用的晶片,比如:使用GPU加速深度學習計算,在AI推理時使用神經網路加速晶片-含光NPU,以及使用FPGA晶片進行高效能的網路資料處理(阿里雲神龍伺服器上使用的神龍MOC卡)等等。

曾經還有人想過設計可以直接執行Java位元組碼的處理器,雖然最終因為複雜度太高而放棄。

這一切都說明,硬體也是一直在根據使用場景在不斷的進化之中的,永遠要充滿想像。

相關文章