ES深入学习第一课

1、 什么是分片分配 (shard allocation)

分片分配 (shard allocation),是指在索引创建、副本增减、节点增减、分片重平衡等过程将索引分片落实到实际的物理节点的操作,包括但不限于:

平衡集群内各节点的分片数量,减少集群内的热节点

控制分片迁移,比如限制迁移动作占用的集群资源,避免大量分片迁移造成集群负载飙升

感知在节点物理分布,限制主从分片不落在同一机房、机架或宿主机

隔离不同索引的分片到不同的物理节点,用来消除索引间资源竞争带来的性能抖动

将待下线节点所属的分片预先迁移,用来避免节点突然下线造成的服务抖动

利用索引生命周期管理针对时序数据自动将冷热索引的分片分配到对应的冷热节点

分片分配一方面是为了提高系统的可用性,如当集群中一台机器宕机使得该节点上的分片不可用时,分布在其他机器上的分片能通过重新选举继续工作 (但是仍要保证同一分片的主从副本不全在宕机节点上) ;另一方面是为了提高系统的容量和读写性能,如通过增加节点横向扩容,将集群中部分分片 rebalance 到新节点,既可以利用新节点的存储容量提升索引存储容量,迁移过来的分片又可以利用新节点增加的算力提供服务。比如控制热索引的分片只分配在标记出的高配机器,避免热点数据落入低配机器导致的木桶效应,当热索引性能不足时只需横向扩容高配节点而不必担心冷数据分片侵入;或者隔离商品、订单索引的分片到不同的节点,避免商品索引的大规模重建导致订单索引性能抖动等。

从调控粒度来分,分片分配可以分为集群维度的分配和索引维度的分配两种:

集群维度:粒度较大,对所有分片视为等同,不区分分片来自哪个索引,倾向于平衡集群各节点的负载来保证集群整体的稳定性,比如平衡各节点分片数、机架感知、控制分片迁移数量等;

索引维度:粒度较小,通常针对具体的单个索引调整,倾向于保证目标索引占用的资源最优化,比如隔离不同业务索引、时序数据的按周期滚动迁移等。

通常分片分配会混合使用集群维度和索引维度的分配策略来保证单索引和整体集群的资源利用率最优化,以商品、订单检索业务为例,如果两者混布在一个集群中,理想情况下我们期望:

  1. 商品索引任何操作不影响订单索引的服务,包括不限于性能表现、稳定性等
  2. 不能出现1+1<2的情况,也就是索引的负载要均匀分布在各节点,不能出现扩容节点后负载仍旧集中于少数几个节点的情况

这时可以通过 cluster.routing.allocation.total_shards_per_node index.routing.allocation.total_shards_per_node

控制各节点的分片数量,使集群将分片相对均匀分配到各节点,并利用index.routing.allocation.include.{attributes}将不同业务的索引限定到不同的节点分组。

对节点进行分组,使用的是 节点属性。

1.1 什么是节点属性 (node attributes)

总结一下分片分配过程,可以分为几个步骤:

标记节点:通过不同的属性将节点划分为不同的组
创建/修改/删除索引:向集群内添加/删除分片,比如新增索引、修改副本数、删除索引等
分片分配:master 节点根据当前的分片数以及分片分配策略重新规划分片分配并发动分片迁移达到新的平衡状态

那么第一步,集群内的各节点是如何被识别和标记的呢?

答案是节点属性 (node attributes),包括内置属性自定义属性两种。

Elasticsearch 会将常见用于区分不同机器的标记,如主机名 (_host)、IP 地址 (_ip)、节点名称 (_name) 等作为内置属性供分片分配时区分节点的标记使用,具体包括:

_name: 节点名称,即在 elasticsearch.yml 中定义的 node.name 属性

_host_ip、_publish_ip、_ip: 节点的 IP 地址,一般情况下使用 _ip 即可,具体含义可以查阅官方帮助文档

_host: 主机名

_id: 集群为节点自动分配的唯一标识符,手动调控时使用较少

_tier: 节点的数据角色,比如存储冷热数据的 data_cold、data_hot 等,可以在 elasticsearch.yml 中指定 node.roles 属性

