本文介紹了服務(wù)器程序性能優(yōu)化的一般性方法,以及部分常見服務(wù)器程序的性能優(yōu)化步驟。服務(wù)器程序指的是接收客戶端程序請求,執(zhí)行對應(yīng)操作,并將結(jié)果返回給客戶端的程序,如Nginx、Tomcat、SQLite、Berkeley DB等。
1 優(yōu)化方法
服務(wù)器性能優(yōu)化是為了提高服務(wù)器性能而進(jìn)行的一系列操作,本文關(guān)注的是程序(包括操作系統(tǒng))層面的優(yōu)化,因此不涉及諸如增加硬件、升級硬件或升級固件版本等方法。本文提到的性能優(yōu)化,指的是通過調(diào)整程序參數(shù)或程序代碼,提高程序性能的行為。本文主要關(guān)注工程方面的優(yōu)化,不涉及算法優(yōu)化等技術(shù)。
2 優(yōu)化目標(biāo)
本文關(guān)注于服務(wù)器程序,因此采用吞吐量(throughput)和時延(latency)作為性能度量指標(biāo)。其他的性能度量指標(biāo),比如網(wǎng)絡(luò)流量和耗電量等,不在考慮范圍之內(nèi)。
吞吐量是單位時間內(nèi)服務(wù)器處理的請求數(shù)量平均值。時延是客戶端從發(fā)送請求到接收應(yīng)答所經(jīng)歷的時間平均值。在本文中,性能優(yōu)化的目標(biāo)是提高吞吐量,降低時延。
3 計算機模型
計算機分為處理器、存儲器和通信線路。處理器負(fù)責(zé)執(zhí)行指令,進(jìn)行運算。存儲器負(fù)責(zé)存儲數(shù)據(jù),數(shù)據(jù)以字節(jié)為單位。存儲器分為順序存儲器和隨機存儲器。順序存儲器只能按順序存取字節(jié),隨機存儲器沒有這樣的限制。通信線路有兩個端點,一個連接到處理器,另外一個連接到存儲器或處理器。通信線路負(fù)責(zé)將數(shù)據(jù)在兩個端點之間傳遞。通信線路上傳遞的數(shù)據(jù)也叫做消息。由多個通信線路連接在一起的一組處理器和存儲器組成網(wǎng)絡(luò)。
下面的Java代碼展示了這些模型的接口:
public interface Processor {
void get_next_instrument();
void execute_instrument();
};
public interface SequentialStorage {
long getBlockSize();
void rewind();
// Block 是固定長度的字節(jié)數(shù)組,比如byte[512]。
Block read();
void write(Block data);
}
public interface RandomAccessStorage extends SequentialStorage {
long getSize();
void moveTo(long position);
}
public interface CommunicationLine {
// End可以是處理器或存儲器,但不允許兩個End都是存儲器。
void establish(End end1, End end2);
void sendToEnd1(byte[] data);
void sendToEnd2(byte[] data);
};
度量處理器性能的指標(biāo)是每秒執(zhí)行的指令數(shù)(MIPS)。存儲器的性能指標(biāo)是訪問時間和容量。對于隨機存儲器,訪問數(shù)據(jù)操作包含尋找數(shù)據(jù)位置和傳輸數(shù)據(jù)兩個操作,因此訪問時間是這兩個步驟耗時之和。對于順序存儲器,我們可以將moveTo操作定義為
void moveTo(long position) {
rewind();
int skipBlockCount = position / BLOCK_COUNT;
while (skipBlockCount-- > 0) {
read();
}
}
這樣就可以基于順序存儲器構(gòu)建一個隨機存儲器。因此“訪問時間=尋址時間+傳輸時間”這一公式也適用于順序存儲器。度量通信線路性能的指標(biāo)是帶寬和時延。帶寬是單位時間內(nèi)通信線路可以傳遞的比特數(shù),以bps為單位。時延時從開始發(fā)送消息到接收第一個字節(jié)所經(jīng)歷的時間。時延通常由通信線路的長度所決定。
如果可以保證數(shù)據(jù)的接收順序和發(fā)送順序一致,那么通信線路看起來很像是一個順序存儲器。但二者存在兩點重要區(qū)別:一是從存儲器中讀取數(shù)據(jù)后,數(shù)據(jù)仍然保存在存儲器上,可以再次讀取。而從通信線路中接收消息后,消息從從通信線路中移除。二是從存儲器中存取單位數(shù)據(jù)時,無論成功或失敗,操作時間存在一個上限。而通信線路無法滿足這樣的條件,因為某個端點可能長時間不發(fā)送消息。
4 性能優(yōu)化模型
4.1 基礎(chǔ)模型
客戶端程序(簡稱客戶端)是一個處理器,服務(wù)器程序(簡稱服務(wù)器)由若干個處理器、存儲器和通信線路組成??蛻舳撕头?wù)器以通信線路連接??蛻舳税l(fā)送消息給服務(wù)器,服務(wù)器程序執(zhí)行相應(yīng)的操作,然后將處理結(jié)果返回給客戶端??蛻舳税l(fā)送的消息叫做請求,服務(wù)器返回的對應(yīng)請求的處理結(jié)果叫做應(yīng)答。
這里提到的處理器、存儲器和通信線路都是邏輯上的模型,并非特指CPU、硬盤和以太網(wǎng)。比如CPU、線程、進(jìn)程都可以是處理器,L1緩存、內(nèi)存、磁盤、磁帶都可以是存儲器,TCP連接、消息隊列、數(shù)據(jù)總線都可以是通信線路。
客戶端從發(fā)送請求到接收應(yīng)答的過程可以分為三個階段:客戶端將請求發(fā)送到服務(wù)器、服務(wù)器處理請求、服務(wù)器將應(yīng)答發(fā)送給客戶端。假設(shè)這三個階段分別耗時t1、t2和t3,并假各客戶端按順序依次發(fā)送請求,那么服務(wù)器的時延是t1+t2+t3,吞吐量是1/(t1+t2+t3)。
4.2 隊列模型
假設(shè)有兩個客戶端向服務(wù)器發(fā)送請求,在基礎(chǔ)模型下,服務(wù)器的處理過程如下:
接收請求1。
處理請求1。
發(fā)送應(yīng)答1。
接收請求2。
處理請求2。
發(fā)送應(yīng)答2。
顯然“發(fā)送應(yīng)答1”和“接收請求2”兩個任務(wù)之間沒有依賴關(guān)系,因此可以并行處理,以提高系統(tǒng)吞吐量。這就是隊列模型。 在隊列模型中,服務(wù)器將收到請求保存到請求隊列,處理器循環(huán)從請求隊列中讀取請求并進(jìn)行處理。這個處理過程和從其他客戶端接收消息的操作是并行或并發(fā)的。類似的,應(yīng)答的發(fā)送和業(yè)務(wù)邏輯處理也是并行或并發(fā)的。假設(shè)請求隊列的長度是l,那么第二階段的耗時變?yōu)?/p>
t2' = 請求在隊列中等待調(diào)度的時間 + 實際處理時間 = (l - 1)t2 + t2 = lt2
因此服務(wù)器的時延變?yōu)閠1+l*t2+t3,吞吐量變?yōu)?/t2。隊列機制會增加吞吐量,代價是時延也隨之增加。隊列模型是基本模型一般化推廣,當(dāng)隊列長度為1時,隊列模型和基本模型非常相似。
按照前面的計算機模型,當(dāng)系統(tǒng)中存在n個客戶端時,請求隊列由一個處理器(叫做隊列處理器)和n+1個消息線路組成。隊列處理器和每個客戶端之間都存在一個通信線路,最后一個通信線路連接到服務(wù)器的業(yè)務(wù)邏輯處理器上。隊列處理器從每個客戶端接收消息,將消息發(fā)送到最后一個通信線路上,傳遞給業(yè)務(wù)邏輯處理器。應(yīng)答隊列也是類似的,只是消息傳遞的順序相反。請求隊列和應(yīng)答隊列也可以叫做輸入隊列和輸出隊列。
在服務(wù)器中通常包含多個模塊,每個模塊都可以看成是由一個業(yè)務(wù)邏輯處理器、一個輸入隊列、一個輸出隊列組成的網(wǎng)絡(luò)。假設(shè)模塊i的處理時間是t[i],輸入隊列長度是len[i],那么這個模塊的時延就是len[i]*t[i],服務(wù)器的時延就是這些模塊時延的和。
此外,t1和t3的任務(wù)是傳輸數(shù)據(jù),t2的任務(wù)是執(zhí)行業(yè)務(wù)邏輯。這是兩類不同類型的任務(wù)。如果沒有隊列,CPU和程序需要在這兩類任務(wù)之間頻繁切換,一方面使程序變得復(fù)雜,容易出錯;另一方面,不利于充分發(fā)揮CPU性能,也不方便進(jìn)行針對性優(yōu)化。
隊列有兩種常見的實現(xiàn)方式,一種是單線程批處理方式,一種是多線程異步隊列方式。下面的代碼展示了這兩種方式。
public class SingleThreadBatch {
public void processLoop() {
while (notQuit) {
Queue<Request> inputQueue = receiveFromAllClients(maxQueueSize, maxWaitTime);
Queue<Response> outputQueue = new Queue<>();
for (Request request: inputQueue) {
Response response = process(request);
outputQueue.put(response);
}
sendAllResponse(outputQueue);
}
}
}
public class MultiThreadAsyncQueue {
AsyncQueue<Request> inputQueue = new AsyncQueue<>();
AsyncQueue<Request> outputQueue = new AsyncQueue<>();
private receiveThread = new Thread() {
@Override
public void run() {
while (notQuit) {
for (Client client: allClients) {
Request request = client.receiveNoWait();
if (request != null) {
inputQueue.enqueue(request);
}
}
}
}
};
private sendThread = new Thread() {
@Override
public void run() {
while (notQuit) {
Response response = outputQueue.dequeue();
send(response);
}
}
};
public void processLoop() {
while (notQuit) {
Request request = inputQueue.dequeue();
Response response = process(request);
outputQueue.enqueue(response);
}
}
}
5 性能優(yōu)化思路
為了提高吞吐量,必須充分利用CPU資源,讓CPU滿載。CPU滿載后,請求不斷堆積在隊列中。為了避免時延過長,服務(wù)器需要進(jìn)行控制隊列長度。這個操作叫做流控。因此性能優(yōu)化分為兩步:提高CPU使用率、然后進(jìn)行流控。
5.1 提高CPU使用率
CPU使用率低的原因有三點:一是過早流控,引發(fā)處理線程饑餓;二是處理線程在等待IO;三是線程調(diào)度不充分,沒有充分利用多核的優(yōu)勢。
過早流控是因為隊列長隊設(shè)置得過小,通過觀察隊列丟包情況可以判斷這一點。確認(rèn)后適當(dāng)增加隊列長度,就可以提高CPU使用率。對于等待IO的情況,可以使用異步調(diào)用或多線程同時處理IO,降低CPU等待IO的時間。線程調(diào)度不充分的表現(xiàn)為部分CPU核心使用率非常高,其余核心使用率非常低,這時可以通過調(diào)整處理線程數(shù)量來進(jìn)行優(yōu)化。
CPU滿載并非表示這個階段的優(yōu)化完成,必須保證CPU時間都用在處理業(yè)務(wù)邏輯上。要確認(rèn)這一點需要對程序進(jìn)行跟蹤。通常CPU滿載卻沒有用于處理業(yè)務(wù)邏輯的原因在于同步和線程調(diào)用。
5.2 流控
流控要保證在CPU滿載的同時,盡量縮短隊列長度。流控通常在接收客戶端請求的隊列進(jìn)行。如果在中間隊列進(jìn)行,會浪費CPU處理時間和隊列空間。
6 參考資料
- 《事務(wù)處理:概念與技術(shù)》
- 在Linux下做性能分析1:基本模型 https://zhuanlan.zhihu.com/p/22124514