# 01. 选择合理的硬件配置:尽可能使用 SSD
使用SSD通常比机械硬盘查询速度快5~10倍,写入性能提升不明显。对于文档检索类查询性能要求较高的场景,建议考虑 SSD 作为存储,同时按照 1:10 的比例配置内存和硬盘。对于日志分析类查询并发要求较低的场景,可以考虑采用机械硬盘作为存储,同时按照 1:50 的比例配置内存和硬盘。单节点存储数据建议在2TB以内,不要超过5TB,以避免系统不稳定。
# 02. 给JVM配置机器一半的内存,但是不建议超过32G
修改conf/jvm.options配置,-Xms 和 -Xmx 设置为相同的值,推荐设置为机器内存的一半左右,剩余一半留给操作系统缓存使用。JVM 内存建议不要低于 2G,JVM 建议不要超过 32G,否则 JVM 会禁用内存对象指针压缩技术,造成内存浪费。机器内存大于 64G 内存时,推荐配置 -Xms30g -Xmx30g。JVM 堆内存较大时,内存垃圾回收暂停时间比较长,建议配置 ZGC(低延迟垃圾回收器) 或 G1(垃圾优先收集器) 垃圾回收算法。
# 03. 规模较大的集群配置专有主节点,避免脑裂问题
ES 主节点负责集群元信息管理、index 的增删操作、节点的加入剔除,定期将最新的集群状态广播至各个节点。在集群规模较大时,建议配置专有主节点只负责集群管理,不存储数据,不承担数据读写压力。
# 专有主节点配置(conf/elasticsearch.yml):
node.master:true
node.data: false
node.ingest:false
# 数据节点配置(conf/elasticsearch.yml):
node.master:false
node.data:true
node.ingest:true
Elasticsearch 默认每个节点既是候选主节点,又是数据节点。最小主节点数量参数minimum_master_nodes 推荐配置为候选主节点数量一半以上,该配置告诉 ES 当没有足够的 master 候选节点的时候,不进行 master 节点选举,等 master 节点足够进行选举。
例如对于 3 节点集群,最小主节点数量从默认值 1 改为 2。
# 最小主节点数量配置(conf/elasticsearch.yml):
discovery.zen.minimum_master_nodes: 2
# 04. Linux操作系统调优
关闭交换分区,防止内存置换降低性能。
# 将/etc/fstab 文件中包含swap的行注释掉
sed -i '/swap/s/^/#/' /etc/fstab
swapoff -a
# 单用户可以打开的最大文件数量,可以设置为官方推荐的65536或更大些
echo "* - nofile 655360" >> /etc/security/limits.conf
# 单用户线程数调大
echo "* - nproc 131072" >> /etc/security/limits.conf
# 单进程可以使用的最大map内存区域数量
echo "vm.max_map_count = 655360" >> /etc/sysctl.conf
# 参数修改立即生效
sysctl -p
# 05. 设置合理的索引分片数和副本数
索引分片数建议设置为集群节点的整数倍,初始数据导入时副本数设置为0,生产环境副本数建议设置为 1(设置 1 个副本,集群任意 1 个节点宕机数据不会丢失;设置更多副本会占用更多存储空间,操作系统缓存命中率会下降,检索性能不一定提升)。单节点索引分片数建议不要超过3个,每个索引分片推荐 10-40GB ,索引分片数设置后不可以修改,副本数设置后可以修改。
# 索引设置
curl -XPUT http://localhost:9200/fulltext001?pretty -H 'Content-Type: application/json'
-d '{
"settings": {
"refresh_interval": "30s",
"merge.policy.max_merged_segment": "1000mb",
"translog.durability": "async",
"translog.flush_threshold_size": "2gb",
"translog.sync_interval": "100s",
"index": {
"number_of_shards": “15", //5节点+3索引分片单机
"number_of_replicas": "0"
}
}
}'
# mapping 设置
curl -XPOST http://localhost:9200/fulltext001/doc/_mapping?pretty -H 'Content-Type: application/json'
-d '{
"doc": {
"_all": {
"enabled": false
},
"properties": {
"content": {
"type": "text",
"analyzer": "ik_max_word"
},
"id": {
"type": "keyword"
}
}
}
}'
# 写入数据示例
curl -XPUT 'http://localhost:9200/fulltext001/doc/1?pretty' -H 'Content-Type: application/json'
-d '{
"id": "https://www.huxiu.com/article/215169.html",
"content": "“娃娃机,迷你KTV,VR体验馆,堪称商场三大标配‘神器’。”一家地处商业中心在过去的这几个月里,几乎所有的综合体都“标配”了三种“设备”…"
}'
# 修改副本数示例
curl -XPUT "http://localhost:9200/fulltext001/_settings" -H 'Content-Type: application/json'
-d '{
"number_of_replicas": 1
}'
# 06. 使用批量请求
使用批量请求将产生比单文档索引请求好得多的性能。写入数据时调用批量提交接口,推荐每批量提交5~15MB数据。如单条记录 1KB 大小,每批次提交 10000 条左右记录写入性能较优。
# 批量请求接口API
curl -XPOST "http://localhost:9200/_bulk" -H 'Content-Type: application/json'
-d'
{ "index" : { "_index" : "test", "_type" : "_doc", "_id" : "1" } }{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_type" : "_doc", "_id" : "2" } }
{ "create" : { "_index" : "test", "_type" : "_doc", "_id" : "3" } }{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_type" : "_doc", "_index" : "test"} }{ "doc" : {"field2" : "value2"} }'
# 07. 通过多进程/线程发送数据
单线程批量写入数据往往不能充分利用服务器 CPU 资源,可以尝试调整写入线程数或者在多个客户端上同时向 ES 服务器提交写入请求。通过测试确定最佳的 worker 数量。可以通过逐渐增加工作任务数量来测试,直到集群上的 I/O 或 CPU 饱和。
# 08. 调大写入的refresh interval
ES写入和打开一个新段的轻量过程叫做refresh,可以通过设置 refresh_interval,降低每个索引的刷新频率。
# 设置 refresh interval API
curl -XPUT "http://localhost:9200/index" -H 'Content-Type: application/json'
-d'{
"settings": {
"refresh_interval": "30s"
}
}'
refresh_interval 可以在已经存在的索引上进行动态更新,生产环境中建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时再把它们调回来。
curl -XPUT "http://localhost:9200/index/_settings" -H 'Content-Type: application/json'
-d'{ "refresh_interval": -1 }'
curl -XPUT "http://localhost:9200/index/_settings" -H 'Content-Type: application/json'
-d'{ "refresh_interval": "30s" }'
# 09. 配置写入的事务日志参数
事务日志 translog 的设计目的是帮助 shard 恢复操作,否则数据可能会从内存 flush 到磁盘时发生意外而丢失。事务日志 translog 的落盘(fsync)是 ES 在后台自动执行的,默认每5秒钟提交到磁盘上,或者当 translog 文件大小大于 512MB 提交,或者在每个成功的索引、删除、更新或批量请求时提交。 索引创建时,可以调整默认日志刷新间隔index.translog.sync_interval: "60s"。创建索引后可以动态调整translog参数,"index.translog.durability":"async" 相当于关闭了 index、bulk 等操作的同步 flush translog 操作,仅使用默认的定时刷新、文件大小阈值刷新机制。
# 动态设置 translog API
curl -XPUT "http://localhost:9200/index" -H 'Content-Type: application/json'
-d'{
"settings": {
"index.translog.durability": "async",
"translog.flush_threshold_size": "2gb"
}
}'
# 10. 设计mapping配置合适的字段类型
ES 写入文档时,如果请求中指定的索引名不存在,会自动创建新索引,并根据文档内容猜测可能的字段类型。但这不是最高效的,可以根据应用场景来设计合理的字段类型。根据业务场景设计索引配置合理的分片数、副本数,设置字段类型、分词器。如果不需要合并全部字段,禁用_all字段,通过copy_to来合并字段。
curl -XPOST "http://localhost:9200/twitter/doc/_mapping?pretty" -H 'Content-Type: application/json'
-d'{
"doc": {
"_all": {
"enabled": false
},
"properties": {
"user": {
"type": "keyword"
},
"post_date": {
"type": "date"
},
"message": {
"type": "text",
"analyzer": "cjk"
}
}
}
}’
document 模型设计
ES里复杂的关联查询尽量别用,最好是先在 Java 里就完成关联,将关联好的数据直接写入ES中。搜索时就不需要利用ES的搜索语法来完成 join 之类的关联搜索。所以document 模型设计是非常重要的,复杂操作尽量在 document 模型设计和写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。
# 11. 使用过滤器缓存和分片查询缓存
默认情况下ES 的查询会计算返回的每条数据与查询语句的相关度,但对于非全文索引的使用场景只想精确地查找目标数据时,通过 filter 让 ES 不计算评分,并且尽可能地缓存 filter 的结果集,供后续包含相同 filter 的查询使用,提高查询效率。
# 普通查询
curl -XGET "http://localhost:9200/twitter/_search" -H 'Content-Type: application/json'
-d'{
"query": {
"match": {
"user": "kimchy"
}
}
}'
# 过滤器(filter)查询
curl -XGET "http://localhost:9200/twitter/_search" -H 'Content-Type: application/json'
-d'{
"query": {
"bool": {
"filter": {
"match": {
"user": "kimchy"
}
}
}
}
}'
分片查询缓存的目的是缓存聚合、提示词结果和命中数(它不会缓存返回的文档,因此,它只在 search_type=count 时起作用)。分片缓存的大小默认情况下是 JVM 堆的 1% 大小,也可手动设置在config/elasticsearch.yml文件里。
indices.requests.cache.size: 1%
查看缓存占用内存情况(name 表示节点名, query_cache 表示过滤器缓存,request_cache 表示分片缓存,fielddata 表示字段数据缓存,segments 表示索引段)。
curl -XGET "http://localhost:9200/_cat/nodes?h=name,query_cache.memory_size,request_cache.memory_size,fielddata.memory_size,segments.memory&v"
# 12. 使用路由 routing
ES写入文档时,文档会通过一个公式路由到一个索引中的一个分片上。默认的公式如下:
shard_num = hash(_routing) % num_primary_shards
_routing 字段的取值,默认是 _id 字段,可以根据业务场景设置经常查询的字段作为路由字段。例如可以考虑将用户 id、地区作为路由字段,查询时可以过滤不必要的分片,加快查询速度。
# 写入时指定路由
curl -XPUT "http://localhost:9200/my_index/my_type/1?routing=user1" -H 'Content-Type: application/json'
-d'{
"title": "This is a document",
"author": "user1"
}'
# 查询时不指定路由,需要查询所有分片,查询时指定路由,只需要查询1个分片
curl -XGET "http://localhost:9200/my_index/_search?routing=user1" -H 'Content-Type: application/json'
-d'{
"query": {
"match": {
"title": "document"
}
}
}'
# 返回结果
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
}
... ...
}
# 13. 强制合并只读索引,关闭历史数据索引
只读索引可以从合并成一个单独的大segment中收益,减少索引碎片,减少 JVM 堆常驻内存。强制合并索引操作会耗费大量磁盘 IO,尽量配置在业务低峰期执行。历史数据索引如果业务上不再支持查询请求,可以考虑关闭索引,减少 JVM 内存占用。
# 索引forcemerge API
curl -XPOST "http://localhost:9200/abc20180923/_forcemerge?max_num_segments=1"
# 索引关闭API
curl -XPOST "http://localhost:9200/abc2017*/_close"
# 14. 配置合适的分词器
ES 内置了很多分词器,也可以安装自研/开源分词器。根据业务场景选择合适的分词器,避免全部采用默认 standard 分词器。
常用分词器:
cjk:根据二元索引对中日韩文分词,可以保证查全率。
IK:比较热门的中文分词,能按照中文语义切分,可以自定义词典。
pinyin:可以让用户输入拼音,就能查找到相关的关键词。
aliws:阿里巴巴自研分词,支持多种模型和分词算法,词库丰富,分词结果准确,适用于对查准要求高的场景。
# 分词效果测试API
curl -XPOST "http://localhost:9200/_analyze" -H 'Content-Type: application/json'
-d'{
"analyzer": "ik_max_word",
"text": "南京市长江大桥"
}'
# 15. 配置查询聚合节点
查询聚合节点可以发送粒子查询请求到其他节点,收集和合并结果,以及响应发出查询的客户端。通过给查询聚合节点配置更高规格的 CPU 和内存,可以加快查询运算速度、提升缓存命中率。
# 查询聚合节点配置(conf/elasticsearch.yml):
node.master:false
node.data:false
node.ingest:false
# 16. 设置查询读取记录条数和字段
默认的查询请求通常返回排序后的前 10 条记录,最多一次读取 10000 条记录,通过 from 和 size 参数控制读取记录范围,避免一次读取过多的记录。通过 _source 参数可以控制返回字段信息,尽量避免读取大字段。
# 查询请求示例
curl -XGET http://localhost:9200/fulltext001/_search?pretty -H 'Content-Type: application/json'
-d '{
"from": 0,
"size": 10,
"_source": "id",
"query": {
"bool": {
"must": [
{
"match": {
"content": "虎嗅"
}
}
]
}
},
"sort": [
{
"id": {
"order": "asc"
}
}
]
}'
# 17. 设置 teminate_after 查询快速返回
配teminate_after指定每个 shard 最多匹配 N 条记录后返回,设置查询超时时间 timeout。在查询结果中可以通过 “terminated_early” 字段标识是否提前结束查询请求。
# teminate_after 查询语法示例
curl -XGET "http://localhost:9200/twitter/_search" -H 'Content-Type: application/json'
-d'{
"from": 0,
"size": 10,
"timeout": "10s",
"terminate_after": 1000,
"query": {
"bool": {
"filter": {
"term": {
"user": "elastic"
}
}
}
}
}'
# 18. 避免查询深度翻页
ES 默认只允许查看排序前 10000 条的结果,当翻页查看排序靠后的记录时,响应耗时一般较长。ES的分页陷阱-假如每页是10条数据,现在要查询第 100 页,实际上是会把每个 shard 上存储的前 1000 条数据都查到一个协调节点上,如果你有个5个shard,那么就有5000条数据,接着协调节点对这5000 条数据进行一些合并处理,再获取到最终第 100 页的 10 条数据。 分布式集群中,必须得从每个 shard 都查 1000 条数据过来,然后根据需求进行排序、筛选等操作,最后再次分页,拿到里面第 100 页的数据。所以翻页越深,每个 shard 返回的数据就越多,而且协调节点处理的时间越长,非常坑爹。所以用不允许深度分页。
如果是APP下拉可以用 scroll api,scroll 会一次性生成所有数据的一个快照,然后每次滑动向后翻页就是通过游标 scroll_id移动,获取下一页,性能会比分页要高很多,基本上都是毫秒级的。但是这个适合于下拉翻页的不能随意乱跳页的场景。所以现在很多产品都不允许随意翻页的。初始化时必须指定scroll参数,告诉ES要保存此次搜索的上下文多长时间。你需要确保用户不会持续不断翻页翻几个小时,否则可能因为超时而失败。
除了用 scroll api,也可用 search_after 来做,search_after 的思想是使用前一页的结果来帮助检索下一页的数据,显然,这种方式也不允许你随意翻页,你只能一页页往后翻。初始化时,需要使用一个唯一值的字段作为 sort 字段。使用search_after方式查询会更轻量级,如果每次只需要返回 10 条结果,则每个 shard 只需要返回 search_after 之后的 10 个结果即可,返回的总数据量只是和 shard 个数以及本次需要的个数有关,和历史已读取的个数无关。
# search_after查询语法示例
curl -XGET "http://localhost:9200/twitter/_search" -H 'Content-Type: application/json'
-d'{
"size": 10,
"query": {
"match": {
"message": "Elasticsearch"
}
},
"sort": [
{
"_score": {
"order": "desc"
}
},
{
"_id": {
"order": "asc"
}
}
],
"search_after": [
0.84290016, //上一次response中某个doc的score
"1024" //上一次response中某个doc的id
]
}’
# 19. 避免前缀模糊匹配
Elasticsearch 默认支持通过 *? 正则表达式来做模糊匹配,如果在一个数据量较大规模的索引上执行模糊匹配,尤其是前缀模糊匹配,通常耗时会比较长,甚至可能导致内存溢出。尽量避免在高并发查询请求的生产环境执行这类操作。
如查询请求"角色:*A8848*"查询时,往往导致整个集群负载较高。通过对数据预处理,增加冗余字段 "角色.keyword",并事先将所有角色按照1、2、3...7分词后存储至该字段,字段存储内容示例:京,A,8,4,京A,A8,88,84,48,京A8...京A88488。通过查询"角色.keyword:A8848"即可解决原来的性能问题。
# 20. 避免索引稀疏
Elasticsearch7.X 版本只允许 type 值为“_doc”,在一个索引下面创建多个字段不一样的 type,或者将几百个字段不一样的索引合并到一个索引中,会导致索引稀疏问题。 建议每个索引下只创建一个 type,字段不一样的数据分别独立创建index,不要合并成一个大索引。每个查询请求根据需要去读取相应的索引,避免查询大索引扫描全部记录,加快查询速度。
# 21. 扩容集群节点个数,升级节点规格
# 22. 数据预热
后台监控系统每隔一段时间去系统搜索一下热数据,刷到 filesystem cache 里去。对于访问高频数据,最好做一个专门的缓存预热子系统,就是对热数据每隔一段时间,就提前访问一下,让数据进入 filesystem cache 里面去,这样二次访问性能提升很多。
# 23. 数据冷热分离
ES可以做类似于mysql的水平拆分,即将低频冷数据和高频热数据分别写不同的索引,这样可确保热数据在被预热之后,尽量都留在 filesystem os cache 里,不受冷数据冲掉。比如有6台机器,两个索引,一个放冷数据,一个放热数据,每个索引 3 个shard。3台机器放热数据 index,另外3台机器放冷数据 index。这样大量的时间是在访问热数据 index,热数据量的占比小就能保证其保留在 filesystem cache 里,确保热数据的访问高性能。由于冷热索引的隔离,即使访问冷数据都在磁盘上,此时性能差点,但访问低频就能接受。
# 24. 合理选取搜索字段
比如现在有一行数据,id,name,age .... 30 个字段。但是现在搜索只需要根据 id,name,age 三个字段来搜索。仅仅写入 es 中要用来检索的少数几个字段就可以了,比如说就写入ES id,name,age 三个字段,然后你可以把其他的字段数据存在mysql/hbase 里,用 ES+ hbase 架构伸缩性更好。因为Hbase的特点是适用于海量数据的在线存储,就是对 Hbase 可以写入海量数据,但是不要做复杂的搜索,而是做很简单的一些根据 id 或者范围进行查询的操作。从 es 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 doc id,然后根据 doc id 到 Hbase 里去查询每个 doc id 对应的完整的数据,查出来返回给前端。写入ES的数据最好小于等于或者是略微大于ES的 filesystem cache 的内存容量。先从ES检索,然后再根据 ES返回的 id 去 hbase 里查询。
注25:性能优化的关键点——filesystem cache
往ES写的数据,实际上都写到磁盘文件里去了,查询时操作系统会将磁盘文件里的数据自动缓存到 filesystem cache 里。ES搜索引擎严重依赖于底层的filesystem cache,如果给 filesystem cache 更多的内存,尽量让内存可以容纳所有的 idx segment file 索引数据文件,那么搜索时基本都是走内存的,性能会非常高。性能差距根据之前很多的测试和压测,如果走磁盘一般肯定上秒,搜索性能绝对是秒级别的,如果是走 filesystem cache,是走纯内存的,那么一般来说性能比走磁盘要高一个数量级,基本上就是毫秒级的,从几毫秒到几百毫秒不等。
如原先公司ES节点有 3 台机器,每台机器内存64G,总内存就是 64 * 3 = 192G。每台机器给ES jvm heap 是 32G,那么留给 filesystem cache的就是每台机器32G,总共集群里filesystem cache 的内存就是 32 * 3 = 96G。而此时整个磁盘上索引数据文件,在 3 台机器上共占用了1T 磁盘容量,ES数据量是 1T,那么每台机器的数据量是 300G。filesystem cache 的内存才 100G,十分之一的数据可以放内存,其他的都在磁盘,然后你执行搜索操作,大部分操作都是走磁盘,性能肯定差。要让ES性能要好,最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的50%。
最佳的情况是仅仅在ES中就存少量的数据,就是你要用来搜索的那些索引,如果内存留给 filesystem cache 的是 100G,那么你就将索引数据控制在 100G 以内,这样的话,你的数据几乎全部走内存来搜索,性能很高1 秒以内。