简介
本章分析单个节点的启动和关闭流程。看看进程是如何解析配置、检查环境、初始化内部模块的,以及在节点被“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参数:
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
中都实现了doStop
和doClose
,用于处理这个模块的正常关闭流程。节点总的关闭流程位于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方法。
服务 | 简介 |
---|---|
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加写锁。IndicesService
的doStop
方法对本节点上全部索引并行执行removeIndex
,当执行到Engine
的flushAndClose
(先flush 然后关闭Engine),也会对Engine加写锁。由于写入操作已经加了写锁,此时写锁会等待,直到写入执行完毕。因此数据写入过程不会被中断。但是由于网络模块被关闭,客户端的连接会被断开。客户端应当作为失败处理,虽然ES服务端的写流程还在继续。
读取过程中关闭:线程在读取数据时,会对Engine加读锁。flushAndClose 时的写锁会等待读取过程执行完毕。但是由于连接被关闭,无法发送给客户端,导致客户端读失败。
下图展示了Engine的flushAndClose过程。
节点关闭过程中,IndicesService
的doStop
对Engine
设置了超时,如果flushAndClose
一直等待,则CountDownLatch.await
默认1天才会继续后面的流程。
主节点被关闭
主节点被关闭时,没有想象中的特殊处理,节点正常执行关闭流程,当TransportService 模块被关闭后,集群重新选举新Master。因此,滚动重启期间会有一段时间处于无主状态。
小结
- 总体来说,节点启动流程做的就是初始化和检查工作,各个子模块启动后异步地工作,加载本地数据,或者选主、加入集群等,在后面的章节中单独介绍。.
- 节点在关闭时有机会处理未写完的数据,但是写完后可能来不及通知客户端。包括线程池中尚未执行的任务,在一定的超时时间内都有机会执行完。
集群健康从Red变为Green的时间主要消耗在维护主副分片的一致性上。我们也可以选择在集群健康为Yellow时就允许客户端写入,但是会牺牲一些数据安全性。