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

2021-08-20 18:06:11 浏览数 (1)

简介

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

启动流程做了什么

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

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

启动流程分析

启动脚本

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

代码语言:javascript复制
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参数:

代码语言:javascript复制
bin/elasticsearch -d 

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

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

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

参数

含义

-E

设定某项配置。例如,设置集群名称: -E "cluster.name=my_cluster",一般通过配置文件来设置,而不是在命令行设置

-V,--version

打印版本号信息

-d,--daemonize

后台启动

-h,--help

打印帮助信息

-p,--pidfile

启动时在指定路径创建一个pid文件,其中保存了当前进程的pid,之后可以通过查看这个pid文件来关闭进程

-q,--quiet

关闭控制台的标准输出和标准错误输出

-s,--silent

终端输出最少信息(默认为normal)

-v,--verbose

终端输出详细信息

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

代码语言:javascript复制
bin/elasticsearch -d -p es.pid

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

代码语言:javascript复制
elasticsearch.yml #主要配置文件
1og4j2.properties #日志配置文件

加载安全配置

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

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

检查内部环境

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

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

检测外部环境

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执行。

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

代码语言:javascript复制
sysctl -wvm.max_map_count=262144

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

代码语言:javascript复制
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 等)。这样被攻击的损失仅限于当前进程,而不是整个操作系统及其他数据。

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

代码语言:javascript复制
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版本。

启动内部模块

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

代码语言:javascript复制
discovery.start();
clusterService. start();
nodeConnectionsService.start();

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

启动keepalive线程

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

节点关闭流程

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

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

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

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

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

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

关闭流程分析

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

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

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

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

服务

简介

ResourceWatcherService

通用资源监视服务

HttpServerTransport

HTTP传输服务,提供REST接口服务

SnapshotsService

快照服务

SnapshotShardsService

负责启动和停止shard级快照

IndicesClusterStateService

收到集群状态信息后,处理其中索引相关操作

Discovery

集群拓扑管理

RoutingService

处理reroute (节点之间迁移shard)

ClusterService

集群管理服务,主要处理集群任务,发布集群状态

NodeConnectionsService

节点连接管理服务

MonitorService

提供进程级、系统级、文件系统和JVM的监控服务

GatewayService

负责集群元数据持久化与恢复

SearchService

处理搜索请求

TransportService

底层传输服务

plugins

当前的所有插件

IndicesService

负责创建、删除索引等索引操作

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

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

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

分片读写过程中执行关闭

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

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

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

下图展示了Engine的flushAndClose过程。

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

主节点被关闭

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

小结

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

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

0 人点赞