内置属性 可以用来区分不同节点,但是对于将节点划分成组来说不是很便利,比如我们希望将索引分配到高配节点,如果使用内置属性,比如 _ip 的话,需要在索引设置 index.routing.allocation.include._ip 中指定多个机器的 IP 地址,在集群增删高配节点的情况下需要同时调整对应索引的分片分配设置,显得不太便捷。我们很自然的希望除了内置属性之外,还可以根据机器配置的高低、是否同属于一个网段等情况来标记节点,方便我们将不同类型的节点划分为一个个组,继而将索引的分配配置到节点组,这样在集群增删节点时,只要节点配置了对应的组,集群就会根据对应组的节点变化自动的将分片重新调整,而不需要我们再手动的同步每一个索引的分配设置。
自定义节点属性 解决的便是这个问题,我们可以通过:

elasticsearch.yml 中新增配置项,如node.attr.zone=zone1或在启动命令中增加变量,如bin/elasticsearch -Enode.attr.zone=zone1
的方式来为节点增加自定义属性。

1.2 小结

分布式架构为我们带来了众多容量、性能和可用性方面的优势,但是相应的也提高了保障难度,因为分片在集群自动分配的情况下不一定能达到我们期望的”平衡”状态,需要我们对分片分配机制有较高的掌握程度来调控集群内分片的分配状况,比如主从副本不分布在同一物理节点、解决节点数据倾斜导致新分片集中于某几台负载较低节点的热点问题等。

2、调控分片分配

Elasticsearch集群中 master 节点的一项重要功能就是决定分片如何以最佳的方式均衡分布到集群内的各个节点上,除了自动分配之外,我们也可以从粗粒度的集群维度和细粒度的索引维度手动调控分片在各节点的分配。集群维度 (cluster level) 的分片分配是将所有分片纳入一起考虑,不会单独考虑某个索引的分片分配情况。举个例子,我们有两个索引,每个索引包含两个分片,将分片分配到两个节点组成的集群,则状态一 (同一个索引的全部分片分配到同一个节点):

1
2
3
4
{
"node_1": ["index_1_shard_1", "index_1_shard_2"],
"node_2": ["index_2_shard_1", "index_2_shard_2"]
}

以及状态二 (同索引的分片均匀分配到各节点):

1
2
3
4
{
"node_1": ["index_1_shard_1", "index_2_shard_2"],
"node_2": ["index_2_shard_1", "index_1_shard_2"]
}

从集群维度都可以被认为是”平衡”状态,但是从实际角度看,只有状态二是我们期望的平衡状态,因为如果 index_1 和 index_2 负载不均,在状态一下很可能导致集群内节点负载不均,使得服务整体表现不能达到预期。

要达到我们期望的状态二,就可以使用索引维度 (index level) 的分片分配控制方法,比如通过 index.routing.allocation.total_shards_per_node 参数控制每个节点的分片数量为 1 就可以达到我们的目的了。

2.1 集群维度分片分配

从集群层面来说,分片分配控制的是集群内各索引的分片集合在各节点的分布,并且在分片分配过程中添加一些硬性限制以控制集群负载在合理范围内。
以一个实际操作中可能会遇到的情况来说,公司准备新上一批业务,需要用到大规模的数据检索功能,自然的,首先需要处理搭建集群的任务,在开始搭建集群之前,让我们先看看实际的业务场景。
系统用于收集应用打点日志,提供一周内数据供查询。

2.1.1 分片平衡的启发式参数

日志类型数据存储,带有非常明显的时效性,一般情况下当日数据的读写频繁,非当日数据几乎不会有写操作,离当前时间越久的数据读操作也越少。
通过上述判断,可以认为索引的分片会平均分布于集群内各节点越好,因为可以充分利用全部节点的算力来分摊当日数据的高频读写负载。为了达到这个目的,我们可以通过 ES 提供的部分启发式参数,让 master 在决策分片如何分配时更多的向我们期望的方向考虑:

