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的状态。

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值