文章內有借鑑別人,也有自己分析,權且當做轉載吧,並不重要了
thrift優點不再多說,說說個人理解的缺點:
1. 如何判斷TSocket IsOpen 是否斷開?內部只是判斷是否已經連接過,如果網絡斷開,只查看套接字是否有效無法判斷連接狀態。
優化方法:增加個ping空接口,定時調用下,如果調用失敗,自然就是斷網
2. select實現的線程通知機制
現狀:該函數內部實現不好select數組長度最大1024。併發量很大的話,會崩潰。所以此處必須修復。
優化方法:poll
bool TNonblockingIOThread::notify(TNonblockingServer::TConnection* conn)
3. io線程給業務線程投遞AddTask機制,不太好,併發量很大的場景下,多個io thread同時push業務線程task隊列,可能會造成性能瓶頸。現狀:多個iothread投遞一個task隊列,多個業務線程提取task執行。
優化方法:多個業務線程 多個 task 隊列
std::queue<shared_ptr<Task> > tasks_;
Mutex mutex_;
4. io線程 第0號線程工作有點複雜,除了accept 客戶端套接字外,還處理分配到自己線程內的 transtion ,個人理解,最好是
0號線程只負責accept客戶端套接字,然後分發io任務給其他iothread。保證職責清晰,提升併發性能。
執行邏輯流程圖:
呃 可以持續優化的地方應該還有,想到再寫吧。
下面是調試輸出圖,大概看下:
以下是參考資料和邏輯圖
關於調用流程,有幾點需要着重解釋的:
1. 監聽線程只有一個,即#0號IO線程。 當新連接被分配(accept)給0號線程,該連接會進入狀態機轉移註冊相應IO事件,其它IO線程會通過pipe通知直接進入狀態轉移;
2. #0號IO線程與其它IO線程之間、IO線程與業務線程之間的通信是基於socketpair系統調用創建的本地套接字進行通信,實現簡潔高效;
2.1 創建線程間通訊socketpair
if (evutil_socketpair(AF_LOCAL, SOCK_STREAM, 0, notificationPipeFDs_) == -1) {
GlobalOutput.perror("TNonblockingServer::createNotificationPipe ", EVUTIL_SOCKET_ERROR());
throw TException("can't create notification pipe");
}
if (evutil_make_socket_nonblocking(notificationPipeFDs_[0]) < 0
|| evutil_make_socket_nonblocking(notificationPipeFDs_[1]) < 0) {
::THRIFT_CLOSESOCKET(notificationPipeFDs_[0]);
::THRIFT_CLOSESOCKET(notificationPipeFDs_[1]);
throw TException("TNonblockingServer::createNotificationPipe() THRIFT_O_NONBLOCK");
}
2.2 TNonblockingIOThread::notify
通知iothread函數,內部默認使用的是 ret = select(fd + 1, NULL, &wfds, &efds, NULL);
客戶端併發量不大情況下沒問題。量大則此處會崩潰,因爲select內部你懂得
可以換成 ret = poll(&pfd, 1, -1); 感覺此處還可以優化。
2.3 TNonblockingIOThread::notifyHandler
iothread線程收到通知後,調用
TNonblockingServer::TConnection* connection = 0;
const int kSize = sizeof(connection);
// 從管道中取出connection的指針地址
long nBytes = recv(fd, cast_sockopt(&connection), kSize, 0);
if (nBytes == kSize) {
if (connection == NULL) {
// this is the command to stop our thread, exit the handler!
return;
}
connection->transition();// 進入狀態轉換函數
進入接受數據包,拼接數據幀操作。數據幀拼接完成後封裝爲Task,投遞給業務線程,由業務線程處理。
業務線程完成後,回繼續調用notifyHandler通知iothread返回給客戶。
transition 也就是下面 3. 4. 提到的狀態機。
3. IO事件均依賴libevent庫註冊相關事件事件回調,這樣使得框架更多關注於如編解碼、任務封裝、具體的業務執行等
4. 連接狀態機邏輯
// 1. APP_INIT 初始狀態。
// 2. APP_READ_FRAME_SIZE 讀取幀數據。
// 3. APP_READ_REQUEST 讀取請求的數據,並根據請求的數據 進行數據的解析和任務的生成,並且將任務扔進線程池。
// 4. APP_WAIT_TASK 等待任務的完成
// 5. APP_SEND_RESULT 任務已經完成,將任務結果發送。
// 6. APP_CLOSE_CONNECTION 關閉連接。
// 每次app狀態轉移由 TConnetion::transition 函數完成:
// void transition();
// 狀態3 -> 狀態4 -> 狀態5 轉移很關鍵,涉及到線程池和主線程的交互。
5. 任務(Task)封裝是包含連接(TConnection)在內,而連接的創建包含了業務線程的執行體(TProcessor)且創建時機是在監聽到新連接分配IO線程時,連接生命週期遠長於Task,而IO線程在數據接收完成後進行Task封裝,業務線程僅僅去執行Task對應的TProcessor而不關注其它額外如初始化工作等。
// transition()爲狀態遷移函數
void TNonblockingServer::TConnection::transition() { ... }
TConnection的主要兩個方法:workSocket 和 transition,前者負責收發socket數據;後者負責業務邏輯狀態遷移;
狀態機分爲兩部分
socket狀態機 | app狀態機 | |
create connection | 第一個狀態值 SOCKET_RECV_FRAMING 代表進入該狀態就是有幀頭(數據包的大小)可以讀取 | appstate=APP_INIT |
appstate=APP_READ_FRAME_SIZE 讀取4字節數據幀頭 |
||
繼續接受客戶端發送數據 | socketstate=SOCKET_RECV |
4字節幀頭+後續數據接收完成後appstate=APP_READ_REQUEST 整理收到的完整數據幀,打包task給業務線程addTask(task),同時清理socket讀寫事件不再關心socket是否有可讀、可寫 appstate=APP_WAIT_TASK |
appstate=APP_WAIT_TASK ===> APP_SEND_RESULT ===> APP_INIT 業務線程在APP_READ_REQUEST狀態處,介入處理處理客戶端請求。 |
+--> APP_INIT -----> APP_READ_FRAME_SIZE ---> APP_READ_REQUEST ---+
| |
| |
| |
+------------------- APP_SEND_RESULT <--- APP_WAIT_TASK <-----+
6. 業務線程缺點分析:所有業務線程是由一個線程池管理類ThreadManager::newSimpleThreadManager管理,內部只有一個任務隊列;
friend class ThreadManager::Task;
std::queue<shared_ptr<Task> > tasks_;
Mutex mutex_;
個人認爲這裏可以修改爲每個業務線程可獨佔一個任務隊列,由主線程對task做負載分配(如輪詢),可明顯減少多個業務線程爭用同一task隊列的全局鎖開銷。
這個網絡IO模型很多庫也是類似做法,比如: muduo ===》 主線程 + iothread + work池模式
簡單分析下主要類的功能和流程:
TNonblockingIOThread:
IO線程(主線程),運行着libevent的主循環
主要成員包括 主線程指針,listensocket,pipefd等
入口: serve()
創建監聽套接字,啓動iothread(1-n),主線程直接調iothread[0]的run
啓動:run()
初始化libevent的東西,registerEvents()
關注listensocket的read事件(id=0時,即爲主線程),創建pipefd,關注pipefd的read事件
運行libevent的主循環
主線程accept, handleEvent:
accept客戶端連接,如果有過載保護,且過載了則可能踢掉這個鏈接,如果過載策略是清理隊列,則執行drainPendingTask(清理並關閉連接)
設置非阻塞,創建一個connection,這個conn歸屬到那個iothread是round-robin方式,即自增%size形式,加入後並不關注read/write事件
如果是單線程就在這裏調用transition,否則調notifyIOThread().
notifyIOThread:
如其名,就是通知iothread,具體是給該線程的pipefd發送this(conn的)指針.
notifyHandler:
iothread獲得pipefd事件後調用,讀取一個指針要的大小,獲得connection後調用transition.
connection:
connection內部維護了appstate(transition用),socketstate(workSocket用)
appstate初始化爲appinit
transition(appinit):
appstate->read_frame_size, socketstate->recvframe
初始化一些信息,關注讀事件
workSocket(recvframe):
嘗試收取一個uint32_t的數據,長度不可超過最大framesize
如果收完則調transition
transition(read_frame_size):
appstate->read_reqsocketstate->recv
爲讀取準備相關數據
workSocket(recv):
儘可能多讀取一些數據,如果讀完了一個包則調transition
transition(read_req):
appstate->wait_task
如果是有woker線程,則封裝input/output/processor爲一個Task,加入到woker線程池裏
不再關注此conn上的讀寫事件,只等處理結果
否則,就地處理
work處理完畢後會調notifyIOThread
transistion(wait_task):
appstate->send_result, socketstate = send
根據output獲取outbuffer內容
關注寫事件
workSocket(send):
儘可能發完outbuffer數據,
如果發送完畢,調transition
transition(send_result):
處理下in/outbuffer限制
回到初始狀態,相當調用一次transition(appinit)
線程池的一些簡要分析
hreadManager::Impl
線程管理類
addWorker:
new出N個ThreadManager::Worker並保存,累計workcount,依次對每個線程調用start,更改線程狀態爲starting
等待workerCount==wokerMaxCount(workerMonitor.wait())
start:
如果state==uninited,monitor.notifyall()通知從線程可以開始了
monitor.wait()?似乎沒必要,state_沒看到設爲starting的地方
stop:
調removeWorker,改變下狀態
這裏有個join參數,如果傳入爲true,則state=join,這種情況下線程不會立刻退出,而是等任務都完成
如果state!=join,線程就不會考慮沒有完成的任務而直接退出
removeWorker:
修改wokerMaxCount,調用monitor.notify()
等待workerCount==workerMaxCount(wokerMontior.wait())
從蒐集到的deadworker中刪掉這些線程
ThreadManager::Worker
具體worker類
isActive:
manager的workerCount<= workerMaxCount or (manager處於JOINING且有任務)
線程退出取決於這個條件
run:
更新manager的wokerCount,如果達到wokerMaxCount,則下面通過workerMonitor.notify()使得主線程中addWorker返回
下面開始循環:
如果isActive 且 manager沒有任務
manager.idle++,monitor_.wait(),等待manager通知,掛起自己.
Active? 是 刪除manager超時任務,取出隊列中的任務,狀態爲executing
如果有最大task限制,此處如果隊列少於最大task,則maxMontior.notify().喚醒可能在add上的阻塞線程
否(被刪除了一些worker數量 and (不在joining or 沒有任務)) manager.workerCount--,這裏是要退出循環了
退出時,蒐集這個線程到dead裏,供主線程刪除.
如果workerCount==workerMaxCount則通過wokerMonitor通知主線程(removeWorker中)
add:
刪除超時的任務,如果task超過最大task限制,則可能嘗試等待
加入到task中,如果有idle的線程,則通過monitor.notify喚醒(等待task中)
時序圖分析
啓動Thrift TNonblockingServer : public TServer時,可啓動兩類線程,一是TNonblockingIOThread,另一是Worker:
TNonblockingIOThread負責接受連接,和收發數據;而Worker負責回調服務端的用戶函數。
TNonblockingIOThread::registerEvents主要做了兩件事:
1) 註冊TNonblockingIOThread::listenHandler(),這個是用來接受連接請求的;
2) 註冊TNonblockingIOThread::notifyHandler(),這個是用來監聽管道的。
TNonblockingIOThread和Worker兩類線程間通過隊列進行通訊,隊列類型爲std::queue
class ThreadManager::Task: public Runnable
{
public:
void run()
{
// runnable_實際爲TNonblockingServer::TConnection::Task
runnable_->run();
}
private:
// 這裏的Runnable實際爲TNonblockingServer::TConnection::Task
// 在TNonblockingServer::TConnection::transition()中被push進來
boost::shared_ptr<Runnable> runnable_;
};
2. TNonblockingServer::TConnection::transition()
transition()爲狀態切換函數,狀態有兩種:一是socket的狀態,另一是rpc會話的狀態。APP開頭的是rpc會話的狀態,SOCKET開頭的是socket的狀態。