ES性能优化


一、分片优化

1、分片概念

文档存储在分片中,然后分片分配到集群中的节点上。当集群扩容或缩小,Elasticsearch 将会自动在节点间迁移分片,以使集群保持平衡。

一个分片(shard)是一个最小级别“工作单元(worker unit)”,它只是保存了索引中所有数据的一部分。

分片可以是主分片(primary shard)**或者是复制分片(分片副本)(replica shard)**。

注意:索引建立后,主分片个数是不可以更改的,如需调整分片数,需要重建索引 reindex

ES默认为一个索引创建5个主分片, 并分别为其创建一个副本分片

curl -H "Content-Type: application/json" -XPUT localhost:9200/blogs -d '
{
    "settings": {
        "number_of_shards": 3,
        "number_of_replicas": 1
    }
}'

在索引建立时需要手动指定索引分片数和副本数

  • number_of_shards:主分片数
  • number_of_replicas:分片副本数

假设向 blogs 索引中插入15条数据 (15个文档),那么这15条数据会尽可能平均的分为5条存储在三个分片中,并且每个分片都有一个副本。

2、主分片

在一个多分片的索引中写入数据时,通过路由来确定具体写入哪一个分片中,大致路由过程如下:

shard = hash(routing) % number_of_primary_shards

routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。routing 通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到余数 。这个在 0 到 number_of_primary_shards 之间的余数,就是所寻求的文档所在分片的位置。

这解释了为什么要在创建索引的时候就确定好主分片的数量并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了

索引中的每个文档属于一个单独的主分片,所以主分片的数量决定了索引最多能存储多少数据(实际的数量取决于数据、硬件和应用场景)。

3、分片副本

复制分片只是主分片的一个副本,它可以防止硬件故障导致的数据丢失,同时可以提供读请求,比如搜索或者从别的 shard 取回文档

每个主分片都有一个或多个副本分片,当主分片异常时,副本可以提供数据的查询等操作。主分片和对应的副本分片是不会在同一个节点上的,所以副本分片数的最大值是 n -1(其中 n 为节点数)。

当索引创建完成的时候,主分片的数量就固定了,但是复制分片的数量可以随时调整,根据需求扩大或者缩小规模。如把复制分片的数量从原来的 1 增加到 2 :

curl -H "Content-Type: application/json" -XPUT localhost:9200/blogs/_settings -d '
{
    "number_of_replicas": 2
}'

分片本身就是一个完整的搜索引擎,它可以使用单一节点的所有资源。主分片或者复制分片都可以处理读请求——搜索或文档检索,所以数据的冗余越多,能处理的搜索吞吐量就越大。

对文档的新建、索引和删除请求都是写操作,必须在主分片上面完成之后才能被复制到相关的副本分片,ES 为了提高写入的能力这个过程是并发写的,同时为了解决并发写的过程中数据冲突的问题,ES 通过乐观锁的方式控制,每个文档都有一个 _version (版本)号,当文档被修改时版本号递增。一旦所有的副本分片都报告写成功才会向协调节点报告成功,协调节点向客户端报告成功。

4、如何确定分片数

确定索引分片数时,应考虑下面因素:

  • 1、每一个分片数据文件小于30GB
  • 2、每一个索引中的一个分片对应一个节点
  • 3、节点数大于等于分片数

5、分片使用优化

(1)案例场景

在一些复杂的应用场景中使用Elasticsearch,经常会遇到分片过多引发的一系列问题。起初在支撑内部某业务时,单集群内有约1000个子业务,大部分子业务保留31天的数据。如果每个子业务按天滚动建立Index,每个Index 5个分片、一主两从共三副本的情况下,集群内部会有多达45w~个分片。在集群内分片过多时,经常遇到下面这些问题:

  1. 创建分片慢:Elasticsearch创建分片的速度会随着集群内分片数的增加而变慢。以ES 5.5.2版本、3节点集群为例,在默认配置下,当集群分片数超过1w时,创建index的耗时一般在几十秒甚至以上。   
  2. 集群易崩溃:在凌晨触发Elasticsearch自动创建Index时,由于创建速度太慢,容易导致大量写入请求堆积在内存,从而压垮集群。
  3. 写入拒绝:分片过多的场景中,如果不能及时掌控业务变化,可能经常遇到单分片记录超限、写入拒绝等问题。

