节点的启动和关闭
启动流程做了什么
总体来说,节点启动流程的任务是做下面几类工作:
- 解析配置,包括配置文件和命令行参数。
- 检查外部环境和内部环境,例如,JVM版本、操作系统内核参数等。
- 初始化内部资源,创建内部模块,初始化探测器。
- 启动各个子模块和keepalive线程。
启动流程分析
启动脚本
当我们通过启动脚本bin/elasticsearch启动ES时,脚本通过exec加载Java程序。代码如下:
解析命令行参数和配置文件
目前支持的命令行参数有下面几种,默认启动时都不使用
实际工程应用中建议在启动参数中添加-d和-p,例如:
bin/elasticsearch -d -p es.pid
此处解析的配置文件有下面两个,jvm.options是在启动脚本中解析的。
elasticsearch.yml #主要配置文件 log4j2.properties #日志配置文件
加载安全配置
此处的“安全配置”是为了解决有些敏感的信息不适合放到配置文件中的,因为配置文件是明文保存的,虽然文件系统有基于用户权限的保护,但这仍然不够。因此ES把这些敏感配置信息加密,单独放到一个文件 中:config/elasticsearch.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执行。
要通过这项检查,可以执行下面的命令(临时生效,重启后失 效): sysctl -w vm.max_map_count=262144
或者在 /etc/sysctl.conf 文 件 中 添 加 一 行 vm.max_map_count=262144,然后执行下面的命令(立即,且永 久生效) sysctl –p
- 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
- OnError与OnOutOfMemoryError检查 如果JVM遇到致命错误( OnError )或OutOfMemoryError ( OnOutOfMemoryError ),那么JVM选项OnError和OnOutOfMemoryError可以执行任意命令。但是, 默认情况下,ES的系统调用过滤器是启用的(seccomp),fork会被阻止。 因此,使用OnError或OnOutOfMemoryError和系统调用过滤器不兼容。
若要通过此项检查,则不要启用OnError 或OnOutOfMemoryError,而是升级到Java 8u92 并使用ExitOnOutOfMemoryError。
- Early-access检查 OpenJDK 为即将发布的版本提供了 early-access 快照,这些发 行版不适合生产环境。若要通过此项检查,则需要让ES运行在JVM的 稳定版。
- G1GC检查 JDK 8的早期版本有些问题,会导致索引损坏,JDK 8u40之前的 版本都会受影响。本项检查验证是否是早期的HotSpot JVM版本。
启动内部模块
环境检查完毕,开始启动各子模块。子模块在Node类中创建,启 动它们时调用各自的start()方法,例如:
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中都实现了doStop和doClose,用于处理这 个模块的正常关闭流程。节点总的关闭流程位于Node#close,在 close方法的实现中,先调用一遍各个模块的doStop,然后再次遍历 各个模块执行doClose。主要实现代码如下:
各模块的关闭有一定的顺序关系,以 doStop 为例,按下表所示 的顺序调用各模块 doStop方法。
综合来看,关闭顺序大致如下:
- 关闭快照和HTTPServer,不再响应用户REST请求。
- 关闭集群拓扑管理,不再响应ping请求。
- 关闭网络模块,让节点离线。
- 执行各个插件的关闭流程。
- 关闭IndicesService。 最后才关闭IndicesService,是因为这期间需要等待释放的资源 最多,时间最长。
分片读写过程中执行关闭
写入过程中关闭
线程在写入数据时,会对Engine加写锁。 IndicesService 的 doStop 方 法 对 本 节 点 上 全 部 索 引 并 行 执 行removeIndex,当执行到Engine的flushAndClose(先flush然后关 闭Engine),也会对Engine加写锁。由于写入操作已经加了写锁,此 时写锁会等待,直到写入执行完毕。因此数据写入过程不会被中断。 但是由于网络模块被关闭,客户端的连接会被断开。客户端应当作为 失败处理,虽然ES服务端的写流程还在继续。
读取过程中关闭
线程在读取数据时,会对Engine加读锁。 flushAndClose时的写锁会等待读取过程执行完毕。但是由于连接被 关闭,无法发送给客户端,导致客户端读失败。
节点关闭过程中,IndicesService的doStop对Engine设置了超 时,如果flushAndClose一直等待,则CountDownLatch.await默认 1天才会继续后面的流程。
主节点被关闭
主节点被关闭时,没有想象中的特殊处理,节点正常执行关闭流 程,当TransportService模块被关闭后,集群重新选举新Master。因 此,滚动重启期间会有一段时间处于无主状态。
最后
- (1)总体来说,节点启动流程做的就是初始化和检查工作,各个 子模块启动后异步地工作,加载本地数据,或者选主、加入集群等
- (2)节点在关闭时有机会处理未写完的数据,但是写完后可能来 不及通知客户端。包括线程池中尚未执行的任务,在一定的超时时间 内都有机会执行完。
集群健康从Red变为Green的时间主要消耗在维护主副分片的一 致性上。我们也可以选择在集群健康为Yellow时就允许客户端写入, 但是会牺牲一些数据安全性。