Linux | c&cpp | Email | github | QQ群:425043908 关注本站

itarticle.cc

您现在的位置是:网站首页 -> 底层开发 文章内容

基于Epoll内置Leader-Follower服务端实现(50万qps)[转载]-itarticl.cc-IT技术类文章记录&分享

发布时间: 8年前底层开发 116人已围观返回

最近在探索借助epoll做为reactor, 设计高效的服务端的方法.

常见的基于epoll的编程方式主要为单线程的事件循环, 用于一些非阻塞的业务逻辑开发是比较高效并且简单易懂的.


但实际开发业务的时候, 往往面临着查数据库, 访问磁盘, 通过网络访问其他主机的需求, 耗时往往较长, 所以单线程的epoll并不能轻松的适用, 往往需要做一些额外的设计与构思才能得到解决.


解决此类慢处理的服务端架构主要以leader-follower架构以及half-sync-half-async为主, 通过多线程的并发能力来满足同时执行多个慢处理业务逻辑. 其中, leader-follower因为较half-sync-half-async而言减少了从异步层 与 同步层间必要的内存拷贝, 并且half-sync-half-async的异步层是单线程实现, 是很容易达到单核CPU瓶颈的, 所以基本完败于leader-follower.

公司的UB框架使用的应该是简化版的leader-follower模型, 即线程池共享同一个epoll set, 而epoll set中只注册了监听socket, 从而保证同一个时刻只由leader线程accept得到connected socket并使用带超时的阻塞read接口先后读取nshead header与nshead body, 所以UB的设计是不适合暴露给外网的, 很容易受到攻击而影响服务.


正统的leader-follower应当由线程池共享同一个epoll set, 并将所有的socket注册到该Epoll set中, 然后由leader负责epoll_wait获取一个event socket并将其从epoll set中暂时移除, 之后Leader便可转换为follower并处理event socket, 并在处理完成后将event socket重新注册回epoll set以便继续监监听.

可以看到, leader-follower按照程序设计方法, 需要使用mutex+cond实现leader-follower职责间的转换, 在恢复event socket的时候, 因为epoll set被多线程共享的原因, 涉及到epoll_ctl的线程安全问题, 简单的例子: 假设A线程希望恢复socket1的事件注册, 但此时B线程正在epoll_wait, 那么A线程是否可以线程安全的使用epoll_ctl恢复socket1的事件.

幸运的是, epoll内置了leader-follower的选项支持, 可以避免mutex+cond的使用, 并且保障了epoll_ctl的线程安全性, 同时保证了epoll_wait是线程安全的并且会负载均衡事件到多个调用者, 不会引发惊群问题.

为了做到这一点, 只需要为每一个event socket设置选项: EPOLLONESHOT即可.

EPOLLONESHOT

Sets the One-Shot behaviour for the associated file descriptor. It means that after an event is pulled out with

epoll_wait(2) the associated file descriptor is internally disabled and no other events will be reported by the epoll

interface. The user must call epoll_ctl(2) with EPOLL_CTL_MOD to re-enable the file descriptor with a new event mask.


更关键的说明需要man epoll, 其中:

On the contrary, when used as a Level Triggered interface, epoll is by all means a faster poll(2), and can be used wherever

the latter is used since it shares the same semantics. Since even with the Edge Triggered epoll multiple events can be gen-

erated up on receival of multiple chunks of data, the caller has the option to specify the EPOLLONESHOT flag, to tell epoll

to disable the associated file descriptor after the receival of an event with epoll_wait(2). When the EPOLLONESHOT flag is

specified, it is caller responsibility to rearm the file descriptor using epoll_ctl(2) with EPOLL_CTL_MOD.

有了这个机制的保障, 在实现leader-follower的时候, 只要创建多个线程, 所有线程epoll_wait同一个epoll set, 并且保证epoll set中每个注册的event socket都携带了EPOLLONESHOT选项即可.

最初, epoll set里只有监听socket. 当多个线程调用epoll_wait时, 每个线程均会得到一批不同的event socekt. 这是因为在epoll_wait返回之前会为你设置发生event的socket的注册事件为0, 即其他线程不可能再次检测到该socket的事件, 相当于epoll_wait将poll与epoll_ctl(DEL)做了原子化, 这是浑然天成的leader-follower, 代码与单线程相比几乎一模一样. 在读写并处理完event socket后, 需要使用epoll_ctl(MOD)恢复event socket到epoll set中, 而epoll_ctl保证这是线程安全的, 并且如果socket有事件会立即通知某epoll_wait返回.

有了上面的理论基础, 我试着开发了一个多线程基于epoll的server, 测试了长连接的qps, 只跑echo服务, 发现可以达到15万, 但出现了大面积线程D状态, 继续提高压力qps也没有提升, 程序的系统调用也仅仅是read/write/accept/close/epoll_ctl/epoll_wait, 程序是无内存分配的, 并且每个核心的idle还剩余60多.

