惊群效应

2018-09-18 02:07:40 浏览数 (1)

一、服务器网络模型和惊群

传统的服务器使用“listen-accept-创建通信socket”完成客户端的一次请求服务。在高并发服务模型中,服务器创建很多进程-单线程(比如apache mpm)或者n进程:m线程比例创建服务线程(比如nginx event)。机器上运行着不等数量的服务进程或线程。这些进程监听着同一个socket。这个socket是和客户端通信的唯一地址。服务器父子进程或者多线程模型都accept该socket,有几率同时调用accept。当一个请求进来,accept同时唤醒等待socket的多个进程,但是只有一个进程能accept到新的socket,其他进程accept不到任何东西,只好继续回到accept流程。这就是惊群效应。如果使用的是select/epoll accept,则把惊群提前到了select/epoll这一步,多个进程只有一个进程能acxept到连接,因为是非阻塞socket,其他进程返回EAGAIN。

二、accept惊群的解决

所有监听同一个socket的进程在内核中中都会被放在这个socket的wait queue中。当一个tcp socket有IO事件变化,都会产生一个wake_up_interruptible()。该系统调用会唤醒wait queue的所有进程。所以修复linux内核的办法是只唤醒一个进程,比如说替换wake函数为wake_one_interruptoble()。

2.1、改进版本accept reuse port

没有开启reuse选项的socket只有一个wait queue,假设在开启了socket REUSE_PORT选项,内核中为每个进程分配了单独的accept wait queue,每次唤醒wait queue只唤醒有请求的进程。协议栈将socket请求均匀分配给每个accept wait queue。reuse部分解决了惊群问题,但是本身存在一些缺点或bug,比如REUSE实现是根据客户端ip端口实现哈希,对同一个客户请求哈希到同一个服务器进程,但是没有实现一致性哈希。在进程数量扩展新的进程,由于缺少一致性哈希,当listen socket的数目发生变化(比如新服务上线、已存在服务终止)的时候,根据SO_REUSEPORT的路由算法,在客户端和服务端正在进行三次握手的阶段,最终的ACK可能不能正确送达到对应的socket,导致客户端连接发生Connection Reset,所以有些请求会握手失败。

三、select/epoll模型

在一个高并发的服务器模型中,每秒accept的连接数很多。accept成为一个占用cpu很高的系统调用。考虑使用多进程来accept。select由于可扩展性能比如epoll,select遍历所有socket,select对每次操作都是要循环遍历所有的fd,所以在高并发场景下,select性能差。在高并发场景epoll使用场景更多。

3.1、epoll的EPOLL_EXCLUSIVE选项

liunx 4.5内核在epoll已经新增了EPOLL_EXCLUSIVE选项,在多个进程同时监听同一个socket,只有一个被唤醒。

四、应用层解决

同一时间只让一个进程accept/select/epoll一个监听端口。这是应用层解决惊群的办法,伪代码如下

semop(...); // lock

epoll_wait(...);

accept(...);

semop(...); // unlock

... // manage the request

多进程使用sysv实现semop,多线程则使用mutex。比如说mpm模式下的httpd,nginx都是这种实现办法。但是这种办法sysv是个固定的内存大小。比如在终端敲入ipcs。ipcs是机器共享固定大小空间。所以一旦有很多进程分配忘了手动释放,有内存泄漏风险。

4.1、httpd

for (;;) {

accept_mutex_on ();

for (;;) {

fd_set accept_fds;

...

rc = select (last_socket 1, &accept_fds, NULL, NULL, NULL);

...

for (i = first_socket; i <= last_socket; i) {

if (FD_ISSET (i, &accept_fds)) {

new_connection = accept (i, NULL, NULL);

}

accept_mutex_off ();

process the new_connection;

}

}

4.2、nginx

void ngx_process_events_and_timers(ngx_cycle_t *cycle) { ...

if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {

return;

}

...

if (ngx_accept_mutex_held) {

flags |= NGX_POST_EVENTS;

}

...

(void) ngx_process_events(cycle, timer, flags);

ngx_event_process_posted(cycle, &ngx_posted_accept_events);

if (ngx_accept_mutex_held) {

ngx_shmtx_unlock(&ngx_accept_mutex);

}

ngx_event_process_posted(cycle, &ngx_posted_events);

}

4.3、少量进程监听同一个socket

当然在低负债的环境下,也可以分配少量的进程,即使有惊群,影响也是不大的。

五、tornado/golang/其他

5.1、tornado

tornado使用IOLoop模块,在Python3,IOLoop是个asyncio event循环。Python 2则使用了epoll (Linux) or kqueue (BSD and Mac OS X) 否则选用select()。所以python tornado在面对惊群问题其实是没有解决的。所以就是系统不解决惊群问题丢给应用层解决,应用层不解决丢给用户解决。笔者在tornado模拟业务源站行为,曾经开启了几百个进程。模拟行为很纯粹,就是根据X-Flux头的指定的大小,返回给用户相应大小的2xx响应。该程序不涉及磁盘io,不涉及内存大量拷贝操作,本应是cpu运算型,但是发现客户端在压测tornado,并发度1w左右,却服务端cpu跑满,并且连接超时的概率竟然有百分之四五十。使用python分析程序发现epoll wait函数占用了40%左右的cpu时间。很显然就是遇到了惊群响应。后面用golang重新实现了服务器,就没有了惊群。

5.2、golang

为啥golang就没有惊群响应呢?笔者查看了一个关键包netFD的accept实现。

func (fd *netFD) accept() (netfd *netFD, err error) {

//在这里序列化accept,避免惊群效应

if err := fd.readLock(); err != nil {

return nil, er

}

defer fd.readUnlock()

......

for {

s, rsa, err = accept(fd.sysfd)

if err != nil {

if err == syscall.EAGAIN {

//tcp还没三次握手成功,阻塞读直到成功,同时调度控制权下放给gorontine

if err = fd.pd.WaitRead(); err == nil {

continue

}

} else if err == syscall.ECONNABORTED {

//被对端关闭

continue

}

}

break

}

netfd, err = newFD(s, fd.family, fd.sotype, fd.net)

......

//fd添加到epoll队列中

err = netfd.init()

......

lsa, _ := syscall.Getsockname(netfd.sysfd)

netfd.setAddr(netfd.addrFunc()(lsa), netfd.addrFunc()(rsa))

return netfd, nil

}

5.3 lighttpd及其他

linghttpd使用场景建议只有一个worker,多个worker会有些功能兼容上的问题,所以lighttpd官方其实也没有解决惊群问题。其他服务器tomcat、nodejs等等因为其实在高并发上会搭配apache或者nginx协同使用,所以研究意义不大。

六、总结

管中窥豹、惊群问题说大不大,但是如果碰到,可能是限制高并发性能的重要一个瓶颈,在探索惊群问题解决上,对各个服务器模型的分析以及内核层调研中整理了这些想法,希望对大家有所帮助。

0 人点赞