(2)解决方案

  1. 拆分集群 对于存在明显分界线的业务,可以按照业务、地域使用不同集群,这种拆分集群的思路是非常靠谱的。Elasticsearch官方建议使用小而美的集群,避免巨无霸式的集群,我们在实际使用过程中对这一点也深有体会。但对于我们的场景,已经按照地域拆分了集群,且同一地域的子业务间分界线不明显,拆分过多的集群维护成本较高。
  2. 调整滚动周期 根据保留时长调整index滚动周期是最简单有效的思路。例如保留3天的数据按天滚动,保留31天的数据按周滚动,保留一年的数据按月滚动。合理的滚动周期,可以在存储成本增加不大的情况下,大幅降低分片数量。 对于我们的场景,大部分数据保留31天,在按周滚动的情况下,集群的总分片数可以下降到6.5w~个。
  3. 合理设置分片数和副本数 集群内部除个别子业务压力较高外,大部分业务压力较小,合理设置单Index的分片数效果也不错。我们的经验是单个分片的大小在10GB30GB之间比较合适,对于压力非常小的业务可以直接分配1个分片。其他用户可结合具体场景考虑,同时注意单分片的记录条数不要超过上限2,147,483,519。 在平衡我们的业务场景对数据可靠性的要求 及 不同副本数对存储成本的开销 两个因素之后,我们选择使用一主一从的副本策略。 目前我们集群单Index的平均分配数为3,集群的总分片数下降到3w个。
  4. 分片分配流程优化 默认情况下,ES在分配分片时会考虑分片relocation对磁盘空间的影响。在分片数较少时,这个优化处理的副作用不明显。但随着单机分片数量的上升,这个优化处理涉及的多层循环嵌套过程耗时愈发明显。可通过cluster.routing.allocation.disk.include_relocations: false关闭此功能,这对磁盘均衡程度影响不明显。
  5. 预创建Index 对于单集群3w分片的场景,集中在每周某天0点创建Index,对集群的压力还是较大,且存储空间存在波动。考虑到集群的持续扩展能力和可靠性,我们采用预创建方式提前创建分片,并把按Index的创建时间均匀打散到每周的每一天。
  6. 持续调整分片数 对于集群分片的调整,通常不是一蹴而就的。随着业务的发展,不断新增的子业务 或 原有子业务规模发生突变,都需要持续调整分片数量。 默认情况下,新增的子业务会有默认的分片数量,如果不足,会在测试阶段及上线初期及时发现。随着业务发展,系统会考虑Index近期的数据量、写入速度、集群规模等因素,动态调整分片数量。

二、内存优化

1、ES数据存储在哪里?

ES数据可以存储在磁盘和内存中。默认情况下,ES会将数据存储在磁盘上,使用lucene搜索引擎来实现数据的索引和搜索。但是,ES也提供了一些内存级别的缓存,如filter cache和field data cache来提高搜索性能。此外,ES还提供了基于内存的搜索引擎,如Elasticsearch in-memory engine (EIM)等,可以将数据完全存储在内存中,以提供更快的搜索响应时间。

2、堆内存设置建议

默认情况下,ES JVM使用堆内存最小和最大大小为2GB(5.X版本以上)。可以在jvm.options 中配置

  • 将最小堆大小(Xms)和最大堆大小(Xmx)设置为彼此相等。

  • ES可用的堆越多,可用于缓存的内存就越多。但请注意,太多的堆内存可能会使得我们长时间垃圾收集暂停。

  • 将Xmx设置为不超过物理内存的50%,以确保有足够的物理内存留给内核文件系统缓存。

  • 不要将Xmx设置为JVM超过32GB

  • 一般取宿主机内存大小的一半和31GB中的,两个值的最小值。

