Elasticsearch 网络通信线程分析

阿里云 Elasticsearch 团队求贤若渴,内核研发,运维开发各类研发岗位均有,如有兴趣,欢迎私聊或将简历邮件发送 xinyu.jxy@alibaba-inc.com

通过本文,可以获得

  1. 当一个请求过来时,es 是如何处理的?
  2. http,tcp 请求都是在哪个线程处理的?
  3. es 节点间的 tcp 通讯是如何发送和接收的?
  4. 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的所有请求均由一个线程进行响应。

image.png
image.png

  • 对于selector而言,则是一直在轮询他上面所管理的channel有没有ready的,如果有就按照pipeline进行消费。netty中的逻辑可见 io.netty.channel.nio.NioEventLoop#run , nio中的可见 org.elasticsearch.nio.NioSelector#runLoop
  • pipeline的执行,默认在selector线程中执行,所以一旦pipeline存在阻塞,则整个selector将会阻塞,其管理的所有channel均会无法响应。

image.png

  • 整体网络模型如下图,不过es中有一点比较特殊,无论是在netty还是自研nio框架中,bossGroup也就是用来accept请求的线程池都是和worker group进行复用的,也就是那个transport_worker 线程池

image.png
image.png
image.png

ES HTTP 通信流程

  • 众所周知,ES进程有两个端口,一个是http(9200),用来和用户进行通讯,一个是tcp(9300), 用于内部的管理。
  • 下面我们就以Netty框架为例,来看一下当一个http请求来时,是怎么处理的。
    1. netty接收到channel,进行 register  ChannelInitializer#channelRegisteredimage.png
    2. 由es自定义的channelHandler进行pipeline注册 HttpChannelHandler#initChannelimage.png
    3. pipeline开始执行 AbstractChannelHandlerContext#invokeChannelRegistered,可以看到所有pipeline执行均在channel.eventLoop中完成,而这个eventloop其实就是 register 到的 selector 所在的线程image.png
    4. pipeline最后一个handler便会调用我们在RestAction中自行编写的逻辑。我们开发中可以定义直接返回,如RestCatAction中的直接sendResponse,也可利用回调进行sendResponse,如RestBulkAction。(但一定不要阻塞)

image.png

image.png

image.png

  • 由上文可知,整个http的处理流程,如果在业务层没有转交,那么则全部运行在http的selector线程中。
  • 另外需要注意一点, 从 SharedGroupFactory#getHttpGroup 可以看到,如果没有设置httpWorkerCount,就会复用tcp的eventGroup, 也就是 http 和 tcp 其实是同一个 selector

image.png

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 链接。

image.png

  • 不过 tcp 和 HTTP/1 的单 channel 同步请求不同的是,ES 的 tcp 通讯是类似 HTTP/2 的多路复用的。也就是一个链接不必要在 request 后等待 response ,而是可能 request->request->response->response 这种乱序发送。
  • 那么ES是怎么管理这种多路复用的呢?类似与 HTTP/2 利用帧首部的流标识进行重组,ES 的每个 tcp 请求也都有一个唯一的 requestId

image.png

  • 具体流程如下,首先是 TransportService#sendRequestInternal 注册requestId,将handler存下来,然后发送到远端,等Response时根据requestId再把handler拿出来,继续处理。如果没有单独设置线程池,则均在Transport线程中执行。

image.png

开发需注意问题

  • 由上文可知,ES 的网络框架主要的风险点在于线程池的复用,一旦出现问题,就会阻塞一大批请求
    1. 如果全部 transport 线程阻塞,那么链接都会无法 accept,整个节点无法响应任何请求
    2. 由于 默认 TCP 和 HTTP 复用同样的线程池,如果HTTP请求过大,或者 RestAction 的逻辑存在阻塞,不仅会阻塞同一selector 管理的其他 http channel,tcp channel 也会被阻塞。节点间通信将会出现问题,可能间歇性无法收到response,可能被认为节点掉线,可能间歇性无法写入(由于阻塞住的有可能是不同功能的 channel)等。
  • 所以,在开发过程中,一定要注意
    1. 不要阻塞 RestAction ,如果一定要同步调用,一定要放入新的线程池中
    2. 在进行跨节点通信时,无论是 listener 还是 transportHandler 都尽量设置明确的线程池,不要用默认的SAME

参考文章

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享