by obroot
| posted: 2023年7月28日
0 comment
1、首先需要乙個記憶體池,目的在於:
減少頻繁的分配和釋放,提高效能的同時,還能避免記憶體碎片的問題;能夠儲存變長的資料,不要很傻瓜地只能預分配乙個最大長度;基於slab演算法實現記憶體池是乙個好的思路:分配不同大小的多個塊,請求時返回大於請求長度的最小塊即可,對於容器而言,處理固定塊的分配和**,相當容易實現。當然,還要記得需要設計成執行緒安全的,自旋鎖比較好,使用讀寫自旋鎖就更好了。分配內容的增長管理是乙個問題,比如第一次需要1kb空間,隨著資料源源不斷的寫入,第二次就需要4kb空間了。擴充空間容易實現,可是擴充的時候必然 涉及資料拷貝。甚至,擴充的需求很大,上百兆的資料,這樣就不好辦了。暫時沒更好的想法,可以像stl一樣,指數級增長的分配策略,拷貝資料雖不可避免, 但是起碼重分配的機率越來越小了。上面提到的,如果是上百兆的資料擴充套件需要,採用記憶體對映檔案來管理是乙個好的辦法:對映檔案後,雖然佔了很大的虛擬記憶體,但是物理記憶體僅在寫入的時候才會被分配,加上madvice()來加上順序寫的優化建議後,物理記憶體的消耗也會變小。用string或者vector去管理記憶體並不明智,雖然很簡單,但伺服器軟體開發中不適合使用stl,特別是對穩定性和效能要求很高的情況下。
2、第二個需要考慮的是物件池,與記憶體池類似:
減少物件的分配和釋放。其實c++物件也就是struct,把構造和析構脫離出來手動初始化和清理,保持對同乙個緩衝區的迴圈利用,也就不難了。可以設計為乙個物件池只能存放一種物件,則物件池的實現實際就是固定記憶體塊的池化管理,非常簡單。畢竟,物件的數量非常有限。
3、第三個需要的是佇列:
如果可以預料到極限的處理能力,採用固定大小的環形佇列來作為緩衝區是比較不錯的。乙個生產者乙個消費者是常見的應用場景,環形佇列有其經典的鎖無關演算法,在乙個執行緒讀乙個執行緒寫的場景下,實現簡單,效能還高,還不涉及資源的分配和釋放。實在是好!
4、第四個需要的是對映表,或者說hash表:
因為epoll是事件觸發的,而一系列的流程可能是分散在多個事件中的,因此,必須保留下中間狀態,使得下乙個事件觸發的時候,能夠接著上次處理的位置繼續處理。要簡單的話,stl的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控制代碼放到傳送執行緒佇列中去。對於任務佇列,接收執行緒是生產者,多個工作執行緒是消費者;對於傳送執行緒佇列,多個工作執行緒是生產者,傳送執行緒是消費者。在這裡需要注意鎖的問題。
11、僅僅只用scoket控制代碼作為hash表的鍵,並不夠:
假設這樣一種情況:事件執行緒剛把某socket因發生epollin事件放入了接收佇列,可是隨即客戶端異常斷開了,事件執行緒又因為epollerr事件刪除了hash表中的這一項。假設接收佇列很長,發生異常的socket還在佇列中,等到接收執行緒處理到這個socket的時候,並不能通過socket控制代碼索引到hash表中的物件。索引不到的情況也好處理,難點就在於,這個socket控制代碼立即被另乙個客戶端使用了,接入執行緒為這個scoket建立了hash表中的某個物件。此時,控制代碼相同的兩個socket,其實已經是不同的兩個客戶端了。極端情況下,這種情況是可能發生的。解決的辦法是,使用socket fd + sequence為hash表的鍵,sequence由接入執行緒在每次accept()後將乙個整型值累加而得到。這樣,就算socket控制代碼被重用,也不會發生問題了。
12、監控,需要考慮:
框架中最容易出問題的是工作執行緒:工作執行緒的處理速度太慢,就會使得各個佇列暴漲,最終導致伺服器崩潰。因此必須要限制每個佇列允許的最大大小,且需要監視每個工作執行緒的處理時間,超過這個時間就應該採用某個辦法結束掉工作執行緒。
高效能的伺服器處理框架
終於開始學習epoll了,雖然不明白的地方還是很多,但從理論到實踐,相信自己動手去寫乙個具體的框架後,一切會清晰很多。1 首先需要乙個記憶體池,目的在於 減少頻繁的分配和釋放,提高效能的同時,還能避免記憶體碎片的問題 能夠儲存變長的資料,不要很傻瓜地只能預分配乙個最大長度 基於slab演算法實現記憶...
epoll學習 思考一種高效能的伺服器處理框架
1 首先需要乙個記憶體池,目的在於 減少頻繁的分配和釋放,提高效能的同時,還能避免記憶體碎片的問題 能夠儲存變長的資料,不要很傻瓜地只能預分配乙個最大長度 基於slab演算法實現記憶體池是乙個好的思路 分配不同大小的多個塊,請求時返回大於請求長度的最小塊即可,對於容器而言,處理固定塊的分配和 相當容...
epoll學習 思考一種高效能的伺服器處理框架
終於開始學習epoll了,雖然不明白的地方還是很多,但從理論到實踐,相信自己動手去寫乙個具體的框架後,一切會清晰很多。1 首先需要乙個記憶體池,目的在於 減少頻繁的分配和釋放,提高效能的同時,還能避免記憶體碎片的問題 能夠儲存變長的資料,不要很傻瓜地只能預分配乙個最大長度 基於slab演算法實現記憶...