文章内有借鉴别人,也有自己分析,权且当做转载吧,并不重要了
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的状态。