3、堆内存为什么不能超过物理机内存的一半

堆对于Elasticsearch绝对重要。
它被许多内存数据结构用来提供快速操作。但还有另外一个非常重要的内存使用者:Lucene。

Lucene旨在利用底层操作系统来缓存内存中的数据结构。 Lucene段(segment)存储在单个文件中。因为段是一成不变的,所以这些文件永远不会改变。这使得它们非常容易缓存,并且底层操作系统将愉快地将热段(hot segments)保留在内存中以便更快地访问。这些段包括倒排索引(用于全文搜索)和文档值(用于聚合)。

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

标准建议是将可用内存的50%提供给Elasticsearch堆,而将其他50%空闲。它不会被闲置; Lucene会高兴地吞噬掉剩下的东西。

如果您不在字符串字段上做聚合操作(例如,您不需要fielddata),则可以考虑进一步降低堆。堆越小,您可以从Elasticsearch(更快的GC)和Lucene(更多内存缓存)中获得更好的性能。

4、堆内存为什么不能超过32GB

在Java中,所有对象都分配在堆上并由指针引用。普通的对象指针(OOP)指向这些对象,传统上它们是CPU本地字的大小:32位或64位,取决于处理器。

对于32位系统,这意味着最大堆大小为4 GB。对于64位系统,堆大小可能会变得更大,但是64位指针的开销意味着仅仅因为指针较大而存在更多的浪费空间。并且比浪费的空间更糟糕,当在主存储器和各种缓存(LLC,L1等等)之间移动值时,较大的指针消耗更多的带宽。

Java使用称为压缩oops的技巧来解决这个问题。而不是指向内存中的确切字节位置,指针引用对象偏移量。这意味着一个32位指针可以引用40亿个对象,而不是40亿个字节。最终,这意味着堆可以增长到约32 GB的物理尺寸,同时仍然使用32位指针。

一旦你穿越了这个神奇的〜32 GB的边界,指针就会切换回普通的对象指针。每个指针的大小增加,使用更多的CPU内存带宽,并且实际上会丢失内存。实际上,在使用压缩oops获得32 GB以下堆的相同有效内存之前,需要大约40-50 GB的分配堆。

以上小结为:即使你有足够的内存空间,尽量避免跨越32GB的堆边界。
否则会导致浪费了内存,降低了CPU的性能,并使GC在大堆中挣扎。

5、禁用交换分区

Swapping 是性能的坟墓:在选择 ES 服务器时,要尽可能地选择与当前应用场景相匹配的服务器。

  • 如果服务器配置很低,则意味着需要更多的节点,节点数量的增加会导致集群管理的成本大幅度提高。

  • 如果服务器配置很高,而在单机上运行多个节点时,也会增加逻辑的复杂度。

在计算机中运行的程序均需在内存执行,若内存消耗殆尽将导致程序无法进行。为了解决这个问题,操作系统使用一种叫作虚拟内存的技术。

当内存耗尽时,操作系统就会自动把内存中暂时不使用的数据交换到硬盘中,需要使用的时候再从硬盘交换到内存。

如果内存交换到磁盘上需要 10 毫秒,从磁盘交换到内存需要 20 毫秒,那么多的操作时延累加起来,将导致几何级增长。

不难看出 Swapping 对于性能是多么可怕。所以为了使 ES 有更好等性能,强烈建议关闭 Swap。

关闭 Swap 的方式如下:

①暂时禁用。

如果我们想要在 Linux 服务器上暂时关闭,可以执行如下命令,但在服务器重启后失效:

sudo swapoff -a

②永久性关闭。

我们可以修改 /etc/sysctl.conf(不同的操作系统路径有可能不同),增加如下参数:

vm.swappiness = 1 //0-100,则表示越倾向于使用虚拟内存。

注意:Swappiness 设置为 1 比设置为 0 要好,因为在一些内核版本,Swappness=0 会引发 OOM(内存溢出)。

