Linux高性能服務(wù)器處理框架

責(zé)任編輯:cres

2019-07-31 16:32:24

摘自:簡書

終于開始學(xué)習(xí)epoll了,雖然不明白的地方還是很多,但從理論到實踐,相信自己動手去寫一個具體的框架后,一切會清晰很多。

終于開始學(xué)習(xí)epoll了,雖然不明白的地方還是很多,但從理論到實踐,相信自己動手去寫一個具體的框架后,一切會清晰很多。
 
1、首先需要一個內(nèi)存池,目的在于:
 
·減少頻繁的分配和釋放,提高性能的同時,還能避免內(nèi)存碎片的問題;
 
·能夠存儲變長的數(shù)據(jù),不要很傻瓜地只能預(yù)分配一個最大長度;
 
·基于SLAB算法實現(xiàn)內(nèi)存池是一個好的思路:分配不同大小的多個塊,請求時返回大于請求長度的最小塊即可,對于容器而言,處理固定塊的分配和回收,相當(dāng)容易實現(xiàn)。當(dāng)然,還要記得需要設(shè)計成線程安全的,自旋鎖比較好,使用讀寫自旋鎖就更好了。
 
·分配內(nèi)容的增長管理是一個問題,比如第一次需要1KB空間,隨著數(shù)據(jù)源源不斷的寫入,第二次就需要4KB空間了。擴充空間容易實現(xiàn),可是擴充的時候必然 涉及數(shù)據(jù)拷貝。甚至,擴充的需求很大,上百兆的數(shù)據(jù),這樣就不好辦了。暫時沒更好的想法,可以像STL一樣,指數(shù)級增長的分配策略,拷貝數(shù)據(jù)雖不可避免, 但是起碼重分配的幾率越來越小了。
 
·上面提到的,如果是上百兆的數(shù)據(jù)擴展需要,采用內(nèi)存映射文件來管理是一個好的辦法:映射文件后,雖然占了很大的虛擬內(nèi)存,但是物理內(nèi)存僅在寫入的時候才會被分配,加上madvice()來加上順序?qū)懙膬?yōu)化建議后,物理內(nèi)存的消耗也會變小。
 
·用string或者vector去管理內(nèi)存并不明智,雖然很簡單,但服務(wù)器軟件開發(fā)中不適合使用STL,特別是對穩(wěn)定性和性能要求很高的情況下。
 
2、第二個需要考慮的是對象池,與內(nèi)存池類似:
 
·減少對象的分配和釋放。其實C++對象也就是struct,把構(gòu)造和析構(gòu)脫離出來手動初始化和清理,保持對同一個緩沖區(qū)的循環(huán)利用,也就不難了。
 
·可以設(shè)計為一個對象池只能存放一種對象,則對象池的實現(xiàn)實際就是固定內(nèi)存塊的池化管理,非常簡單。畢竟,對象的數(shù)量非常有限。
 
3、第三個需要的是隊列:
 
·如果可以預(yù)料到極限的處理能力,采用固定大小的環(huán)形隊列來作為緩沖區(qū)是比較不錯的。一個生產(chǎn)者一個消費者是常見的應(yīng)用場景,環(huán)形隊列有其經(jīng)典的“鎖無關(guān)”算法,在一個線程讀一個線程寫的場景下,實現(xiàn)簡單,性能還高,還不涉及資源的分配和釋放。好啊,實在是好!
 
·涉及多個生產(chǎn)者消費者的時候,tbb::concurent_queue是不錯的選擇,線程安全,并發(fā)性也好,就是不知道資源的分配釋放是否也管理得足夠好。
 
4、第四個需要的是映射表,或者說hash表:
 
·因為epoll是事件觸發(fā)的,而一系列的流程可能是分散在多個事件中的,因此,必須保留下中間狀態(tài),使得下一個事件觸發(fā)的時候,能夠接著上次處理的位置繼續(xù)處理。要簡單的話,STL的hash_map還行,不過得自己處理鎖的問題,多線程環(huán)境下使用起來很麻煩。
 
·多線程環(huán)境下的hash表,最好的還是tbb::concurent_hash_map。
 
5、核心的線程是事件線程:
 
·事件線程是調(diào)用epoll_wait()等待事件的線程。例子代碼里面,一個線程干了所有的事情,而需要開發(fā)一個高性能的服務(wù)器的時候,事件線程應(yīng)該專注于事件本身的處理,將觸發(fā)事件的socket句柄放到對應(yīng)的處理隊列中去,由具體的處理線程負責(zé)具體的工作。
 
6、accept()單獨一個線程:
 
·服務(wù)端的socket句柄(就是調(diào)用bind()和listen()的這個)最好在單獨的一個線程里面做accept(),阻塞還是非阻塞都無所謂,相比整個服務(wù)器的通訊,用戶接入的動作只是很小一部分。而且,accept()不放在事件線程的循環(huán)里面,減少了判斷。
 
7、接收線程單獨一個:
 
·接收線程從發(fā)生EPOLLIN事件的隊列中取出socket句柄,然后在這個句柄上調(diào)用recv接收數(shù)據(jù),直到緩沖區(qū)沒有數(shù)據(jù)為止。接收到的數(shù)據(jù)寫入以socket為鍵的hash表中,hash表中有一個自增長的緩沖區(qū),保存了客戶端發(fā)過來的數(shù)據(jù)。
 
