日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關咨詢
選擇下列產(chǎn)品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
怎么做好Java性能優(yōu)化

引言

性能優(yōu)化是一個很復雜的工作,且充滿了不確定性。

成都創(chuàng)新互聯(lián)是一家集網(wǎng)站建設,柳河企業(yè)網(wǎng)站建設,柳河品牌網(wǎng)站建設,網(wǎng)站定制,柳河網(wǎng)站建設報價,網(wǎng)絡營銷,網(wǎng)絡優(yōu)化,柳河網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強企業(yè)競爭力??沙浞譂M足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時我們時刻保持專業(yè)、時尚、前沿,時刻以成就客戶成長自我,堅持不斷學習、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實用型網(wǎng)站。

它不像Java業(yè)務代碼,可以一次編寫到處運行(write once, run anywhere),往往一些我們可能并不能察覺的變化,就會帶來驚喜/驚嚇。

能夠全面的了解并評估我們所負責應用的性能,我認為是提升技術(shù)確定性和技術(shù)感知能力的非常有效的手段。

本文盡可能簡短的總結(jié)我自己在性能優(yōu)化上面的一些體會和經(jīng)驗,從實踐的角度出發(fā)盡量避免過于啰嗦和生硬,但相關的知識實在太多,受限于個人經(jīng)驗和技術(shù)深度,不足之外還請大家補充。

  • 第1部分是偏背景類知識的介紹,有這方面知識的同學可以直接跳過。

一. 了解運行環(huán)境

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

1.1 服務器

目前我們大量使用Intel 64位架構(gòu)的Xeon處理器,除此之外還會有AMD x64處理器、ARM服務器處理器(如:華為鯤鵬、阿里倚天)、未來還會有RISC-V架構(gòu)的處理器、以及一些專用FPGA芯片等等。

我們這里主要聊聊目前我們大量使用的阿里云ECS使用的Intel 8269CY處理器。

1.1.1 處理器

Intel Xeon Platinum 8269CY,阿里云使用的這一款處理器是阿里定制款,并不能在Intel的官方手冊中查詢到,不過我們可以通過下方Intel處理器的命名規(guī)則了解到不少的信息,它是一款這樣的處理器:主頻2.5GHz(睿頻3.2GHz、最大睿頻3.8GHz),26核心52線程(具備超線程技術(shù)),6通道DDR4-2933內(nèi)存,最大配置內(nèi)存1T,Cascade Lake微架構(gòu),48通道PCI-E 3.0,14nm光刻工藝,205W TDP。

它是這一代至強處理器中性能比較強的型號了,最大支持8路部署。

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

最大配置內(nèi)存1TB,代表處理器具備48bit的VA(虛擬地址),也就是通常需要四級頁表(下一代具備57bit VA的處理器已經(jīng)在設計中了,通常需要五級頁表),過深的頁表顯然是極大的影響內(nèi)存訪問的性能以及占用內(nèi)存(頁表也是存儲在內(nèi)存中的)的,所以Intel設計了大頁(2MB、1GB大頁)機制,以減少過深的頁表帶來的影響。

6通道2933MHz的內(nèi)存總線代表它具備總計約137GB/s(內(nèi)存總線是64bit位寬)的寬帶,不過需要記住他們是高度并行設計的。

這個微處理的CPU核架構(gòu)如下圖所示,采用8發(fā)射亂序架構(gòu), 32KB指令+32KB數(shù)據(jù) L1 Cache,1MB L2 Cache。

發(fā)射單元是CPU內(nèi)部真正的計算單元,它的多少是CPU性能的關鍵因素。8個發(fā)射單元中有4個單元都可以進行基本整數(shù)運算(ALU單元),只有2個可以進行整數(shù)乘除和浮點運算,所以對于大量浮點運算的場景并行效率會偏低。

1個CPU核對應的2個HT(這里指超線程技術(shù)虛擬出的硬件線程)是共享8個發(fā)射單元的,所以這兩個HT之間將會有非常大的相互影響(這也會導致操作系統(tǒng)內(nèi)CPU的使用率不再是線性值,具體請查閱相關資料),L1、L2 Cache同樣也是共享的,所以也會相互影響。

Intel Xeon處理器在分支預測上面花了很多功夫,所以在較多分支代碼(通常就是if else這類代碼)時性能往往也能做的很好,比大多數(shù)的ARM架構(gòu)做的都要好。

Java常用的指針壓縮技術(shù)也受益于x86架構(gòu)靈活的尋址能力(如:mov eax, ecx * 8 + 8),可以一條指令完成,同時也不會帶來性能的損失,但是這在ARM、RISC-V等RISC(精簡指令集架構(gòu),Reduced Instruction Set Computing)處理器上就不適用了。

從可靠渠道了解道,下一代架構(gòu)(Sunny Cove)將大幅度的進行架構(gòu)優(yōu)化,升級為10發(fā)射,同時L1 Cache將數(shù)據(jù)部分增加到48KB,這代表接下來的處理器將更加側(cè)重于提升SIMD(單指令多操作數(shù),Signle Instruction Multiple Data)等的數(shù)據(jù)計算性能。

一顆8269CY內(nèi)部有26個CPU核,采用如下的拓撲結(jié)構(gòu)進行連接。這一代處理器最多有28個CPU核,8269CY屏蔽掉了2個核心,以降低對產(chǎn)品良率的要求(節(jié)約成本)。

可以看到總計35.75MB的L3 Cache(圖中為LLC: Last Level Cache)被分為了13塊(圖中是14塊,屏蔽2個核心的同時也屏蔽了與之匹配的L3 Cache),每塊2.75MB,并不是簡單意義理解上的是一大塊統(tǒng)一的區(qū)域。

6通道的內(nèi)存控制器也分布在左右兩側(cè),與實際主板上內(nèi)存插槽的位置關系是對應的。

這些信息都告訴我們,這是一顆并行能力非常強的多核心處理器。

1.1.2 服務器

阿里云通常都是采用雙Intel處理器的2U機型(基于散熱、密度、性價比等等的考慮),基本都是2個NUMA(非一致性內(nèi)存訪問,Non Uniform Memory Access)節(jié)點。

具體到Intel 8269CY,代表一臺服務器具備52個物理核心,104個硬件線程,通常阿里云會稱之為104核。

NUMA技術(shù)的出現(xiàn)是硬件工程師的妥協(xié)(他們實在沒有能力做到在多CPU的情況下還能實現(xiàn)訪問任何地址的性能一致性),所以做的不好也會嚴重的降低性能,大多數(shù)情況下虛擬機/容器調(diào)度團隊要做的是將NUMA打開,同時將一個虛擬機/容器部署到同一個NUMA節(jié)點上。

這幾年AMD的發(fā)展很好,它的多核架構(gòu)與Intel有很大的不同,不久的將來阿里云將會部署不少采用AMD處理器的機型。

AMD處理器的NUMA節(jié)點將會更多,而且拓撲關系也會更復雜,阿里自研的倚天(采用ARM架構(gòu))就更復雜了。這意味著虛擬機/容器調(diào)度團隊夠得忙了。

多數(shù)情況下服務器大都采用CPU:內(nèi)存為1:2或1:4的配置,即配置雙Intel 8269CY的物理機,通常都會配備192GB或384GB的內(nèi)存。

