性能優(yōu)化那些事兒(2)

『不管項(xiàng)目大小,一旦上線,或多或少都會遇到性能問題』性能問題就像是魔咒一般藏繞著我們。

性能優(yōu)化應(yīng)該什么時候開始

有些性能問題是隨著時間的積累慢慢產(chǎn)生的,比如系統(tǒng)一開始數(shù)據(jù)量很小的時候,沒有什么問題,等到數(shù)據(jù)積累到一定程度,問題就暴露出來了;有些問題是由于訪問量的過大造成的,比如系統(tǒng)平時沒問題,一到搞活動時就掛;也有些問題是遺留系統(tǒng)經(jīng)過太多人去維護(hù)修改,導(dǎo)致各種壞代碼味道性能問題仿佛到處存在。性能問題就如同一顆定時炸彈,只要數(shù)據(jù)量訪問量一上來,或者各個團(tuán)隊(duì)在開發(fā)迭代中沒有注重性能的意識,早晚會炸。既然遲早會出問題,那我們應(yīng)該什么時候開始進(jìn)行性能優(yōu)化呢?是等出了問題后在進(jìn)行優(yōu)化,還是在編碼的過程中就嘗試避免那些錯誤的代碼模式呢,或者采用一些手段盡可能的避免踩坑呢?

有人會說項(xiàng)目壓力大,如果開始過程中要考慮性能問題那么會影響進(jìn)度。我覺得這是在給自己或者給后人挖坑,我們在一開始設(shè)計(jì)接口的時候,就應(yīng)該考慮性能問題,不僅僅要考慮接口的合理性易用性,同時也要考慮接口是否有批量調(diào)用的情況。最簡單的方法,就是在設(shè)計(jì)接口的時候就直接設(shè)計(jì)批量接口,這樣這個接口又能支持批量又能支持單個,當(dāng)然考慮到批量會有額外的工作要做,但總比出了問題到處去填坑強(qiáng)吧,這需要我們有能力識別未來業(yè)務(wù)上對批量的需求,并不是每個接口都需要支持批量操作。

我們還可以用很多方法來保證代碼質(zhì)量以提高系統(tǒng)性能的,比如:

  • 使用合理的數(shù)據(jù)結(jié)構(gòu)和算法,比如,同樣是列表,LinkedList 就比 ArrayList 的插入性能高很多
  • 多線程環(huán)境下合理選擇鎖的類型和使用場景
  • 編寫高效 SQL、合理使用索引和事務(wù)來提升數(shù)據(jù)庫性能,使用ORM工具時注意N+1問題,有些看起來很便捷的方法請理解其細(xì)節(jié)再去使用。
  • 多考慮接口的使用場景,是否有批量的可能,如果有提供批量接口
  • 如果對性能要求很高,是否考慮使用Netty等異步手段

你的腦袋里應(yīng)該有一大堆這樣的手段,在開發(fā)過程中,可以盡情發(fā)揮。但有一點(diǎn)需要著重強(qiáng)調(diào):不要使用任何你不知道背后原理的優(yōu)化技巧。

這里有個有爭議的優(yōu)化手段:“不使用的對象應(yīng)手動賦值為 NULL”有利于 GC 更早地回收內(nèi)存,但在大多數(shù)場景下,不使用的局部變量是否設(shè)置為 NULL,對 GC 沒有任何影響,畢竟方法執(zhí)行完畢,棧幀就從操作數(shù)棧中彈出,方法中的局部變量就沒了,是否設(shè)置為 NULL 也就沒有任何影響。但是如果你是開發(fā)中間件的,或者某個復(fù)雜算法,那么手動設(shè)置為NULL確實(shí)在某些情況會有利于GC,比如臨時變量占用了大量內(nèi)存當(dāng)遇到『安全點(diǎn)』時如果不主動設(shè)置為NULL在JDK運(yùn)行在『解釋』階段時確實(shí)會導(dǎo)致GC回收的比較慢。你可以在J.U.C包中經(jīng)常看到xxx=null,注釋都是help gc,但是我們經(jīng)常寫業(yè)務(wù)代碼的其實(shí)沒必要這么做。

在系統(tǒng)開發(fā)完成以后,可以根據(jù)一些預(yù)期的指標(biāo) ( 比如,并發(fā)數(shù) ) 和硬件資源來對系統(tǒng)進(jìn)行測試,通過各種分析統(tǒng)計(jì)工具來判斷各項(xiàng)指標(biāo)是否在預(yù)期范圍內(nèi)。等到系統(tǒng)上線后,還要根據(jù)日志、監(jiān)控系統(tǒng)來觀測系統(tǒng)性能,一旦發(fā)現(xiàn)問題,就要及時分析并修復(fù)。這里可以使用的軟件很多,比如Dynatrace等各類APM工具,但如果你的系統(tǒng)比較定制也比較奇特的話,那么恐怕很難找到現(xiàn)成的工具,我們可以自己開發(fā)一套監(jiān)控系統(tǒng),其實(shí)知道原理也很簡單的。

