《Elasticsearch 源码解析与优化实战》第4章:节点启动和关闭

Posted 宝哥大数据

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Elasticsearch 源码解析与优化实战》第4章:节点启动和关闭相关的知识,希望对你有一定的参考价值。

简介

本章分析单个节点的启动和关闭流程。看看进程是如何解析配置、检查环境、初始化内部模块的,以及在节点被“kill”的时候是如何处理的。

二、启动流程

2.1、启动流程做了什么

总体来说,节点启动流程的任务是做下面几类工作:

  • 解析配置,包括配置文件和命令行参数。
  • 检查外部环境和内部环境,例如,JVM版本、操作系统内核参数等。
  • 初始化内部资源,创建内部模块,初始化探测器。
  • 启动各个子模块和keepalive线程。

2.2、启动流程分析

2.2.1、启动脚本

当我们通过启动脚本 bin/elasticsearch启动 ES 时,脚本通过exec加载Java程序。代码如下:

exec \\    #执行命令
    "$JAVA" \\     #Java 程序路径
    $ES JAVA OPTS \\     #JVM 选项
    -Des . path. home="$ES_ HOME" \\     #设置path. home路径
    -Des. path.conf="$ES_ PATH_ CONF" \\     #设置path.conf路径
    -cp "$ES_ CLASSPATH" \\     #设置java classpath
    org. elasticsearch. bootstrap.Elasticsearch \\     #指定main函数所在类
    "$@"     #传递给main函数命令行参数

ES_JAVA_OPTS 变量保存了JVM参数,其内容来自对config/jvm.options配置文件的解析。 如果执行启动脚本时添加了-d参数:

bin/elasticsearch -d 

则启动脚本会在exec中添加<&-&。<&-的作用是关闭标准输入,即进程中的0号fd。&的作用是让进程在后台运行。

2.2.2、解析命令行参数和配置文件

目前支持的命令行参数有下面几种,默认启动时都不使用,如下表所示。

实际工程应用中建议在启动参数中添加-d和-p,例如:

bin/elasticsearch -d -p es.pid

此处解析的配置文件有下面两个,jvm.options 是在启动脚本中解析的。

elasticsearch.yml #主要配置文件
1og4j2.properties #日志配置文件

2.2.3、加载安全配置

什么是安全配置?本质上是配置信息, 既然是配置信息,一般是写到配置文件中的。ES的几个配置文件在之前的章节提到过。此处的“安全配置”是为了解决有些敏感的信息不适合放到配置文件中的,因为配置文件是明文保存的,虽然文件系统有基于用户权限的保护,但这仍然不够。因此ES把这些敏感配置信息加密,单独放到一个文件中:configlelasticsearch.keystore。然后提供一些命令来查看、添加和删除配置。

哪种配置信息适合放到安全配置文件中?例如,X-Pack中的security 相关配置,LDAP 的 base_dn 等信息(相当于登录服务器的用户名和密码)。

2.2.4、检查内部环境

内部环境指ES软件包本身的完整性和正确性。包括:

  • 检查Lucene版本,ES各版本对使用的Lucene版本是有要求的,在这里检查Lucene版本以防止有人替换不兼容的jar包。
  • 检测jar冲突(JarHell),发现冲突则退出进程。

2.2.5、检测外部环境

ES中的“节点”在实现时被封装为Node模块。在Node类中调用其他内部组件,同时对外提供启动和关闭方法,对外部环境的检测就是在Node.start()中进行的。

外部环境指运行时的JVM、操作系统相关参数,这些在ES中称为“Bootstrap Check"。在早期的ES版本中,ES检测到一些不合理的配置会记录到日志中继续运行。但是有时候用户会错过这些日志。为了避免后期才发现问题,ES在启动阶段对那些很重要的参数做检查,一些影响性能的配置会被标记为错误,让用户足够重视这些参数。

所有这些检查被单独封装在BootstrapChecks类中。目前有下面这些检测项。

1、堆大小检查

如果JVM初始堆大小(Xms) 与最大堆大小(Xmx)的值不同,则使用期间JVM堆大小调整时可能会出现停顿。因此应该设置为相同值。

如果开启了 bootstrap.memory_lock,则JVM将在启动时锁定堆的初始大小。如果初始堆大小与最大堆大小不同,那么在堆大小发生变化后,可能无法保证所有JVM堆都锁定在内存中。

要通过本项检查,就必须配置堆大小。

2、文件描述符检查

UNIX架构的系统中,“文件”可以是普通的物理文件,也可以是虚拟文件,网络套接字也是文件描述符。ES 进程需要非常多的文件描述符。例如,每个分片有很多段,每个段都有很多文件。同时包括许多与其他节点的网络连接等。

要通过此项检查,就需要调整系统的默认配置,在Linux下,执行ulimit -n 65536 (只对当前终端生效),或者在 /etc/security/limits.conf 文件中配置* - nofile 65536(所有用户永久生效)。Ubuntu下limits.conf 默认被忽略,需要开启pam_limits.so 模块。

由于Ubuntu版本更新比较快,而生产环境不适合频繁更新,因此我们推荐使用CentOS作为服务器操作系统。

