ElasticSearch的调优
跟Mysql类似的,ES的调优也可以主要分为几个方面的优化,首先是数据结构的声明方面,其次是ES操作方面的优化,最后是操作系统方面的调优。
在结构方面
在声明mapping的时候,我们可以尽量避免以下的操作来尽量避免影响性能问题出现。
-
少声明字段:如果我们声明字段过多,在写入存储的时候Index Buffer就只能存入更少的文档进行插入;而在查询读取的方面,由于ES本身通过倒排索引等方式进行的优化,性能其实不会被太大影响,但过多的字段仍然会导致网络方面的压力。
推荐的做法是:ES之中只记录一些比较关键的的信息,构成所谓的 search + fetch ,search从ES之中将目标数据的ID等可用于查询的信息获得,然后再到Mysql / hbase之中获得目标文档的数据。 -
数据尽量维持扁平化 :同样的在介绍es的数据结构的时候,有提到里有提到三种比较特殊的数据结构,一种是 nested 也就是对象类型、父子级别的对象类型、还有一种是兄弟级别的对象类型。在某些场景下使用当然非常方便,但是需要注意的是,nested 这种数据类型在索引和查询处理的时候对es来说是非常麻烦的,这也就导致了使用这种类型会 导致性能非常差
-
不查询字段关闭Index:这个其实在elasticsearch文档之中介绍过,也就是在ES之中,如果我们索引之中的某个字段,本身是不会针对其做查询的,最好的做法是在mapping声明的时候,直接指定它的index是false
"mappings": { "properties": { "content": { "type": "text" }, "name": { "type": "text", "index": false } } }
-
禁用 Dynamic Mapping:我们知道es在索引声明中,支持通过映射关系动态的增加字段,这个功能看起来就灵活好用,但他的问题在于,Dynamic Mapping新增的字段我们无法对其做设置,也就是即使我们不需要构建某个 field 索引,es也会自动创建。(也就是说这一点跟 不需要搜索的字段不要构建索引 是一样的原因)
-
不评分字段关闭Norms:跟index类似的,es针对字段查询的时候,往往会对查询的字段做评分处理,这个工作当然也是需要消耗性能来实现的,如果我们不需要使用
-
尽量不使用text的fileddata:同样在介绍es的数据结构的时候,介绍过text在由于自身的索引结构是倒排索引结构,这也就导致了text本身无法支持聚合。如果需要针对text做倒排索引就需要使用doc_values明确指定是true。但这会导致性能严重下降。
-
不聚合、排序字段关闭doc_values:跟上文text类似的,在其他的类型之中使用doc_values构建正排索引虽然消耗没有text构建fielddata那么夸张,但实际上也会产生一定的性能影响,因此实际上针对不需要排序聚合的字段,推荐关闭 doc_values。
需要注意的是,如果后续想要根据该字段开启doc_values只能重建索引。
-
尽量使用更小的数据类型 & 更准确的数据类型:
-
更小的数据类型:我们在 ES 的倒排索引和存储介绍的内容里面知道,ES会使用整形压缩等手段做数据的压缩尽量提高存储效率和查询性能。但在使用索引的时候还是推荐直接根据需要来使用更小的数据结构;而对于准确使用类型方面
-
更准确的数据类型:针对ID 和 枚举类型的时候,应当使用 keyword,而不是使用text。针对标题时,直接使用text。
(使用Keyword在针对聚合和排序、做term、prefix 查询的时候,性能明显会比text好,存储效率上keyword也会比text好很多;并且keyword不会被分词处理)
-
-
text字段使用合适分词器:在经过es的学习之后,我们当然知道实际上text的保存和查询其实都需要分词器做分词处理,如果我们使用的分词器不合适,这当然也会对我们的查询和插入的性能上造成一定的影响
批量插入的优化
当然我们的优化肯定不仅仅只能在结构的层面进行优化,还可以通过修改和配置ES查询和es的一些配置项的方式来进行优化。(需要注意的是,下面的所有的优化方式,其实都是针对大量数据导入的场景下提出的)
-
bulk : 在使用kibana或者java api对es做操作或者是针对其他数据库进行操作的时候,不要忘记我们实际上的操作本身都是通过请求的方式来发起的,而在ES之中请求协议使用的是TCP,在大量文档需要导入的场景下,我们通过TCP逐条导入的话,势必是低效的。组合多个任务执行,通过组合执行构建task之后再获取执行效果的方式来做批量处理的方式 & 效果,从而提高性能。
需要注意:这个批次的处理并非是堆越多越好的,当数量的大小超过 10M 之后就会反而无法继续提高性能。一般来说,假设一条数据的量是 1k,而ES推荐批量处理数据是 5M - 10M,因此如果想要通过使用bulk进行优化,**推荐最先设置批量处理的数据大小是 5000 ,然后逐条数据量提高,到8000,到12000。**直到感觉不到性能提升为止。 -
减少index refresh :其实就是通过降低触发 index buffer 调用 refersh 生成新的 Lucene Segment的频率的方式,从而减少实际上生成 Segment 的数量,进而提高实际上的存储效率。
-
indices.memory.index_buffer_size
indices.memory.index_buffer_size
其实是一个静态的配置项,需要在elasticsearch.yml
之中进行配置,需要注意的是,这个配置需要重启es集群的服务才能实际生效。index_buffer_size
本身是一个配置项目,允许我们针对index_buffer
大小做修改,需要注意的是,这里配置的index_buffer_size
其实是针对空间内的所有的节点做分片共享的。 (index_buffer_size
的默认大小是堆大小的 10% )elasticsearch.yml (需要重启)
需要注意的是这里的index_buffer是每个分片都会具有的大小,所以如果有三个节点,那么实际上是 3 * index_buffer_size 的大小缓冲共享的
indices.memory.index_buffer_size: mb / gb # 静态配置 默认 10%,可选[10-20%] indices.memory.min_index_buffer_size: mb / gb # 配置最少index_buffer大小(可以默认值) indices.memory.max_index_buffer_size: mb / gb # 配置最大index_buffer大小(可以默认值)
-
index.refresh_interval
index_refresh_interval
实际上改值就是修改index_buffer刷新写入到 index_refresh_interval 的值来调整 refresh 的时间间隔,该值如果更大,那么在合适的情况下,会提高Segement的利用率。值得一提的是,如果将改值的配置为 -1 ,那么就会完全禁止自动的 refersh 行为,而是更改为只有当 index_buffer 满了之后,才会自动进行 refresh,在大量批量导入的时候,可以使用该配置。
通过请求来修改
// 将 refresh 间隔修改为 -1 不自动触发 或者在 创建索引的时候设置 PUT /${index}/_settings { "refresh_interval": -1 } // 修改完毕之后,通过命令查看当前索引的 refresh_interval 是多少 GET ${index}/_settings // 批量插入完毕之后,可以通过以下配置恢复正常 PUT /${index}/_settings { "index" : { "refresh_interval" : null } }
-
-
加大 transaction log flush 间隔 :在ES介绍存储的时候,介绍过 写入到 index_buffer -> refersh -> systemfile cache (transaction log) -> flush -> 持久化磁盘 (commit log)。也在上次的文档之中提到过,进行 transaction log flush 进行持久化处理之前文档其实都在缓存之中,es为了避免出现意外情况系统宕机等情况出现,往往会尽快的做 flush 动作将文档刷盘到磁盘之中。
但如果我们的操作是大批量的文档导入,这个时候,频繁的flush反而会成为我们批量插入性能影响的来源,因此 加大 flush 的时间间隔也成为优化的一种方式。模板
PUT ${index}/_settings { "index.translog.durability":"async", // 关闭transtion log (改为异步处理) "index.translog.sync_interval": "240s", // 加大持久化间隔 -- 使得触发更少 "index.translog.flush_threshold_size": "512m" //加大translog触发大小 //让触发频率更低 }
批量导入之后,如果想要恢复默认配置
PUT /${index}/_settings { "index.translog.durability": null, "index.translog.sync_interval": null, "index.translog.flush_threshold_size": null }
恢复完毕之后,如果有需要可以手动触发flush
POST /${index}/_flush
-
负载均衡让请求落到不同的节点处理:其实就是直接不指定文档的ID,而是使用默认情况下es提供的ID生成,自动ID生成的值,可以让数据更加均匀的分布在各个节点上,从而提高写入的效率(不需要检查ID是否已经存在),并且避免节点查询时,由于数据分配不均匀导致可能会出现的部分节点压力很大的问题。
-
减少副本节点的数量:显然的,我们设置副本的目的是为了保证主分片挂掉的情况下,es能快速的使用副本来恢复集群的服务,但是从写入的角度来看,大量的副本自然而然的也会导致同步压力的增大,越多的副本,最终实现数据一致性的压力、消耗的时间、资源也久越大,所以并不推荐设置过多的副本尤其在大量数据导入的时候。
因此推荐的做法是,在大批量导入的时候,我们可以先把副本的数量减少,最好减少的0,先完成导入操作,然后再恢复副本。// 使用_settings重新设定副本数 PUT /${index}/_settings { "number_of_replicas": n // 先设置为0,然后再重新恢复 }
总结
总结以上所有的ES层面批量导入的优化,我们可以在导入之前事先使用以下配置最大程度上优化,并最后将配置还原为默认配置。(当然如果原先索引本身不是默认配置,可以先使用请求查看配置方便后续恢复)
查看原先配置
GET ${index}/_settings
// 查询结果
{
"articles" : {
"settings" : {
"index" : {
"routing" : {
"allocation" : {
"include" : {
"_tier_preference" : "data_content"
}
}
},
"refresh_interval" : "-1",
"number_of_shards" : "3",
"translog" : {
"flush_threshold_size" : "512m",
"sync_interval" : "240s",
"durability" : "async"
},
"provided_name" : "articles",
"creation_date" : "1728712534571",
"number_of_replicas" : "1",
"uuid" : "Fux4F9-FQKev8wVjBVxwpA",
"version" : {
"created" : "7130099"
}
}
}
}
}
修改配置
如果我们可以重启 es 集群,可以先修改 elasticsearch.yml 配置文件,先修改单个分片允许使用的 index_buffer 的大小
indices.memory.index_buffer_size:${elasticsearch jvm 堆 10 ~ 20%}
indices.memory.min_index_buffer_size:${48mb} // 默认 48mb,可以更小,但是一般不会改
indices.memory.max_index_buffer_size: ${512mb} // 默认无限制,可以加一个大小限制,免得单个分片消耗过多
修改完静态配置之后,我们可以通过使用请求做动态配置的修改
PUT ${index}/_settings
{
"settings": {
"number_of_replicas": 0,
"index.refresh_interval": "-1",
"index.translog.durability":"async",
"index.translog.sync_interval": "240s",
"index.translog.flush_threshold_size": "512m"
}
}
使用 bluk 做批量的数据导入
通过动态配置将配置重新修改回默认情况
PUT ${index}/_settings
{
"settings": {
"number_of_replicas": n,
"index.refresh_interval": null,
"index.translog.durability": $["request" / null],
"index.translog.sync_interval": null,
"index.translog.flush_threshold_size": null
}
}
恢复默认的静态配置 (如果原先没有,这里直接删掉就行)
使用层面上的优化
除了上文的在批量导入的场景下,我们可以通过一些参数的修改来提供使用的性能,而在平时时候的时候,我们也有一些办法可以让我们的性能得到一定程度上的提升。
-
深分页或者返回的结构集不易过大
-
查询返回的字段应该尽量的少
-
聚合的字段不可以有存在大量不重复值,这会导致聚合性能很差
-
模糊查询有可能会导致内存溢出
-
不要对text做排序
-
插入文档不要自己指定文档 ID
-
大量的段合并会导致 I/O 资源爆炸
-
大量插入必须考虑上文的优化
-
不要将ES的堆空间 -Xms -Xmx设置过大,Lucene本身还会占用一些内存空间,建议是不要超过机器本身一半内存
-
GC回收算法应当尽量选择更好的更适合的,否则内存不够
数据搜索优化示例
在针对类似于消费记录、短信推送记录、登录记录、签到记录一类的数据做统计消费人员类型级别的统计处理的时候,往往有一些场景下需要针对记录数量多、价格统计总和多、时间排序前的人做聚合处理。这个时候往往会涉及到聚合功能方面上的查询及优化问题。
在集群节点、分片分配都完全合理的情况下,如果我们的记录本身有大量的数据的话,往往还是需要大量的时间做统计计算。几乎百分百会请求超时。例如下面的例子:
GET sms/_search
{
"aggs": {
"sms_count": {
"terms": {
"field": "phone",
"size": 1000
}
}
},
"size": 0
}
// 哪怕我们直接加上时间范围的过滤,还是一样的慢
GET sms/_search
{
"query": {
"range": {
"sent_time": {
"gte": "2022-08-03T00:00:00Z",
"lt": "2022-08-04T00:00:00Z"
}
}
},
"aggs": {
"sms_count": {
"terms": {
"field": "phone",
"size": 100
}
}
},
"size": 0
}
那为什么会这么慢呢?其实原因就跟 Cardinality 有关,实际上es在发现我们查询的字段本身是keyword并且使用到了 Global Ordinals的时候,就会默认的在内部先构建 Global Ordinals,甚至这个优先的操作,会高于我们的 query 行为查询,而实际上构建的这个行为其实相当消耗性能。这就导致了我们查询会这么慢的原因。(关于Ordinals的介绍在另外一个文档,ES存储 & 搜索原理之中有所介绍,可以看那个文档)
如何解决这个问题呢?
(High Cardinality 问题,也就是聚合结果数量没多少,但是缺需要在聚合之前做Global Ordinal Mapping消耗大量时间)
在Ordinals介绍的时候,我们就已经知道实际上Ordinals不开启Global Ordinals的情况下也会在各个分片下的 Segment 内部就已经存在,而如果我们开启 Global Ordinals 就需要将Segment的Ordinals统一规划到Shard级别,也就是分片级别上,显然的在数据量一样的情况下,我们分片Shard的数量越多,单个Shard需要处理出来的Global Ordinals Mapping就会越少,因此我们可以通过增加分片的方式来优化。(也显然,增加分片进行优化的话,太过理想)
其次,同样的在针对 Ordinals 进行介绍的时候,其实就已经提到过,我们可以通过使用 egaer_global_ordinals
让构造从使用聚合查询的时候,提前到数据写入的时候提前构造。这样可以有效的提高我们在做聚合查询时候的性能。
还有一种办法,也可以提高一部分情况下的聚合性能,通过修改 terms 聚合下的 execution_hint 的值直接逃避创建 global_ordinals_mapping
而是直接通过字段的值进行聚合,当然这种方式不适合聚合的组很多的情况。
总的来说,就是可以采取以下三种方式来尝试优化上面的聚合
- 增加分片的数量,可以降低单个分片的聚合计算压力
- 配置
eager_global_ordinals
并且将index_refersh
调整参数来true - 修改 terms 聚合 exection_hint 值来跳过构造 Global Ordinals Mapping 来构造步骤,在terms聚合重 execution_hit 可选用 map 或者 global_ordinals
- map:直接使用字段的值,来分桶聚合统计,适合在聚合少量数据的使用,也就是不使用 Global Ordinals Mapping
- global_ordinals,使用 Global Ordinals Mapping 来辅助聚合数据
GET sms/_search
{
"query": {
"range": {
"sent_time": {
"gte": "2022-08-03T00:00:00Z",
"lt": "2022-08-04T00:00:00Z"
}
}
},
"aggs": {
"sms_count": {
"terms": {
"field": "phone",
"execution_hint": "map",
"size": 100
}
}
},
"size": 0
}