cluster.routing.allocation.balance.shard 节点中分片总数对权重的影响因子,默认为 0.45,该值越大则各节点的分片数越趋向于相等。
cluster.routing.allocation.balance.index 节点中来自不同索引的分片数对权重的影响因子,默认为 0.55,该值越大则各索引的分片更倾向于均匀分配到各节点。
cluster.routing.allocation.balance.threshold 默认为 1.0,该值越大则集群对不平衡状态的容忍程度越高。
我们可以适当放大 cluster.routing.allocation.balance.index 的权重来使得集群在分配分片时更倾向于将一个索引的不同分片均匀分布到各个节点,不过需要注意的是这只是一个启发式参数,更多的是”建议”,而不是”命令”,要完全达到我们的期望,还需要借助索引维度的分配调控手段。

2.1.2 分片迁移的流量控制

回到本节起始提到的日志业务,系统上线运行一段时间后,随着索引量的不断增加,我们需要适时的清理掉过期数据,清理过程中自然的会删除过期数据所在的索引,释放存储空间供新的索引使用。在集群删除索引时,因为集群内分片总数发生了变化,自然的分片在各节点的分配状态也随之发生变化,可能会出现分片的”不平衡”状态,这时,默认情况下集群会自动触发分片的重平衡操作,将分片在各节点间适当的迁移以使得分片在集群重新达到”平衡”状态。在日志类数据情况下,单个分片包含的数据量可能会较大,达到若干 GB,这样在分片发生迁移时,发生分片迁移的节点必然会触发大量的 IO 操作,为了避免大量的 IO 操作对节点造成冲击,使得集群服务发生抖动,我们可以通过分片迁移的流量控制参数进行干预:

cluster.routing.allocation.node_concurrent_incoming_recoveries用于控制可同时在一个节点上进行初始化或恢复的最大分片数,默认为2,设置过大可能导致节点负载过高 (同时写入大量数据),调整时需要考虑节点的硬件配置。
cluster.routing.allocation.node_concurrent_outgoing_recoveries用于控制该节点可同时为其他节点分片恢复或迁移提供数据源的最大分片数,默认为2,调整同理。

注1: 从节点的角度,分片会出现两种流向:流入 和 流出,其中,流入是指来自某个索引的分片新落入到该节点,流出是指该在其他节点的分片以该节点所属的分片为数据源进行副本恢复或者数据迁移。
注2: 如果不希望集群自动平衡导致的分片迁移,可以通过 cluster.routing.rebalance.enable 关闭,自动平衡相关的其他配置可以查阅官方文档。

2.1.3 节点的水位线

随着打点应用的接入越来越多,单日的日志索引量上涨迅速,节点的磁盘水位吃紧,我们希望新的分片在分配时能考虑到节点存储容量的状态,避免将新分片分配到磁盘容量快满的节点。

这个情况下如果要自行通过节点属性来调控,至少需要:

自动监测磁盘水位并为节点打上 low/medium/high 的属性,而且更改属性还需要重启节点;
为新分配的索引设置 index.routing.allocation.require.* 属性来让索引避开高水位节点;
在节点的磁盘水位属性变更时自动为集群内的索引更新 allocation 配置来避免自动平衡。

看上去就很麻烦,那么有没有简便方法呢?
答案是使用内置的水位 cluster.routing.allocation.disk.watermark.* 属性。

水位限制分为高低两种,其中:

cluster.routing.allocation.disk.watermark.low低水位,默认为磁盘容量的85%,ES 会避免将分片分布至磁盘容量超过低水位的节点。
但是新创建索引的主分片 (primary shards)仍然可以分配到超过低水位的节点。

cluster.routing.allocation.disk.watermark.high高水位,默认为磁盘容量的90%,ES 会将磁盘容量超过高水位节点上的分片迁移至其他节点。

cluster.routing.allocation.disk.watermark.flood_stage警戒水位,默认为磁盘容量的95%,当节点磁盘容量超过警戒水位时,该节点所属分片所在的索引都会被执行写禁止操作,即索引变为只读状态。比如 A 索引的 shard 1 分布在 N1 节点,shard 2 分布在 N2 节点,如果 N1 节点磁盘容量超过警戒水位,索引 A 即被执行写禁止操作,成为只读索引。

除了使用百分比表示磁盘水位外,也可以使用容量绝对值来表示高低水位,如
cluster.routing.allocation.disk.watermark.low: 500mb 表示避免将分片分布到磁盘容量低于500mb的节点。
但是容量绝对值和百分比不能混用,比如指定了磁盘低水位为 500mb,则高水位相应的也必须使用绝对值表示。
索引的增删改都会对所在节点的磁盘水位产生影响,为了动态的感知磁盘水位,相应的就有了水位采集参数:

