博客

写写 ElasticSearch 的系统配置和内存使用

动机

之前在制作词典网站时,我用 ElasticSearch 来索引和搜索数据,而平时的工作也与 ES 有所接触。

这里想简单记录一下,配置和运行 ElasticSearch 的几个事儿:

  • 前半部分围绕 Linux 系统配置和一些集群配置,
  • 后半部分写一写 ES 的内存配置和 JVM 的内存指针

这些配置将影响 ElasticSearch 的性能,可用性和稳定性。

这篇文章不包括 ElasticSearch 的调优 - 因为我觉得这个非常取决于使用场景。

 

系统配置

这儿讨论的许多系统配置和思路也适用于其他的数据密集型系统,比如 Kafka 消息系统。

 

集群的网络环境

通常人们会使用多个 ElasticSearch 节点,每个运行在一台机器上,让它们形成集群。

节点之前的数据传输是频繁的,同时,选举集群的首脑 / 主节点 master 也于依赖网络传输。

如果网络传输不稳定或者比较慢,集群选举可能失败,数据的备份和搜索都可能变得缓慢。

所以呢,这些节点应该在地理位置上是相近的。

ElasticSearch 是支持节点间用 DNS 名称来连接的,但那样依赖于定期的 DNS 查询,显然就慢一些。

所以,节点最好在同一个子网下,并且它们通过提前配置好的 IP 地址来沟通。

现在大伙儿都喜欢把基础设施搬上云,所以通常节点们会在同一个 虚拟云 / VPC (virual private network) 的同一个子网内。

 

数据传输加密

节点间的交流是加密的,ElasticSearch 支持节点间使用 TLS 协议沟通。

这意味着大伙儿要给每一个节点配置一个域名证书 / TLS certificate 和密钥 / private key.

当然啦,取决于网络环境和对数据的安全需求,证书的签发者 (Certificate Authority) 可以是开发者自己,公司,或者一个公共的 CA.

具体步骤可以看 ElasticSearch 的官方文档

 

节点数量

常见的做法是:使用大于1的奇数个节点用于集群管理。主要原因是,分布式系统的选举是一个少数服从多数的过程。

  • 选举时,一个集群需要 roundDown(nodes / 2) + 1 张票来决定一个主节点。

使用大于1个管理节点的好处是:当有1个管理节点挂了的时候,集群仍然能依赖于其他管理节点继续工作。

使用2个节点时,roundDown(nodes / 2) + 1 = 2 当一个管理节点挂了后便无法选举(无法获得2张票)也就无法工作。

能够容纳的崩溃节点的数量来看,使用偶数 (2N) 个管理节点是等价于 (2N - 1) 个节点的。

  • 比如4个管理节点,roundDown(nodes / 2) + 1 = 3 在挂了两个之后便无法完成选举了,这方面与3节点相同。
  • 读者可能想说,使用4个管理节点时,挂俩节点的概率是小于使用3个节点的,所以稳定性更高 - 这也对。

比如要配置一个拥有3个管理节点的集群,在管理节点上的配置文件是这样的:

node.master: true
discovery.zen.minimum_master_nodes: 2

另一方面,人们通常把数据节点与管理节点区分开来,来增加整体的稳定性。管理节点本身不存储文档数据,也不处理搜索请求。

 

内存映射区

ElasticSearch 依赖于文件系统来管理(索引存储和搜索)数据。

如果一个 ES 节点上存着大量的文档,它可能在搜索时打开大量的文件,并把文件内容加载到内存。

这个操作在 Linux 系统上意味着创建大量的内存映射区域 / memory map regions - 而它的数量是有上限的。

在 Linux 系统中,我们可以通过修改系统配置文件/etc/sysctl.conf 来增加可用的内存映射区数量 - 比如添加这样一行:

vm.max_map_count=262144

这将把内存映射区的数量上限更改为 262,144 这将是默认值 65536 (还是 65535) 的4倍 -

当然啦,这个数值取决于系统自身的内存大小,也取决于 ES 需要索引的文档的数量,搜索的文档数量,等等。

如果你使用像 Kafka 这样的依赖于文件系统的程序,同样可能需要增加内存映射区的数量,来避免 OOM 的问题。

最后想说的是,系统配置修改后需要运行 sysctl -p 并重启才能让它生效。

 

内存交换区

当系统的可用内存不足时,一部分被使用的内存就会被放到交换区里 - 也就是磁盘文件上。

相对于正常的内存分配和加载,这个过程(和从交换区加载数据)是十分缓慢的,我们不希望它影响到 ElasticSearch 程序。

大伙儿可以通过运行这个指令来临时关闭它:

sudo swapoff -a

如果想永久改动,要在 Linux 操作系统的文件系统配置文件 /etc/fstab 里把 swap 那一行给删了 / 注释掉。

另一方面呢,Linux 系统也提供一些方式,来把程序的内存锁定住,防止它们被移到交换区。

这需要我们启用一个 ES 的配置选项:

bootstrap.memory_lock: true

普通的进程是没有权限来锁定部分内存的,所以需要另一个系统配置改动,在/etc/security/limits.conf 加上:

elasticsearch soft memlock unlimited
elasticsearch hard memlock unlimited

这里假设了 ElasticSearch 进程是以用户 elasticsearch 的权限运行的,如果用户名不同,大家要更改一下每行的第一个词。

或者呢,大伙儿也可以用 * 来给予所有进程权限,尽管这么做并不建议。

 

进程的文件数量

程序可以通过系统 API 开打开文件,但每个进程能同时打开的文件数量也有上限:65536 (还是 65535).

这个上限可以在系统配置文件 /etc/security/limits.conf 中修改:

* soft nofile 262144
* hard nofile 262144

这俩个配置与系统的软硬链接没啥关系 soft 设置的数值可能在程序运行时被修改,而 hard 则不行(除非程序以 root 权限运行)

另外想提一下的是,大家应该注意一下/etc/security/limits.d/ 这个目录,这里可能也有一部分配置文件。

  • 比如说我们从 AWS EC2 上选定或自定义某个 image / 镜像,它通常会有一些自定义的系统配置。

目录 /etc/security/limits.d/ 下的配置文件是按文件名的字母排序后被加载的 - 之后被加载的配置可以覆盖之前的。

另一个常见的做法是在这里创建一个自定义的配置文件,确保它在(字母排序)最后被加载。

 

ElasticSearch 需要的内存

1 - 建议的最小堆内存是 1GB

2 - 建议的堆内存上限是 26 - 30GB

3 - 建议堆内存上下限一致

4 - 建议内存不超过系统内存的一半。

可以通过环境变量 ES_JAVA_OPTS 来设置他们,比如把它俩都设置为2GB:

ES_JAVA_OPTS="-Xms2g -Xmx2g" ./bin/elasticsearch

 

堆内存上下限一致

如果它俩不一致的话,JVM 可以动态地调整一个 Java 程序的内存使用。动态调整意味着两种操作:

1 - 将不需要的内存归还给系统

2 - 向系统请求更多的内存

这两个操作都需要调用系统内核的 API,需要时间和 CPU 资源来完成。

把堆内存上的下限设为同一数值意味着禁止这样的动态调整,也就避免了这些操作。

另一方面呢,如果程序初始内存比较小 (即便上限比较大),在内存扩容之前,必然有更多的时间和 CPU 被用在垃圾回收上。

在严重的时候,垃圾回收是可能中断程序的 / Stop The World. 这个现象我在工作时见过一次,此时 ES 无法搜索也无法接收数据。

 

不超过系统内存的一半

ElasticSearch 既依赖于系统的网络,来提供搜索服务和与其他节点传递数据,又依赖于文件系统存储数据。

系统本身需要一定的内存来提供这些功能,所以从(业界的)经验来看,单个 ES 程序占用 50% 以内的系统内存是没问题的。

这里还有一个潜在假设:在一台机器上就运行着一个 ElasticSearch 程序,没有其他服务等等 - 这也是常见做法。

 

最小1GB

最小值 1GB 也比较容易解释:ElasticSearch 程序本身运行时就需要至少这么多,读者可以本地运行一个试试。

从我个人的经验来看,单个 ElasticSearch (7.4版本) 进程消耗 1GB - 1.2 GB 左右的内存,运行若干个月没出现过问题。

而且我用的的虚拟机 / VPS 总共只有 2GB 的内存,上面还跑了 Nginx, Postgres DB 和一个 Web 服务器。

当然啦,在负载不大的时候,这些程序本身对内存的消耗也不高。

但最大值设定就需要一些篇幅来解释了:这与 JVM 的内存管理和对象指针有关。

 

Ordinary Object Pointer

JVM 在设计 / 实现时,内部采用「普通内存指针 / Ordinary Object Pointer」来存储对象的地址。

取决于程序使用多少内存,JVM 可能使用不同类型 / 大小的指针:比如 32 比特 (4字节) 的,或者 64 比特 (8字节) 的。

当然啦,32比特的指针更节约空间,所以被优先使用。

内存空间的访问最小可以精确到每个 byte / 字节 - 似乎也有人把它称作访问细粒度。