在猜测尝试了一些方法后, 包括单线程跑epoll, 竟然发现qps也是15万, 但单核CPU可以跑满, 怀疑了一顿网卡之后, 发现测试是单机的, 根本没走网卡, 所以不是网卡软中断过高的问题.

根据epoll_ctl/epoll_wait线程安全这个设计, 我猜测多个线程访问同一个epoll set是有锁的, 从而导致了程序瓶颈, 于是我创建了多个epoll set, 每个epoll set作为一个组, 组内有若干worker线程共享该epoll set, 从而减小了锁的面积.

但这样设计, 需要考虑到监听socket怎么处理, 因为每个组都有自己的epoll set, 不能将监听socket注册给多个epoll set, 那样会有惊群问题. 于是, 我独立创建了一个监听线程, 并创建了一个独立的epoll set跑事件循环来专门做accept, 而因为epoll_ctl的线程安全特性, 所以监听线程采用了round robin轮转的向每个组的epoll set进行conntected socket的事件注册.


通过以上的优化, 惊喜的发现服务端的qps达到了近40万, 而cpu idle也终于从60榨到了不到10%.

唯一的缺点就是round robin的分发策略可能因为客户端主动断开行为导致各个组内的负载不均, 希望找到一种无锁并且代价低的解决方案, (PS: 多线程server, 对业务数据并发访问的同步问题是个老问题)暂时就到这里了.


程序在论坛众亲友的给力指导下, 在公司内核部门的指导之下, 长连接性能最终达到了50万qps, 短连接性能最终达到了7万qps, 测试机器CPU为12核心, 64MB内存, 测试环境为单机lo网口.

程序基于epoll的EPOLLONESHOT选项, 充分利用了epoll的线程安全特性, 通过独立的监听线程最大化连接建立速率, 通过线程池配合epoll简易的实现了Leader-Follower的程序结构, 对于各类业务逻辑能够普遍适用.

程序优化过程中, 主要是2个瓶颈点的化解:

1, epoll线程安全, 所以内部的锁会造成多线程共享epoll fd的瓶颈, 通过创建多组epoll fd, 减小锁的竞争可以化解瓶颈, 充分利用硬件性能.

2, 短连接建立能力差, 未经过优化网络参数, 只能达到3万/秒的建立能力, 经过参数优化, 可以达到8万/秒的建立能力, 具体参数参考代码里的Readme.

代码如下,最初的255行小代码, 有明显的瓶颈:(15万QPS)

server.tar.gz

解决上述两个问题后的代码, 能够支撑50万qps:

lfepoll.tar.gz

这是引入了lua的代码, 不支持yield, 因为考虑到需要把代码改成状态机的太费劲, 并且程序本身就是leader-folllower的, 所以提供一些异步connect等实用接口的必要性不大:

lfepoll(1).tar.gz

测试lua版本性能, 发现luaL_loadstring频繁调用性能损耗严重, 所以做成了只加载一次, 然后保持一份引用在注册表里, 以便重复使用, 现在程序瓶颈已经转移到了lua_pcall, 目测已经无法优化了.(注意测试前修改logic.lua, 改为纯echo服务, 否则会导致test压力工具工作不正常)

lfepoll(2).tar.gz

上面四个文件已打包成一个文件:下载地址:http://pan.baidu.com/s/1o8coMCi


使用方法:

客户端就用楼主给的那个。

里边的nohup.out就是最后执行结果。

编译后:

nohup ./server &

就可以了。

可以直接用telnet测试单连接。


下面是一些评论:

短连接, qps是每秒请求次数(QPS = req/sec = 请求数/秒), D是top里那个D, 我之前遇到过就是发生在分配大内存mmap或者磁盘繁忙会引起, 现在strace跟的话调用基本上就是accept/read/write/epoll_ctl, 不知道卡在哪一个调用上了


TCP的连接是开销非常大的,不仅3次握手,而且涉及大量流资源的分配与回收,因此不建议在应用中使用短连接。

实际中我们在客户端使用连接池来平衡这个短连接的需求与性能的矛盾。

另外应该加上资源快速重用的选项:

//避免 TIME_WAIT

so_linger.l_onoff=1;

so_linger.l_linger=0;

ret=setsockopt(sock, SOL_SOCKET, SO_LINGER, &so_linger, sizeof so_linger);


嗯, 是memcached的架构, 我今天也思考过为什么要在一个epoll fd下开N个线程, 而不是一个epoll fd一个线程, 最终的结论是:一个epoll fd一个线程的伸缩性差, 因为一个epoll fd下的所有socket是串行的, 有一个慢就没得选择只能都等着.

而leader-follower的多个线程共享一个epoll fd, 优点就是一个fd卡了, 其他fd可以使用其他线程工作.

如果正常来说, 我认为一个线程一个epoll和多个线程一个epoll的性能是一样的.



发布时间: 8年前底层开发116人已围观返回回到顶端

很赞哦! (1)

文章评论

  • 请先说点什么
    热门评论
    115人参与,0条评论

站点信息

  • 建站时间:2016-04-01
  • 文章统计:728条
  • 文章评论:82条
  • QQ群二维码:扫描二维码,互相交流