原创

惊群效应

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

传统的服务器使用“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协同使用,所以研究意义不大。

六、总结

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

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • socket接口api的深度探究

    Linux内核net/socket.c定义了一套socket的操作api。图1展示了socket层所处与TCP/IP协议栈之上和应用层之下。

    mariolu
  • 也谈谈c语言的协程

    服务器并发场景是在程序IO密集型有优势。因为IO操作速度远没有CPU的计算速度快。程序阻塞IO将浪费大量CPU时间。程序计算密集型的,并发编程反而没有优势。

    mariolu
  • 为什么要用fish shell

    什么是fish,Fish又称为Friendly shell for interactive use。Fish设计之初能兼容其他shell的命令, 并且会比其他s...

    mariolu
  • 推荐系统

    本文结构: 推荐系统 常用方法 简介 模型 cost, gradient 表达式 代码实现 应用实例 参考: Coursera-Andrew Ng 的 Ma...

    杨熹
  • 漫画:腾讯面试题(面试官问我会不会修供暖器,我说没问题!然后他给我一道题)

    今天是小浩算法“365刷题计划”第75天。当然不能让你真的去修供暖器,但是如果你真的很有兴趣,可以参考下面步骤:

    程序员小浩
  • 奇偶数线程交替执行问题

    一个面试题:两个线程,一个打印偶数,一个打印奇数,并且轮流打印,我们可以看到这种场景模式肯定是需要通过同步来实现,

    小勇DW3
  • ubuntu 12.04 配置内核崩溃自动重启及转存

    默认ubuntu12.04没有配置内核崩溃自动重启及转存,造成发生内核崩溃的时候,没有core dump文件去分析,并且卡死在内核崩溃界面,为了方便查找内核崩溃...

    力哥聊运维与云计算
  • 独家解读 | 矩阵视角下的BP算法

    有深度学习三巨头之称的YoshuaBengio、Yann LeCun、Geoffrey Hinton共同获得了2018年的图灵奖,得奖理由是他们在概念和工程上取...

    马上科普尚尚
  • All In! 我学会了用强化学习打德州扑克

    选自willtipton 机器之心编译 参与:Jane W、蒋思源 最近,强化学习(RL)的成功(如 AlphaGo)取得了大众的高度关注,但其基本思路相当简单...

    机器之心
  • 挖洞经验 | 如何在一条UPDATE查询中实现SQL注入

    前段时间,我在对Synack漏洞平台上的一个待测试目标进行测试的过程中发现了一个非常有意思的SQL注入漏洞,所以我打算在这篇文章中好好给大家介绍一下这个有趣的漏...

    FB客服

扫码关注云+社区

领取腾讯云代金券