3、内存锁定检查

ES允许进程只使用物理内存,避免使用交换分区。实际上,我们建议生产环境中直接禁用操作系统的交换分区。现在已经不是因为内存不足而需要交换到硬盘上的时代,对于服务器来说,当内存真的用完时,交换到硬盘上会引起更多问题。

开启bootstrap.memory_lock 选项来让ES锁定内存,在开启本项检查,而锁定失败的情况下,本项检查执行失败。

4、最大线程数检查

ES将请求分解为多个阶段执行,每个阶段使用不同的线程池来执行。因此ES进程需要创建很多线程,本项检查就是确保ES进程有创建足够多线程的权限。本项检查只对Linux系统进行。你需要调节进程可以创建的最大线程数,这个值至少是2048。

要通过这项检查,可以修改/etc/security/limits.conf文件的nproc来完成配置。

5、最大虚拟内存检查

Lucene使用mmap来映射部分索引到进程地址空间,最大虚拟内存检查确保ES进程拥有足够多的地址空间,这项检查只对Linux执行。

要通过这项检查,可以修改/etc/security/limits.conf文件,设置as为unlimited。

6、最大文件大小检查

段文件和事务日志文件存储在本地磁盘中,它们可能会非常大,在有最大文件大小限制的操作系统中,可能会导致写入失败。建议将最大文件的大小设置为无限。

要通过这项检查,可以修改 /etc/security/limits.conf 文件,修改 fsize 为 unlimited。

7、虚拟内存区域最大数量检查

ES进程需要创建很多内存映射区,本项检查是要确保内核允许创建至少262144个内存映射区。该检查只对Linux执行。

要通过这项检查,可以执行下面的命令(临时生效,重启后失效):

sysctl -wvm.max\\_map\\_count=262144

或者在/etc/sysctl.conf文件中添加一行vm.max_map_count=262144, 然后执行下面的命(立即,且永久生效)

sysctl -P

8、JVM Client模式检查

OpenJDK提供了两种JVM的运行模式:client JVM模式与server JVM模式。client JVM调优了启动时间和内存消耗,server JVM提供了更高的性能。要想通过此检查,需要以server的方式来启动ES,这也是默认的。

9、串行收集检查

串行收集器(serial collector) 适合单逻辑CPU的机器或非常小的堆,不适合ES。使用串行收集器对ES有非常大的负面影响。本项检查就是确保没有使用串行收集器。ES默认使用CMS垃圾收集器。

10、系统调用过滤器检查

根据不同的操作系统,ES安装各种不同的系统调用过滤器( 在Linux下使用seccomp)。这些过滤器可以阻止一些攻击行为。

作为一个服务端进程,当由于某些系统漏洞被攻击者取得进程的权限时,攻击者可以使用启动当前进程的用户权限执行一些操作。首先,以普通用户权限启动进程可以降低安全风险。其次,把服务本身不需要的系统调用通过过滤器关闭,当进程被攻击者取得权限时,进一步的权限提升等行为会增加攻击难度(例如,创建子进程执行其他程序,获得一个shell 等)。这样被攻击的损失仅限于当前进程,而不是整个操作系统及其他数据。

要通过此项检查,可能需要解决过滤器安装期间遇到的错误,或者通过下面的设置来关闭系统调用过滤器:

bootstrap.system_call_filter: false

11、OnError与OnOutOfMemoryError检查

如果JVM遇到致命错误(OnError)或OutOfMemoryError (OnOutOfMemoryError), 那么JVM选项OnError和OnOutOfMemoryError可以执行任意命令。

但是,默认情况下,ES的系统调用过滤器是启用的(seccomp),fork 会被阻止。因此,使用OnError或OnOutOfMemoryError和系统调用过滤器不兼容。

若要通过此项检查,则不要启用OnError或OnOutOfMemoryError,而是升级到Java 8u92并使用ExitOnOutOfMemoryError。

12、Early-access检查

OpenJDK为即将发布的版本提供了early-access 快照,这些发行版不适合生产环境。若要通过此项检查,则需要让ES运行在JVM的稳定版。

13、G1GC检查

JDK 8的早期版本有些问题,会导致索引损坏,JDK 8u40之前的版本都会受影响。本项检查验证是否是早期的HotSpotJVM版本。

2.2.5、启动内部模块

环境检查完毕,开始启动各子模块。子模块在Node类中创建,启动它们时调用各自的start()方法,例如:

discovery.start();
clusterService. start();
nodeConnectionsService.start();

子模块的start方法基本就是初始化内部数据、创建线程池、启动线程池等操作。

2.2.5、启动keepalive线程

调用keepAliveThread.start()方法启动keepalive线程,线程本身不做具体的工作。主线程执行完启动流程后会退出,keepalive 线程是唯一的用户线程, 作用是保持进程运行。在Java程序中,至少要有一个用户线程。当用户线程数为零时退出进程。

三、节点关闭

3.1、节点关闭流程

现在我们探讨一下单个节点的关闭流程。设想当我们为ES集群更新配置、升级版本时,需要通过“kill" ES进程来关闭节点。但是kill操作是否安全?如果此时节点有正在执行的读写操作会有什么影响?如果节点是Master该如何处理?关闭流程是怎么实现的?kill 节点都会带来哪些风险?