Swappiness 默认值为 60,当设置为 0 时,在某些操作系统中有可能会触发系统级的 OOM-killer,例如在 Linux 内核的内存不足时,为了防止系统的崩溃,会自动强制 Kill 一个“bad”进程。

③在 ES 中设置。

如果上面的方法都不能做到,你需要打开配置文件中的 mlockall 开关,它的作用就是运行 JVM 锁住内存,禁止 OS 交换出去。

elasticsearch.yml 配置如下:

bootstrap.mlockall: true

三、脑裂现象

数据节点node.data: true,负责数据的存储和相关的操作,例如对数据进行增、删、改、查和聚合等操作,所以数据节点(data节点)对机器配置要求比较高,对CPU、内存和I/O的消耗很大。通常随着集群的扩大,需要增加更多的数据节点来提高性能和可用性。

候选主节点node.master: true,可以被选举为主节点(master节点),集群中只有候选主节点才有选举权和被选举权,其他节点不参与选举的工作。主节点负责创建索引、删除索引、跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点、追踪集群中节点的状态等,稳定的主节点对集群的健康是非常重要的。

一个节点既可以是候选主节点也可以是数据节点,但是由于数据节点对CPU、内存核I/0消耗都很大,所以如果某个节点既是数据节点又是主节点,那么可能会对主节点产生影响从而对整个集群的状态产生影响。

因此为了提高集群的健康性,我们应该对Elasticsearch集群中的节点做好角色上的划分和隔离。可以使用几个配置较低的机器群作为候选主节点群。

主节点和其他节点之间通过Ping的方式互检查,主节点负责Ping所有其他节点,判断是否有节点已经挂掉。其他节点也通过Ping的方式判断主节点是否处于可用状态。

虽然对节点做了角色区分,但是用户的请求可以发往任何一个节点,并由该节点负责分发请求、收集结果等操作,而不需要主节点转发,这种节点可称之为协调节点,协调节点是不需要指定和配置的,集群中的任何节点都可以充当协调节点的角色。

脑裂现象

同时如果由于网络或其他原因导致集群中选举出多个Master节点,使得数据更新时出现不一致,这种现象称之为脑裂,即集群中不同的节点对于master的选择出现了分歧,出现了多个master竞争。

“脑裂”问题可能有以下几个原因造成:

  1. 网络问题:集群间的网络延迟导致一些节点访问不到master,认为master挂掉了从而选举出新的master,并对master上的分片和副本标红,分配新的主分片
  2. 节点负载:主节点的角色既为master又为data,访问量较大时可能会导致ES停止响应(假死状态)造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。
  3. 内存回收:主节点的角色既为master又为data,当data节点上的ES进程占用的内存较大,引发JVM的大规模内存回收,造成ES进程失去响应。

为了避免脑裂现象的发生,我们可以从原因着手通过以下几个方面来做出优化措施:

  1. 适当调大响应时间,减少误判通过参数 discovery.zen.ping_timeout设置节点状态的响应时间,默认为3s,可以适当调大,如果master在该响应时间的范围内没有做出响应应答,判断该节点已经挂掉了。调大参数(如6s,discovery.zen.ping_timeout:6),可适当减少误判。
  2. 选举触发我们需要在候选集群中的节点的配置文件中设置参数 discovery.zen.munimum_master_nodes的值,这个参数表示在选举主节点时需要参与选举的候选主节点的节点数,默认值是1,官方建议取值 (master_eligibel_nodes/2)+1,其中 master_eligibel_nodes为候选主节点的个数。这样做既能防止脑裂现象的发生,也能最大限度地提升集群的高可用性,因为只要不少于discovery.zen.munimum_master_nodes个候选节点存活,选举工作就能正常进行。当小于这个值的时候,无法触发选举行为,集群无法使用,不会造成分片混乱的情况。
  3. 角色分离即是上面我们提到的候选主节点和数据节点进行角色分离,这样可以减轻主节点的负担,防止主节点的假死状态发生,减少对主节点“已死”的误判。

