終於開始學習epoll了,雖然不明白的地方還是很多,但從理論到實踐,相信自己動手去寫乙個具體的框架後,一切會清晰很多。
1、首先需要乙個記憶體池,目的在於:
·減少頻繁的分配和釋放,提高效能的同時,還能避免記憶體碎片的問題;
·能夠儲存變長的資料,不要很傻瓜地只能預分配乙個最大長度;
·基於slab演算法實現記憶體池是乙個好的思路:分配不同大小的多個塊,請求時返回大於請求長度的最小塊即可,對於容器而言,處理固定塊的分配和**,相當容易實現。當然,還要記得需要設計成執行緒安全的,自旋鎖比較好,使用讀寫自旋鎖就更好了。
·分配內容的增長管理是乙個問題,比如第一次需要1kb空間,隨著資料源源不斷的寫入,第二次就需要4kb空間了。擴充空間容易實現,可是擴充的時候必然涉及資料拷貝。甚至,擴充的需求很大,上百兆的資料,這樣就不好辦了。暫時沒更好的想法,可以像stl一樣,指數級增長的分配策略,拷貝資料雖不可避免,但是起碼重分配的機率越來越小了。
·上面提到的,如果是上百兆的資料擴充套件需要,採用記憶體對映檔案來管理是乙個好的辦法:對映檔案後,雖然佔了很大的虛擬記憶體,但是物理記憶體僅在寫入的時候才會被分配,加上madvice()來加上順序寫的優化建議後,物理記憶體的消耗也會變小。
·用string或者vector去管理記憶體並不明智,雖然很簡單,但伺服器軟體開發中不適合使用stl,特別是對穩定性和效能要求很高的情況下。
2、第二個需要考慮的是物件池,與記憶體池類似:
·減少物件的分配和釋放。其實c++物件也就是struct,把構造和析構脫離出來手動初始化和清理,保持對同乙個緩衝區的迴圈利用,也就不難了。
·可以設計為乙個物件池只能存放一種物件,則物件池的實現實際就是固定記憶體塊的池化管理,非常簡單。畢竟,物件的數量非常有限。
3、第三個需要的是佇列:
·如果可以預料到極限的處理能力,採用固定大小的環形佇列來作為緩衝區是比較不錯的。乙個生產者乙個消費者是常見的應用場景,環形佇列有其經典的「鎖無關」演算法,在乙個執行緒讀乙個執行緒寫的場景下,實現簡單,效能還高,還不涉及資源的分配和釋放。好啊,實在是好!
·涉及多個生產者消費者的時候,tbb::concurent_queue是不錯的選擇,執行緒安全,併發性也好,就是不知道資源的分配釋放是否也管理得足夠好。
4、第四個需要的是對映表,或者說hash表:
·因為epoll是事件觸發的,而一系列的流程可能是分散在多個事件中的,因此,必須保留下中間狀態,使得下乙個事件觸發的時候,能夠接著上次處理的位置繼續處理。要簡單的話,stl的hash_map還行,不過得自己處理鎖的問題,多執行緒環境下使用起來很麻煩。
·多執行緒環境下的hash表,最好的還是tbb::concurent_hash_map。
5、核心的執行緒是事件執行緒:
·事件執行緒是呼叫epoll_wait()等待事件的執行緒。例子**裡面,乙個執行緒幹了所有的事情,而需要開發乙個高效能的伺服器的時候,事件執行緒應該專注於事件本身的處理,將觸發事件的socket控制代碼放到對應的處理佇列中去,由具體的處理執行緒負責具體的工作。
6、accept()單獨乙個執行緒:
·服務端的socket控制代碼(就是呼叫bind()和listen()的這個)最好在單獨的乙個執行緒裡面做accept(),阻塞還是非阻塞都無所謂,相比整個伺服器的通訊,使用者接入的動作只是很小一部分。而且,accept()不放在事件執行緒的迴圈裡面,減少了判斷。
7、接收執行緒單獨乙個:
·接收執行緒從發生epollin事件的佇列中取出socket控制代碼,然後在這個控制代碼上呼叫recv接收資料,直到緩衝區沒有資料為止。接收到的資料寫入以socket為鍵的hash表中,hash表中有乙個自增長的緩衝區,儲存了客戶端發過來的資料。
·這樣的處理方式適合於客戶端發來的資料很小的應用,比如http伺服器之類;假設是檔案上傳的伺服器,則接受執行緒會一直處理某個連線的海量資料,其他客戶端的資料處理產生了飢餓。所以,如果是檔案上傳伺服器一類的場景,就不能這樣設計。
8、傳送執行緒單獨乙個:
·傳送執行緒從傳送佇列獲取需要傳送資料的socket控制代碼,在這些控制代碼上呼叫send()將資料發到客戶端。佇列中指儲存了socket控制代碼,具體的資訊還需要通過socket控制代碼在hash表中查詢,定位到具體的物件。如同上面所講,客戶端資訊的物件不但有乙個變長的接收資料緩衝區,還有乙個變長的傳送資料緩衝區。具體的工作執行緒傳送資料的時候並不直接呼叫send()函式,而是將資料寫到傳送資料緩衝區,然後把socket控制代碼放到傳送執行緒佇列。
·socket控制代碼放到傳送執行緒佇列的另一種情況是:事件執行緒中發生了epollout事件,說明tcp的傳送緩衝區又有了可用的空間,這個時候可以把socket控制代碼放到傳送執行緒佇列,一邊觸發send()的呼叫;
·需要注意的是:傳送執行緒傳送大量資料的時候,當頻繁呼叫send()直到tcp的傳送緩衝區滿後,便無法再傳送了。這個時候如果迴圈等待,則其他使用者的傳送工作受到影響;如果不繼續傳送,則epoll的et模式可能不會再產生事件。解決這個問題的辦法是在傳送執行緒內再建立佇列,或者在使用者資訊物件上設定標誌,等到執行緒空閒的時候,再去繼續傳送這些未傳送完成的資料。
9、需要乙個定時器執行緒:
·一位將epoll使用的高手說道:「單純靠epoll來管理描述符不洩露幾乎是不可能的。完全解決方案很簡單,就是對每個fd設定超時時間,如果超過timeout的時間,這個fd沒有活躍過,就close掉」。
·所以,定時器執行緒定期輪訓整個hash表,檢查socket是否在規定的時間內未活動。未活動的socket認為是超時,然後伺服器主動關閉控制代碼,**資源。
10、多個工作執行緒:
·工作執行緒由接收執行緒去觸發:每次接收執行緒收到資料後,將有資料的socket控制代碼放入乙個工作佇列中;工作執行緒再從工作佇列獲取socket控制代碼,查詢hash表,定位到使用者資訊物件,處理業務邏輯。
·工作執行緒如果需要傳送資料,先把資料寫入使用者資訊物件的傳送緩衝區,然後把socket控制代碼放到傳送執行緒佇列中去。
·對於任務佇列,接收執行緒是生產者,多個工作執行緒是消費者;對於傳送執行緒佇列,多個工作執行緒是生產者,傳送執行緒是消費者。在這裡需要注意鎖的問題,如果採用tbb::concurrent_queue,會輕鬆很多。
11、僅僅只用scoket控制代碼作為hash表的鍵,並不夠:
·假設這樣一種情況:事件執行緒剛把某socket因發生epollin事件放入了接收佇列,可是隨即客戶端異常斷開了,事件執行緒又因為epollerr事件刪除了hash表中的這一項。假設接收佇列很長,發生異常的socket還在佇列中,等到接收執行緒處理到這個socket的時候,並不能通過socket控制代碼索引到hash表中的物件。
·索引不到的情況也好處理,難點就在於,這個socket控制代碼立即被另乙個客戶端使用了,接入執行緒為這個scoket建立了hash表中的某個物件。此時,控制代碼相同的兩個socket,其實已經是不同的兩個客戶端了。極端情況下,這種情況是可能發生的。
·解決的辦法是,使用socket fd + sequence為hash表的鍵,sequence由接入執行緒在每次accept()後將乙個整型值累加而得到。這樣,就算socket控制代碼被重用,也不會發生問題了。
12、監控,需要考慮:
·框架中最容易出問題的是工作執行緒:工作執行緒的處理速度太慢,就會使得各個佇列暴漲,最終導致伺服器崩潰。因此必須要限制每個佇列允許的最大大小,且需要監視每個工作執行緒的處理時間,超過這個時間就應該採用某個辦法結束掉工作執行緒。
一種高效能的伺服器處理框架
by obroot posted 2014年7月28日 0 comment 1 首先需要乙個記憶體池,目的在於 減少頻繁的分配和釋放,提高效能的同時,還能避免記憶體碎片的問題 能夠儲存變長的資料,不要很傻瓜地只能預分配乙個最大長度 基於slab演算法實現記憶體池是乙個好的思路 分配不同大小的多個塊,...
Linux 高效能伺服器程式框架
伺服器解構主要分為如下三個主要模組 1 i o處理單元。接收客戶端傳送的資料都屬於i o處理單元。2 邏輯單元。接收到資料之後進行的一些處理都屬於邏輯單元。3 儲存單元。如下圖所示 伺服器程式設計框架 1 c s模型 c就是客戶端,s就是伺服器。所以這個模型也稱客戶端 伺服器模型。c s模型如下圖所...
高效能伺服器設計
原文 http blog.chinaunix.net u 5251 showart 236329.html 先後檢視了 haproxy l7sw 和lighttpd 的相關原始碼,無一例外,他們一致認為多路復用是效能最好的伺服器架構 事實也確實應該如此,程序的出現一方面就是為了儲存任務的執行上下文從...