服務(wù)器軟件性能優(yōu)化

本文介紹了服務(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 參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 專業(yè)考題類型管理運行工作負(fù)責(zé)人一般作業(yè)考題內(nèi)容選項A選項B選項C選項D選項E選項F正確答案 變電單選GYSZ本規(guī)程...
    小白兔去釣魚閱讀 10,489評論 0 13
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,629評論 1 32
  • 一、計算機網(wǎng)絡(luò)在信息時代中的作用 網(wǎng)絡(luò)分為:電信網(wǎng)絡(luò)、有線電視網(wǎng)絡(luò)、計算機網(wǎng)絡(luò) 網(wǎng)絡(luò)向用戶提供的功能:①連通性(用...
    dmmy大印閱讀 1,837評論 0 2
  • 在服務(wù)器端程序開發(fā)領(lǐng)域,性能問題一直是備受關(guān)注的重點。業(yè)界有大量的框架、組件、類庫都是以性能為賣點而廣為人知。然而...
    dreamer_lk閱讀 1,100評論 0 17
  • 每年的6、7月份總會有同一個熱門話題,席卷大江南北,就是高考。前兩天,我看到這樣一篇報道,大意是說現(xiàn)在的高考狀元,...
    我是王小鈺閱讀 1,085評論 0 6

友情鏈接更多精彩內(nèi)容