答案是:ES进程会捕获SIGTERM信号(kill 命令默认信号)进行处理,调用各模块的stop方法,让它们有机会停止服务,安全退出。

进程重启期间,如果主节点被关闭,则集群会重新选主,在这期间,集群有一个短暂的无主状态。如果集群中的主节点是单独部署的,则新主当选后,可以跳过gateway和recovery流程,否则新主需要重新分配旧主所持有的分片:提升其他副本为主分片,以及分配新的副分片。

如果数据节点被关闭,则读写请求的TCP连接也会因此关闭,对客户端来说写操作执行失败。但写流程已经到达Engine环节的会正常写完,只是客户端无法感知结果。此时客户端重试,如果使用自动生成ID,则数据内容会重复。

综合来说,滚动升级产生的影响是中断当前写请求,以及主节点重启可能引起的分片分配过程。提升新的主分片一般都比较快,因此对集群的写入可用性影响不大。

当索引部分主分片未分配时,使用自动生成ID的情况下,如果持续写入,则客户端对失败重试可能会成功(请求到达已分配成功的主分片),但是会在不同的分片之间产生数据倾斜,倾斜程度视期间数量而定。

3.2、关闭流程分析

在节点启动过程中,Bootstrap#setup 方法中添加了shutdown hook, 当进程收到系统SIGTERM (kill 命令默认信号)或SIGINT信号时,调用Node#close方法,执行节点关闭流程。

每个模块的Service中都实现了doStopdoClose,用于处理这个模块的正常关闭流程。节点总的关闭流程位于Node#close,在close方法的实现中,先调用一遍各个模块的doStop,然后再次遍历各个模块执行doClose。主要实现代码如下:

if (lifecycle.started()) {
    stop(); //调用 各模块的doStop 方法
    List<Closeable> toClose = new ArrayList<>();
    //在toClose中添加所有需要关闭的Service,以nodeService为例
    toClose.add(nodeService);
    //调用各模块doClose方法
    IOUtils.close (toClose);

各模块的关闭有一定的顺序关系,以doStop为例,按下表所示的顺序调用各模块doStop方法。

综合来看,关闭顺序大致如下:

  • 关闭快照和HTTPServer,不再响应用户REST请求。
  • 关闭集群拓扑管理,不再响应ping请求。
  • 关闭网络模块,让节点离线。
  • 执行各个插件的关闭流程。
  • 关闭IndicesService。

最后才关闭IndicesService,是因为这期间需要等待释放的资源最多,时间最长。

3.3、分片读写过程中执行关闭

下面分别对读和写执行过程中关闭节点进行分析。

写入过程中关闭:线程在写入数据时,会对Engine加写锁。IndicesServicedoStop方法对本节点上全部索引并行执行removeIndex,当执行到EngineflushAndClose (先flush 然后关闭Engine),也会对Engine加写锁。由于写入操作已经加了写锁,此时写锁会等待,直到写入执行完毕。因此数据写入过程不会被中断。但是由于网络模块被关闭,客户端的连接会被断开。客户端应当作为失败处理,虽然ES服务端的写流程还在继续。

读取过程中关闭:线程在读取数据时,会对Engine加读锁。flushAndClose 时的写锁会等待读取过程执行完毕。但是由于连接被关闭,无法发送给客户端,导致客户端读失败。

下图展示了Engine的flushAndClose过程。

节点关闭过程中,IndicesServicedoStopEngine设置了超时,如果flushAndClose 一直等待,则CountDownLatch.await默认1天才会继续后面的流程。

3.4、主节点被关闭

主节点被关闭时,没有想象中的特殊处理,节点正常执行关闭流程,当TransportService 模块被关闭后,集群重新选举新Master。因此,滚动重启期间会有一段时间处于无主状态。

四、小结

  1. 总体来说,节点启动流程做的就是初始化和检查工作,各个子模块启动后异步地工作,加载本地数据,或者选主、加入集群等,在后面的章节中单独介绍。.
  2. 节点在关闭时有机会处理未写完的数据,但是写完后可能来不及通知客户端。包括线程池中尚未执行的任务,在一定的超时时间内都有机会执行完。

集群健康从Red变为Green的时间主要消耗在维护主副分片的一致性上。我们也可以选择在集群健康为Yellow时就允许客户端写入,但是会牺牲一些数据安全性。

关注我的公众号【宝哥大数据】,更多干货。。。

以上是关于《Elasticsearch 源码解析与优化实战》第4章:节点启动和关闭的主要内容,如果未能解决你的问题,请参考以下文章

《Elasticsearch 源码解析与优化实战》第19章:搜索速度优化

《Elasticsearch 源码解析与优化实战》第19章:搜索速度优化

《Elasticsearch 源码解析与优化实战》第18章:写入速度优化

《Elasticsearch 源码解析与优化实战》第18章:写入速度优化

《Elasticsearch 源码解析与优化实战》样章-第 6 章 数据模型

《Elasticsearch 源码解析与优化实战》第9章:Search流程