百分点大数据技术团队:Elasticsearch 多数据中心大规模集群的实战经验
编者按 :Elasticsearch(简称 ES)作为一种分布式、高扩展、高实时的搜索与数据分析引擎,能使数据在生产环境变得更有价值,自 ES 从诞生以来,其应用越来越广泛,特别是大数据领域,功能也越来越强大。但当前,ES 多数据中心大规模集群依然面临着数据量大、查询周期长、集群规模大、聚合分析要求高等诸多挑战。
本文针对当前面临的问题,结合百分点大数据技术团队在某海外国家级多数据中心的 ES 集群建设经验,总结了 ES 集群规划与性能调优方法,供工程师们参考。
一、ES 集群建设实践
1. 集群拆分
集群规模过大会导致 Master 节点压力比较大,造成索引的创建删除、分片分配等操作较慢,严重影响集群稳定性。所以将集群进行拆分,业务上 ES 集群存储三种业务类型数据 A、B、C,数据占比大约 A:B:C=8:3:1,根据业务类型,拆分 A 数据类型 6 个集群,B 数据类型 2 个集群,C 数据类型 2 个集群,存储在两个中心机房,每个集群不超过 100 个节点。(推荐集群节点数不要超过服务器核心数 * 5)使用跨集群搜索(Cross-cluster search),客户端在查询数据的时候连接 Query 集群,通过 Query 集群查询 10 个数据集群的数据。
2. 角色分离
ES 集群中有多种角色,协调(coordinator)节点,主(master)节点,数据(data)节点。
一台节点可以配置成多种角色,角色分离可以避免各种角色性能互相影响。比如一个节点既是数据节点也是协调节点,可能协调角色聚合时占用大量资源导致数据角色写入数据出现异常。
通过配置 elasticsearch.yml 来使一个节点只承担一种角色。
主节点:当有一半主节点下线整个集群也就不可用了,一般一个集群设置 3 台主节点,可以容许一台主节点下线。不要配置偶数台主节点,因为配置 4 台主节点也仅能容许一台主节点下线。
node.master: truenode.data: false
数据节点:根据自己的数据情况配置合适的节点数量。数据节点下线会导致数据不完整,集群仍能正常工作。
node.master: falsenode.data: true
协调节点:任何一个节点都可以是协调节点,我们通过配置几个仅协调节点来单独行使协调功能。
node.master: falsenode.data: false
3. 版本选择
在当初项目选型的时候,ES 刚刚发布 7.X,我们选择相对稳定的 6.7.2 版本,在后期大规模测试过程中发现,6.X 版本有些局限性,此时 ES 已经发布 7.8 版本了。通过调研最终选用 7.6.2 版本,原因主要有下面两点:
(1)元数据压力
在 6.7.2 版本,集群 shard 个数达到 5w 时,更新 template 或创建 index 会出现大于 30s 的情况。详细参考问题页:https://github.com/elastic/elasticsearch/pull/47817
在 7.6.2 版本,集群 shard 个数达到 5w 时,更新 template 或创建 index 在 3s 内。
我们 shard 个数最多的集群达到了 4.4w。
(2)跨集群搜索(Cross-cluster search)
当存在三个集群:Query 集群、data1 集群、data2 集群时,配置 data1、data2 集群为 Query 集群的远程集群,此时可以通过向 Query 集群发送请求来获取 data1、data2 集群的数据。
跨集群搜索提供了两个处理网络延迟的选项:
最小化网络传输
您向 Query 集群发送跨集群搜索请求,Query 集群中的协调节点接收并解析请求;
协调节点向每个集群(包括 Query 集群)发送单个搜索请求,每个集群独立执行搜索请求,将其自己的集群级别设置应用于请求;
每个远程集群将其搜索结果发送回 Query 集群的协调节点;
从每个集群收集结果后,Query 集群的协调节点在跨集群搜索响应中返回最终结果。
不使用最小化网络传输
您向 Query 集群发送跨集群搜索请求,Query 集群中的协调节点接收并解析请求;
协调节点向每个远程集群发送搜索分片 API 请求;
每个远程集群将其响应发送回协调节点,此响应包含有关将在其上执行跨集群搜索请求的索引和分片的信息;
协调节点向每个分片发送搜索请求,包括其自己集群中的分片,每个分片独立执行搜索请求;
每个分片将其搜索结果发送回协调节点;
从每个集群收集结果后,协调节点在跨集群搜索响应中返回最终结果。
最小化网络传输会减少与远程集群之间的网络往返次数,这减少了网络延迟对搜索速度的影响,同时各个远程集群的协调节点会预先将自己集群的数据聚合一次。即便如此,Query 集群协调节点压力还会比较大,因为要聚合所有集群返回的数据。
我们根据最小化网络传输流程图,分析如下聚合时协调节点的压力:
coordinator node2 和 coordinator node3 接收各自数据节点的 1w 条数据(shard 数 * shard_size)并全量返回给 coordinator node1,最终在 coordinator node1 上会有 2w 条数据,取前 10 条(size)返回给客户端。当我们查询的集群、索引或者索引的 shard 更多时,coordinator node1 的压力会越来越大。测试过程中总会出现 OOM 的情况。
基于此考虑,我们修改部分源码,增加了 coordinator_size 参数,在第 3 步数据集群将搜索结果发回 coordinator node1 时只返回 TOP N(前 coordinator_size)。对于一个集群,通过 shard_size 来平衡精度与性能;对于整个跨集群方案,通过 coordinator_size 来平衡精度与性能。
不使用最小化网络传输由于数据不会经过 coordinatornode2 和 coordinator node3,所以不支持这样的修改。
在 6.7.2 版本,跨集群搜索只支持不使用最小化网络传输的方式。
在 7.6.2 版本,默认使用最小化网络传输的方式进行跨集群搜索,可以在请求中添加 ccs_minimize_roundtrips:false 参数来选择不使用最小化网络传输。
4. 拆分索引
业务上存储的是日志数据,只有增加,没有变更,按时间累积,天然对索引的拆分友好支持,并且如果按天拆分索引有以下好处:
方便数据删除,超过保存周期的数据直接使用定时脚本在夜间删除索引即可;
提升搜索聚合的效率,业务上对数据的搜索必须要携带时间范围的参数,根据该时间参数转化为具体的索引这样搜索的 shard 就会比较少;
方便后期修改 shard 个数和 mapping,虽然 shard 个数和 mapping 一般不修改,但也会遇到特殊情况,如果需要修改,我们只需要修改 template,之后新索引都会应用最新的 shard 设置和 mapping 设置,等业务滚动数据存储周期天数后所有数据就都会应用最新规则。
5. 副本数量
越多的副本数量会增加搜索的并发数,但是同时也会影响写入索引的效率,占用磁盘空间。可以根据数据安全性来设置副本的数量,一般一个副本是足够的,同时可以考虑在索引创建时拥有更多的副本,当数据超过一定时间而变得不那么重要后,通过 API 减少副本个数。
二、ES 集群配置经验
1. 内存和 CPU
(1)内存分配
Lucene 能很好利用文件系统的缓存,它是通过系统内核管理的。如果没有足够的文件系统缓存空间,性能会受到影响。此外,专用于堆的内存越多意味着其他所有使用 doc values 的字段内存越少。参考以下原则:
当机器内存小于 64G 时,遵循通用的原则,50% 给 ES,50% 留给 lucene。
当机器内存大于 64G 时,遵循以下原则:
如果主要的使用场景是全文检索,那么建议给 ES Heap 分配 4~32G 的内存即可;其它内存留给操作系统,供 lucene 使用(segments cache),以提供更快的查询性能;
如果主要的使用场景是聚合或排序,并且大多数是 numerics,dates,geo_points 以及非分词的字符串,建议分配给 ES Heap 4~32G 的内存即可,其余部分留给操作系统来缓存 doc values;
如果使用场景是基于分词字符串的聚合或排序,意味着需要 fielddata,这时需要更多的 heap size,建议机器上运行多 ES 实例,每个实例保持不超过 50%的 ES heap 设置。
内存配置不要超过 32G,如果堆大小小于 32GB,JVM 可以利用指针压缩,这可以大大降低内存的使用:每个指针 4 字节而不是 8 字节。这里 32G 可能因为某些因素的影响有些误差,最好配置到 31G。
内存最小值(Xms)与最大值(Xmx)的大小配置相等,防止程序在运行时改变堆内存大小,这是一个很耗系统资源的过程。
配置 jvm.options
-Xms31g-Xmx31g
(2)GC 设置
保持 GC 的现有设置,默认设置为:Concurrent-Mark and Sweep(CMS),别换成 G1 GC,因为目前 G1 还有很多 BUG。
(3)禁止 swap
禁止 swap,一旦允许内存与磁盘的交换,会引起致命的性能问题,可以通过在 elasticsearch.yml 中配置以下参数以保持 JVM 锁定内存,保证 ES 的性能。
bootstrap.memory_lock: true
(4)核心数
processors 配置参数的值决定了节点 allocated_processors 的参数值,而 ES 很多线程池的大小都是基于 allocated_processors 的值来计算的。
修改 elasticsearch.yml
elasticsearch.yml
node.processors: 56
在以下情况可以考虑调整该参数:
在一台服务器部署多个 ES 实例,此时调整参数为处理器实际核心数一半;
错误地检测处理器的数量,此时调整参数进行修正;
实际处理器核心数大于 32,ES 默认处理器核心数最大限制为 32 个,如果物理机的处理器核心数超过了 32 个,为了更充分利用 CPU,可以调整参数为实际处理器核心数。如果可以选择 CPU,更多的核心数比更快的 CPU 更有意义。
2. 写入
(1)增加 Refresh 时间间隔
ES 写入数据时先写入 memory buffer 中,memory buffer 会周期性(index.refresh_interval 默认 1s)或者写满后做 refresh 操作,将内容写入到一个新的 segment 中。此时数据可以被搜索,这就是为什么 ES 提供的是近实时的搜索。如果系统对数据延迟要求不高的话,通过延长 refresh 时间间隔(比如 index.refresh_interval 设置为 30s),可以有效地提高索引速度,同时减少 segment 个数降低 segment 合并压力。
修改索引的 settings:
PUT /my_index/_settings{ "index" : { "refresh_interval" : "30s" }}
在导入大量数据的时候可以暂时设置 index.refresh_interval: -1 和 index.number_of_replicas:0 来提高性能,数据导入完成后还原设置。
(2)修改 index_buffer_size 的设置
上一条说 memory buffer 写满时也会触发 refresh 操作,为了减少 refresh 操作,我们同时也要配合增加 memory buffer 的大小。这是一个全局静态配置,会应用于一个节点上所有的分片上。
修改 elasticsearch.yml:
# 接受百分比或字节大小值。它默认为 10%,这意味着 10%分配给节点的总堆中的将用作所有分片共享的索引缓冲区大小。indices.memory.index_buffer_size: 10%# 如果 index_buffer_size 指定为百分比,则此设置可用于指定绝对最小值。默认为 48mb。indices.memory.min_index_buffer_size: 48mb# 如果 index_buffer_size 指定为百分比,则此设置可用于指定绝对最大值。默认为无界。indices.memory.max_index_buffer_size: 10240mb
(3)修改 translog 相关的设置
refresh 操作后,数据写入 segment 文件中,此时 segment 在 OS Cache 中,以上所有数据都保存在内存里,如果服务器异常重启则数据都不可恢复。所以数据在写入 memory buffer 的同时,记录当前操作到 translog,每 30 分钟或者当 translog 中的数据大小达到阈值后,会触发一次 flush 操作将 OS Cache 中的 segment 落盘,同时清理 translog。
translog 默认在每次索引、删除、更新或批量请求后会提交到磁盘。我们可以通过设置使 translog 异步提交来提高性能:
PUT /my_index/_settings{ "index" : { "translog.durability": "async", # 刷新方式。默认 request 同步, async 异步 "translog.sync_interval": "10s" # 刷新频率。默认 5s,不能低于 100ms }}
也可以控制 translog 的阈值来降低 flush 的频率:
PUT /my_index/_settings{ "index" : { "translog.flush_threshold_size": "1024mb" # translog 阈值。默认 512mb。如果达到则会强制 flush,否则需要等待 30 分钟 }}
3. 分配
(1)延迟分配配置
当集群中某个节点离开集群时:
master 节点会将此节点上的主分片对应的副本分片提升为主分片;
在其他节点上重建因节点下线而丢失的分片;
重建完成后很可能还会触发集群数据平衡;
如果节点又重新加入集群,集群数据自动平衡,将一些分片迁移到此节点。
节点很可能因为网络原因或硬件原因短暂离开集群,过几分钟又重新加入集群,触发上述操作会导致集群有比较大的开销,是完全没有必要的。当设置了延时分配为 5 分钟时,节点下线时,只会执行上述第 1 步操作,此时的集群处于 yellow 状态,在 5 分钟内下线的节点重新加入集群则集群直接恢复 green。避免了很多分片的迁移。通过 API 修改延时分配时间,值为 0 则表示会立即分配。
cluster.routing.allocation.balance.shard:默认 0.45f,定义分配在该节点的分片数的因子阈值=因子*(当前节点的分片数-集群的总分片数/节点数,即每个节点的平均分片数);
cluster.routing.allocation.balance.index:默认 0.55f,定义分配在该节点某个索引的分片数的因子,阈值=因子*(当前节点的某个索引的分片数-索引的总分片数/节点数,即每个节点某个索引的平均分片数);
cluster.routing.allocation.balance.threshold:默认 1.0f,超出这个阈值就会重新分配分片。
根据配置可以算出,当某一节点超过每个节点平局分片数 2.2(1/0.45)个分片时会触发 rebalance。
当某个节点 my_index 的分片数超过每个节点 my_index 的平均分片数 1.8(1/0.55)个分片时会触发 rebalance。
4. Mapping
(1)字段类型配置
不需要被分词的字段应使用 not_analyzed;
不需要被搜索的字段设置 index:false;
不需要聚合的字段设置 doc_value:false;
仅用于精确匹配而不进行范围查询的数值字段使用 keyword 类型的效率更高。numeric 类型从 lucene6.0 开始,使用了一种名为 block KD tree 的存储结构。这种结构比较适用于范围查找,在精确匹配方面没有倒排索引的性能好。
(2)使用自动生成的_id
避免自定义_id,建议用 ES 的默认 ID 生成策略,ES 在写入对 id 判断是否存在时对自动生成的 id 有优化。同时避免使用_id 字段进行排序或聚合,如果有需求建议将该_id 字段的内容复制到自定义已启用 doc_values 的字段中。
(3)禁用_source
_source 存储了原始的 document 内容,如果没有获取原始文档数据的需求,可通过设置 includes、excludes 属性来定义放入_source 的字段。
"mappings":{ "_source": { "excludes": [ "content" ] } }
案例:在我们的方案中,考虑在架构上,原始数据保存在分布式文件系统。所以在 ES 中可以不存储 content 字段(其他字段仍然存储),只为 content 字段建立倒排索引用于全文检索,而实际内容从分布式文件系统中获取。
收益:
降低 ES 中存储;
提高查询性能(OS cache 中能装更多的 Segment);
shard 的 merge、恢复和迁移成本降低。
限制:
此字段不能高亮;
update、update_by_query、reindex APIs 不能使用。
下面是我们根据业务数据特点测试不存储 content 字段对存储空间和查询的影响。
(1)测试不存储 content 字段对磁盘存储的影响
数据分布:
可以看出,不存储 content 字段可以加快搜索,对聚合影响不大。
总的来说,需要根据业务场景考虑益弊,比如是否对数据进行更新、reindex、高亮,或者说通过其他方式实现对数据的更新、reindex、高亮的成本如何。
三、ES 集群设计经验
1. 批量提交
bulk 批量写入的性能比你一条一条写入的性能要好很多,并不是 bulk size 越大越好,而是根据你的集群等环境具体要测试出来的,因为越大的 bulk size 会导致内存压力过大,最好不要超过几十 m。
2. 多线程写入
单线程发送 bulk 请求是无法最大化 ES 集群写入的吞吐量的。如果要利用集群的所有资源,就需要使用多线程并发将数据 bulk 写入集群中。为了更好的利用集群的资源,这样多线程并发写入,可以减少每次底层磁盘 fsync 的次数和开销。
3. Merge 只读索引
合并 Segment 对 ES 非常重要,过多的 Segment 会消耗文件句柄、内存和 CPU 时间,影响查询速度。Segment 的合并会消耗掉大量系统资源,尽量在请求较少的时候进行,比如在夜里两点 ForceMerge 前一天的索引。
POST my_index/_forcemerge?only_expunge_deletes=false&max_num_segments=1&flush=true
4. Filter 代替 Query
如果涉及评分相关业务使用 Query,其他场景推荐使用 Filter 查询。在做聚合查询时,filter 经常发挥更大的作用。因为没有评分 ES 的处理速度就会提高,提升了整体响应时间,同时 filter 可以缓存查询结果,而 Query 则不能缓存。
5. 避免深分页
分页搜索:每个分片各自查询的时候先构建 from+size 的优先队列,然后将所有的文档 ID 和排序值返回给协调节点。协调节点创建 size 为 number_of_shards *(from + size) 的优先队列,对数据节点的返回结果进行合并,取全局的 from ~ from+size 返回给客户端。
什么是深分页?
协调节点需要等待所有分片返回结果,然后再全局排序。因此会创建非常大的优先队列。比如一个索引有 10 个 shard,查询请求 from:9990,size:10(查询第 1000 页),那么每个 shard 需要返回 1w 条数据,协调节点就需要对 10w 条数据进行排序,仅仅为了获取 10 条数据而处理的大量的数据。且协调节点中的数据量会被分片的数量和页数所放大,因而一旦使用了深分页,协调节点会需要对大量的数据进行排序,影响查询性能。
如何避免深分页?
限制页数,限制只能获取前 100 页数据。翻页操作一般是人为触发的,并且人的行为一般不会翻页太多。ES 自身提供了 max_result_window 参数来限制返回的数据量,默认为 1w。每页返回 100 条数据,获取 100 页以后的数据就会报错。
使用 Scroll 或 search_after 代替分页查询,Scroll 和 search_after 都可以用于深分页,不支持跳页,适合拉取大量数据,目前官方推荐使用 search_after 代替 scroll。
6. 硬盘
固态硬盘比机械硬盘性能好很多;
使用多盘 RAID0,不要以为 ES 可以配置多盘写入就和 RAID0 是一样的,主要是因为一个 shard 对应的文件,只会放到其中一块磁盘上,不会跨磁盘存储,只写一个 shard 的时候其他盘是空闲的,不过 RAID0 中一块盘出现问题会导致整个 RAID0 的数据丢失。
7. 枚举空间大的字段聚合方案
(1)根据字段路由到固定 shard
这样在聚合时每个 shard 的 bucket 少,并且精度几乎不损失,但是会造成数据倾斜。如果字段数据比较平均可以选用,但是我们业务场景不适用。
(2)调整字段的存储类型
在字段类型配置里介绍了精确匹配时 keyword 比数值类型效率高,我们测试了相同数据 keyword 和 long 的聚合性能。
集群创建 4 个索引(4 天数据),每个索引 120 个 shard,每个 shard 大小为 30G,总数据量为:3.5T。
其数据分布为 1k 的占比 50%、10k 的占比 30%、100k 的占比 20%。
结束语
实时数据分析和文档搜索是 ES 的常用场景之一,结合客户数据特点,百分点大数据技术团队对 ES 进行了优化和一定的改造,并将这些能力沉淀到了我们的大数据平台上,以更好的满足客户的业务需求。通过调优,在生产环境中 ES 集群已经稳定运行近两年的时间了。在实际部署前,对集群稳定性和性能进行了多次大规模测试,也模拟了多种可能发生的故障场景,正是不断地测试,发现了一些局限性,对版本升级,对源码修改,也在不断测试中增加了更多的优化项来满足需求。
文中的优化实践总体上非常的通用,希望可以给大家带来一定的参考价值。
文章的最后,就以 ILM 作为彩蛋吧!
ILM 生命周期管理:
索引生命周期的四个阶段
Hot:index 正在查询和更新,性能好的机器会设置为 Hot 节点来进行数据的读写。
Warm:index 不再更新,但是仍然需要查询,节点性能一般可以设置为 Warm 节点。
Cold:index 不再被更新,且很少被查询,数据仍然可以搜索,但是能接受较慢的查询,节点性能较差,但有大量的磁盘空间。
Delete:数据不需要了,可以删除。
#节点属性可以通过 elasticsearch.yml 进行配置 # node.attr.xxx: xxx,hot warm cold node.attr.data: warm
这四个阶段按照 Hot,Warm,Cold,Delete 顺序执行,上一个阶段没有执行完成是不会执行下一个阶段的,对于不存在的阶段,会跳过该阶段进入到下一个阶段。
示例:创建索引生命周期策略来管理 elasticsearch_metrics-YYYY.MM.dd 日志数据。
策略如下:
在 index 创建后立即进入 hot 阶段:当 index 创建超过 1 天或者文档数超过 3000w 或者主分片大小超过 50g 后,生成新 index;
旧 index 进入到 warm 阶段,segment 数量 merge 为 1,index 迁移至属性 data 为 warm 的节点;
warm 阶段完成后,进入 delete 阶段,index rollover 时间超过 30 天后,将 index 删除。
(4)后续数据读写使用指定的别名 elasticsearch_metrics
Actions
各阶段支持的 actions 参考:Index lifecycle actions 选择对应 ES 版本。(https://www.elastic.co/guide/en/elasticsearch/reference/7.6/ilm-actions.html)
不同版本各个阶段支持的 action 有变化,因此建议手动测试一下,因为 7.6 版本官方文档说明在 hot 阶段如果存在 rollover 则可指定 forceMerge,但实际测试 7.6 所有版本都不支持,7.7.0 之后才可以这样设置。
参考资料
[1] https://www.elastic.co/guide/en/elasticsearch/reference/7.6/index.html
[2] https://cloud.tencent.com/developer/article/1661414
[3] 《elastic stack 实战手册》
评论