现在维护的很多后端模块都是采用线程池模型网络服务,经常遇到一些超时等问题,在分析这类问题前先要了解epoll的多线程模型。这个模型下,监听线程负责建立网络连接,将连接放入队列; 工作线程负责从队列中取出连接,网络收发,计算。因此,响应时间取决于四个部分:
连接等待建立的时间 + 队列中的时间 + 网络IO时间 + 处理时间。
当然我们讨论的前提是CPU没有被完全耗尽的情况。当CPU完全被耗尽,如果不考虑连接拒绝的情况,请求的处理事件是可能无限长的。
- 连接等待建立的时间取决于监听线程被调度的频率。当服务器压力很低时,一旦有网络请求,epoll_wait就被激活,
连接就会建立。但是当服务器压力很高时,监听线程需要和工作线程争夺cpu时间片,而工作线程一般数量众多,使得 监听线程难以有被调度的机会。这样每当监听线程被激活时,就会收到一大批请求,使得连接等待建立的时间变长。 而大批请求使得更多的线程被激活,进一步恶化的CPU争用。- 队列中的时间取决于空闲工作线程的调度。当CPU争用时,空闲线程被调度的时间也会延长。不过由于工作线程数量较多,
问题不会太严重。如果每次线程被激活都生成大量的请求,队列中的时间还取决于队列的长度。我们的前提是CPU没有被耗尽, 因此长度不会无限增长。但是受负载的统计波动,队列中的请求可能在瞬间较高。- 对于网络IO较轻的后端服务模块,网络IO时间对于相同大小的包来说,基本是恒定的。
- 处理时间主要取决于请求的复杂度。但是考虑到多个工作线程争用cpu的情况,争用cpu会使每个请求的处理事件都变长。
改变工作线程的数量可减轻CPU争用。严格来说,工作线程数不应该超过内核数量。但由于我们使用的同步模型, 工作线程还以阻塞方式负责网络IO,如果不提高工作线程的数量,难以充分利用CPU。这个不加推导给出这个公式:工作线程数量 = 内核数量 * (网络延迟 + 处理延迟)/ 处理延迟
例如对于某模块来说,8核机器上,网络延迟为3ms,处理延迟为8ms,则11个工作线程最为合适。 实践中可略微提高到16,避免网络拥塞造成的网络延迟变化。
测试表明,在高负载下30工作线程的连接建立时间会迅速增加,直至崩溃。而15线程可以稳定工作,处理时间仅略有增加。
但是,通过计算工作线程的数量无法应对复杂变化的网络环境,而且没有本质解决CPU争用的问题。 回想刚才问题的本质是:
利用pthread提供的线程调度控制,提高监听线程的优先级,对工作线程采用动态优先级。 以下API需要root权限。
1、提高监听线程得优先级
pthread_attr_t attr;
pthread_attr_init(&attr);
if(pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED))
{
perror(NULL);
printf("error %d\n", __LINE__);
exit(-1);
}
if(pthread_attr_setschedpolicy(&attr, SCHED_FIFO))
{
perror(NULL);
exit(-1);
}
sched_param param = {0};
param.sched_priority = 10;
if(pthread_attr_setschedparam(&attr, ¶m))
{
perror(NULL);
exit(-1);
}
|
这个操作将监听线程优先级调整到10。
2、 动态调整工作线程的优先级
当工作线程没有开始处理请求时,我们将他的优先级调低,避免它参与CPU竞争。 一旦工作线程开始处理请求,我们将他的优先级提高(但是依然低于监听线程的优先级),使他能一直获得CPU,直到请求处理完成。 另外SCHED_FIFO属性可以减少不必要的线程切换。
param.sched_priority = 1;
if(pthread_setschedparam(self, SCHED_FIFO, ¶m))
{
perror(NULL);
printf("error %d\n", __LINE__);
exit(-1);
}
sem_wait(&sem);
param.sched_priority = 5;
if(pthread_setschedparam(self, SCHED_FIFO, ¶m))
{
perror(NULL);
printf("error %d\n", __LINE__);
exit(-1);
}
|
通过这两项技术,在同样的30工作线程,超高负载,CPU基本耗尽的情况下, 服务器依然可以稳定工作,延迟相比空载基本没有变化,且极其稳定。
阅读(3495) | 评论(1) | 转发(1) |