如果虛擬機/容器需要的內(nèi)存:CPU過大的情況下,將很難實現(xiàn)內(nèi)存在CPU對應的NUMA節(jié)點上就近分配了,也就是說性能就不能得到保證。

由于2U機型的物理高度是1U機型的2倍,所以有更多的空間放下更多的SSD盤、高性能PCI-E設備等。

不過云廠商肯定是不愿意直接將物理機賣給用戶(畢竟他們已經(jīng)不再是以前的托管物理機公司)的,再怎么也得在上面架一層,也就是做成ECS再賣給客戶,這樣諸如熱遷移、高可用等功能才能實現(xiàn)。

前述的"架一層"是通過虛擬化技術(shù)來實現(xiàn)的。

1.1.3 虛擬化技術(shù)

一臺物理機性能很強大,通常我們只需要里面的一小塊,但我們又希望不要感知到其他人在共享這臺物理機,所以催生了虛擬化技術(shù)(簡單來說就是讓可以讓一個CPU工作起來就像多個CPU并行運行,從而使得在一臺服務器內(nèi)可以同時運行多個操作系統(tǒng))。

早期的虛擬機技術(shù)是通過軟件實現(xiàn)的,老牌廠商如VMWare,但是性能犧牲的有點多,硬件廠商也看好虛擬機技術(shù)的前景,所以便有了硬件虛擬化技術(shù)。

各廠商的實現(xiàn)并不相同,但差異不是很大,好在有專門的虛擬化處理模塊去兼容就可以了。

Intel的虛擬化技術(shù)叫Intel VT(Virtualization Technology),它包括VT-x(處理器的虛擬化支持)、VT-d(直接I/O訪問的虛擬化)、VT-c(網(wǎng)絡連接的虛擬化),以及在網(wǎng)絡性能上的SR-IOV技術(shù)(Single Root I/O Virtualization)。

這里面一個很重要的事情是,原本我們訪問內(nèi)存的一層轉(zhuǎn)換(線性地址->物理地址)會變成二層轉(zhuǎn)換(VM內(nèi)線性地址->Host線性地址->物理地址),這會引入更多的內(nèi)存開銷以及頁表的轉(zhuǎn)換工作。

所以大多數(shù)云廠商會在Host操作系統(tǒng)上開啟大頁(Linux 操作系統(tǒng)通常是使用透明大頁技術(shù)),以減少內(nèi)存相關的虛擬化開銷。

服務器對網(wǎng)絡性能的要求是很高的,現(xiàn)在的網(wǎng)絡硬件都支持網(wǎng)卡多隊列技術(shù),通常情況下需要將VM中的網(wǎng)絡中斷分散給不同的CPU核來處理,以避免單核轉(zhuǎn)發(fā)帶來的性能瓶頸。

Host操作系統(tǒng)需要管理它上面的一個或多個VM(虛擬機,Virtual Machine),以及前述提及的處理網(wǎng)卡中斷,這會帶來一定的CPU消耗。

阿里云上一代機型,服務器總計是96核(即96個硬件線程HT,實際是48個物理核),但最多只能分配出88核,需要保留8個核(相當于物理機CPU減少8.3%)給Host操作系統(tǒng)使用,同時由于I/O相關的虛擬化開銷,整機性能會下降超過10%。

阿里云為了最大限度的降低虛擬化的開銷,研發(fā)了牛逼的“彈性裸金屬服務器 - 神龍”,號稱不但不會因為虛擬化降低性能,反而會提升部分性能(主要是網(wǎng)絡轉(zhuǎn)發(fā))。

1.1.4 神龍服務器

為了避免虛擬化對性能的影響,阿里云(類似還有亞馬遜等云廠商的類似方案)研發(fā)了神龍服務器。

簡單來說就是設計了神龍MOC卡,將大部分虛擬機管理工作、網(wǎng)絡中斷處理等從CPU offload到MOC卡進行處理。

神龍MOC卡是一塊PCI-E 3.0設備,其內(nèi)有專門設計的用于網(wǎng)絡處理的FPGA芯片,以及2顆低功耗的x86處理器(據(jù)傳是Intel Atom),最大限度的接手Host操作系統(tǒng)的虛擬化管理工作。

通過這樣的設計,在網(wǎng)絡轉(zhuǎn)發(fā)性能上甚至能做到10倍于裸物理機,做到了當之無愧的裸金屬。

104核的物理機可以直接虛擬出一臺104核的超大ECS,再也不用保留幾個核心給Host操作系統(tǒng)使用了。

1.2 VPC

VPC(虛擬專有云,Virtual Private Cloud),大多數(shù)云上的用戶都希望自己的網(wǎng)絡與其它的客戶隔離,就像自建機房一樣,這里面最重要的是網(wǎng)絡虛擬化技術(shù),目前阿里云采用的是VxLAN協(xié)議,它底層采用UDP協(xié)議進行數(shù)據(jù)傳輸,整體數(shù)據(jù)包結(jié)構(gòu)如下圖所示。

VxLAN在VxLAN幀頭中引入了類似VLAN ID的網(wǎng)絡標識,稱為VxLan 網(wǎng)絡標識VNI(VxLAN Network ID),由24比特組成,理論上可支持多達16M的VxLAN段,從而滿足了大規(guī)模不同網(wǎng)絡之間的標識、隔離需求。

這一層的引入將會使原始的網(wǎng)絡包增加50 Bytes的固定長度的頭。當然,還需要與之匹配的交換機、路由器、網(wǎng)關等等。

1.3 容器技術(shù)

虛擬化技術(shù)的極致優(yōu)化雖然已經(jīng)極大解決了VM層虛擬化的額外開銷問題,但VM操作系統(tǒng)層的開銷是無法避免的,同時如今的Java應用大多都可以做到單進程部署,VM操作系統(tǒng)這一層的開銷顯得有一些浪費(當前,它換來了極強的隔離性和安全性)。

容器技術(shù)構(gòu)建于操作系統(tǒng)的支持,目前主要使用Linux操作系統(tǒng),容器最有名的是Docker,它是基于Linux 的 cgroup 技術(shù)構(gòu)建的。

VM的體驗實在是太好了,所以容器的終極目標就是具備VM的體驗的同時還沒有VM操作系統(tǒng)層的開銷。在容器里,執(zhí)行top、free等命令時,我們只希望看到容器視圖,同時網(wǎng)絡也是容器視圖,不得不排查問題需要抓包時可以僅抓容器網(wǎng)絡的包。

目前阿里云ECS 16GB內(nèi)存的機型,實際上操作系統(tǒng)內(nèi)看到的可用內(nèi)存只有15GB,32GB的機型則只有30.75GB。

容器沒有這個問題,因為實際上在容器上運行的任務僅僅是操作系統(tǒng)上的一個或多個進程而已。

由于容器的這種邏輯隔離特性,所以不同企業(yè)的應用基本上是不太可能部署到同一個操作系統(tǒng)上的(即同一臺ECS)。

容器最影響性能的點是容器是否超賣、是否綁核、核分配的策略等等,以及前述的眾多知識點都會對性能有不小的影響。

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

在Docker與VM之間,其實還存在別的更均衡的容器技術(shù),大多數(shù)公司稱之為安全容器,它采用了硬件虛擬化來實現(xiàn)強隔離,但并不需要一個很重的VM操作系統(tǒng)(比如 Linux),取而代之的是一個非常輕的微內(nèi)核(它僅支持實現(xiàn)容器所必須的部分內(nèi)核功能,同時大多數(shù)工作會轉(zhuǎn)發(fā)給Host操作系統(tǒng)處理)。