网络异常可能会导致集群中节点划分出多个区域,区域发现没有 Master 节点的时候,会选举出了自己区域内 Maste 节点 r,导致一个集群被分裂为多个集群,使集群之间的数据无法同步,我们称这种现象为脑裂。

为了防止脑裂,我们需要在 Master 节点的配置文件中添加如下参数:

discovery.zen.minimum_master_nodes=(master_eligible_nodes/2)+1 //默认值为1

其中 master_eligible_nodes 为 Master 集群中的节点数。这样做可以避免脑裂的现象都出现,最大限度地提升集群的高可用性。

只要不少于 discovery.zen.minimum_master_nodes 个候选节点存活,选举工作就可以顺利进行。

四、elasticsearch配置

elasticsearch.yml 部分可用配置项如下

# 配置 ES 的集群名称
cluster.name: elasticsearch

# 集群中的节点名,在同一个集群中不能重复。节点的名称一旦设置,就不能再改变了。当然,也可以设置成服务器的主机名称,例如 node.name:${HOSTNAME}。
node.nam: node1

# 指定该节点是否有资格被选举成为 Master 节点,默认是 True,如果被设置为 True,则只是有资格成为 Master 节点,具体能否成为 Master 节点,需要通过选举产生。
noed.master: true

# 指定该节点是否存储索引数据,默认为 True。数据的增、删、改、查都是在 Data 节点完成的。
node.data: true

# 设置默认索引分片个数,默认是 5 片。也可以在创建索引时设置该值,具体设置为多大都值要根据数据量的大小来定。如果数据量不大,则设置成 1 时效率最高。
index.number_of_shards: 5

# 设置默认的索引副本个数,默认为 1 个。副本数越多,集群的可用性越好,但是写索引时需要同步的数据越多。
index.number_of_replicas: 1

# 设置配置文件的存储路径,默认是 ES 目录下的 Conf 文件夹。建议使用默认值。
path.conf: /path/to/conf

# 设置索引数据多存储路径,默认是 ES 根目录下的 Data 文件夹。切记不要使用默认值,因为若 ES 进行了升级,则有可能数据全部丢失。可以用半角逗号隔开设置的多个存储路径,在多硬盘的服务器上设置多个存储路径是很有必要的。
path.data: /path/to/data1,/path/to/data2

# 设置日志文件的存储路径,默认是 ES 根目录下的 Logs,建议修改到其他地方。
path.logs: /path/to/logs

# 设置第三方插件的存放路径,默认是 ES 根目录下的 Plugins 文件夹。
path.plugins: /path/to/plugins

# 设置为 True 时可锁住内存。因为当 JVM 开始 Swap 时,ES 的效率会降低,所以要保证它不 Swap。
bootstrap.mlockall: true

# 设置本节点绑定的 IP 地址,IP 地址类型是 IPv4 或 IPv6,默认为 0.0.0.0。
network.bind_host: 192.168.0.1

# 设置其他节点和该节点交互的 IP 地址,如果不设置,则会进行自我判断。
network.publish_host: 192.168.0.1

# 用于同时设置 bind_host 和 publish_host 这两个参数。
network.host: 192.168.0.1

# 设置对外服务的 HTTP 端口,默认为 9200
http.port: 9200

# 设置集群内部的节点间交互的 TCP 端口,默认是 9300
transport.tcp.port: 9300

# 设置在节点间传输数据时是否压缩,默认为 False,不压缩。
transport.tcp.compress: true

# 设置在选举 Master 节点时需要参与的最少的候选主节点数,默认为 1。如果使用默认值,则当网络不稳定时有可能会出现脑裂。建议:主节点数/2 + 1
discovery.zen.minimum_master_nodes: 1

# 设置在集群中自动发现其他节点时 Ping 连接的超时时间,默认为 3 秒。
# 在较差的网络环境下需要设置得大一点,防止因误判该节点的存活状态而导致分片的转移。
discovery.zen.ping.timeout: 3s

  目录