不管是新系統(tǒng)還是老系統(tǒng),也不管是上線前還是上線后,做性能優(yōu)化都要遵循兩原則三步驟:

  • 兩原則:不去優(yōu)化沒有測試的軟件(單元測試要有,不然優(yōu)化出了bug都不知道)、不去優(yōu)化你不了解的軟件
  • 三步驟:測試、分析、調(diào)優(yōu)

性能測試的主要指標(biāo)

一般來說,衡量系統(tǒng)的性能,主要有以下幾個指標(biāo):

  • 響應(yīng)時間

可以從端到端的響應(yīng)時間細(xì)分下去:比如數(shù)據(jù)庫的響應(yīng)時間,IO的響應(yīng)時間,HTTPClient的響應(yīng)時間。當(dāng)我們優(yōu)化系統(tǒng)的時候,通過收集這些響應(yīng)時間可以精確定位性能問題出現(xiàn)在哪。

  • 并發(fā)數(shù)

并發(fā)數(shù)是指系統(tǒng)能夠同時處理請求的數(shù)量,這個數(shù)字也反映了系統(tǒng)的負(fù)載承受能力。

  • 吞吐量

吞吐量是指單位時間內(nèi)系統(tǒng)處理的請求數(shù)量,體現(xiàn)的是系統(tǒng)的處理能力。在 Web 系統(tǒng)中,常常用 TPS ( 每秒事務(wù)處理量 ) 或者 QPS ( 每秒查詢量 ) 來衡量系統(tǒng)的吞吐量。在不考慮網(wǎng)卡等網(wǎng)絡(luò)設(shè)備限制的情況下,可以使用下面的公式來大致估算系統(tǒng)的吞吐量:

吞吐量 = (1000/響應(yīng)時間 ms) x 并發(fā)數(shù)

如何嚴(yán)謹(jǐn)?shù)刈鲂阅軠y試

那如何更嚴(yán)謹(jǐn)?shù)刈鲂阅軠y試?分享一個做性能測試比較科學(xué)的方法(來源自COOLSHELL):

  1. 定義一個系統(tǒng)的響應(yīng)時間 latency,建議是 TP99,以及成功率。比如路透的定義:99.9%的響應(yīng)時間必須在 1ms 之內(nèi),平均響應(yīng)時間在 1ms 以內(nèi),100%的請求成功。當(dāng)然一般的 Web 系統(tǒng)不用定義的這么苛刻,99.9%的響應(yīng)時間在 100ms 內(nèi)即可。

  2. 在這個響應(yīng)時間的限制下,來測試系統(tǒng)的吞吐量。測試用的數(shù)據(jù),需要有大中小各種尺寸的數(shù)據(jù),并可以混合。最好使用生產(chǎn)線上的測試數(shù)據(jù)。

  3. 在這個吞吐量做浸泡測試,比如:使用第二步測試得到的吞吐量連續(xù) 7 天的不間斷的壓測系統(tǒng)。然后收集 CPU,內(nèi)存,硬盤/網(wǎng)絡(luò) IO,等指標(biāo),查看系統(tǒng)是否穩(wěn)定,比如,CPU 是平穩(wěn)的,內(nèi)存使用也是平穩(wěn)的。那么,這個值就是系統(tǒng)的性能。

  4. 找到系統(tǒng)的極限值。比如:在成功率 100%的情況下 (不考慮響應(yīng)時間的長短),系統(tǒng)能保持 10 分鐘的吞吐量。

  5. 做 Burst Test。用第二步得到的吞吐量執(zhí)行 5 分鐘,然后在第四步得到的極限值執(zhí)行 1 分鐘,再回到第二步的吞吐量執(zhí)行 5 分鐘,再到第四步的權(quán)限值執(zhí)行 1 分鐘,如此往復(fù)個一段時間,比如 2 天。收集系統(tǒng)數(shù)據(jù):CPU、內(nèi)存、硬盤/網(wǎng)絡(luò) IO 等,觀察他們的曲線,以及相應(yīng)的響應(yīng)時間,確保系統(tǒng)是穩(wěn)定的。

  6. 低吞吐量和網(wǎng)絡(luò)小包的測試。有時候,在低吞吐量的時候,可能會導(dǎo)致延遲上升,比如 TCP_NODELAY 的參數(shù)沒有開啟會導(dǎo)致延遲上升,而網(wǎng)絡(luò)小包會導(dǎo)致帶寬用不滿也會導(dǎo)致性能上不去,所以,性能測試還需要根據(jù)實(shí)際情況有選擇的測試一下這兩個場景。

