Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 高性能服務端漫談

高性能服務端漫談

編輯:關於Android編程

進入多核時代已經很久了,大數據概念也吵得沸沸揚揚,不管你喜歡不喜歡,不管你遇到沒遇到,big-data或bigger-data都必須正視.

處理大數據,基本都離不開分布式計算和分布式存儲,這其中以hadoop最為使用廣泛和經典。

分布式系統,就離不開計算系統、網絡系統、文件系統和數據庫系統。

這麼多系統,之間又是如何協作的呢?

通訊過程又是如何保障高性能的呢?

1.單處理器

在以前的單核心cpu下,我們要實現文件I/O、網絡I/O,可以妥妥的使用單線程循環處理任務。

但是如果想“同時”做點其它事情,就得多開線程,比如:

在下載遠程文件的同時,顯示下載進度。

2.多線程

我們會用主線程a來更新界面元素,這裡是更新下載進度條,同時用一個額外的線程b去下載遠程文件。

3.阻塞

當需要下載的時候,我們必須使a阻塞,否則,我們的下載線程b將無法獲得cpu時間。

而當需要更新界面時,我們必須使b阻塞,原因也是為了獲得cpu時間。

阻塞:使一個線程進入阻塞或等待的狀態,釋放它所占有的cpu時間.

阻塞的原因,是因為任何操作,無論是更新界面還是下載文件(網絡I/O+磁盤I/O),都會轉化成為一條一條cpu可以執行的指令,而這些指令的讀取、執行都需要消耗cpu的時鐘周期。

事實上,我們可以使用類似wait、await、sleep、read、write等操作使當前調用的線程進入阻塞。

顯然

阻塞的目的是

1.當前有更重要的事情要交給別的線程來做.

2.調用線程可能面臨一個長時間的I/O操作,占據cpu時間顯然是一種浪費.

3.同步需求.

雖然用戶看起來是下載文件和更新界面“同時”運行,但實際上,任何一個時刻,在單核心cpu環境下,都只有一個線程會真正的運行,所以多線程之間是“並發”而非真正的“並行”。

4.多處理器

單核心時代早已過去,多核、多處理無論在企業級服務器還是家用桌面電腦、平板和智能手機上都已經是主流。

注:圖片來源intel

如果多處理器的核心是真實的而非虛擬化的,那麼多線程就可以真正的並行。

可以看到,t1、t2、t3的運行時間可以出現重疊.

但實際上,操作中運行的進程遠不止幾個,那麼相應的線程數會遠大於cpu的核心數,所以即使上圖中假設是4核心處理器,那麼真正能同時執行的線程也只有4個,所以也會出現運行的中斷,即阻塞。

二、高性能通訊

在了解了多核、多處理器、多線程、阻塞的概念後,我們來看看通訊,顯然,任何一個通訊框架都試圖追求高性能、高吞吐、高tps能力。

但是,任何一個來自用戶或其它服務器的請求都不可能只是要求一個簡單的echo返回,所以請求執行的任務幾乎都會包含:

1.計算 比如MapReduce、SQL、坐標計算等等.

2.I/O 訪問數據庫、磁盤、緩存、內存或者其它設備.

站在用戶的角度,總是希望自己的請求會被優先、快速的響應,而站在服務器的角度,總是希望所有的請求同時能夠被處理.

1.同步/異步

同步的意思如字面一般簡單:

同步就是多個對象的步調一致,這種步調是一種約定。

比如,時間上約定10點同時到達,先到達的就會等待。

比如,邏輯上約定必須取得結果,調用才能返回。

比如,資源上約定read和write不可同時進行,但read之間可同時執行。

下面的圖顯示了時間上的同步約定:

而異步,就是步調無須一致:

異步,就是多個對象之間的行為無須遵守顯式或隱式的約定。

比如,老婆沒到,你也可以進場看電影。

比如,可以不必等結果真正出現,就立即返回。

比如,read和read之間可以亂序訪問文件或資源。

2.同步與阻塞的關系

服務器的能力是有限的,為了能夠滿足所有用戶的請求,服務器必須能夠進行高並發的處理。這一點可以通過兩種方式達到:

1.單線程 + 異步I/O. (node.js)

多線程的建立是需要開銷的,線程數越多,線程上下文的切換就會越頻繁,而異步I/O在“理想”情況下不會阻塞,調用完畢即返回,通過回調callback或事件通知來處理結果.

2.多線程 + 異步或同步I/O. (nginx)

單線程的一個缺點就是無法充分利用多處理器的並行能力,同時異步I/O不是在任何情況下都是真正異步的。

