java NIO Selector惊群现象研究

序言:

本文讨论的问题都是基于JDK8的版本

在使用java的NIO实现一个server之前,本人也有接触过c/c++/php的server实现,在“单进程单线程”下,本身是串行的,也无需讨论“惊群问题”,在“多进程单线程” 模式下(以php为例),一般是在主进程中创建好server socket、绑定好端口等操作,然后fork出多个worker进程(每个worker进程都会accept连接),父子进程间因fork有“写时复制机制”的存在,可以共用同一个端口下的server socket,当有客户端连接上来时,多个worker进程使用select/poll/epoll系统调用,只有一个进程会被唤醒并成功accept连接,所以也不用操心“惊群问题”,那么本文要讨论的就是“单进程多线程”模式下的“惊群问题”,而java NIO实现的server就是“单进程多线程”的场景。

对于多线程下的“惊群问题”,常规的解决方案就是:每个线程都创建一个server socket,各自select/poll自己独立的server socket。因为端口默认是不能重复bind的,所以需要设置socket options:SO_REUSEPORT,这样相当于在操作系统内核层面做了“负载均衡”,当一个连接到来时,只唤醒一个thread去accept。

思路1 :

在使用NIO开发http server时,因本人写过其他语言的server,习惯性的就会采用“one loop per thead”的架构,即:cpu核数个thread,每个thead都独立创建一个server socket,bind同一个port,并设置socket options:SO_REUSEPORT。当我打算这么做时,却发现jdk8下无法“复用端口”。上代码:

 看到这里,笔者就不打算使用one loop per thread了,此路不通。

在c++的多线程模型中,一般是多个线程都有自己的EventLoop,每个线程都要处理(accept/read/write),  而java中的做法一般是:accept交个独立的一个线程处理,读写事件再分给其他线程(accept线程的selector只关注OP_ACCEPT, 读写线程的selector关注OP_READ/OP_WRITE), 这样的做法,至少TCP的backlog应该调优,否则当大量新连接接到来时,只有一个核accpet,accept队列很容易堆积满,只不过对于大部分的应用场景,accpet事件都不太可能成为瓶颈,加上读写可以多核处理,CPU也能充分利用起来。

思路2:

上面发现无法端口复用,但是笔者又希望能用多线程去处理accept事件,在没有内核级特殊处理的情况下,应该是会“惊群的”,实际的结果也确实如此。笔者用了两种做法去尝试:

1、主线程创建一个server socket、一个selector,多个子线程共用同一个server socket、selector。当server启动时,客户端curl访问一次。 上代码:

通过上图“圈出来的代码”可以做个“断言”,如果没有惊群现象,那么应该只会打印一次“accpet success”,因为当有新的连接过来时,只有一个线程会被accept事件唤醒。但是实际结果如下:

所有的线程都被唤醒了,但是只有一个线程成功accpet。“惊群发生了”。 

2、主线程创建一个server socket、,多个子线程各自创建一个selector并共用同一个server socket。 这种情况也是会发生“惊群问题”。但是略有区别,上截图:

通过和1、对比,会发现截图的控制台输出少了一些内容:empty loop loopWorker:X,输出这些内容的核心代码位置:

通过对比发现,当每个线程都拥有独立的selector时,一旦被select唤醒,每个线程的selectedKeys中都拥有这个新上来的连接,而当多个线程共用一个selector时,此时存在“竞态条件” 所以在select方法上有锁争用,输出empty loop则表示没获得select方法上的锁,直到所有线程都在select上成功返回时(失败返回即:锁未成功获取),都进入accpet,但最终只有一个线程accpet sucess。跟踪select方法源码可以发现内部有锁争用的:

总结:在jdk8以前老老实实的用 “一个selector线程 + worker线程池” 的模式

终极解决方案:

在jdk9之后,开始支持TCP端口复用了,即:SO_REUSEPORT可以被支持了。只可惜这个功能在macos下jdk9版本存在bug!!!:虽然没有惊群现象,但是每次被唤醒的都是同一个thread,等同于退化成单线程的版本了。

 不过好在linux下没这个问题。

下面贴出在macos下 的测试结果:

可以发现这个bug挺有意思,永远都是最后一个bind端口的线程会被成功唤醒accept连接, 笔者使用apache ab压测2w次,截图只有一部分,但是2w次唤醒的都是loopWorker:3

然后再贴出同样的代码在linux下的结果:

 可以看出,没有 “惊群现象”,且accpet事件被内核“负载均衡”到各个线程,完美!!!

 而且ab压测QPS还挺高:

在同一台linux上,性能和c++ boost.asio实现的http server基本无差异了。 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值