一、分片的基本认知
Shard即数据分片,是ES的数据载体。如果将索引比作数据库的一张表,那么分片就是将这张表分割出来的每一个表片段。在ES中数据分为:
primary shard(主分片)replica shard(副分片)
每一个主分片承载单个索引的一部分数据,分布于各个节点,副分片为某个主分片的副本,即备份。分片分配的原则是尽量均匀的分配在集群中的各个节点,以最大程度降低部分分片在出现意外时对整个集群乃至服务造成的影响。每个分片就是一个Lucene的实例,具有完整的功能。
二、分片的路由机制
当你索引一个文档,它被存储到master节点上的一个主分片上。ES是如何知道文档属于哪个分片的呢?
ES将具有相关Hash值的文档存放到同一个主分片中,分片位置计算算法如下:
shard = hash(routing) % number_of_primary_shards
算法说明:
routing值是一个字符串,它默认是文档_id,也可以自定义。这个routing字符串通过哈希函数生成一个数字,然后除以主切片的数量得到一个余数(remainder),余数的范围是[0 , number_of_primary_shards-1],这个数字就是特定文档所在的分片。- 创建索引时需要指定主分片数量,创建完成后不能修改。这是因为如果主分片的数量在未来改变了,所有先前的路由值就失效了,文档也就永远找不到了。
- 该算法基本可以保证所有文档在所有分片上平均分布,不会导致数据分布不均(数据倾斜)的情况。
- 默认情况下
routing值是文档的_id。我们创建文档时可以指定id的值;如果不指定id时,ES将随机生成文档的_id值。这将导致在查询文档时,ES不能确定文档的位置,需要将请求广播到所有的分片节点上。
三、分片分配的基本策略
ES使用数据分片(shard)来提高服务的可用性,将数据分散保存在不同的节点上以降低当单个节点发生故障时对数据完整性的影响,同时使用副本(repiica)来保证数据的完整性。关于分片的默认分配策略,在7.x之前,默认五个主分片,每个主分片默认分配一个副分片,即5主1副,而7.x之后,默认1主1副。ES在分配单个索引的分片时会将每个分片尽可能分配到更多的节点上。但是,实际情况取决于集群拥有的分片和索引的数量以及它们的大小,不一定总是能均匀地分布。- 主分片只能在索引创建时配置数量,而副分片可以在任何时间分配,并且主分片支持读和写操作,而副分片只支持客户端的读取操作,数据由
ES自动管理,从主分片同步。 ES不允许主分片和它的副分片放在同一个节点中,并且同一个节点不接受完全相同的两个副分片。- 同一个节点允许多个索引的分片同时存在。
四、分片的存储
4.1、写索引过程
ES集群中每个节点通过路由都知道集群中的文档的存放位置,所以每个节点都有处理读写请求的能力。
在一个写请求被发送到某个节点后,该节点即为协调节点,协调节点会根据路由公式计算出需要写到哪个分片上,再将请求转发到该分片的主分片节点上。假设shard = hash(routing) % 4 = 0 ,则过程大致如下:
- 客户端向
ES1节点(协调节点)发送写请求,通过路由计算公式得到值为0,则当前数据应被写到主分片S0上。 ES1节点将请求转发到S0主分片所在的节点ES3,ES3接受请求并写入到磁盘。- 并发将数据复制到两个副本分片
R0上,其中通过乐观并发控制数据的冲突。一旦所有的副本分片都报告成功,则节点ES3将向协调节点报告成功,协调节点向客户端报告成功。
4.2、存储原理
4.2.1、索引的不可变性
写入磁盘的倒排索引是不可变的。
优点:
- 不需要锁。因为如果从来不需要更新一个索引,就不必担心多个程序同时尝试修改,也就不需要锁。
- 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性,只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
- 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
- 写入单个大的倒排索引,可以压缩数据,较少磁盘IO和需要缓存索引的内存大小。
缺点:
- 当对旧数据进行删除时,旧数据不会马上被删除,而是在
.del文件中被标记为删除。而旧数据只能等到段更新时才能被移除,这样会造成大量的空间浪费。- 若有一条数据频繁的更新,每次更新都是新增新的标记旧的,则会有大量的空间浪费。
- 每次新增数据时都需要新增一个段来存储数据。当段的数量太多时,对服务器的资源例如文件句柄的消耗会非常大。
- 在查询的结果中包含所有的结果集,需要排除被标记删除的旧数据,这增加了查询的负担。
4.2.2、段的引入
在全文检索的早些时候,会为整个文档集合建立一个大索引,并且写入磁盘。只有新的索引准备好了,它就会替代旧的索引,最近的修改才可以被检索。这无疑是低效的。
因为索引的不可变性带来的好处,那如何在保持不可变同时更新倒排索引?
答案是,使用多个索引。不是重写整个倒排索引,而是增加额外的索引反映最近的变化。每个倒排索引都可以按顺序查询,从最老的开始,最后把结果聚合。
这就引入了段 (segment) :
- 新的文档首先写入内存区的索引缓存,这时不可检索。
- 时不时(默认 1s 一次),内存区的索引缓存被 refresh 到文件系统缓存(该过程比直接到磁盘代价低很多),成为一个新的段(segment)并被打开,这时可以被检索。
- 新的段提交,写入磁盘,提交后,新的段加入提交点,缓存被清除,等待接收新的文档。
分片下的索引文件被拆分为多个子文件,每个子文件叫作段, 每一个段本身都是一个倒排索引,并且段具有不变性,一旦索引的数据被写入硬盘,就不可再修改。
段被写入到磁盘后会生成一个提交点,提交点是一个用来记录所有提交后段信息的文件。一个段一旦拥有了提交点,就说明这个段只有读的权限,失去了写的权限。相反,当段在内存中时,就只有写的权限,而不具备读数据的权限,意味着不能被检索。
在 Lucene 中的索引(Lucene 索引是 ES 中的分片,ES 中的索引是分片的集合)指的是段的集合,再加上提交点(commit point),如下图:
在底层采用了分段的存储模式,使它在读写时几乎完全避免了锁的出现,大大提升了读写性能。
索引文件分段存储并且不可修改,那么新增、更新和删除如何处理呢?
- 新增,新增很好处理,由于数据是新的,所以只需要对当前文档新增一个段就可以了。
- 删除,由于不可修改,所以对于删除操作,不会把文档从旧的段中移除,而是通过新增一个
.del文件(每一个提交点都有一个 .del 文件),包含了段上已经被删除的文档。当一个文档被删除,它实际上只是在.del文件中被标记为删除,依然可以匹配查询,但是最终返回之前会被从结果中删除。 - 更新,不能修改旧的段来进行反映文档的更新,其实更新相当于是删除和新增这两个动作组成。会将旧的文档在
.del文件中标记删除,然后文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就会被移除。
4.2.3、延迟写策略--近实时搜索--fresh
ES 是怎么做到近实时全文搜索?
磁盘是瓶颈。提交一个新的段到磁盘需要fsync操作,确保段被物理地写入磁盘,即时电源失效也不会丢失数据。但是fsync是昂贵的,严重影响性能,当写数据量大的时候会造成 ES 停顿卡死,查询也无法做到快速响应。
所以fsync不能在每个文档被索引的时就触发,需要一种更轻量级的方式使新的文档可以被搜索,这意味移除fsync。
为了提升写的性能,ES 没有每新增一条数据就增加一个段到磁盘上,而是采用延迟写的策略。
每当有新增的数据时,就将其先写入到内存中,在内存和磁盘之间是文件系统缓存,当达到默认的时间(1秒钟)或者内存的数据达到一定量时,会触发一次刷新(Refresh),将内存中的数据生成到一个新的段上并缓存到文件缓存系统 上,稍后再被刷新到磁盘中并生成提交点。
这里的内存使用的是ES的JVM内存,而文件缓存系统使用的是操作系统的内存。新的数据会继续的被写入内存,但内存中的数据并不是以段的形式存储的,因此不能提供检索功能。由内存刷新到文件缓存系统的时候会生成了新的段,并将段打开以供搜索使用,而不需要等到被刷新到磁盘。
在 Elasticsearch 中,这种写入和打开一个新段的轻量的过程叫做 refresh (即内存刷新到文件缓存系统)。默认情况下每个分片会每秒自动刷新一次。 这就是为什么说 Elasticsearch 是近实时的搜索了:文档的改动不会立即被搜索,但是会在一秒内可见。
也可以手动触发 refresh。 POST /_refresh 刷新所有索引, POST /index/_refresh刷新指定的索引:
PUT /my_logs
{
"settings": {
"refresh_interval": "30s"
}
}
Tips:尽管刷新是比提交轻量很多的操作,它还是会有性能开销。当写测试的时候,手动刷新很有用,但是不要在生产环境下每次索引一个文档都去手动刷新。而且并不是所有的情况都需要每秒刷新。在使用 Elasticsearch 索引大量的日志文件,可能想优化索引速度而不是近实时搜索,这时可以在创建索引时在
settings中通过调大refresh_interval="30s"的值 , 降低每个索引的刷新频率,设值时需要注意后面带上时间单位,否则默认是毫秒。当refresh_interval=-1时表示关闭索引的自动刷新。
4.2.4、持久化--flush
没用fsync同步文件系统缓存到磁盘,不能确保电源失效,甚至正常退出应用后,数据的安全。为了 ES 的可靠性,需要确保变更持久化到磁盘。
虽然通过定时 Refresh 获得近实时的搜索,但是 Refresh 只是将数据挪到文件缓存系统,文件缓存系统也是内存空间,属于操作系统的内存,只要是内存都存在断电或异常情况下丢失数据的危险。
为了避免丢失数据,Elasticsearch添加了事务日志(Translog) ,事务日志记录了所有还没有持久化到磁盘的数据。
有了事务日志,过程现在如下:
- 当一个文档被索引,它被加入到内存缓存,同时加到事务日志。不断有新的文档被写入到内存,同时也都会记录到事务日志中。这时新数据还不能被检索和查询。
- 当达到默认的刷新时间或内存中的数据达到一定量后,会触发一次 refresh:
- 将内存中的数据以一个新段形式刷新到文件缓存系统,但没有fsync;
- 段被打开,使得新的文档可以搜索;
- 缓存被清除。
- 随着更多的文档加入到缓存区,写入日志,这个过程会继续。
- 随着新文档索引不断被写入,当日志数据大小超过 512M 或者时间超过 30 分钟时,会进行一次全提交:
- 内存缓存区的所有文档会写入到新段中,同时清除缓存;
- 文件系统缓存通过
fsync操作flush到硬盘,生成提交点; - 事务日志文件被删除,创建一个空的新日志。
事务日志记录了没有flush到硬盘的所有操作。当故障重启后,ES 会用最近一次提交点从硬盘恢复所有已知的段,并且从日志里恢复所有的操作。
在 ES 中,进行一次提交并删除事务日志的操作叫做flush。分片每 30 分钟,或事务日志过大会进行一次flush操作。flush API 也可用来进行一次手动flush,POST/ _flush针对所有索引有效,POST /index/_flush则指定的索引:
通常很少需要手动flush,通常自动的就够了。
总体的流程大致如下:
4.2.5、合并段
由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。每一个段都会消耗文件句柄、内存和 cpu 运行周期。更重要的是,每个搜索请求都必须轮流检查每个段然后合并查询结果,所以段越多,搜索也就越慢。
ES 通过后台合并段解决这个问题。小段被合并成大段,再合并成更大的段。这时旧的文档从文件系统删除的时候,旧的段不会再复制到更大的新段中。合并的过程中不会中断索引和搜索。
段合并在进行索引和搜索时会自动进行,合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中,这些段既可以是未提交的也可以是已提交的。
合并结束后老的段会被删除,新的段被 flush 到磁盘,同时写入一个包含新段且排除旧的和较小的段的新提交点,新的段被打开可以用来搜索。
合并大的段会消耗很多 IO 和 CPU,如果不检查会影响到搜素性能。默认情况下,ES 会限制合并过程,这样搜索就可以有足够的资源进行。
五、分片的数量分配多少
- 避免分片过多:大多数搜索会命中多个分片。每个分片在单个 CPU 线程上运行搜索。虽然分片可以运行多个并发搜索,但跨大量分片的搜索会耗尽节点的搜索线程池。这会导致低吞吐量和缓慢的搜索速度。
- 分片越少越好:每个分片都使用内存和 CPU 资源。在大多数情况下,一小组大分片比许多小分片使用更少的资源。
六、分片的大小决策
- 分片的合理容量:10GB-50GB。虽然不是硬性限制,但 10GB 到 50GB 之间的分片往往效果很好。根据网络和用例,也许可以使用更大的分片。在索引的生命周期管理中,一般设置50GB为单个索引的最大阈值。
- 堆内存容量和分片数量的关联:小于20分片/每GB堆内存,一个节点可以容纳的分片数量与节点的堆内存成正比。例如,一个拥有 30GB 堆内存的节点最多应该有 600 个分片。如果节点超过每 GB 20 个分片,考虑添加另一个节点。
查询当前节点堆内存大小:
GET _cat/nodes?v=true&h=heap.current
- 避免重负载节点:如果分配给特定节点的分片过多,会造成当前节点为重负载节点
七、重要的配置
7.1 自定义属性
node.attr.{attribute}
如何查看节点属性?
GET _cat/nodeattrs?v
7.2 索引级配置
index.routing.allocation.include.{attribute}:表示索引可以分配在包含多个值中其中一个的节点上。index.routing.allocation.require.{attribute}:表示索引要分配在包含索引指定值的节点上(通常一般设置一个值)。index.routing.allocation.exclude.{attribute}:表示索引只能分配在不包含所有指定值的节点上。
//索引创建之前执行
PUT <index_name>
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"index.routing.allocation.include._name": "node1"
}
}
7.3 集群级配置
elasticsearch修改集群范围设置提供两种方式,
persistent:永久性修改,persistent相关的修改保存在了/path.data/cluster.name/nodes/0/_state/global-n.st,如果想删除设置,删除此文件即可。transient:集群重启后失效。
PUT _cluster/settings
{
"persistent": {
"cluster.routing.allocation.awareness.attributes": "rack_id"
}
}
八、索引分片分配:Index Shard Allocation
8.1 分片均衡策略:shard rebalance
当集群在每个节点上具有相同数量的分片而没有集中在任何节点上的任何索引的分片时,集群是平衡的。Elasticsearch运行一个称为rebalancing 的自动过程,它在集群中的节点之间移动分片以改善其平衡。重新平衡遵循所有其他分片分配规则,例如分配过滤和强制意识,这可能会阻止它完全平衡集群。在这种情况下,重新平衡会努力在您配置的规则内实现最平衡的集群。如果您使用数据层,然后Elasticsearch会自动应用分配过滤规则将每个分片放置在适当的层中。这些规则意味着平衡器在每一层内独立工作。
cluster.routing.rebalance.enable
动态为特定类型的分片启用或禁用重新平衡:
all-(默认)允许对所有类型的分片进行分片平衡。primaries- 只允许主分片的分片平衡。replicas- 仅允许对副本分片进行分片平衡。none- 任何索引都不允许进行任何类型的分片平衡。
cluster.routing.allocation.allow_rebalance
动态指定何时允许分片重新平衡:
always- 始终允许重新平衡。indices_primaries_active- 仅当集群中的所有主节点都已分配时。indices_all_active-(默认)仅当集群中的所有分片(主分片和副本)都被分配时。
8.2 延迟分配策略(默认1m):
当节点出于任何原因(有意或无意)离开集群时,主节点会做出以下反应
- 将副本分片提升为主分片以替换节点上的任何主分片。
- 分配副本分片以替换丢失的副本(假设有足够的节点)。
- 在其余节点之间均匀地重新平衡分片。
这些操作旨在通过确保尽快完全复制每个分片来保护集群免受数据丢失。即使我们在节点级别和集群级别限制并发恢复 ,这种“分片洗牌”仍然会给集群带来很多额外的负载,如果丢失的节点可能很快就会返回,这可能是不必要的
8.3 分片过滤:即(Shard allocation filtering,控制那个分片分配给哪个节点)。
- index.routing.allocation.include.{attribute}:表示索引可以分配在包含多个值中其中一个的至少节点上。
- index.routing.allocation.require.{attribute}:表示索引要分配在包含索引指定值的节点上(通常一般设置一个值)。
- index.routing.allocation.exclude.{attribute}:表示索引只能分配在不包含所有指定值的节点上。
8.4 分片分配感知策略:Shard Allocation Awareness
Shard Allocation Awareness的设计初衷是为了提高服务的可用性,通过自定义节点属性作为感知属性,让 Elasticsearch 在分配分片时将物理硬件配置考虑在内。如果 Elasticsearch 知道哪些节点位于同一物理服务器上、同一机架中或同一区域中,则它可以分离主副本分片,以最大程度地降低在发生故障时丢失数据的风险。
启用分片感知策略
# 配置节点属性
node.attr.rack_id: rack1
通过以下设置告诉主节点在分配分片的时候需要考虑哪些属性。这些信息会保存在每个候选节点的集群状态信息中
PUT _cluster/settings
{
"persistent": {
"cluster.routing.allocation.awareness.attributes": "rack_id"
}
}
8.5 强制感知策略:Forced awareness
默认情况下,如果一个区域发生故障,Elasticsearch 会将所有故障的副本分片分配给其他区域。但是剩余区域可能没有足够的性能冗余来承载这些分片。
为了防止在发生故障时单个位置过载,您可以设置为cluster.routing.allocation.awareness.force不分配副本,直到另一个位置的节点可用。
部署强制感知策略
设置强制感知策略,告诉主节点当前通过某个属性来划分区域,并且告知区域有哪些值
cluster.routing.allocation.awareness.attributes: zone
cluster.routing.allocation.awareness.force.zone.values: zone1,zone2