比如文件在緩存中(通過映射到內存)、文件壓縮、擴展、緩沖區拷貝等操作,會使得異步I/O被操作系統偷偷地轉換為同步。

假如文件已經在緩存中,使用同步I/O的結果會更快。

這裡你可能會疑惑,同步看起來很像“阻塞”,但仔細看本篇中對它們的說明,就會發現:

阻塞是調用線程的一種狀態或行為,它的作用是放棄占用的cpu.

同步是多個線程之間的協調機制,它的作用是為了保證操作的順序是正確可預期。

同步可以使用阻塞來實現,也可以使用非阻塞來實現。

而有的情況下,因為同步是不得已的行為,比如要hold住一個來自其他服務器的session,以防止立即返回後的上下文失效,我們往往會這樣:

//還沒有結果
bool haveResponse = false;
//調用異步I/O,從遠程數據庫執行sql,並返回結果
rpc.callAsync(database,sql,
 function(resp){ 
 response = resp;
 haveResponse = true;
 });
//通過循環阻塞來hold住這個線程的上下文和session
while(!response){
 //這裡將阻塞100毫秒
 if(!response){
 await(100);
 }else{
 break;
 }
}
//通過請求的session返回結果
httpContext.currentSession.Respond(response);

這是一種 多線程 + 異步 轉為了 多線程 + 同步的方式,因為Web應用服務器處理session時采用的往往是線程池技術,而我們又沒有服務器推(server push)或者用戶的調用請求一直在等待結果,所以,即使訪問數據庫采用的是異步I/O,也不得不通過這種方法來變成同步。

與其如此,還不如:

//調用同步I/O,從遠程數據庫執行sql,並返回結果
//調用時,此線程阻塞
response = rpc.callSync(database,sql);
//通過請求的session返回結果
httpContext.currentSession.Respond(response);

上面的代碼,使用了簡單的同步I/O模型,因為一般的訪問數據庫操作是很費時的操作,所以處理當前session的線程符合被阻塞的目的,那麼同步調用就被實現為阻塞的方式。

事實上,從用戶的角度來看,用戶發出請求後總是期待會返回一個確定的結果,無論服務端如何處理用戶的請求,都必須將結果返回給用戶,所以采用異步I/O雖然是最理想的狀態,但必須考慮整個應用的設計,即使你這裡使用了異步,別的地方也可能需要同步,如果這種“額外”同步的設計復雜性遠高於使用異步帶來的好處,那麼請考慮“同步/阻塞式”設計。

如果業務邏輯上,要求依賴性調用,比如DAG,那麼同步也是必須的。

三、IOCP和epoll

1. IOCP(完成端口)

windows提供了高效的異步I/O的線程模型,完成端口:

完成端口可以關聯很多的文件句柄(這裡的文件是廣義的,文件、socket或者命名管道都可以是)到一個完成端口上,稱為關聯完成端口的引用,,這些引用都必須支持(Overlapped I/O,重疊式I/O)。

重疊式I/O是異步I/O的基石,通過進行重疊I/O,可以讓調用I/O操作的線程與I/O操作線程並行執行而無須阻塞。

多線程雖然可以充分發揮多處理器的並行優勢,但卻不是銀彈。

當線程數增加,可“同時”處理的請求量上去了,這樣吞吐量會很高,但可用於每個用戶請求的時間變少,每個用戶請求的響應時間隨之下降,最後吞吐率下降。

同時,線程的啟動和銷毀是有開銷的,雖然可以通過線程池(ThreadPool)來預先分配一定量的活動線程,但線程越多,其上下文切換(Context Switch)的次數就越頻繁。

考慮一種情況:

當線程的棧很大而線程被阻塞的時間很長,操作系統可能會將此線程的堆棧信息置換到硬盤上以節約內存給其它線程使用,這增加了磁盤I/O,而磁盤I/O的速度是非常慢的。

而且,線程的頻繁切換也會降低指令和數據的locality(局部性),cpu的緩存命中率會下降,這又加劇了性能的下降。

完成端口的設計目標是:

1.任一給定時刻,對於任一處理器,都有一個活動線程可用。

2.控制活動線程的數量,盡量減少線程上下文的切換。

可以看出,IOCP主要是針對線程模型的優化。

創建完成端口時,需要指定一個Concurrent Value = c的值,來指示:

當活動線程的數量 v >= c,就將其它關聯在完成端口上的線程阻塞,直到活動線程的數量 v < c.

當一個活動線程進行I/O時,會阻塞,活動線程數量v就會下降.

這一點是IOCP的精髓。

完成端口的原理是:

在創建了完成端口後,將socket關聯到這個端口上,一旦socket上的I/O操作完成,操作系統的I/O管理模塊會發送一個通知(Notification)給完成端口,並將I/O操作的完成結果(completion packet)送入完成端口的等待隊列WQ,這個隊列的順序是先入先出(FIFO)。

