thrift TNonblockingServer 內 iothread workthread 執行邏輯

文章內有借鑑別人,也有自己分析,權且當做轉載吧,並不重要了

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的狀態。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章