·這樣的處理方式適合于客戶端發(fā)來的數(shù)據(jù)很小的應(yīng)用,比如HTTP服務(wù)器之類;假設(shè)是文件上傳的服務(wù)器,則接受線程會一直處理某個連接的海量數(shù)據(jù),其他客戶端的數(shù)據(jù)處理產(chǎn)生了饑餓。所以,如果是文件上傳服務(wù)器一類的場景,就不能這樣設(shè)計。
 
8、發(fā)送線程單獨一個:
 
·發(fā)送線程從發(fā)送隊列獲取需要發(fā)送數(shù)據(jù)的SOCKET句柄,在這些句柄上調(diào)用send()將數(shù)據(jù)發(fā)到客戶端。隊列中指保存了SOCKET句柄,具體的信息 還需要通過socket句柄在hash表中查找,定位到具體的對象。如同上面所講,客戶端信息的對象不但有一個變長的接收數(shù)據(jù)緩沖區(qū),還有一個變長的發(fā)送 數(shù)據(jù)緩沖區(qū)。具體的工作線程發(fā)送數(shù)據(jù)的時候并不直接調(diào)用send()函數(shù),而是將數(shù)據(jù)寫到發(fā)送數(shù)據(jù)緩沖區(qū),然后把SOCKET句柄放到發(fā)送線程隊列。
 
·SOCKET句柄放到發(fā)送線程隊列的另一種情況是:事件線程中發(fā)生了EPOLLOUT事件,說明TCP的發(fā)送緩沖區(qū)又有了可用的空間,這個時候可以把SOCKET句柄放到發(fā)送線程隊列,一邊觸發(fā)send()的調(diào)用;
 
·需要注意的是:發(fā)送線程發(fā)送大量數(shù)據(jù)的時候,當(dāng)頻繁調(diào)用send()直到TCP的發(fā)送緩沖區(qū)滿后,便無法再發(fā)送了。這個時候如果循環(huán)等待,則其他用戶的 發(fā)送工作受到影響;如果不繼續(xù)發(fā)送,則EPOLL的ET模式可能不會再產(chǎn)生事件。解決這個問題的辦法是在發(fā)送線程內(nèi)再建立隊列,或者在用戶信息對象上設(shè)置 標(biāo)志,等到線程空閑的時候,再去繼續(xù)發(fā)送這些未發(fā)送完成的數(shù)據(jù)。
 
9、需要一個定時器線程:
 
·一位將epoll使用的高手說道:“單純靠epoll來管理描述符不泄露幾乎是不可能的。完全解決方案很簡單,就是對每個fd設(shè)置超時時間,如果超過timeout的時間,這個fd沒有活躍過,就close掉”。
 
·所以,定時器線程定期輪訓(xùn)整個hash表,檢查socket是否在規(guī)定的時間內(nèi)未活動。未活動的SOCKET認為是超時,然后服務(wù)器主動關(guān)閉句柄,回收資源。
 
10、多個工作線程:
 
·工作線程由接收線程去觸發(fā):每次接收線程收到數(shù)據(jù)后,將有數(shù)據(jù)的SOCKET句柄放入一個工作隊列中;工作線程再從工作隊列獲取SOCKET句柄,查詢hash表,定位到用戶信息對象,處理業(yè)務(wù)邏輯。
 
·工作線程如果需要發(fā)送數(shù)據(jù),先把數(shù)據(jù)寫入用戶信息對象的發(fā)送緩沖區(qū),然后把SOCKET句柄放到發(fā)送線程隊列中去。
 
·對于任務(wù)隊列,接收線程是生產(chǎn)者,多個工作線程是消費者;對于發(fā)送線程隊列,多個工作線程是生產(chǎn)者,發(fā)送線程是消費者。在這里需要注意鎖的問題,如果采用tbb::concurrent_queue,會輕松很多。
 
11、僅僅只用scoket句柄作為hash表的鍵,并不夠:
 
·假設(shè)這樣一種情況:事件線程剛把某SOCKET因發(fā)生EPOLLIN事件放入了接收隊列,可是隨即客戶端異常斷開了,事件線程又因為EPOLLERR事 件刪除了hash表中的這一項。假設(shè)接收隊列很長,發(fā)生異常的SOCKET還在隊列中,等到接收線程處理到這個SOCKET的時候,并不能通過 SOCKET句柄索引到hash表中的對象。
 
·索引不到的情況也好處理,難點就在于,這個SOCKET句柄立即被另一個客戶端使用了,接入線程為這個SCOKET建立了hash表中的某個對象。此時,句柄相同的兩個SOCKET,其實已經(jīng)是不同的兩個客戶端了。極端情況下,這種情況是可能發(fā)生的。
 
·解決的辦法是,使用socket fd + sequence為hash表的鍵,sequence由接入線程在每次accept()后將一個整型值累加而得到。這樣,就算SOCKET句柄被重用,也不會發(fā)生問題了。
 
12、監(jiān)控,需要考慮:
 
·框架中最容易出問題的是工作線程:工作線程的處理速度太慢,就會使得各個隊列暴漲,最終導(dǎo)致服務(wù)器崩潰。因此必須要限制每個隊列允許的最大大小,且需要監(jiān)視每個工作線程的處理時間,超過這個時間就應(yīng)該采用某個辦法結(jié)束掉工作線程。

鏈接已復(fù)制,快去分享吧

企業(yè)網(wǎng)版權(quán)所有?2010-2024 京ICP備09108050號-6京公網(wǎng)安備 11010502049343號