影響系統(tǒng)性能的主要因素

我們要先了解下一般情況哪些因素會影響到系統(tǒng)的性能,這樣我們可以逐個排查。

  • 硬件

一般硬件是我們首先考慮的因素,如果可以提升硬件那么一般可以解決一些性能問題。常見的影響因素有CPU、內(nèi)存、磁盤 I/O 、網(wǎng)絡(luò)等,如果內(nèi)存不夠或者CPU長期滿負(fù)載那么就需要升級硬件了,如果業(yè)務(wù)中IO很重那么要考慮換個SSD硬盤,如果流量很大要考慮網(wǎng)絡(luò)帶寬夠不夠,網(wǎng)卡性能跟得上不。

  • 系統(tǒng)

系統(tǒng)相關(guān)的點(diǎn)實(shí)在是太多了,這里簡單介紹幾種常見的情況:

1)Linux文件描述符限制,有時候默認(rèn)的值比較低,影響并發(fā)。

2)Linux中Swap強(qiáng)烈建議關(guān)閉,打開壞處多于好處,會有意想不到的問題。

3)高流量的應(yīng)用需要注意網(wǎng)卡中斷問題,使用CPU親和性綁定網(wǎng)卡。

  • 軟件

一般有幾個因素需要重點(diǎn)關(guān)注

1)數(shù)據(jù)庫:數(shù)據(jù)庫操作不僅涉及大量的內(nèi)存以及 CPU 計(jì)算,還涉及到大量的磁盤讀寫。對數(shù)據(jù)庫的性能優(yōu)化是整個系統(tǒng)的核心,比如,我們常用的各種緩存都是為了減少對數(shù)據(jù)庫的壓力。開啟慢SQL搜集,通過分析慢SQL來優(yōu)化系統(tǒng)中效率低下的SQL語句。

2)鎖競爭:單機(jī)環(huán)境下,鎖的使用可能會帶來大量的線程資源浪費(fèi),從而給系統(tǒng)帶來性能開銷;而分布式環(huán)境下,使用分布式鎖也可能造成大量的請求堆積,影響整個系統(tǒng)性能。優(yōu)化重心在于鎖粒度的控制,以及如何采用無鎖模型去替代。

3)線程池:線程池的不恰當(dāng)申明和配置也會帶來問題,請確保你的線程池都是有界的,確保你的線程池大小是合理的。

4)異步系統(tǒng)與同步IO:確保你理解Netty相關(guān)知識,不要在Reactor線程中去使用同步IO。

5)循環(huán)與外部請求:不要將外部請求放到循環(huán)中,而是應(yīng)該盡可能通過批量方式一次請求。

6)看似便利確暗藏殺機(jī):很多庫提供了看似便利的方法,其實(shí)暗藏殺機(jī),不要使用你不了解原理的所謂高級用法。

兜底策略

性能優(yōu)化做得再好,系統(tǒng)總會存在極限,因此,兜底的策略也是性能優(yōu)化的一部分,常見的兜底策略有限流、降級和熔斷。很多中間件都有這樣的功能,我們應(yīng)當(dāng)合理使用。還有我們可以通過減少涌入服務(wù)器的流量來避免高流量對我們服務(wù)器的沖擊,比如接入CDN,利用CDN的節(jié)點(diǎn)優(yōu)化和緩存能力能很好的優(yōu)化我們的性能,當(dāng)然能使用更高級的邊緣計(jì)算技術(shù)那么在某些場景下會有質(zhì)的飛躍。

需要著重強(qiáng)調(diào)的是任何的性能優(yōu)化都得結(jié)合業(yè)務(wù)場景明確已知的性能問題和性能目標(biāo),不能為了優(yōu)化而優(yōu)化。市面上有很多APM工具和性能分析工具可以幫助你定位性能問題,但如果你的系統(tǒng)非常的復(fù)雜且并不是標(biāo)準(zhǔn)容器,那么很可能你需要自己開發(fā)個APM工具來幫助你定位性能問題了,那么如何開發(fā)自己的性能分析工具呢,請聽下回分解。


文/Thoughtworks張錦程
原文鏈接:https://insights.thoughtworks.cn/performance-turning-practice-2/
更多精彩洞見,請關(guān)注微信公眾號Thoughtworks洞見。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容