也就是說,調用線程可不比等待socket的I/O操作完成,就立即返回做其它的事情。

而當活動線程的數量下降,小於指定的並發約束(concurrent value)時,操作系統將會喚醒最近被加入阻塞隊列BQ的線程,讓它從完成包的等待隊列中取出一個最老的I/O結果進行處理。這裡可以看出,BQ的順序是後入先出(LIFO)。

IOCP所謂的異步是:

與完成端口關聯的文件(file、socket、named pipeline)句柄上的I/O操作是異步的。

調用線程只負責將socket I/O丟給完成端口,然後就可以做其它事。而無需向同步那樣等待。

但是,如果一個調用線程在處理這個從完成隊列取出的數據後,又在當前線程進行了其它I/O操作,比如讀取文件、訪問數據庫,那麼這個調用線程同樣會阻塞,但不是阻塞到完成端口的隊列上。

這一點,對數據的處理就涉及不同的業務邏輯需求,I/O線程是否應該與邏輯線程分開,分開後,邏輯線程應該是如何控制數量,如果分開,就要求在拿到數據後,要麼另起線程處理數據,要麼將數據扔進線程池(Threadpool)。無論是何種方式,都會增加線程上下文切換的次數,反過來影響IOCP的可用資源。

所以,要從應用的實際需求出發,來總體控制整個服務器的並發線程數量,否則,無論多麼高效的通訊模型,都會被業務模型(往往需要對文件或數據庫的訪問)所拖累,那麼整個應用的性能就會下降。

2. epoll

linux上的高效I/O模型則是epoll.

epoll是對select/poll模型的一種改進.

1.既然是對select/poll的改進,就是一種I/O多路復用模型。

2.支持的文件(同樣是廣義)描述符fileDescriptor巨大,具體多大與內存大小直接相關。

3.wait調用在活躍socket數目較少時,可高效返回。

在傳統的select/poll模型中,內核會遍歷所有的fileDescriptor(這裡只說socket),而不管socket是否活躍,這樣,隨著socket數目的增加,性能會很快下降。

而epoll模型,采用了向內核中斷處理注冊回調的方式,當某個socket上的I/O就緒,中斷就會發出,接著就會將這個結果推入一個就緒隊列Q中,Q采用單鏈表實現,所以擴展性是天生的。

同時,由於采用了適宜頻繁寫的平衡樹-紅黑樹的結構來存儲fileDescriptors,所以當需要向fileDescriptors中加入、刪除、查找socket時,就會非常高效,另外還有一層內核級頁高速緩存。

最後,由於活動的socket比較少時,I/O就緒的中斷次數相應減少,所以向就緒隊列Q中插入數據的次數相應減少,當wait操作被調用時,內核會考察Q,如果不空就立即返回,同時通過內存映射來講就緒的I/O數據從內核態拷貝到用戶態,達到少而快的效果。

epoll的主要調用接口如下:

/* 創建可保證size個效率epoll,返回epfd*/
int epoll_create(int size); 
/* 設置應當注冊的事件類型IN/OUT/ET/LT,並設置用於返回事件通知的events */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 
/* epoll進入阻塞,events用於設置返回事件通知的events */
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

邊沿觸發(ET)

考慮上面的圖,隨著時間的增加,高低電平交替變化。

所謂邊沿觸發,就是當達到邊沿(一個臨界條件)時觸發,如同0到1.

epoll中的邊沿觸發,是指當I/O就緒,中斷到達時,執行對應的回調,將結果推入`等待隊列Q`中,之後便不再關心這個結果。

這樣導致的結果是,當wait調用返回時,如果對應的事件沒有被處理完,比如讀操作沒有將buffer中的數據讀取完,就返回,將沒有機會再處理剩余的數據。

水平觸發(LT)

所謂水平觸發,就是每到上邊沿時就觸發,比如每次到1.

epoll中的邊沿觸發,是指當I/O就緒,中斷到達時,執行對應的回調,將結果推入`等待隊列Q`中,當隊列被清空後,再次將結果推入隊列。

這樣的結果是,當wait調用返回時,如果對應的時間沒有處理完,比如寫數據,寫了一部分,就返回,也會在下次wait中收到通知,從而得以繼續處理剩余數據。

水平觸發流程簡單穩定,需要考慮的事情少,且支持阻塞/非阻塞的socket I/O。

而邊沿觸發,在大並發情況下,更加高效,因為通知只發一次,但只支持非阻塞的socket I/O。

下圖是ET方式的epoll簡略流程:

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved