目录

Elasticsearch的内存调优与解析

Elasticsearch的堆内存默认配置是1GB。对于几乎所有的部署来说,这个数字通常都太小了。如果使用默认堆配置,则集群可能会出现问题。

有两种方法可以在Elasticsearch中更改堆大小。最简单的方法是设置一个名为ES_HEAP_SIZE的环境变量。当服务器进程启动时,它将读取这个环境变量并相应地设置堆大小。例如,可以通过命令行设置,如下所示:

1
export ES_HEAP_SIZE=10g

也可以通过es进程启动时的命令行参数传入的形式:

1
./bin/elasticsearch -Xmx10g -Xms10g
提示:
一定要确保最小(Xms)和最大(Xmx)设置相同,以防止堆在运行时自动调整大小,因为这个过程代价很高。

以上两种方式,环境变量ES_HEAP_SIZE的优先级通常会高于命令行参数 -Xmx-Xms

给Lucene预留大约一半的内存

一个常见的问题是es的堆内存配置太大。

堆对于Elasticsearch绝对是重要的。它被许多内存中的数据结构用来提供快速的操作。

但话虽如此,还有一个主要的内存用户不在堆中,那就是 Lucene

Lucene被设计用来利用底层操作系统的内存缓存(caching in-memory)中的数据结构。Lucene segments 存储在单独的文件中。因为 segments 是不可变的,所以这些文件永远不会改变。这使得它们对缓存非常友好,并且底层操作系统将乐于将热数据段(hot segments)驻留在内存中,以便更快地访问。这些 segments 包括 倒排索引(用于全文搜索)doc值(用于聚合)

Lucene的性能依赖于与操作系统的交互。但是如果你把所有可用的内存都给了Elasticsearch的堆,那么就没有剩余的内存给Lucene了。这可能会严重影响性能。

标准的建议是将50%的可用内存分配给Elasticsearch堆,而将剩下的50%留给空闲内存。它并不会闲置,Lucene会很高兴地吃掉剩下的任何东西。

如果你没有基于字符串分析的聚合需求(例如,你不需要开启字段的fielddata ),你可以考虑降低堆大小甚至降到更小。堆越小,意味着 Elasticsearch将拥有更快的GCsLucene将有更多的系统文件内存缓存,也就意味着性能越好。

堆大小不要超过 32 GB

不要给Elasticsearch分配大量堆内存还有另一个原因:事实证明,当堆内存分配小于32GB,HotSpot JVM 虚拟机 会使用一种技巧来压缩对象指针。

在Java中,所有对象都分配在堆上并由指针引用。 普通对象指针(OOP)指向这些对象,并且大小通常是CPU的运算位数:32位或64位,具体取决于处理器。 而指针引用的是该值的具体字节位置。

对于32位系统,这意味着最大堆大小为4 GB。

对于64位系统,堆大小可以变得更大,但是64位的指针会浪费更多的空间,因为指针很大。另外,比浪费空间更糟糕的是,当在主存和各种缓存(LLC、L1等)之间移动值时,较大的指针会消耗更多带宽

Hotspot JVM 虚拟机 使用一种叫做 普通对象压缩指针(CompressedOops) 的技巧来解决这个问题。指针不是指向内存中的精确字节位置,而是引用对象偏移量。这意味着32位指针可以引用40亿个对象,而不是40亿个字节。最终,这意味着即便堆可以增长到32 GB左右的物理大小,而仍然使用32位指针。

一旦你越过这个神奇的32GB边界,指针就会切换回普通对象指针。每个指针的大小会增长,CPU与内存之间的带宽会使用得更多,从而损失更多的内存性能。实际上,大约普通对象指针下40~50GB的堆大小才能获得与压缩对象指针下32GB的堆大小相同的效果。

这意味着:

即使您有多余的内存,也要尽量避免跨越 32GB 堆边界。

否则它将浪费内存,降低CPU性能,并使GC更难处理。

确切的压缩指针堆大小边界

这取决于不同的JVM虚拟机和平台。一般情况下,将堆大小设置成31GB就可以安全地小于堆边界。

或者,你也可以结合java的-Xmx参数和-XX:+PrintFlagsFinal参数,并查看UseCompressedOops是否等于true,这会使你在不同JVM与不同平台下找到准确的压缩指针堆边界

例如,我们在MacOSX平台中的Java 1.7下,发现能够使用压缩对象指针的最大堆大小约为32600MB (~31.83GB)

1
2
3
4
$ JAVA_HOME=`/usr/libexec/java_home -v 1.7` java -Xmx32600m -XX:+PrintFlagsFinal 2> /dev/null | grep UseCompressedOops
     bool UseCompressedOops   := true
$ JAVA_HOME=`/usr/libexec/java_home -v 1.7` java -Xmx32766m -XX:+PrintFlagsFinal 2> /dev/null | grep UseCompressedOops
     bool UseCompressedOops   = false

而在同一台机器上,Java 1.8 的值却是 32766MB (~31.99GB)

1
2
3
4
$ JAVA_HOME=`/usr/libexec/java_home -v 1.8` java -Xmx32766m -XX:+PrintFlagsFinal 2> /dev/null | grep UseCompressedOops
     bool UseCompressedOops   := true
$ JAVA_HOME=`/usr/libexec/java_home -v 1.8` java -Xmx32767m -XX:+PrintFlagsFinal 2> /dev/null | grep UseCompressedOops
     bool UseCompressedOops   = false
这意味着:
确切的压缩对象指针堆边界,是因不同JVM和不同平台而异的。如果要设置确切的值,要额外地小心。

从Elasticsearch v2.2.0开始,启动日志实际上会告诉您JVM是否正在使用压缩OOPs。您将看到一条日志消息,如下所示:

1
[2015-12-16 13:53:33,417][INFO ][env] [Illyana Rasputin] heap size [989.8mb], compressed ordinary object pointers [true]

[true] 代表正在使用压缩对象指针。否则它会显示 [false]

如果拥有一台1TB内存的机器

32GB的堆边界相当重要。当您的机器有很多内存时,我们该怎么办呢?目前拥有 512 768 GB RAM 的超级服务器已经变得越来越普遍。

首先,我们建议避免使用这样的大型机器(参见ES硬件部署建议 )。

但如果已经有这种大型服务器,我们有三项建议:

如果主要用于全文检索:
考虑给Elasticsearch分配4-32GB堆内存,让Lucene通过操作系统文件系统缓存使用剩余的内存。所有的内存将缓存segments,用于快速的全文搜索。
如果需要进行大量排序和聚合:
若大多数聚合都基于数字,日期,geo_points和not_analyzed字符串,很幸运,这样的聚合将得益于基于内存的doc values完成! 考虑给Elasticsearch分配4-32GB堆内存,其余的留给OS用来在内存中缓存doc values
如果需要对analyzed字符串进行大量排序和聚合:

例如,针对单词标签或SigTerm等,不幸的是,这意味着您需要开启fielddata,这意味着您需要堆空间。 可以考虑在一台计算机上运行两个或多个实例,而不是部署一个大堆内存的实例。 当然,实例堆大小总数仍然要遵守小于50%的规则。

如果您的计算机具有 128GB 的RAM,请运行两个实例,每个实例的内存不要超过 32GB。 这意味着少于 64GB 用于堆内存,剩余的 64GB 用于Lucene。

另外,需要在ES配置文件中配置cluster.routing.allocation.same_shard.host: true 这将阻止主副本分片被分配到同一台物理机,以提高可用性。

性能杀手:Swap交换

这个大家都懂,但还是要强调一下:将主内存swap交换到硬盘中,会损失很多性能。因为纯内存操作 (in-memory operation) 才能使程序运行地更快。

如果将内存置换到硬盘,0.1ms的操作将会变成10ms,如果大量的操作耗时都增加了一个数量级,可想而知 swap交换 对性能的损害有多严重。

一、最好的办法是在您的系统上完全禁用swap:

1
2
3
4
5
# 临时禁用
sudo swapoff -a

# 永久禁用,需要操作 /etc/fstab,禁用swap挂载
sudo vim /etc/fstab

二、如果不希望完全禁用swap,您可以尝试降低swappiness:

这个值可以影响操作系统交换内存的积极程度。虽然阻止在正常情况下交换,但无法避免操作系统在紧急内存情况下进行交换。

对于大多数Linux系统,可以使用sysctl配置:

1
vm.swappiness = 1 

三、如果以上两种方法都不想采用,则应该启用mlockall:

1
2
3
# 在elasticsearch.yml添加
# 这允许JVM锁定内存,从而防止操作系统将其交换到硬盘。
bootstrap.mlockall: true
提示:

mlockall() 是一个C语言的系统调用,它将当前映射的进程内存锁到RAM中,从而避免进行Swap交换。

如果要在JVM中使用它,一种简单方法是通过 JNA (Java Native Access) 来调用。例如:mlockall-agent

慎用 vm.swappiness=0

设置 vm.swappiness=1vm.swappiness=0 更好,因为在最新的 Linux 内核中,设置0可能会触发 OOM-killer

提示:
OOM-killer: Out-of-Memory (OOM) Killer 是一种保护机制,用于当内存严重不足时,为了系统的继续运转,内核迫不得已挑选一个进程,将其杀死,以释放内存,缓解内存不足带来的系统问题。

此前,vm.swappiness = 0 只是Linux在判断是否交换内存(swap)的一个“倾向”参考值,而并不是说,设置为0以后,Linux就完全不会使用内存交换空间。

但是,在较新的内核中(2.6.32-303.el6及以后),vm.swappiness = 0 的默认行为修改了,当机器的内存严重不足时,根据Linux的默认策略,它会首先把内存占用最大的进程kill掉,从而导致应用故障。

这个修改是在内核3.5-rc1中提交的,并且合并到了2.6.32-303.el6及之后的各个版本。先让我们来看看这个patch:

mm: avoid swapping out with swappiness==0

Sometimes we’d like to avoid swapping out anonymous memory. In particular, avoid swapping out pages of important process or process groups while there is a reasonable amount of pagecache on RAM so that we can satisfy our customers' requirements.

OTOH, we can control how aggressive the kernel will swap memory pages with

/proc/sys/vm/swappiness for global and

/sys/fs/cgroup/memory/memory.swappiness for each memcg.

But with current reclaim implementation, the kernel may swap out even if we set swappiness=0 and there is pagecache in RAM.

This patch changes the behavior with swappiness==0. If we set swappiness==0, the kernel does not swap out completely (for global reclaim until the amount of free pages and filebacked pages in a zone has been reduced to something very very small (nr_free + nr_filebacked < high watermark)).

/img/2020/es/diff_mm_vmscan.jpg
diffstat (mm/vmscan.c)

就像Satoru Moriya所说的那样,在之前的版本中,就算我们设置了swappiness=0并且RAM中还有pagecache,内核也可能会交换出部分匿名内存页。

而为了“满足用户的需求”,这个patch修改了swappiness=0的行为:即如果设置swappiness=0,那么只有在(nr_free + nr_filebacked < high watermark)才会交换内存,也就是说空闲内存和文件缓存基本没有了才会触发内存swap

这样的话,副作用在于:内存如果不够了,Linux有可能触发OOM,从而kill掉耗费内存最多的进程。

在2.6.32-303.el6 RHEL/CentOS及更新版本的内核中,该patch就已经被合并进来:

1
2
3
* Mon Aug 27 2012 Jarod Wilson <jarod@redhat.com> [2.6.32-303.el6]
...
- [mm] avoid swapping out with swappiness==0 (Satoru Moriya) [787885]

RHEL/CentOS 6.3 的内核版本是2.6.32-279,而 RHEL/CentOS 6.4 的内核为2.6.32-358,从这个版本开始,swappiness的行为就已经修改了,使用这个版本及之后版本的同志们需要特别注意一下。

其他分发版本的Linux(比如Debian,Ubuntu)的版本中,可以自己查阅一下,看看是什么时候合并的该patch。

解决的办法其实也很简单:

  1. 尽量保证Linux操作系统还有足够的内存。
  2. 最新的内核,建议把vm.swappiness设置1。
  3. 考虑设置 /proc/(es的pid)/oom_adj 为较小的值来尽量避免ES由于内存不足而被关闭。