這個技術(shù)是云廠商很想要的,這是他們售賣可靠FaaS(功能即服務,F(xiàn)unction as a Service)的基礎。

二. 獲取性能數(shù)據(jù)

進行性能優(yōu)化前,我們需要做的是收集到足夠、準確以及有代表性的性能數(shù)據(jù),分析性能瓶頸,然后才能進行有效的優(yōu)化。

評估一個應用的性能無疑是一件非常復雜的事,大多數(shù)情況下一個應用會有很多個接口,且同一個接品會因為入?yún)⒌牟煌蛘邇?nèi)部業(yè)務邏輯的不同帶來非常大的執(zhí)行邏輯的變化,所以我們得首先想清楚,我們到底是要優(yōu)化什么業(yè)務場景下的性能(對于訂單來說,也許就是下單)。

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

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

  • Instrumentation(代碼嵌入):

指的是可以用獨立于應用程序之外的代理(Agent)程序來監(jiān)測運行在JVM上的應用程序,包括但不限于獲取JVM運行時狀態(tài),替換和修改類定義等。

通俗點理解就是在函數(shù)的執(zhí)行前后插代碼,統(tǒng)計函數(shù)執(zhí)行的耗時。

了解基本原理后,我們大概會知道,這種方式對性能的影響是比較大的,函數(shù)越簡短執(zhí)行的次數(shù)越多影響也會越大,不同它的好處也是顯而易見的:可以統(tǒng)計出函數(shù)的執(zhí)行次數(shù)以及不漏過任何一個細節(jié)。

這種方式一般用于應用早期的優(yōu)化分析。

  • Sampling(采樣):

采用固定的頻率打斷程序的執(zhí)行,然后拉取各線程的執(zhí)行棧進行統(tǒng)計分析。

采樣頻率的大小決定了觀測結(jié)果的最小粒度和誤差,一些執(zhí)行次數(shù)較多的小函數(shù)可能會被統(tǒng)計的偏多,一些執(zhí)行次數(shù)較少的小函數(shù)可能不會被統(tǒng)計到。

主流的操作系統(tǒng)都會從內(nèi)核層進行支持,所以這種方式對應用的性能影響相對較少(具體多少和采樣頻率強相關)。

在性能數(shù)據(jù)里,時間也是一個非常重要的指標,主要有兩類:

  • CPU Time(CPU時間):

占用的CPU時間片的總和。

這個時間主要用來分析高CPU消耗。

  • Wall Time(墻上時間):

真實流逝的時間。

除了CPU消耗,還有資源等待的時間等等,這個時間主要用來分析rt(響應時間,Response time)。

性能數(shù)據(jù)獲取方式+時間指標一共有四種組合方式,每一種都有它們的最適用的場景。

不過需要記住,Java應用通常都需要至少5分鐘(這是一個經(jīng)驗值,通常Server模式的JVM需要在方法執(zhí)行5000~10000次后才會進行JIT編譯)的大流量持續(xù)測試才能使應用的性能達到穩(wěn)定狀態(tài),所以除非你要分析的是應用正在預熱時的性能,否則你需要等待5分鐘以上再開始收集性能數(shù)據(jù)。

Linux系統(tǒng)上面也有不少非常好用的性能監(jiān)控工具,如下圖:

2.1 構(gòu)造性能測試用例

通常我們都會分析一個或多個典型業(yè)務場景的性能,而不僅僅是某一個或多個API接口。

比如對于雙11大促,我們要分析的是導購、交易、發(fā)優(yōu)惠券等業(yè)務場景的性能。

好的性能測試用例的需要能夠反映典型的用戶和系統(tǒng)行為(并不是所有的,我們無法做到100%反映真實用戶場景,只能逐漸接近它),比如一次下單平均購買多少個商品(實際的用例里會細分為:購買一個商品的占比多少,二個商品的占比多少,等等)、熱點售買商品的數(shù)量與成交占比、用戶數(shù)、商品數(shù)、熱點庫存分布、買家人均有多少張券等等。

像淘寶的雙11的壓測用例,像這樣關鍵的參數(shù)會多達200多個。

實際執(zhí)行時,我們期望測試用例是可以穩(wěn)定持續(xù)的運行的(比如不會跑30分鐘下單發(fā)現(xiàn)庫存沒了,優(yōu)惠券也沒了),緩存的命中率、DB流量等等的外部依賴也可以達到一個穩(wěn)定狀態(tài),在秒級時間(一般不需要更細了)粒度上應用的性能也是穩(wěn)定的(即請求的計算復雜度在時間粒度上是均勻分布的,不會一會高一會低的來回抖動)。

為了配合性能測試用例的執(zhí)行,有的時候還需要應用系統(tǒng)做一些相應的改造。

比如對于會使用到緩存的場景來說,剛開始命中率肯定是不高的,但跑了一會兒過后就會慢慢的變?yōu)?00%,這顯然不是通常真實的情況,所以可能會配合寫一些邏輯,來讓緩存命中率一直維持在某個特定值上。

綜上,一套好的性能測試用例是開展后續(xù)工作所必不可少的,值得我們在它上面花時間。

2.2 真實的測試環(huán)境

保持測試環(huán)境與其實環(huán)境的一致性是極其重要的,但是往往也是很難做到的,所以大多數(shù)互聯(lián)網(wǎng)公司的全鏈路壓測方案都是直接使用線上環(huán)境來做性能測試。

如果我們無法做到使用線上環(huán)境來做性能測試,那么就需要花上不少的精力來仔細對比我們所使用的環(huán)境與線上環(huán)境的差異,確保我們知道哪一些性能數(shù)據(jù)是可以值得相信的。

直接使用線上環(huán)境來做性能測試也并不是那么簡單,這需要我們有一套整體解決方案來讓壓測流量與真實流量進行區(qū)分,一般都是在流量中加一個壓測標進行全鏈路的透傳。

同時基本上所有的基礎組件都需要進行改造來支持壓測,主要有:

  • DB:

為業(yè)務表建立對應的壓測表來存儲壓測數(shù)據(jù)。不使用增加字段做邏輯隔離的原因是容易把它們與正式數(shù)據(jù)搞混,同時也不便于單獨清理壓測數(shù)據(jù)。

不使用新建壓測庫的原因是:一方面它違背了我們使用線上環(huán)境做性能壓測的基本考慮,另一方面也會導致應用端多了一倍的數(shù)據(jù)庫連接。

  • 緩存:

為緩存Key增加特殊的前綴,如__yt_。

緩存大多數(shù)沒有表的概念,看起來就是一個巨大的Map存儲一樣,所以除了加固定前綴并沒有太好的辦法。

不過為了減少壓測數(shù)據(jù)的存儲成本,通常需要:1) 在緩存client包中做一些處理來減少壓測數(shù)據(jù)的緩存過期時間;2)緩存控制臺提供專門清理壓測數(shù)據(jù)的功能。

  • 消息:

在發(fā)送、消費時透傳壓測標。

盡量做到不需要業(yè)務團隊的開發(fā)同學感知,在消息的內(nèi)部結(jié)構(gòu)中增加是否是壓測數(shù)據(jù)的標記,不需要業(yè)務團隊申請新的壓測專用的Topic之類。

  • RPC:

透傳壓測標。

當然,HTTP、DUBBO等具體的RPC接口透傳的方案會是不同的。

  • 緩存、數(shù)據(jù)庫 client包:

根據(jù)壓測標做請求的路由。

這需要配合前面提到的具體緩存、DB的具體實現(xiàn)方案。

  • 異步線程池:

透傳壓測標。

為了減少支持壓測的改造代價,通常都會使用ThreadLocal來存儲壓測標,所以當使用到異步線程池的時候,需要記得帶上它。

  • 應用內(nèi)緩存:

做好壓測數(shù)據(jù)與正式數(shù)據(jù)的隔離。

如果壓測數(shù)據(jù)的主鍵或者其它的唯一標識符可以讓我們顯著的讓它與正式數(shù)據(jù)區(qū)分開來,也許不用做太多,否則我們也許需要考慮要么再new一套緩存、要么為壓測數(shù)據(jù)加上一個什么特別的前綴。

  • 自建任務:

透傳壓測標。

需要我們自行做一些與前述提到的消息組件類似的事情,畢竟任務和消息從技術(shù)上來說是很像很像的。

  • 二方、三方接口:

具體分析與解決。

需要看二方、三方接口是否支持壓測,如果支持那么很好,我們按照對方期望的方式進行參數(shù)的傳遞即可,如果不支持,那么我們需要想一些別的奇技婬巧(比如開設一個壓測專用的商戶、賬戶之類)了。

為了降低性能測試對用戶的影響,通常都會選擇流量低峰時進行,一般都是半夜。

當然,如果我們能有一套相對獨立的折中方案,比如使用小得物環(huán)境、在支持單元化的系統(tǒng)中使用部分單元等等,就可以做到在任何時候進行性能測試,實現(xiàn)性能測試的常態(tài)化。

2.3 JProfiler的使用

JProfiler是一款非常成熟的產(chǎn)品,很貴很好用,它是專門為Java應用的性能分析所準備的,而且是跨平臺的產(chǎn)品,是我經(jīng)常使用的工具。

它的大體的架構(gòu)如下圖所示,Linux agent加上Windows UI是最推薦的使用方式,它不但同時支持Instrumentation & Sampling,CPU Time & Wall Time的選項,而且還擁有非常易用的圖形界面。

分析時,我們只需要將其agent包上傳到應用中的某個目錄中(如:/opt/jprofiler11.1.2),然后添加JVM的啟動選項來加載它,我通常都這樣配置:

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

接下來我們重啟應用,這里的修改就會生效了。使用這個配置,Java進程在開始啟動時需要等待JProfiler UI的連接才會繼續(xù)啟動,這樣我們可以進行應用啟動時性能的分析了。

JProfiler的功能很多,就不一一介紹了,大家可以閱讀其官方文檔。

采集的性能數(shù)據(jù)還可以保存為*.jps文件,方便后續(xù)的分析與交流。

其典型的分析界面如下圖所示:

JProfiler的一些缺點:

1)需要在Java應用啟動加載agent(當然它也有啟動后attach的方式,但是有不少的限制),不太便于短時間的分析一些緊急的性能問題;

2)對Java應用的性能影響偏大。使用采樣的方式來采集性能數(shù)據(jù)開銷肯定會低很多,但還是沒有接下來要介紹的perf做的更好。

2.4 perf的使用

perf是Linux上面當之無愧的性能分析工具的一哥,這一點需要特別強調(diào)一下。

不但可以用來分析Linux用戶態(tài)應用的性能,甚至還常用來分析內(nèi)核的性能。

它的模塊結(jié)構(gòu)如下圖所示:

想像一下這樣的場景,如果我們換了一家云廠商,或者云廠商的服務器硬件(主要就是CPU了)有了更新迭代,我們想知道具體性能變化的原因,有什么辦法嗎?

perf就能很好的勝任這個工作。

CPU的設計者為了幫助我們分析應用執(zhí)行時的性能,專門設計了相關的硬件電路,PMU(性能監(jiān)控單元,Performance Monitor Unit)就是這其中最重要的部分。

簡單來說里面包含了很多性能計數(shù)器(圖中的PMCs,Performance Monitor Counters),perf可以讀取這些數(shù)據(jù)。

不僅如此,內(nèi)核層面還提供了很多軟件級別的計數(shù)器,perf同樣可以讀取它們。

一些和CPU架構(gòu)相關的關鍵指標,可以了解一下:

  • IPC(每周期執(zhí)行指令條數(shù),Instruction per cycle):

基于功耗/性能的考慮,大多數(shù)服務器處理器的頻率都在2.5~2.8GHz的范圍,這代表同一時間片內(nèi)的周期數(shù)是差異不大的,所以單個周期能夠執(zhí)行的指令條數(shù)越多說明我們的應用優(yōu)化的越好。

過多的跳轉(zhuǎn)指令(即if else這類代碼)、浮點計算、內(nèi)存隨機訪問等操作顯然是非常影響IPC的。

有的人比較喜歡說CPI(Cycle per instruction),它是IPC的倒數(shù)。

  • LLC Cache Miss(最后一級緩存丟失):

偏內(nèi)存型的應用需要關注這個指標,過大的話代表我們沒有利用好處理器或操作系統(tǒng)的緩存預加載機制。

  • Branch Misses(預測錯誤的分支指令數(shù)):

這個值過高代表了我們的分支類代碼設計的不夠友好,應該做一些調(diào)整盡量滿足處理器的分支預測算法的期望。

如果我們的分支邏輯依賴于數(shù)據(jù)的話,做一些數(shù)據(jù)的調(diào)整一樣可以提高性能(比如這個經(jīng)典案例:數(shù)據(jù)有100萬個元素,值在0-255之間,需要統(tǒng)計值小于128的元素個數(shù)。

提前對數(shù)組排序再進行for循環(huán)判斷會運行的更快)。

因為perf是為Linux上的原生應用準備的,所以直接使用它分析Java應用程序的話,它只會把Java程序當成一個普通的C++程序來看待,不能顯示出Java的調(diào)用棧和符號信息。

好消息是perf-map-agent插件項目解決了這個問題,這個插件可以導出Java的符號信息并幫助perf進行Java線程的?;厮荩@樣我們就可以使用perf來分析Java應用程序的性能了。

執(zhí)行 perf top -p 后,就可以看到perf顯示的實時性能統(tǒng)計信息了,如下圖:

perf僅支持采樣 + CPU Time的工作模式,不過它的性能非常好,進行普通的Java性能分析任務時通常只會引入5%以內(nèi)的額外開銷。

使用環(huán)境變量PERF_RECORD_FREQ來設置采樣頻率,推薦值是999。不過如你所見,它是標準的Linux命令行式的交互行為,不是那么方便。

同時雖然他是可以把性能數(shù)據(jù)錄制為文件供后續(xù)繼續(xù)分析的,但要記得同時保存Java進程的符號文件,不然你就無法查看Java的調(diào)用棧信息了。

雖然限制不少,但是perf卻是最適合用來即時分析線上性能問題的工具,不需要任何前期的準備,隨時可用,同時對線上性能的影響也很小,可以很快的找到性能瓶頸點。

在安裝好perf(需要sudo權(quán)限)以及perf-map-agent插件后,通常使用如下的命令來打開它:

export PERF_RECORD_SECONDS=5 && export PERF_RECORD_FREQ=999
./perf-java-report-stack

