阿里云 Elasticsearch 团队求贤若渴,内核研发,运维开发各类研发岗位均有,如有兴趣,欢迎私聊或将简历邮件发送 xinyu.jxy@alibaba-inc.com
通过本文,可以获得
- 当一个请求过来时,es 是如何处理的?
- http,tcp 请求都是在哪个线程处理的?
- es 节点间的 tcp 通讯是如何发送和接收的?
- es 插件开发需注意的问题
ES 网络框架简介
- ES在当前7.x版本之前,一直采用netty作为网络框架,但由于es一直有个自包含的梦想,所以从17年开始就一直想脱离netty,自己写一套基于nio的网络框架,在7.x后终于作为官方插件对外提供[issue 27260]。
- 不过无论是netty还是自研的nio,其本质都是一样的,均是利用了java nio提供的selector和channel实现了一个reactor模型进行多路复用, 每个channel新建时,均会register到一个selector上,然后后续这个channel的所有请求均由这个selector进行响应。而selector则又和一个固定的线程(transport_worker)绑死,所以一个channel的所有请求均由一个线程进行响应。
- 对于selector而言,则是一直在轮询他上面所管理的channel有没有ready的,如果有就按照pipeline进行消费。netty中的逻辑可见
io.netty.channel.nio.NioEventLoop#run
, nio中的可见org.elasticsearch.nio.NioSelector#runLoop
。 - pipeline的执行,默认在selector线程中执行,所以一旦pipeline存在阻塞,则整个selector将会阻塞,其管理的所有channel均会无法响应。
- 整体网络模型如下图,不过es中有一点比较特殊,无论是在netty还是自研nio框架中,bossGroup也就是用来accept请求的线程池都是和worker group进行复用的,也就是那个transport_worker 线程池
ES HTTP 通信流程
- 众所周知,ES进程有两个端口,一个是http(9200),用来和用户进行通讯,一个是tcp(9300), 用于内部的管理。
- 下面我们就以Netty框架为例,来看一下当一个http请求来时,是怎么处理的。
- netty接收到channel,进行 register
ChannelInitializer#channelRegistered
- 由es自定义的channelHandler进行pipeline注册
HttpChannelHandler#initChannel
- pipeline开始执行
AbstractChannelHandlerContext#invokeChannelRegistered
,可以看到所有pipeline执行均在channel.eventLoop中完成,而这个eventloop其实就是 register 到的 selector 所在的线程 - pipeline最后一个handler便会调用我们在RestAction中自行编写的逻辑。我们开发中可以定义直接返回,如RestCatAction中的直接sendResponse,也可利用回调进行sendResponse,如RestBulkAction。(但一定不要阻塞)
- netty接收到channel,进行 register
- 由上文可知,整个http的处理流程,如果在业务层没有转交,那么则全部运行在http的selector线程中。
- 另外需要注意一点, 从
SharedGroupFactory#getHttpGroup
可以看到,如果没有设置httpWorkerCount,就会复用tcp的eventGroup, 也就是 http 和 tcp 其实是同一个 selector
ES TCP 通信线程介绍
- tcp 的框架和 http 实际差不多,只是 pipeline 有些区别,另外 http 的 channel 是随请求链接构建的,而 tcp 的则是在初始化时便打开的。
- 熟悉 ES 的同学都清楚,ES 节点在初始化时便会构建一个完全图,两两节点均会建立 tcp 链接,也就是 channel,然后随机注册个一个 selector,也就是一个 transport_work,后续这个 tcp 链接的所有请求,均由这个 transport_work 线程承担。
- 从源码可以看到,es 默认会打开 2 个 recovery 链接,3 个 bulk 链接,6 个通用链接,1 个 state 链接,和 1 个ping 链接。
- 不过 tcp 和 HTTP/1 的单 channel 同步请求不同的是,ES 的 tcp 通讯是类似 HTTP/2 的多路复用的。也就是一个链接不必要在 request 后等待 response ,而是可能 request->request->response->response 这种乱序发送。
- 那么ES是怎么管理这种多路复用的呢?类似与 HTTP/2 利用帧首部的流标识进行重组,ES 的每个 tcp 请求也都有一个唯一的 requestId
- 具体流程如下,首先是
TransportService#sendRequestInternal
注册requestId,将handler存下来,然后发送到远端,等Response时根据requestId再把handler拿出来,继续处理。如果没有单独设置线程池,则均在Transport线程中执行。
开发需注意问题
- 由上文可知,ES 的网络框架主要的风险点在于线程池的复用,一旦出现问题,就会阻塞一大批请求
- 如果全部 transport 线程阻塞,那么链接都会无法 accept,整个节点无法响应任何请求
- 由于 默认 TCP 和 HTTP 复用同样的线程池,如果HTTP请求过大,或者 RestAction 的逻辑存在阻塞,不仅会阻塞同一selector 管理的其他 http channel,tcp channel 也会被阻塞。节点间通信将会出现问题,可能间歇性无法收到response,可能被认为节点掉线,可能间歇性无法写入(由于阻塞住的有可能是不同功能的 channel)等。
- 所以,在开发过程中,一定要注意
- 不要阻塞 RestAction ,如果一定要同步调用,一定要放入新的线程池中
- 在进行跨节点通信时,无论是 listener 还是 transportHandler 都尽量设置明确的线程池,不要用默认的SAME
参考文章
- 超详细Netty入门,看这篇就够了! developer.aliyun.com/article/769…
- 为什么建议 Netty 的 I/O 线程与业务线程分离 cloud.tencent.com/developer/a…
- Elasticsearch 7.10 源码
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END