07.ES优化
# 01.硬件优化
# 1、CPU 配置
CPU 繁忙的原因有以下几个
线程中有无限空循环、无阻塞、正则匹配或者单纯的计算
发生了频繁的 GC
多线程的上下文切换
CPU 选择
大多数 Elasticsearch 部署往往对 CPU 要求不高
因此,相对其它资源,具体配置多少个(CPU)不是那么关键
你应该选择具有多个内核的现代处理器,常见的集群使用 2 到 8 个核的机器
如果你要在更快的 CPUs 和更多的核数之间选择,选择更多的核数更好
多个内核提供的额外并发远胜过稍微快一点点的时钟频率
# 2、内存配置
如果有一种资源是最先被耗尽的,它可能是内存
排序和聚合都很耗内存,所以有足够的堆空间来应付它们是很重要的
即使堆空间是比较小的时候,也能为操作系统文件缓存提供额外的内存
64 GB 内存的机器是非常理想的,但是 32 GB 和 16 GB 机器也是很常见的
由于 ES 构建基于 lucene,而 lucene 设计强大之处在于 lucene 能够很好的利用操作系统内存来缓存索引数据,以提供快速的查询性能
lucene 的索引文件 segements 是存储在单文件中的,并且不可变
对于 OS 来说,能够很友好地将索引文件保持在 cache 中,以便快速访问
因此,我们很有必要将一半的物理内存留给 lucene
另一半的物理内存留给 ES(JVM heap)
禁止 swap
- 禁止 swap,一旦允许内存与磁盘的交换,会引起致命的性能问题
- 可以通过在 elasticsearch.yml 中 bootstrap.memory_lock: true,以保持 JVM 锁定内存,保证 ES 的性能
# 3、磁盘
硬盘对所有的集群都很重要,对大量写入的集群更是加倍重要(例如那些存储日志数据的)
硬盘是服务器上最慢的子系统,这意味着那些写入量很大的集群很容易让硬盘饱和,使得它成为集群的瓶颈
固态硬盘相比于任何旋转介质(机械硬盘,磁带等),无论随机写还是顺序写,都会对 IO 有较大的提升
# 02.索引优化设置
# 1、批量提交
- 当有大量数据提交的时候,建议采用批量提交(Bulk 操作)
- 此外使用 bulk 请求时,每个请求不超过几十M,因为太大会导致内存使用过大
# 2、增加Refresh时间间隔
为了提高索引性能,Elasticsearch 在写入数据的时候,采用延迟写入的策略
即数据先写到内存中,当超过默认1秒会进行一次写入操作,就是将内存中 segment 数据刷新到磁盘中
此时我们才能将数据搜索出来,所以这就是为什么 ES 提供的是近实时搜索功能,而不是实时搜索功能
如果我们的系统对数据延迟要求不高的话,我们可以通过延长 refresh 时间间隔
可以有效地减少 segment 合并压力,提高索引速度
比如在做全链路跟踪的过程中,我们就将 index.refresh_interval 设置为30s,减少 refresh 次数
在进行全量索引时,可以将 refresh 次数临时关闭,数据导入成功后再打开到正常模式,比如30s
# 3、index_buffer_size
- 索引缓冲的设置可以控制多少内存分配给索引进程
- 这是一个全局配置,会应用于一个节点上所有不同的分片上
indices.memory.index_buffer_size: 10%
indices.memory.min_index_buffer_size: 48mb
2
- indices.memory.index_buffer_size 接受一个百分比或者一个表示字节大小的值
- 默认是10%,意味着分配给节点的总内存的10%用来做索引缓冲的大小
- 这个数值被分到不同的分片(shards)上
- 如果设置的是百分比,还可以设置 min_index_buffer_size (默认 48mb)和 max_index_buffer_size(默认没有上限)
# 4、修改 translog 相关的设置
- 一是控制数据从内存到硬盘的操作频率,以减少硬盘 IO
- 可将 sync_interval 的时间设置大一些(默认为5s)
index.translog.sync_interval: 5s
- 也可以控制 tranlog 数据块的大小,达到 threshold 大小时,才会 flush 到 lucene 索引文件(默认为512m)
index.translog.flush_threshold_size: 512mb
# 5、减少副本数量
ES默认副本数量为3个,虽然这样会提高集群的可用性,增加搜索的并发数,但是同时也会影响写入索引的效率
在索引过程中,需要把更新的文档发到副本节点上,等副本节点生效后在进行返回结束
使用 Elasticsearch 做业务搜索的时候,建议副本数目还是设置为3个
但是像内部 ELK 日志系统、分布式跟踪系统中,完全可以将副本数目设置为1个
# 6、其他
注意 _id 字段的使用
- 应尽可能避免自定义 _id,以避免针对 ID 的版本管理
- 建议使用 ES 的默认 ID 生成策略或使用数字类型 ID 做为主键
注意
_all
字段及_source
字段的使用_all
字段包含了所有的索引字段,方便做全文检索,如果无此需求,可以禁用_source
存储了原始的 document 内容,如果没有获取原始文档数据的需求- 可通过设置 includes、excludes 属性来定义放入 _source 的字段
合理的配置使用 index 属性
- 合理的配置使用 index 属性,analyzed 和 not_analyzed,根据业务需求来控制字段是否分词或不分词
- 只有 groupby 需求的字段,配置时就设置成 not_analyzed,以提高查询或聚类的效率
# 03.查询方面优化
# 1、路由优化
- 当我们查询文档的时候,Elasticsearch 如何知道一个文档应该存放到哪个分片中呢
- 它其实是通过下面这个公式来计算出来的
shard = hash(routing) % number_of_primary_shards
routing 默认值是文档的 id,也可以采用自定义值,比如用户 ID
1)不带 routing 查询
在查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为2个步骤
分发:请求到达协调节点后,协调节点将查询请求分发到每个分片上。
聚合:协调节点搜集到每个分片上查询结果,再将查询的结果进行排序,之后给用户返回结果。
2)带 routing 查询
查询的时候,可以直接根据 routing 信息定位到某个分配查询,不需要查询所有的分配,经过协调节点排序
向上面自定义的用户查询,如果 routing 设置为 userid 的话,就可以直接查询出数据来,效率提升很多。
# 2、Filter VS Query
- 尽可
能使用过滤器上下文(Filter)
替代查询上下文(Query)
- Query:此文档与此查询子句的匹配程度如何?
- Query 需要计算相关性分数
- Filter:此文档和查询子句匹配吗?
- ES 针对
Filter 查询只需要回答「是」或者「否」
,同时Filter结果可以缓存
- ES 针对
# 3、深度翻页
在使用 Elasticsearch 过程中,应尽量避免大翻页的出现
正常翻页查询都是从 from 开始 size 条数据
这样就需要在每个分片中查询打分排名在前面的 from+size 条数据
协同节点收集每个分配的前 from+size 条数据
协同节点一共会受到
N*(from+size)
条数据,然后进行排序,再将其中 from 到 from+size 条数据返回出去如果 from 或者 size 很大的话,导致参加排序的数量会同步扩大很多,最终会导致 CPU 资源消耗增大
# 4、Cache
QueryCache
- ES查询的时候,使用filter查询会使用query cache
- 如果业务场景中的过滤查询比较多,建议将querycache设置大一些,以提高查询速度
FieldDataCache
- 在聚类或排序时,field data cache会使用频繁,可以设置字段数据缓存的大小
- 在聚类或排序场景较多的情形下很有必要,可通过indices.fielddata.cache.size:30% 或具体值10GB来设置
- 但是如果场景或数据变更比较频繁,设置cache并不是好的做法,因为缓存加载的开销也是特别大的
ShardRequestCache
:- 查询请求发起后,每个分片会将结果返回给协调节点(Coordinating Node), 由协调节点将结果整合
- 如果有需求,可以设置开启; 通过设置index.requests.cache.enable: true来开启
- 不过,shard request cache只缓存hits.total, aggregations, suggestions类型的数据,并不会缓存hits的内容
- 也可以通过设置indices.requests.cache.size: 1%(默认)来控制缓存空间大小
# 5、其他优化
- query_string 或 multi_match的查询字段越多, 查询越慢。可以在mapping阶段,利用copy_to属性将多字段的值索引到一个新字段,multi_match时,用新的字段查询
- 日期字段的查询, 尤其是用now 的查询实际上是不存在缓存的,因此, 可以从业务的角度来考虑是否一定要用now, 毕竟利用query cache 是能够大大提高查询效率的
- 查询结果集的大小不能随意设置成大得离谱的值, 如query.setSize不能设置成 Integer.MAX_VALUE, 因为ES内部需要建立一个数据结构来放指定大小的结果集数据。
- 避免层级过深的聚合查询, 层级过深的aggregation , 会导致内存、CPU消耗,建议在服务层通过程序来组装业务,也可以通过pipeline的方式来优化
- 复用预索引数据方式来提高AGG性能:
# 04.数据结构优化
基于 Elasticsearch 的使用场景,文档数据结构尽量和使用场景进行结合,去掉没用及不合理的数据。
# 1、减少不需要字段
如果 ES用于业务搜索服务,一些不需要用于搜索的字段最好不存到 ES 中
这样即节省空间,同时在相同的数据量下,也能提高搜索性能
避免使用动态值作字段,动态递增的 mapping,会导致集群崩溃
同样,也需要控制字段的数量,业务中不使用的字段,就不要索引
控制索引的字段数量、mapping 深度、索引字段的类型,对于 ES 的性能优化是重中之重
以下是 ES 关于字段数、mapping 深度的一些默认设置
index.mapping.nested_objects.limit: 10000
index.mapping.total_fields.limit: 1000
index.mapping.depth.limit: 20
2
3
# 2、Nested vs Parent/Child
尽量避免使用 nested 或 parent/child 的字段,能不用就不用
nested query 慢,parent/child query 更慢,比 nested query 慢上百倍
因此能在 mapping 设计阶段搞定的(大宽表设计或采用比较 smart 的数据结构),就不要用父子关系的 mapping
如果一定要使用 nested fields,保证 nested fields 字段不能过多,目前 ES 默认限制是 50
因为针对 1 个 document,每一个 nested field,都会生成一个独立的 document
这将使 doc 数量剧增,影响查询效率,尤其是 JOIN 的效率
index.mapping.nested_fields.limit: 50
对比 | Nested Object | Parent/Child |
---|---|---|
优点 | 文档存储在一起,因此读取性高 | 父子文档可以独立更新,互不影响 |
缺点 | 更新父文档或子文档时需要更新整个文档 | 为了维护 join 关系,需要占用部分内存,读取性能较差 |
场景 | 子文档偶尔更新,查询频繁 | 子文档更新频繁 |
# 3、选择静态映射
选择静态映射,非必需时,禁止动态映射
尽量避免使用动态映射,这样有可能会导致集群崩溃
此外,动态映射有可能会带来不可控制的数据类型,进而有可能导致在查询端出现相关异常,影响业务
此外,ES作为搜索引擎时,主要承载 query 的匹配和排序的功能
那数据的存储类型基于这两种功能的用途分为两类,一是需要匹配的字段,用来建立倒排索引对 query 匹配用
另一类字段是用做粗排用到的特征字段,如 ctr、点击数、评论数等等
# 4、document 模型设计
对于 MySQL,我们经常有一些复杂的关联查询
在 es 里该怎么玩儿,es 里面的复杂的关联查询尽量别用,一旦用了性能一般都不太好
最好是先在 应用 系统里就完成关联,将关联好的数据直接写入 es 中
搜索的时候,就不需要利用 es 的搜索语法来完成 join 之类的关联搜索了
document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作
es 能支持的操作就那么多,不要考虑用 es 做一些它不好操作的事情
如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成
另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的
# 05.集群架构设计
# 1、节点分离
主节点、数据节点和协调节点分离
Elasticsearch 集群在架构拓朴时,采用主节点、数据节点和负载均衡节点分离的架构
在 5.x 版本以后,又可将数据节点再细分为“Hot-Warm”的架构模式
主(master)节点
- 配置 node.master:true 和 node.data:false,该 node 服务器只作为一个主节点,但不存储任何索引数据
- 我们推荐每个集群运行3 个专用的 master 节点来提供最好的弹性
- 使用时,你还需要将 discovery.zen.minimum_master_nodes setting 参数设置为 2,以免出现脑裂(split-brain)的情况
- 用 3 个专用的 master 节点,专门负责处理集群的管理以及加强状态的整体稳定性
- 因为这 3 个 master 节点不包含数据也不会实际参与搜索以及索引操作,在 JVM 上它们不用做相同的事
- 例如繁重的索引或者耗时,资源耗费很大的搜索
- 因此不太可能会因为垃圾回收而导致停顿
- 因此,master 节点的 CPU,内存以及磁盘配置可以比 data 节点少很多的
# 2、集群分片设置
- ES 一旦创建好索引后,就无法调整分片的设置
- 而在 ES 中,一个分片实际上对应一个 lucene 索引
- 而 lucene 索引的读写会占用很多的系统资源,因此,分片数不能设置过大
- 所以,在创建索引时,合理配置分片数是非常重要的
- 一般来说,我们遵循一些原则
- 控制每个分片占用的硬盘容量不超过 ES 的最大 JVM 的堆空间设置
- 一般设置不超过 32 G,参考上面的 JVM 内存设置原则
- 因此,如果索引的总容量在 500 G 左右,那分片大小在 16 个左右即可
- 即使保持了 1 个以上的副本,同样有可能会导致数据丢失,集群无法恢复
- 所以,一般都设置分片数不超过节点数的 3 倍