所以呢,32比特的指针只能覆盖 40亿 (2的32次方) 个内存位置,也就是一个4GB (40亿字节) 的内存空间。

 

Compressed Ordinary Object Pointer

为了增加它的范围,JVM 调整了对象的内存使用 - 让每个对象的内存占用都是至少8字节,或者它的倍数。

  • 这相当于强制让每个对象在内存中以某种方式对齐 / align 放置。

  • 我们可以通过 JVM 变量来手动改变这个数值 -XX:ObjectAlignmentInBytes

这个调整意味着,每个对象的内存地址都是8的倍数 - 也就是说地址 (的二进制表示) 的后三位数都是0.

  • 比如,在二进制中,1000 表示8,2000 表示16.

既然是这样,那么在保存一个地址时,我们也就不需要最后的3个比特 - 它们总是0.

因此呢,每个32位指针现在只要用29比特就能覆盖整个4GB 的内存空间 - 而32位全用上时,就能覆盖 32GB 的内存空间。

  • 三个比特能表达8(2的3次方)个有效数字,与其他29个比特一组合,它们就能表达原来8倍的空间。

更准确地说,我们现在往指针中存的东西变成了「对象的偏移量 / Object Offset」而不是内存地址,也不是地址偏移量。

Compressed Ordinary Object Pointer

当然啦,这样做的代价是,每次 JVM 获得指针里的数值时,都需要一点运算:把32比特往左推3位 / 相当于把数字乘8.

这样类型的指针也就被称作「压缩型普通指针 / Compressed Ordinary Object Pointer」

像许多配置那样,我们也可以通过 JVM 参数 -XX:+UseCompressedOops 来开启 / 关闭这个特性,它时默认开启的。

理论上来说,当一个 Java 程序使用超过 32GB 内存时,JVM 便需要使用 64-bit 指针,而停止使用压缩型指针。

实际上呢,现实中这个的阈值是低于 32GB 的,大概在26GB左右,这就涉及 JVM 中的另一个概念。

 

Zero-Based Compressed Ordinary Object Pointers

现在使用的 JVM 大多数是64位的,用于64位系统上。像所有的用户进程那样,它们在启动时从系统获得一个虚拟的内存空间。

比较特殊的是,JVM 会尝试请求一个地址从0开始的内存空间。这样做的好处是计算量减少,程序更快,因为计算虚拟内存地址时不需要加(内存空间的)偏移量,拿到指针里的数值后,做个比特推移就好。因此,从虚拟内存转换到物理内存的这个过程也就变得更快。

熟悉程序内存的读者大概会想到栈内存:每个栈都要额外管理一个 base pointer / 基准指针,加载栈上的变量时,都需要通过基准指针 + 偏移量来计算虚拟内存地址。

结合之前的压缩型指针,在这种情况下 JVM 所用的对象指针被称作 「无偏移的压缩型指针 / Zero-Based Compressed Ordinary Object Pointers」

从实验来看,当被请求的虚拟内存大小在 26GB 以下时,目前大多数系统是能满足「地址从0开始的内存空间」这个请求的。

ElasticSearch 本身是一个 Java 程序,所以运行在大多数系统上时,推荐的最大内存设定是 26GB.

 

验证内存上限

理想情况下呢,我们还是希望依照不同的系统 / 硬件来决定,所以可以用这两个 JVM 参数,来帮助我们判断:

-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompressedOopsMode

在它俩被使用之后呢,JVM 会输出更多的日志信息,比如这个:

heap address: 0x000000011be00000, size: 27648 MB, zero based Compressed Oops

当看到这样的信息时,我们就知道当前的内存上限 (27648 MB) 并不会导致程序运行更慢,因为 JVM 能够使用无偏移的压缩型指针.

当然啦,日志信息也可能是这样的:

heap address: 0x0000000118400000, size: 28672 MB, Compressed Oops with base: 0x00000001183ff000

这意味着 JVM 没能从操作系统那儿获得「从0开始的内存空间」也就转而采用普通压缩型指针,程序的运行效率相对降低。

 

虚拟机和 Java 版本

想额外说一句,并不是所有的虚拟机(的不同 Java 版本)都实现了压缩型指针,或者无偏移的压缩型指针。

这篇文章假设了大伙儿都在用比较流行的 HotSpot 虚拟机。

 

参考和引用

比较常用的 HotSpot 虚拟机文档:

https://www.baeldung.com/jvm-compressed-oops

教程网站 Bäldung 给 JVM 指针的实现做了很好的解释

https://www.baeldung.com/jvm-compressed-oops

ElasticSearch 节点间的发现与选举

https://www.elastic.co/guide/en/elasticsearch/reference/6.8/modules-discovery-zen.html