cluster.info.update.interval 磁盘水位采集频率,即每隔多久去检查一次磁盘用量,默认为 30 秒。

cluster.routing.allocation.disk.include_relocations 是否将正在迁移到当前节点的分片磁盘用量 (将占用的磁盘空间) 计入当前节点的磁盘用量,默认打开。

这里有个坑,比如分片 A 正从节点 N1 迁移往节点 N2,在打开
cluster.routing.allocation.disk.include_relocations 参数的情况下,分片 A 的磁盘用量会在 N1 和 N2 节点都被计算一次,也就是 N1 节点的空间其实被多算了。

2.1.3.1 热点问题

虽然根据实际数据更替情况合理配置了节点的高低水位,但是随着时间推移,我们发现集群发生了热点数据倾斜问题,由于冷数据占用了大量的存储空间,导致热点数据 (当日新创建的索引) 被迫分配到空间用量相对较少的几个节点,使得集群的负载不均。针对日志服务等索引频繁创建、删除的场景,数据带有明显的时效性,可以考虑集群分组,对冷热数据使用不同的分配标记 (allocation attributes) 来隔离冷热数据。或者使用(data tier allocation),目的是避免访问较少的冷数据占用磁盘容量,导致集群将新创建的索引分配到少数几个”看起来比较合适”的节点,导致热点问题出现。

Elasticsearch 新版本中已经将类似的功能集成为 data_tier 插件,详见下一小节。

针对索引创建、删除不频繁的场景,比如电商后端常见的商品搜索、订单搜索等,一方面可以考虑将集群节点分组或者部署多个集群,将不同业务进行资源隔离,另一方面创建索引时需要考虑到未来数据量的增长情况以设置合理的分片数量,将分片尽量均匀分配到每个节点以更合理的利用节点硬件资源;一般来说,商品、订单等业务数据长尾效应比较明显,针对热点的店铺、类目等引起的数据倾斜问题,可以将热点数据单独拆出一个索引,配合前端的引擎代理将请求路由到对应的索引。

2.1.4 管理节点

系统资源吃紧,需要通过横向扩容增加集群容量。

2.1.4.1 节点扩/缩容

当向集群扩容节点时,其他节点会迁移部分分片到新节点,如果并发迁移的分片过多,可能造成瞬时的高 IO 负载,引起服务抖动,我们可以通过
cluster.routing.allocation.node_concurrent_incoming_recoveries 参数控制分片迁移的速度,如果是在集群负载较高的情况下横向扩容新节点,建议分开两步操作:

cluster.routing.rebalance.enable设置为none关闭分片自动平衡 (主要是考虑到分片移出后可能会引起集群重平衡操作)。通过手动对目标索引进行 index.routing.allocation.include 配置将新节点纳入到分片的分布范围,逐个迁移索引分片。目的是减少大规模持续的分片迁移导致集群负载继续升高,甚至发生雪球效应。

同样的,在缩容集群时,如果直接关闭节点,可能存在两个风险点:

在集群规模较大情况下会有大量索引同时进行分片主从切换和分片重新分配操作,瞬时对 master 节点带来很大的负载,尤其是日志类数据的大集群,因为分片数较多,可能导致 master 节点 CPU 飙高,使得新创建索引等操作被阻塞;小概率情况下如果其他节点发生意外宕机,索引将存在数据丢失风险。对于类似缩容的存在一定风险性的主动操作,建议与扩容类似,首先设置全部索引的
index.routing.allocation.exclude或者直接在集群范围内设置cluster.routing.allocation.exclude属性将待下线的节点排除,待分片全部移出之后再关闭节点进程。

2.1.4.2 节点重启

除了横向扩容外,对节点纵向扩容,或者升级 Elasticsearch 版本,都需要对节点进行重启操作。

在重启节点时,自然的会有节点的上下线操作,节点下线同时会让该节点所属的分片处于 unassigned 状态,正常情况下集群会将这些未分配分片重新分配到集群内其他节点上,在日常运维的节点重启操作中,这显然不是我们期望的,无端的带来了大量的 IO 操作。