重點需要介紹的信息就是這么多,實踐過程中需要用好perf的話需要再查閱相關的一些文檔。

2.5 內(nèi)核態(tài)與用戶態(tài)

對操作系統(tǒng)有了解的同學會經(jīng)常聽到這兩個詞,也都知道經(jīng)常在內(nèi)核態(tài)與用戶態(tài)之間交互是非常影響性能的。

從執(zhí)行層面來說,它是處理器的設計者設計出來構(gòu)建如今穩(wěn)定的操作系統(tǒng)的基礎。

有了它,用戶態(tài)(x86上面是ring3)進程無法執(zhí)行特權(quán)指令與訪問內(nèi)核內(nèi)存。

大多數(shù)時候為了安全,內(nèi)核也不能把某部分內(nèi)核內(nèi)存直接映射到用戶態(tài)上,所以在進行內(nèi)核調(diào)用時,需要先將那部分參數(shù)寫入到特定的傳參位置,然后內(nèi)核再從這里把它想要的內(nèi)容復制走,所以多會一次內(nèi)存的復制開銷。

你看到了,內(nèi)核為了安全,總是小心翼翼的面對每一次的請求。

在Linux上,TCP協(xié)議支持是在內(nèi)核態(tài)實現(xiàn)的,曾經(jīng)這有很多充分的理由,但內(nèi)核上的更新迭代速度肯定是慢于如今互聯(lián)網(wǎng)行業(yè)的要求的,所以QUIC(Quick UDP Internet Connection,谷歌制定的一種基于UDP的低時延的互聯(lián)網(wǎng)傳輸層協(xié)議)誕生了。

如今主流的發(fā)展思路是能不用內(nèi)核就不用內(nèi)核,盡量都在用戶態(tài)實現(xiàn)一切。

有一個例外,就是搶占式的線程調(diào)度在用戶態(tài)做不到,因為實現(xiàn)它需要的定時時鐘中斷只能在內(nèi)核態(tài)設置和處理。

協(xié)程技術(shù)一直是重IO型Java應用減少內(nèi)核調(diào)度開銷的極好的技術(shù),但是很遺憾它需要執(zhí)行線程主動讓出剩余時間片,不然與內(nèi)核線程關聯(lián)的多個用戶態(tài)線程就可能會餓死。

阿里巴巴的Dragonwell版JVM還嘗試了動態(tài)調(diào)整策略(即用戶態(tài)線程不與固定的內(nèi)核態(tài)線程關聯(lián),在需要時可以切換),不過由于前述的時鐘中斷的限制,也不能工作的很好。

包括如今的虛擬化技術(shù),尤其是SR-IOV技術(shù),只需要內(nèi)核參與接口分配/回收的工作,中間的通信部分完全是在用戶態(tài)完成的,不需要內(nèi)核參與。

所以,如果你發(fā)現(xiàn)你的應用程序在內(nèi)核態(tài)上耗費了太多的時間,需要想一想是否可以讓它們在用戶態(tài)完成。

2.6 JVM關鍵指標

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

 1.GC次數(shù)與時間:包括Young GC、Full GC、Concurrent GC等等,Young GC頻率過高往往代表過多臨時對象的產(chǎn)生。

  1. Java堆大?。喊ㄕ麄€Java堆的大小(由Xmx、Xms兩個參數(shù)控制),年輕代、老年代分別的大小。不同時指定Xms和Xmx很可能會讓你的Java進程一直使用很小的堆空間,過大的老年代空間大多數(shù)時候也意味著內(nèi)存的浪費(多分配一些給年輕代將顯著降低Young GC頻率)。
  2. 線程數(shù):通常我們采用的都是4C8G(4Core vCPU,8GB內(nèi)存)、8C16G的機型,分配出上千個線程大多數(shù)時候都是錯誤的。
  3. Metaspace大小和使用率:不要讓JVM動態(tài)的擴展元空間的大小,盡量通過設置MetaspaceSize、MaxMetaspaceSize讓它固定住。我們需要知道我們的應用到底需要多少元空間,過多的元空間占用以及過快的增長都意味著我們可能錯誤的使用了動態(tài)代理或腳本語言。
  4. CodeCache大小和使用率:同樣的,不要讓JVM動態(tài)的擴展代碼緩存的大小,盡量通過設置InitialCodeCacheSize、ReservedCodeCacheSize讓它固定住。我們可以通過它的變化來發(fā)現(xiàn)最近是不是又引入了新的類庫。
  5. 堆外內(nèi)存大小:限制最大堆外內(nèi)存的大小,計算好JVM各塊內(nèi)存的大小,不要給操作系統(tǒng)觸發(fā)OOM Killer的機會。

2.7 了解JIT

字節(jié)碼的解釋執(zhí)行肯定是相當慢的,Java之所以這么流行和他擁有高性能的JIT(即時,Just in time)編譯器也有很大的關系。

但編譯過程本身也是相當消耗性能的,且由于Java的動態(tài)特性,也很難做到像C/C++這樣的編程語言提前編譯為native code再執(zhí)行,這導致Java應用的啟動是相當慢的(大多數(shù)都需要3分鐘以上,Windows操作系統(tǒng)的啟動都不需要這么久),而且同一個應用的多臺機器之間并不能共享JIT的經(jīng)驗(這顯然是極大的浪費)。

我們使用的JVM都采用分層編譯的策略,根據(jù)優(yōu)化的程度不同從低到高分別是C1、C2、C3、C4,C4是最快的。

JIT編譯器會收集不少運行時的數(shù)據(jù),來指導它的編譯策略,核心假設是可以逐步收集信息、僅編譯熱點方法和路徑。

但是這個假設并不總是對的,比如對于雙11大促的場景來說,我們的流量是到點突然垂直增加的,以及部分代碼分支在某個時間點之前并不會運行(比如某種優(yōu)惠要零點過后才會生效可用)。

2.7.1 編譯

極熱的函數(shù)通常JIT編譯器會函數(shù)進行內(nèi)聯(lián)(inlining)優(yōu)化,就相當于直接把代碼抄寫到調(diào)用它的地方來減少一次函數(shù)調(diào)用的開銷,但是如果函數(shù)體過大的話(具體要看JVM的實現(xiàn),通常是幾百字節(jié))將不能內(nèi)聯(lián),這也是為什么編程規(guī)范里面通常都會說不要將一個函數(shù)寫的過大的原因。

JVM并不會對所有執(zhí)行過的方法都進行JIT優(yōu)化,通常需要5000~10000次的執(zhí)行后才進行,而且它還僅僅編譯那些曾經(jīng)執(zhí)行過的分支(以減少編譯所需要的時間和Code Cache的占用,優(yōu)化CPU的執(zhí)行性能)。

所以在寫代碼的時候,if代碼后面緊跟的代碼塊最好是較大概率會執(zhí)行到的,同時盡量讓代碼執(zhí)行流比較固定。

阿里巴巴的Dragonwell版JVM新增了一些功能,可以讓JVM在運行時記錄編譯了哪些方法,再把它們寫入文件中(還可以分發(fā)給應用集群中別的機器),下次JVM啟動時可以利用這部分信息,在第一次運行這些方法時就觸發(fā)JIT編譯,而不是在執(zhí)行上千次以后,這會極大的提升應用啟動的速度以及啟動時CPU的消耗。

