乐趣区

Elasticsearch系列生产集群部署下

概要

本篇继续讲解 Elasticsearch 集群部署的细节问题

集群重启问题

如果我们的 Elasticsearch 集群做了一些离线的维护操作时,如扩容磁盘,升级版本等,需要对集群进行启动,节点数较多时,从第一个节点开始启动,到最后一个节点启动完成,耗时可能较长,有时候还可能出现某几个节点因故障无法启动,排查问题、修复故障后才能加入到集群中,此时集群会干什么呢?

假设 10 个节点的集群,每个节点有 1 个 shard,升级后重启节点,结果有 3 台节点因故障未能启动,需要耗费时间排查故障,如下图所示:

整个过程步骤如下:

  1. 集群已完成 master 选举(node6),master 发现未加入集群的 node1、node2、node3 包含的 shard 丢失,便立即发出 shard 恢复的指令。
  2. 在线的 7 台 node,将其中一个 replica shard 升级为 primary shard,并且进行为这些 primary shard 复制足够的 replica shard。
  3. 执行 shard rebalance 操作。
  4. 故障的 3 台节点已排除,启动成功后加入集群。
  5. 这 3 台节点发现自己的 shard 已经在集群中的其他节点上了,便删除本地的 shard 数据。
  6. master 发现新的 3 台 node 没有 shard 数据,重新执行一次 shard rebalance 操作。

这个过程可以发现,多做了四次 IO 操作,shard 复制,shard 首次移动,shard 本地删除,shard 再次移动,这样凭空造成大量的 IO 压力,如果数据量是 TB 级别的,那费时费力不讨好。

出现此类问题的原因是节点启动的间隔时间不能确定,并且节点越多,这个问题越容易出现,如果可以设置集群等待多少个节点启动后,再决定是否对 shard 进行移动,这样 IO 压力就能小很多。

针对这个问题,我们有下面几个参数:

  • gateway.recover_after_nodes:集群必须要有多少个节点时,才开始做 shard 恢复操作。
  • gateway.expected_nodes: 集群应该有多少个节点
  • gateway.recover_after_time: 集群启动后等待的 shard 恢复时间

如上面的案例,我们可以这样设置:

gateway.recover_after_nodes: 8
gateway.expected_nodes: 10
gateway.recover_after_time: 5m

这三个参数的含义:集群总共有 10 个节点,必须要有 8 个节点加入集群时,才允许执行 shard 恢复操作,如果 10 个节点未全部启动成功,最长的等待时间为 5 分钟。

这几个参数的值可以根据实际的集群规模来设置,并且只能在 elasticsearch.yml 文件里设置,没有动态修改的入口。

上面的参数设置合理的情况,集群启动是没有 shard 移动的现象,这样集群启动的时候就可以由之前的几小时,变成几秒钟。

JVM 和 Thread Pool 设置

一提到 JVM 的调优,大家都有手痒的感觉,好几百个 JVM 参数,说不定开启了正确的按钮,从此 ES 踏上高性能、高吞吐量的道路。现实情况可能是我们想多了,ES 的大部分参数是经过反复论证的,基本上不用咱们太操心。

JVM GC

Elasticsearch 默认使用的垃圾回收器是 CMS。

## GC configuration
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly

CMS 回收器是并发式的回收器,能够跟应用程序工作线程并发工作,最大程度减少垃圾回收时的服务停顿时间。

CMS 还是会有两个停顿阶段,同时在回收特别大的 heap 时也会有一些问题。尽管有一些缺点,但是 CMS 对于要求低延时请求响应的软件来说,还是最佳的垃圾回收器,因此官方的推荐就是使用 CMS 垃圾回收器。

有一种最新的垃圾回收器叫做 G1。G1 回收器可以比 CMS 提供更少的回收停顿时间,而且能够这对大 heap 有更好的回收表现。它会将 heap 划分为多个 region,然后自动预测哪个 region 会有最多可以回收的空间。通过回收那些 region,就可以最小化停顿时长,而且可以针对大 heap 进行回收。

听起来还挺好的,只是 G1 还是比较年轻的一种垃圾回收器,而且经常会发现一些新的 bug,这些 bug 可能会导致 jvm 挂掉。稳定起见,暂时不用 G1,等 G1 成熟后 ES 官方推荐后再用不迟。

线程池

我们开发 Java 应用系统时,对系统调优的一个常见手段就是调整线程池,但在 ES 中, 默认的 threadpool 设置是非常合理的,对于所有的 threadpool 来说,除了搜索的线程池,都是线程数量设置的跟 cpu core 一样多的。如果我们有 8 个 cpu core,那么就可以并行运行 8 个线程。那么对于大部分的线程池来说,分配 8 个线程就是最合理的数量。

搜索会有一个更加大的 threadpool,线程数量一般被配置为:cpu core * 3 / 2 + 1。

Elasticsearch 的线程池分成两种:接受请求的线程和处理磁盘 IO 操作的线程,前面那种由 ES 管理,后一种由 Lucene 管理,它们之间会进行协作,ES 的线程不会因为 IO 操作而 block 住,所以 ES 的线程设置跟 CPU 核数一样或略大于 CPU 核数即可。

服务器的计算能力是非常有限的,线程池的数量过大会导致上下文频繁切换,更费资源,如果 threadpool 大小设置为 50,100,甚至 500,会导致 CPU 资源利用率很低,性能反而下降。

只需要记住:用默认的线程池,如果真要修改,以 CPU 核数为准。

heap 内存设置最佳实践

Elasticsearch 默认的 jvm heap 内存大小是 2G,如果是研发环境,我会改成 512MB,但在生产环境 2GB 有点少。

在 config/jvm.options 文件下,可以看到 heap 的设置:

# Xms represents the initial size of total heap space
# Xmx represents the maximum size of total heap space

-Xms2g
-Xmx2g

分配规则

Elasticsearch 使用内存主要有两个大户:jvm heap 和 lucene,前者 ES 用来存放很多数据结构来提供更快的操作性能,后者使用 os cache 缓存索引文件,包括倒排索引、正排索引,os cache 内存是否充足,直接影响查询检索的性能。

一般的分配规则是:jvm heap 占用小于一半的内存,剩下的全归 lucene 使用。

如果单台机器总内存 64GB,那么 heap 顶格内存分配为 32GB,因为 32GB 内存以下,jvm 会使用 compressed oops 来解决 object pointer 耗费过大空间的问题,超过 32GB 后,jvm 的 compressed oops 功能关闭,这样就只能使用 64 位的 object pointer,会耗费更多的空间,过大的 object pointer 还会在 cpu,main memory 和 LLC、L1 等多级缓存间移动数据的时候,吃掉更多的带宽。最终的结果可能是 50GB 内存的效果和 32GB 一样,白白浪费了十几 GB 内存。

这里涉及到 jvm 的 object pointer 指针压缩技术,有兴趣可以单独了解一下。

如果单台机器总内存小于 64GB,一般 heap 分配为总内存的一半即可,具体要看预估的数据量是多少。

如果使用超级机器,1TB 内存的那种,官网不建议上那么牛逼的机器,建议分配 4 -32GB 内存给 heap,其他的全部用来做 os cache,这样数据量全部缓存在内存中,不落盘查询,性能杠杠滴。

最佳实践建议

  1. 将 heap 的最小值和最大值设置为一样大。
  2. elasticsearch jvm heap 设置得越大,就有越多的内存用来进行缓存,但是过大的 jvm heap 可能会导致长时间的 gc 停顿。
  3. jvm heap size 的最大值不要超过物理内存的 50%,才能给 lucene 的 file system cache 留下足够的内存。
  4. jvm heap size 设置不要超过 32GB,否则 jvm 无法启用 compressed oops,将对象指针进行压缩,确认日志里有[node-1] heap size [1007.3mb], compressed ordinary object pointers [true] 字样出现。
  5. 最佳实践数据:heap size 设置的小于 zero-based compressed ooops,也就是 26GB,但是有时也可以是 30GB。通过 -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompressedOopsMode 开启对应,确认有 heap address: 0x00000000e0000000, size: 27648 MB, Compressed Oops mode: 32-bit 字样,而不是 heap address: 0x00000000f4000000, size: 28672 MB, Compressed Oops with base: 0x00000000f3ff0000 字样。

swapping 问题

部署 Elasticsearch 的服务尽可能关闭到 swap,如果内存缓存到磁盘上,那查询效率会由微秒级降到毫秒级,会造成性能急剧下降的隐患。

关闭办法:

  1. Linux 系统执行 swapoff -a 关闭 swap,或在 /etc/fstab 文件中配置。
  2. elasticsearch.yml 中可以设置:bootstrap.mlockall: true 锁住自己的内存不被 swap 到磁盘上。

使用命令 GET _nodes?filter_path=**.mlockall 可以查看是否开启 mlockall
响应信息:

{
  "nodes": {
    "A1s1uus7TpuDSiT4xFLOoQ": {
      "process": {"mlockall": true}
    }
  }
}

Elasticsearch 启动的几个问题

  1. root 用户启动实例的问题

如果你用 root 用户启动 Elasticsearch 的实例,将得到如下的错误提示:

org.elasticsearch.bootstrap.StartupException: java.lang.RuntimeException: can not run elasticsearch as root
    at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:140) ~[elasticsearch-6.3.1.jar:6.3.1]
    at org.elasticsearch.bootstrap.Elasticsearch.execute(Elasticsearch.java:127) ~[elasticsearch-6.3.1.jar:6.3.1]
    at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:86) ~[elasticsearch-6.3.1.jar:6.3.1]
    at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:124) ~[elasticsearch-cli-6.3.1.jar:6.3.1]
    at org.elasticsearch.cli.Command.main(Command.java:90) ~[elasticsearch-cli-6.3.1.jar:6.3.1]
    at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:93) ~[elasticsearch-6.3.1.jar:6.3.1]
    at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:86) ~[elasticsearch-6.3.1.jar:6.3.1]
Caused by: java.lang.RuntimeException: can not run elasticsearch as root
    at org.elasticsearch.bootstrap.Bootstrap.initializeNatives(Bootstrap.java:104) ~[elasticsearch-6.3.1.jar:6.3.1]
    at org.elasticsearch.bootstrap.Bootstrap.setup(Bootstrap.java:171) ~[elasticsearch-6.3.1.jar:6.3.1]
    at org.elasticsearch.bootstrap.Bootstrap.init(Bootstrap.java:326) ~[elasticsearch-6.3.1.jar:6.3.1]
    at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:136) ~[elasticsearch-6.3.1.jar:6.3.1]
    ... 6 more

无它,建立一个用户,专门用来启动 Elasticsearch 的,如 esuser,相应的系统目录和数据存储目录都赋予 esuser 账户为归属者。

  1. 启动时提示 elasticsearch process is too low,并且无法启动成功

完整的提示信息:

max file descriptors [4096] for elasticsearch process is too low, increase to at least [65536]
memory locking requested for elasticsearch process but memory is not locked

解决办法:设置系统参数,命令行中的 esuser 为建立的 Linux 用户。

[root@elasticsearch01 bin]# vi /etc/security/limits.conf

# 在文件最后添加
esuser hard nofile 65536
esuser soft nofile 65536
esuser soft memlock unlimited
esuser hard memlock unlimited

设置完成后,可以通过命令查看结果:

# 请求命令
GET _nodes/stats/process?filter_path=**.max_file_descriptors

# 响应结果
{
  "nodes": {
    "A1s1uus7TpuDSiT4xFLOoQ": {
      "process": {"max_file_descriptors": 65536}
    }
  }
}
  1. 提示 vm.max_map_count [65530] is too low 错误,无法启动实例

完整的提示信息:

max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

解决办法:添加 vm.max_map_count 配置项

临时设置:sysctl -w vm.max_map_count=262144

永久修改:修改 vim /etc/sysctl.conf 文件,添加 vm.max_map_count 设置

[root@elasticsearch01 bin]# vim /etc/sysctl.conf

# 在文件最后添加
vm.max_map_count=262144

# 执行命令
[root@elasticsearch01 bin]# sysctl -p

Elasticsearch 实例启停

实例一般使用后台启动的方式,在 ES 的 bin 目录下执行命令:

[esuser@elasticsearch01 bin]$ nohup ./elasticsearch &
[1] 15544
[esuser@elasticsearch01 bin]$ nohup: 忽略输入并把输出追加到 "nohup.out"

这个 elasticsearch 没有 stop 参数,停止时使用 kill pid 命令。

[esuser@elasticsearch01 bin]$ jps | grep Elasticsearch
15544 Elasticsearch
[esuser@elasticsearch01 bin]$ kill -SIGTERM 15544

发送一个 SIGTERM 信号给 elasticsearch 进程,可以优雅的关闭实例。

小结

本篇接着上篇的内容,讲解了集群重启时要注意的问题,JVM Heap 设置的最佳实践,以及 Elasticsearch 实例启动时常见的问题解决办法,最后是 Elasticsearch 优雅关闭的命令。

专注 Java 高并发、分布式架构,更多技术干货分享与心得,请关注公众号:Java 架构社区
可以扫左边二维码添加好友,邀请你加入 Java 架构社区微信群共同探讨技术

退出移动版