正常的滚动重启操作中,建议是:

  1. 通过 cluster.routing.allocation.enable: none 关闭分片分配;
  2. 重启节点;
  3. cluster.routing.allocation.enable 重置为 all 打开分片分配;

2.1.5 调控分片的物理分配

因为日志检索方面表现良好,团队决定将商品、订单等系统的检索功能也迁移到 Elasticsearch 集群,并提供了高配机器用于集群搭建。

2.1.5.1 分片在单台物理机的分配

在实际的部署过程中有时会遇到大容量高配机器,比如 32 核 128GB 内存,我们可以考虑单节点部署,将堆内存数据量扩大到 32GB 以上 (一个是对象指针压缩技术不再可用,造成内存空间膨胀,另一个是堆内存回收压力也增大,可能造成 gc 停顿时间变长);也可以考虑单机多节点的部署方式,在这种情况下,为了数据可用性的考虑,索引内同一分片的主副本数据需要分配到不同的物理节点上,这时可以使用如下参数:

cluster.routing.allocation.same_shard.host阻止同一分片的多个实例 (主副本) 落到同一个主机,同一主机的判定条件为相同的主机名 (host name) 和主机地址 (host address),默认情况下该参数为关闭状态,强烈建议在单机多节点部署的情况打开该配置,避免可能的数据丢失风险。对商品、订单的搜索场景,一般单节点的负载会控制在最大值的 50% 左右,以提供足够的余量来承载瞬时的流量高峰,为了达到这个目的,在分片分配层面,可以设置单节点的分片数上限:cluster.routing.allocation.total_shards_per_node单节点最多能支撑的分片数,当节点包含的分片数高于该数值时,新分片不会在该节点创建。

2.1.5.2 分片在机房内的分配

为了数据高可用的考虑,我们在管理集群的时候可能会考虑如索引的主从分片不要都落到某一台宿主机,或者同一个机架等问题,这个时候可以通过一些提示性参数让集群在选择节点时有一定的倾向性:

cluster.routing.allocation.awareness.attributes设置分片分布时会考虑将分布交叉分布到属性不同的节点上,比如集群包含两个节点 N1(node.attr.zone=zone1)、N2(node.attr.zone=zone2),如果我们在 elasticsearch.yml 中设置
cluster.routing.allocation.awareness.attributes: zone 则我们新建带一个副本的索引时,集群会将同一分片的主副本交叉分布在不同的节点上。节点属性可通过上述自定义属性方式设置。分片如何分配是由 master 节点决定的,因此我们设置 cluster.routing.allocation.awareness.attributes 属性时只需要在 master eligible 节点设置即可。

cluster.routing.allocation.awareness.force.zone.values假设集群节点仍然是 N1、N2,如果 N2 因为故障宕机,默认情况下,N2 所属的分片会在 N1 节点重新恢复出来,但是在同一个节点运行相同分片的主副本并没有实际意义,这时我们可以设置
cluster.routing.allocation.awareness.force.zone.values:zone1,zone2来避免这种操作,集群会在 zone2 属性节点恢复后再将相应的副本分片恢复到该节点。

2.1.5.3 集群分片上限

系统跑起来了,那么集群最多能负载的分片数是多少呢?
对日志类的数据,通常会以应用、时间、日志级别等多个维度建立索引,这样一来集群内的总分片数变得相当可观,那么集群最多能负载的分片数是多少呢?

答案也很简单,就是节点数与单个节点能最多能支撑的分片的积,但是这里的单节点最多支撑的分片数:

cluster.max_shards_per_node 用于限制整个集群最多能支撑的分片数,当集群内活跃分片数大于
cluster.max_shards_per_node * number_of_data_node 集群会阻止新索引的创建,直到有索引被删除或者关闭 (closed) 使得活跃分片总数低于阈值。
这里的活跃分片数是指非 closed 状态的分片,包括 unassigned / initializing / relocating / started。不会像
cluster.routing.allocation.total_shards_per_node 真正的限制单节点的分片数。

2.1.6 小结

我们调整或干预集群的分片参数,从根本上说是为了在集群稳定的情况下将性能最大化,从分片分配 (allocation) 的角度来说,稳定意味着分片尽量少的移动,性能意味着同一个索引的分片尽量均匀分布到各节点,而不要集中到少数几个节点。