不過動態(tài)AOP(Aspect Oriented Programming)代碼以及l(fā)ambda代碼將不能享受這個紅利,因為它們運行時實際生成的函數(shù)名都是形如MethodAccessor$1586 這類以數(shù)字結(jié)尾的不穩(wěn)定的名稱,這次是1586,下一次就不知道什么了。

2.7.2 退優(yōu)化

JIT編譯器的激進優(yōu)化并不總是對的,如果它發(fā)現(xiàn)目前需要的執(zhí)行流在以前的編譯中被省略了的話,它就會進行退優(yōu)化,即重新提交該方法的編譯請求。

在新的編譯請求完成之前,該方法很大可能是進行解釋執(zhí)行(如果存在還未丟棄的低階編譯代碼,比如C1,那么就會執(zhí)行C1的代碼),加上編譯線程的開銷,這會導致短時間內(nèi)應用性能的下降。

在雙11大促這種場景下,也就是零點的高峰時刻,由于退優(yōu)化的發(fā)生,導致應用的性能比壓測時有相當顯著的降低。

阿里巴巴的Dragonwell版JVM在這一塊也提供了一些選項,可以在JIT編譯時去除一些激進優(yōu)化,以防止退優(yōu)化的發(fā)生。

當然,這會導致應用的性能有微弱的下降。

2.8 真實案例

在性能優(yōu)化的實踐過程中,有一句話需要反復深刻的理解:通過數(shù)據(jù)反映一切,而不是聽說或者經(jīng)驗。

下面列舉一個在重構(gòu)項目中進行優(yōu)化的應用的性能比較數(shù)據(jù),以展示如何利用我們前面說到的知識。

這個應用是偏末端的應用,下游基本不再依賴其它應用。

  • 特別說明,【此案例非得物案例】,也不對應任何一個真實的案例。

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

老應用:12.177.126.52,12.177.126.141

新應用:12.177.128.150, 12.177.128.28

測試接口與流量:

時間:2021-05-03 基礎數(shù)據(jù):

緩存訪問:

DB訪問:

2.8.1 初步發(fā)現(xiàn)

通過外部依賴的差異可以發(fā)現(xiàn),新老應用的代碼邏輯會有不同,需要繼續(xù)深入評估差異是什么。通常在做收集性能數(shù)據(jù)的同時,我們需要有一個簡單的分析和判斷,首先確保業(yè)務邏輯的正確性,不然性能數(shù)據(jù)就沒有多少意義。

Java Exception是很消耗性能的,主要消耗在收集異常棧信息,新老應用較大的異常數(shù)區(qū)別需要找到原因并解決。

新應用的接口“下單確認優(yōu)惠”RT有過于明顯的下降,其它接口都是提升的,說明很可能存在執(zhí)行路徑上較大的差別,也需要深入的分析。

三 . 開始做性能優(yōu)化

和獲取性能數(shù)據(jù)需要從底層逐步了解到上層業(yè)務不同,做性能優(yōu)化卻是從上層業(yè)務開始,逐步推進到底層,即從高到低進行分層優(yōu)化。

越高層的優(yōu)化往往難度更低,而且收益還越大,只是需要與業(yè)務的深度結(jié)合。

越低層的優(yōu)化往往難度比較大,很難獲得較大的收益(畢竟一堆技術(shù)精英一直在做著呢),但是通用性比較好,往往可以適用于多類業(yè)務場景。

接下來分別聊一聊每一層可以思考的一些方向和實際的例子。

3.1 優(yōu)化的目的與原則

在聊具體的優(yōu)化措施之前,我們先聊一聊為什么要做性能優(yōu)化。

多數(shù)情況下,性能優(yōu)化的目的都是為了成本、效率與穩(wěn)定。

達到同樣的業(yè)務效果,使用更少的資源,或者帶來更好的用戶體驗(通常是指頁面的響應更快)。

不怎么考慮成本的技術(shù)方案往往沒有太多的挑戰(zhàn),對于電商平臺來說,我們常常用單訂單成本來衡量機器成本,比如淘寶這個值可能在0.17元左右。

業(yè)務發(fā)展的早期往往并不是那么在意成本,反而更加看重效率,等到逐步成熟起來過后,會慢慢的開始重視成本,通俗的講就是開始比的是有沒有,然后比的是好不好。

所以在不同的時期,我們進行性能優(yōu)化的目的和方向會有所側(cè)重。

互聯(lián)網(wǎng)行業(yè)是一個快速發(fā)展的行業(yè),研發(fā)效率對業(yè)務的健康發(fā)展是至關重要的,在進行優(yōu)化的過程中,我們在技術(shù)方案的選擇上需要兼顧研發(fā)效率的提升(至少不能損害過多),給人一種“它本來應該就是這樣”的感覺,而不是做一些明顯無法長期持續(xù)、后期維護成本過高的設計。

好的優(yōu)化方案就像藝術(shù)品一樣,每一個看到的人都會為之贊嘆。

3.2 業(yè)務

大家為什么會在雙11的零點開始上各大電商網(wǎng)站買東西?

春節(jié)前大家為什么都在上午10點搶火車票?

等等,其實都是業(yè)務上的設計。

準備一大波機器資源使用2個月就為了雙11峰值的那幾分鐘,實際上是極大的成本浪費,所以為了不那么浪費,淘寶的雙11預售付尾款的時間通常都放在凌晨1點。

12306早幾年是每臨進春節(jié)必掛,因為想要回家的游子實在是太多,所以后面慢慢按照車次將售賣時間打散,參考其公告:

自今年1月8日起,為避免大量旅客在互聯(lián)網(wǎng)排隊購票,把原來的8點、10點、12點、15點四個時間節(jié)點放票改為15個節(jié)點放票,即:8點-18點,其間每小時或每半小時均有部分新票起售。

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

3.3 系統(tǒng)架構(gòu)

諸如商品詳情頁動靜分離(靜態(tài)頁面與動態(tài)頁面分開不同系統(tǒng)訪問),用戶接口層(即HTTP/S層)與后端(Java層)合并部署等等,都是架構(gòu)優(yōu)化的成功典范。

業(yè)務架構(gòu)師往往會將系統(tǒng)設計為很多層,但是在運行時,他們往往可以部署在一塊兒,以減少跨進程、跨機器、跨地域通信。

淘寶的單元化架構(gòu)在性能上來看也是一個很好的設計,一個交易請求幾乎所有的處理都可以封閉在單元內(nèi)完成,減少了很多跨地域的網(wǎng)絡長傳帶寬需求。

富客戶端方案對于像商品信息、用戶信息等基礎數(shù)據(jù)來說也是很好的方案,畢竟大多數(shù)情況下它們都是訪問Redis等緩存,多一次到服務端的RPC請求總是顯得很多余,當然,后續(xù)需要升級數(shù)據(jù)結(jié)構(gòu)的時候則需要做更多的工作。

關于架構(gòu)的討論是永恒的話題,同時不同的公司有不同的背景,實際進行優(yōu)化時也需要根據(jù)實際情況來取舍。

3.4 調(diào)用鏈路

在分布式系統(tǒng)里不可避免需要依賴很多下游服務才能完成業(yè)務動作,怎么依賴、依賴什么接口、依賴多少次則是需要深入思考的問題。

借助調(diào)用鏈查看工具(在得物,這個工具應該是dependency),我們可以仔細分析每一個業(yè)務請求,然后去思考它是不是最優(yōu)的方式。