2.2 索引维度分片分配

如同设计系统需要先从架构角度考虑系统模块设计,再细化到每一个应用设计应用自身的功能模块,索引分片的调整也需要先从集群角度设定大的分配策略导向,如节点分布 (shard awareness)、平衡 (shard rebalance) 等,再细化到每一个索引考虑单索引分片如何调整以达到最佳的表现。
再以上一节中的商品、订单检索系统为例,因为这分属两个子系统,符合我们预期的理解是即使商品检索系统负载很高,也不应该影响订单系统的检索耗时。

2.2.1 隔离不同索引

对不同业务所属索引进行物理隔离,实现 A 索引在执行大负载操作时不会对 B 产生影响,前提是使用节点属性将集群按照需要分割为多个群组。

比如集群包含以下设置的节点:

节点、ip、属性1、属性2
Node A

10.100.0.1

node.attr.zone=zone1

node.attr.size=medium

Node B

10.100.0.2

node.attr.zone=zone2

node.attr.size=medium

Node C

10.100.0.3

node.attr.zone=zone3

node.attr.size=large

之后,通过 index.routing.allocation.* 配置可以启用索引的分片控制,具体的:
index.routing.allocation.include.{attribute}将索引分片分配到包含任一指定属性的节点上。
{attribute} 可以指定为节点内置属性,如 _ip、_host 等,也可以指定为自定义属性,如 zone 等,也可以混用。index.routing.allocation.require.{attribute}将索引分片分配到指定节点上,节点必须包含指定的全部属性。index.routing.allocation.exclude.{attribute}不要将索引分配到包含任一指定属性的节点上。
如果设置索引的分片控制参数为:

1
2
3
4
{
"index.routing.allocation.include.zone": "zone1,zone2",
"index.routing.allocation.include._ip": "10.100.0.*"
}

则索引分片可以分配到全部三个节点,因为 include 指定的属性只需要满足其中之一即可,也就是只要节点的 zone 属性为 zone1 或 zone2,或者节点的 ip 地址匹配 10.100.0.* 都被认为是可用节点 (注意配置属性时可以支持通配符);
如果参数设置为:

1
2
3
4
{
"index.routing.allocation.require.zone": "zone1,zone2",
"index.routing.allocation.require.size": "medium"
}

则索引分片只能被分配到 Node A 和 Node B,因为只有这两个节点同时满足属性 zone 的值为 zone1 或 zone2,且 size 属性值为 medium;
如果参数设置为:

1
2
3
{
"index.routing.allocation.exclude._ip": "10.100.0.3"
}

则索引分片不会被分配到 Node C。

2.2.1.1 排查分片分配

当我们在创建索引后发现索引分片不能被正常分配时,可以通过 explain 接口来查看原因,如下:
curl -XGET ‘{host:port}/_cluster/allocation/explain’
在 response 中可以看到具体分片未被正常分配的原因,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"index": "test_v1",
"current_state": "unassigned",
"unassigned_info":
{
"reason": "INDEX_CREATED",
"last_allocation_status": "no"
},
"can_allocate": "no",
"allocate_explanation": "cannot allocate because allocation is not permitted to any of the nodes",
"node_allocation_decisions":
[
{
"node_decision": "no",
"deciders":
[
{
"decider": "filter",
"decision": "NO",
"explanation": "node does not match index setting [index.routing.allocation.require] filters [node:\"xxx\",_ip:\"1.1.1.1\"]"
}
]
}
]
}

表示因为没有节点能够同时满足 node.attr.node: xxx 且 IP 地址为 1.1.1.1 的节点 (response内容适当删减了非必要信息)。

2.2.2 平衡索引分片在各节点的分配

在索引按业务分组隔离之后,以商品检索为例,后续又追加了商品评价索引,用于存放商品的评价记录,由于用量较低,我们希望将分组内机器资源主要用来承载商品检索服务。不考虑分片数据倾斜的问题,即每个分片的负载一致,我们可以将索引的分片数 (主分片和副本分片的总和) 设置为与节点个数一致,并通过设置索引分片在各节点的分配个数来强迫索引在各节点间均衡分配。