舉一個我聽說過的例子(特別說明,【此案例非得物案例】):

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

營銷團隊為組團頁提供了一個新的后臺接口,最開始這個接口需要完成這些事:

在“為用戶創(chuàng)建一個新團”這一步,會同時將新團的信息持久化到數(shù)據(jù)庫中,按照業(yè)務的轉(zhuǎn)化率預估,這會有30W*75%=22.5W QPS的峰值流量,基本上我們需要10個左右的數(shù)據(jù)庫實例才能支撐這么高的并發(fā)寫入。

但這是必要的嗎?

顯然不是,我們都知道這類需要用戶轉(zhuǎn)發(fā)的活動的轉(zhuǎn)化率是有多么的低(大多數(shù)不到8%),同時對于那些沒人參與的團,將它們的信息保存在數(shù)據(jù)庫中也是意義不大的。

最后的優(yōu)化方案是:

1)在“為用戶創(chuàng)建一個新團”時,僅將團信息寫入Redis緩存中;

2)在用戶邀請的朋友同意參團時,再將團信息持久化到數(shù)據(jù)庫中。新的設計,僅僅需要1.8W QPS的數(shù)據(jù)庫并發(fā)寫入量,使用原有的單個數(shù)據(jù)庫實例就可以支撐,在業(yè)務效果上也沒有任何區(qū)別。

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

3.5 應用代碼

應用代碼的優(yōu)化往往是我們最熱衷和擅長的,畢竟業(yè)務與系統(tǒng)架構(gòu)的優(yōu)化往往需要架構(gòu)師出馬。

JProfiler或者perf的剖析(Profiling)數(shù)據(jù)是非常有用的參考,任何不基于實際運行數(shù)據(jù)的猜測往往會讓我們誤入歧途,接下來我們需要做的大多數(shù)時候都是“找熱點 -> 優(yōu)化” ,然后“找熱點 -> 優(yōu)化”,然后一直循環(huán)。

找熱點不是那么難,難在準確的分析代碼的邏輯然后判斷到底它應該消耗多少資源(通常都是CPU),然后制定優(yōu)化方案來達到目標,這需要相當多的優(yōu)化經(jīng)驗。

從我做過的性能優(yōu)化來總結(jié),大概主要的問題都發(fā)生在這些地方:

  • 和字符串過不去:非常多的代碼喜歡將多個Java變量使用StringBuilder拼接起來(那些連StringBuilder都不會用,只會使用 + 的家伙就更讓人頭疼了),然后再找準時機spilt成多個String,然后再轉(zhuǎn)換成別的類型(Long等)。好吧,下次使用StringBuilder時記得指定初始的容量大小。
  • 日志滿天飛:管它有用沒有,反正打了是不會錯的,畢竟誰都經(jīng)歷過沒有日志時排查問題的痛苦。怎么說呢,打印有用的、有效的日志是程序員的必修課。
  • 喜愛Exception:不知道是不是某些Java的追隨者吹過頭了,說什么Java的Exception和C/C++的錯誤碼一樣高效。然而事實并不是這樣的,Exception進行調(diào)用棧的回溯是相當消耗性能的,尤其是還需要將它們打印在日志中的時候,會更加糟糕。
  • 容器的深拷貝:List、HashMap等是大家非常喜歡的Java容器,但Java語言并沒有好的機制阻止別人修改它,所以大家常常深拷貝一個新的出來,反正也就是一句代碼的事兒。
  • 對JSON情有獨鐘:將對象序列化為JSON string,將JSON string反序列化為對象。前者主要用來打日志,后者主要用來讀配置。JSON是挺好,只是請別用的到處都是(還把每個屬性的名字都取的老長)。
  • 重復重復再重復:一個請求里查詢同樣的商品3次,查詢同樣的用戶2次,查詢同樣的緩存5次,都是常有的事,也許多查詢幾次一致性更好吧 :(。還有一些基本不會變的配置,也會放到緩存中,每次使用的時候都會從緩存中讀出來,反序列化,然后再使用,嗯,挺重的。
  • 多線程的樂趣:不會寫多線程程序的開發(fā)不是好開發(fā),所以大家都喜歡new線程池,然后異步套異步。在流量很低的時候,看起來多線程的確解決了問題(比如RT的確變小了),但是流量上來過后,問題反而惡化了(畢竟我們主流的機器都是8核的)。

再重申一遍,在這一步,找到問題并不是太困難,找到好的優(yōu)化方案卻是很困難和充滿考驗的。

3.6 緩存

大多數(shù)的緩存都是Key、Value的結(jié)構(gòu),選擇緊湊的Key、Value以及高效的序列化、反序列化算法是重中之重(二進制序列化協(xié)議比文本序列化協(xié)議快的太多了)。

還有的緩存是Prefix、Key、Value的結(jié)構(gòu),主要的區(qū)別是誰來決定實際的數(shù)據(jù)路由到哪臺服務器進行處理。單條緩存不能太大,基本上大于64KB就需要小心了,因為它總是由某一臺實際的服務器在處理,很容易將出口寬帶或計算性能打滿。

3.7 DB

數(shù)據(jù)庫比起緩存來說他能抗的流量就低太多了,基本上是差一個數(shù)量級。

SQL通信協(xié)議雖然很易用,但實際上是非常低效的通信協(xié)議。關于DB的優(yōu)化,通常都是從減少寫入量、減少讀取量、減少交互次數(shù)、進行批處理等等方面著手。

DB的優(yōu)化是一門復雜的學問,很難用一篇文章說清楚,這里僅舉一些我認為比較有代表性的例子:

  • 使用MultiQuery減少網(wǎng)絡交互:MySQL等數(shù)據(jù)庫都支持將多條SQL語言寫到一起,一起發(fā)送給DB服務器,這會將多次網(wǎng)絡交互減少為一次。
  • 使用BatchInsert代替多次insert:這個很常見。
  • 使用KV協(xié)議取代SQL:阿里云數(shù)據(jù)庫團隊在數(shù)據(jù)庫服務器上面外掛了一個KV引擎,可以直接讀取InnoDB引擎中的數(shù)據(jù),bypass掉了數(shù)據(jù)庫的數(shù)據(jù)層,使得基于唯一鍵的查詢可以比使用SQL快10倍。
  • 與業(yè)務結(jié)合:淘寶下單時可以同時使用多達10個紅包,這意味著一次下單需要發(fā)送至多10次update SQL。假設一次下單使用了N個紅包,基于對業(yè)務行為的分析,會發(fā)現(xiàn)前N-1個都是全額使用的,最后一個可能會部分使用。對于使用完的紅包,我們可以使用一條SQL就完成更新。
update red_envelop set balance = 0 where id in (...);
  • 熱點優(yōu)化:庫存的熱點問題是每個電商平臺都面臨的問題,使用數(shù)據(jù)庫來扣減庫存肯定是可靠性最高的方案,但是基本上都很難突破500tps的瓶頸。阿里云數(shù)據(jù)庫團隊設計了新的SQL hint,配合上第1條說的MultiQuery技術(shù),與數(shù)據(jù)庫進行一次交互就可以完成庫存的扣減。同時加上數(shù)據(jù)庫內(nèi)核的針對性優(yōu)化,已經(jīng)可以實現(xiàn)8W tps的熱點扣減能力。下表中的commit_on_success用來表明,如果update執(zhí)行成功就立即提交,這樣可以讓庫存熱點行的鎖占用時間降到最低。target_affect_row(1)以及rollback_on_fail用來限制當庫存售罄時(即inv_count - 1 >= 0不成立)update執(zhí)行失敗并回滾整個事務(即前面插入的庫存流水作廢)。
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 運行環(huán)境

我們的代碼是運行在某個環(huán)境中的,這個環(huán)境有很多知識是我們需要了解的,如果上面所有的優(yōu)化完成后還不能滿足要求,那么我們也不得不向下深入。

這可能是一個困難的過程,但也會是一個有趣的過程,因為你終于有了和各領域的大佬們交流討論的機會。

3.8.1 中間件

目前大多數(shù)中間件的代碼是和我們的業(yè)務代碼運行在一起的,比如監(jiān)控采集、消息client、RPC client、配置推送 client、DB連接組件等等。如果你發(fā)現(xiàn)這些組件的性能問題,那么可以大膽的提出來,不要害怕傷害到誰。

我遇到過這樣的一些場景:

  1. 應用偶爾會大量的發(fā)生ygc:

排查到的原因是,在我們依賴的服務發(fā)生地址列表變化(比如發(fā)生了重啟、掉線、擴容等場景)時,RPC client會接收到大量的推送,然后解析這些推送的信息,然后再更新一大堆內(nèi)存結(jié)構(gòu)。

提出的優(yōu)化建議是:

   1)地址推送從全量推送改變?yōu)樵隽客扑停?/p>

   2)地址列表從掛接到服務接口維度更改為掛接到應用維度。

  1. DB連接組件過多的字符串拼接:

DB連接組件需要進行SQL的解析來計算分庫分表等信息,但實現(xiàn)上面不夠優(yōu)雅,拼接的字符串過多了,導致執(zhí)行SQL時內(nèi)存消耗過多。

3.8.2 容器

關于容器技術(shù)本身大多數(shù)時候我們做不了什么,往往就是盡量采用最新的技術(shù)(比如使用阿里云的神龍服務器什么的),不過在梆核(即容器調(diào)度)方面往往可以做不少事。

我們的應用和誰運行在一起、相互之間有資源爭搶嗎、有沒有跨NUMA調(diào)度、有沒有資源超賣等等問題需要我們關注(當然,這需要容器團隊提供相應的查看工具)。

這里主要有兩個需要考慮的點:

  1. 是不是支持離在線混部:

在線任務要求實時響應,而離線任務的運行又需要耗費非常多的機器。在雙11大促這樣的場景,把離線機器借過來用幾個小時就可以減少相應的在線機器采購,能省下很多錢。

  1. 基于業(yè)務的調(diào)度:

把高消耗的應用和低消耗的應用部署在一起,同時如果雙方的峰值時刻還不完全相同,那就太美妙啦。

3.8.3 JVM

為了解決重IO型應用線程過多的問題開發(fā)了協(xié)程。

為了解決Java容器過多小對象的問題(如HashMap的K, V都只能是包裝類型)開發(fā)了值容器。

為了解決Java堆過大時GC時間過長的問題(當然還有覺得Java的內(nèi)存管理不夠靈活的原因)開發(fā)了GCIH(GC Invisible Heap,淘寶雙11期間部分熱點優(yōu)惠活動的數(shù)據(jù)都是存在GCIH當中的)。

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

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

雖然目前JVM的實現(xiàn)就是你知道的那樣,但是并不代表這樣做就一直是合理的。

3.8.4 操作系統(tǒng)

基本上我們都是使用Linux操作系統(tǒng),新版本的內(nèi)核通常會帶來一些新功能和性能的提升,同時操作系統(tǒng)還需要為支撐容器(即Docker等)做不少事情。

對Host操作系統(tǒng)來說,開啟透明大頁、配置好網(wǎng)卡中斷CPU打散、配置好NUMA、配置好NTP(Network Time Protocol)服務、配置好時鐘源(不然clock_gettime可能會很慢)等等都是必要的。

還有就是需要做好各種資源的隔離,比如CPU隔離(高優(yōu)先級任務優(yōu)先調(diào)度、LLC隔離、超線程技術(shù)隔離等)、內(nèi)存隔離(內(nèi)存寬帶、內(nèi)存回收隔離避免全局內(nèi)存回收)、網(wǎng)絡隔離(網(wǎng)絡寬帶、數(shù)據(jù)包金銀銅等級劃分)、文件IO隔離(文件IO寬帶的上限與下限、特定文件操作限制)等等。

大多數(shù)內(nèi)核級別的優(yōu)化都不是我們能做的,但我們需要知道關鍵的一些影響性能的內(nèi)核參數(shù),并能夠理解大多數(shù)內(nèi)核機制的工作原理。

3.9 硬件

通常我們都是使用Intel的x86架構(gòu)的CPU,比如我們正在使用的Intel 8269CY,不過它的單顆售價得賣到4萬多塊人民幣,卻只有區(qū)區(qū)26C52T(26核52線程)。

相比之下,AMD的EPYC 7763的規(guī)格就比較牛逼了(64C128T,256MB三級緩存,8通道 DDR4 3200MHz內(nèi)存,擁有204GB/s的超高內(nèi)存寬帶),但卻只要3萬多一顆。

當然,用AMD 2021年的產(chǎn)品和Intel 2019的產(chǎn)品對比并不是太公平,主要Intel 2021的新品Intel Xeon Platinum 8368Q處理器并不爭氣,僅僅只是提升到了38C76T而已(雖然和自家的上一代產(chǎn)品相比已經(jīng)大幅提升了近50%)。

除了x86處理器,ARM 64位處理器也在向服務端產(chǎn)品發(fā)力,而且這個產(chǎn)業(yè)鏈還可以實現(xiàn)全國產(chǎn)化。

華為2019年初發(fā)布的鯤鵬920-6426處理器,采用7nm工藝,具備64個CPU核,主頻2.6GHz。

雖然單核性能上其只有Intel 8269CY的近2/3,但是其CPU核數(shù)卻要多上一倍還多,加上其售價親民,同樣計算能力的情況下CPU部分的成本會下降近一半(當然計算整個物理機成本的話其實下降有限)。

2020年雙11開始,淘寶在江蘇南通部署了支撐1萬筆/s交易的國產(chǎn)化機房,正是采用了鯤鵬920-6426處理器,同時在2021年雙11,更是用上了阿里云自主研發(fā)的倚天710處理器(也是采用ARM 64位架構(gòu))。

在未來,更是有可能基于RISC-V架構(gòu)設計自己的處理器。這些事實都在說明,在處理器的選擇上,我們還是有不少空間的。

除了采用通用處理器,在一些特殊的計算領域,我們還可以采用專用的芯片,比如:使用GPU加速深度學習計算,在AI推理時使用神經(jīng)網(wǎng)絡加速芯片-含光NPU,以及使用FPGA芯片進行高性能的網(wǎng)絡數(shù)據(jù)處理(阿里云神龍服務器上使用的神龍MOC卡)等等。

曾經(jīng)還有人想過設計可以直接運行Java字節(jié)碼的處理器,雖然最終因為復雜度太高而放棄。

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


當前標題:怎么做好Java性能優(yōu)化
當前鏈接:http://www.5511xx.com/article/dphegeg.html