要控制索引在单个节点的数量,可以通过 index.routing.allocation.total_shards_per_node 参数设置,比如现有 6 个节点,我们可以将索引的分片数 index.number_of_shards 设置为 3,副本数 index.number_of_replicas 设置为 2,同时将索引在每个节点上的最大分片数 index.routing.allocation.total_shards_per_node 设置为 1,即可以保证索引在每个节点分配一个分片,充分利用每个节点的算力。

虽然集群会倾向于将索引的各个分片铺开到集群内各个节点,然而实际情况下,比如节点水位限制、分片平衡因子等因素可能导致索引分片扎堆到某几个负载较低的节点,看上去节点容量更加均衡,但是对于业务来说,大规模的 ES 集群可能被用成了小规模的子集群,看上去节点众多,实际性能表现很差,这个时候我们就可以对索引的单节点分片总数做个限制,默认情况下是无限制的,也就是单索引的全部分片可以落在 number_of_replicas 台机器上 (因为单索引的主副分片不会同时落到一个节点上)。

2.2.3 冷热隔离/归档

回到日志数据的问题,为了便于回溯问题,我们期望将数据的存档时间从一周延长为半年,但是不要求全部的数据都有同样的可用性和响应时间,具体的一周内需要有高性能的读写能力,一月内数据仅需保证秒级的查询 RT 即可,而一年内的数据不需要实时保证可读,只要能保证数据在必要情况下可恢复使用即可。
对于冷热时效明显的数据场景 (比如日志类) 热数据 (如当日数据) 的读写频率都要明显高于冷数据 (如一周前数据),这个时候从成本的角度出发,我们期望将冷数据存放于容量较大、IO及CPU性能较弱的存储类机器,将热数据存放于IO、CPU性能较好的计算类机器。
我们可以将节点按照存储优化型、计算优化型、通用型等定向优化的类型分组,使用 node.roles 将节点分组,可使用的角色包括:

data_content存储用户定义的业务数据,需要保证高性能的读写。
data_hot存储新的时序数据,读写频繁,CPU、IO 敏感型数据。
data_content/data_hot 都是用于存储热数据,区别在于用户自行创建的索引默认设置 index.routing.allocation.include._tier_preferencedata_content,而基于 Data Stream 的时序数据索引 (如logstash采集的日志数据) 默认使用 data_hot 节点。
data_warm读写频率降低,写很少,可容忍读 RT 变高。
data_cold保留只读数据,比如历史记录。
data_frozen用于保留数据快照,比如对冷数据将其副本作为 searchable snapshot 存放到 data_frozen 节点,并关闭原始索引的副本,在保证数据可用性的情况下进一步减少空间冗余,降低数据成本。
实际应用中通常会搭配 ILM (index lifecycle management) 来使用这类 data tier 属性,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"phases":
{
"hot":
{
"actions":
{
"rollover":
{
"max_age": "1d",
"max_size": "5gb"
}
}
},
"warm":
{
"min_age": "7d",
"actions":
{
"forcemerge":
{
"max_num_segments": 1
}
}
},
"cold":
{
"min_age": "30d",
"actions":
{
"searchable_snapshot":
{
"snapshot_repository": "snapshot_house"
}
}
}
}
}

将一周内数据作为热点数据存储于 data_hot 节点,其他一月内数据作为温数据存储于 data_warm 节点,超过一个月的数据启用 searchable_snapshot,集群内只保留主分片,副本备份到指定的远端路径下。ES 内部使用 index.routing.allocation.include._tier_preference 属性来实现这个操作,与 index.routing.allocation.include._tier 不同,_tier_preference 属性不是仅限定一种角色的节点,比如设置

1
2
3
{
"index.routing.allocation.include._tier_preference": "data_hot,data_warm,data_cold"
}

表示将索引优先存储于 data_hot 节点,如果不存在 data_hot 次选 data_warm,最后使用 data_cold 兜底存储。
在索引的生命周期中,新创建的索引 _tier_preference 先设置为 data_hot,在超过热点周期后,更新 _tier_preference 为 data_warm,data_hot,将其迁移到存在的 data_warm 节点。

通过这样的手段,我们可以根据索引数据的使用场景合理的调配硬件资源,达到成本利用的最优化。