分布式搜索潜在问题
搜索的机制
- Query 阶段
- 用户发出搜索请求到 Elasticsearch 节点。
- 节点收到请求后,会以 Coordinating 节点的身份,给其他选中分片节点发送查询请求。
- 被选中的分片节点执行查询,进行排序,每个分片都会返回 From + Size 个排序后的文档ID和排序值给 Coordinating 节点。
- Fetch 阶段
- 相关性算分不准
- 每个分片都基于自己的分片上的数据进行相关度计算。这回导致打分偏离的清空,特别是数据量很少的时候,相关性算分在分片之间是相对独立的。当文档总数很少的情况下,主分片数越多,相关性算分会越不准。
- 深度分页(性能问题)
- 当一个查询:from = 990,size = 10。会在每个分片上都获取 1000 个文档。页数越深,占用内存越多。为了避免深度分页带来的内存开销。ES 有一个设定,默认限定到 100000 个文档。即 from 或 size 大于等于 1000,Elasticsearch 默认返回异常。
- 最终协调节点需要处理:节点数 * (from + size)数据。
解决相关性算分不准
- 数据量不大的时候,可以将主分片数设为1,当数量足够大时,只要保证文档均匀分散在各个分片上,结果一般就不会出现偏差
使用 DFS Query Then Fetch
不支持指定页数,只能往下翻
- 第一步搜索需要指定 sort,并且保证值是唯一的(可以通过加入 _id 保证唯一性)
- 使用上一次,最后一个文档的 sort 值进行查询 ``` DELETE users POST users/_doc {“name”:”user1”,”age”:10}
POST users/_doc {“name”:”user2”,”age”:20}
POST users/_doc {“name”:”user3”,”age”:30}
POST users/_doc {“name”:”user4”,”age”:40}
POST /users/_search { “size”: 1, “query”: { “match_all” : { } }, “sort”: [ {“age”: “desc”}, {“_id”: “asc”} ] }
POST /users/_search { “size”: 1, “query”: { “match_all” : { } },
# 最后一个排序的 sort 值
"search_after": [
40,
"aX9gr38BTR_twj_8RDhr"
],
"sort": [
{"age": "desc"},
{"_id": "asc"}
]
}
<a name="UAA2z"></a>
## Scroll API 解决深度分页
不要把 scroll 用于实时请求,它主要用于大数据量的场景。例如:将一个索引的内容索引到另一个不同配置的新索引中。scroll 是在 Elasticsearch 中建立一份有限时间的快照,通过游标进行分页的一种技术,一旦建立,新增更改的数据将不会体现在快照中。
users:索引名称
5m:快照建立的时间
POST /users/_search?scroll=5m { “size”: 1, “query”: { “match_all” : { } } }
POST /_search/scroll { “scroll” : “1m”, “scroll_id” : “DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAWAWbWdoQXR2d3ZUd2kzSThwVTh4bVE0QQ==” }
<a name="c0ujl"></a>
# 生产环境集群配置优化
<a name="GS76K"></a>
## 集群配置
- 静态配置文件尽量简洁:按照文档配置所有相关系统参数。elasticsearch.yml 配置文件中尽量只写必备参数。其他设置项可以通过 API 动态进行设定,动态设定分 transient 和 persistent 两种,都会覆盖elasticsearch.yml 中的设置。
- Transient 在集群重启后丢失
- Persistent 在集群重启后不会丢失
- 单个集群不要跨数据中心进行部署(不要使用 WAN)
- 节点之间的 hops 越少越好
- 如果有多块网卡,最好将 transport 和 http 绑定到不同的网卡,并设置不同的防火墙规则
- 按需为 Coordinating Node 或 Ingest Node 配置负载均衡。
- 建议使用中等配置的机器,不建议使用过于强劲的硬件配置
- 不建议在一台服务器上允许多个节点
<a name="By7Gt"></a>
## JVM 设置
- 应避免修改默认配置,配置文件:`config/jvm.options`
- 将内存 Xms 和 Xmx 设置成一样,避免 和 heap resize 时发生停顿。
- Xmx 设置不要超过物理内存的 50%;单个节点上,最大内存建议不要超过 32G 内存。
- 生产环境,JVM 必须使用 Server 模式
- 关闭 JVM Swapping
<a name="iDH8I"></a>
## 容量规划
- 选择合理的硬件,数据节点尽可能使用 SSD
- 搜索等性能要求高的常见,建议 SSD,按照 1:16 的比例配置内存和硬盘
- 日志类和查询并发低的常见,可以考虑使用机械硬盘存储,按照 1:48-1:96 的比例配置内存和硬盘
- 单节点数据建议控制在 2TB 以内,最大不建议超过 5TB
<a name="FszG9"></a>
## 优化分片
- 避免过多的分片,一个查询需要访问每个分片,分片过多,会导致不必要的查询开销
- 结合应用场景,控制单个分片的尺寸,搜索类应用 20GB,日志类应用 40GB
- 使用基于时间序列的索引,将只读的索引进行 Merge 索引合并,减少 segment 数量
<a name="AIunm"></a>
## 关闭 Dynamic Indexes
```yaml
PUT _cluster/settings
{
"persistent": {
"action.auto_create_index": false
}
}
PUT _cluster/settings
{
"persistent": {
"action.auto_create_index": "logstash-*,.kibana*"
}
}
提高集群写入性能
ES 的默认设置,已经综合考虑了数据可靠性,搜索的实时性,写入数据,一般不要盲目修改。一切优化都要基于高质量的数据建模。
- 客户端通过 BULK 批量操作,减少服务器的 IO 开销
- 降低 IO 操作:
- 使用 ES 自动生成文档id,如果是自定义id,ES 会检查冲突
- 降低 refresh 操作频率,通过降低数据实时性换取性能
- 减少 translog 实时落盘操作,通过降低数据安全性换取性能
- 降低 CPU 和存储开销
- 减少不必要的分词
- 避免不必要的 doc_values
- 文档的字段尽量保证相同顺序,可以提高文档的压缩率
- 尽可能做到写入和分片的负载,实现水平扩展(分片均衡分配)
- 热点数据过于集中,可能会产生性能问题
- 调整 Bulk 线程池和队列
关闭无用的功能
```jsonmy_index:索引名称
not_serach_field:字段名称
PUT my_index { “mappings”: { “properties”: { “not_serach_field”: { “index”: false } } } }
```json
# my_index:索引名称
# not_norms_field:字段名称
PUT my_index {
"mappings": {
"properties": {
"not_norms_field": {
"norms": false
}
}
}
}
# my_index:索引名称
# field_name:字段名称
PUT my_index {
"mappings": {
"properties": {
"field_name": {
"index_options": "docs"
}
}
}
}
PUT my_index {
"mappings": {
"dynamic": false
}
}
性能取舍
PUT my_index
{
"settings": {
"number_of_replicas": 0
}
}
降低 Refresh 的频率,通过增大 refresh_interval 的数值(默认为 1s,如果设置成 -1,会禁止自动 refresh),避免频繁的 refresh,生成过多的 segment 文件,降低搜索实时性。
增大静态配置参数 indices.memory.index_buffer_size (默认10%内存,超过自动触发 refresh)。
PUT my_index
{
"settings": {
"index": {
"refresh_interval": "30s"
}
}
}
降低日志写磁盘的频率,牺牲容灾能力。
- translog.durability:默认 request,每个请求都落盘,设置成 async,异步写入
- translog.sync_interval:异步写入间隔时间
translog.flush_threshod_size:默认 512mb,可以适当调大,当 translog 超过该值,触发 flush
PUT my_index
{
"settings": {
"translog": {
"durability": "async",
"sync_interval": "30s"
}
}
}
分片负载
设置合理的主分片数,确保均匀分布再所有数据节点上,通过
Index.routing.allocation.total_shards_per_node
每个索引在每台节点上可分配的主分片数量。PUT my_index
{
"settings": {
"routing": {
"allocation": {
"total_shards_per_node": 2
}
}
}
}
BULK 批量操作
客户端
- 单个 bulk 请求体的数据量不要太大,官方建议大约 5-15 MB
- 写入端的 bulk 请求超时需要足够长,建议 60s 以上
- 写入端尽量将数据轮询打到不同节点
服务器端
Elasticsearch 不等于关系型数据库,应尽可能构建非规范化数据结构,从而获取最佳的性能。
- 尽可能将数据先行计算,然后保存到 Elasticsearch 种,尽量避免查询时的 Script 计算
- 尽量使用 Filter Context,利用缓存机制,减少不必要的算分
- 严禁使用 * 开头通配符 Terms 查询,结合 profile,explain API 分析慢查询的问题,持续优化数据建模
- 聚合查询会消耗内存,特别是针对很大的数据集进行聚合运算,如果可以控制聚合的数量,就能减少内存的开销
```jsonPUT blogs/_doc/1
{
"title":"elasticsearch"
}
GET blogs/_search
{
"query": {
"bool": {
"must": [
{"match": {
"title": "elasticsearch"
}}
],
"filter": {
"script": {
"script": {
"source": "doc['title.keyword'].value.length()>5"
}
}
}
}
}
}
未优化前
GET blogs/_search { “query”: { “bool”: {
} } }"must": [
{"match": {"title": "elasticsearch"}},
{
"range": {
"publish_date": {
"gte": 2017,
"lte": 2019
}
}
}
]
优化后
GET blogs/_search { “query”: { “bool”: { “must”: [ {“match”: {“title”: “elasticsearch”}}, { “filter”: { “range”: { “publish_date”: { “gte”: 2017, “lte”: 2019 } } } } ] } } }
```json
GET test/_search
{
"query": {
"wildcard": {
"title": {
"value": "*elastic*"
}
}
}
}
force merge
当 Index 不再有写入操作的时候,建议对其进行 force merge,提升查询速度,减少内存开销。
# geonames:索引名称
POST geonames/_forcemerge?max_num_segments=1
Merge 优化,降低最大分段大小,避免较大的分段继续参与 Merge,节省系统资源。(最终会有多个分段)
Index.merge.policy.segments_per_tier
,默认为10,越小需要越多的合并操作Index.merge.policy.max_merged_segment
,默认 5 GB,segment file 超过此大小以后,就不再参与后续的合并操作集群压力测试
目的:容量规划、性能优化、版本间性能比较、性能诊断、确定系统稳定性,考察系统功能极限和隐患。
压力测试的方法与步骤:制定测试计划(确定测试场景和测试数据集);脚本开发;测试环境搭建(不同的软硬件配置)、运行测试;分析比较结果。
测试目标测试集群的读写性能、做集群容量规划
- 对 Elasticsearch 配置参数进行修改,评估优化效果
- 修改 Mapping 和 Setting,对数据建模进行优化,并测试评估性能改进
- 测试 Elasticsearch 新版本,结合实际场景和老版本进行比较,评估是否进行升级
测试脚本
- Elasticsearch 本身提供了 REST API,所以,可以通过很多传统的性能测试工具
- Load Runner:商业软件,支持录制+重放+DSL
- JMeter:Apache 开源,Record & Play
- Gatling:开源,支持写 Scala 代码+DSL
- 专门为 Elasticsearch 设计的工具
- ES Pref & Elasticsearch-stress-test
- Elastic Rally
缓存的使用以及 Breaker 限制
缓存主要分类
Node Query Cache
保存的是 Segment 级缓存命中的结果。Segment 被合并后,缓存会失效。
每个节点都有一个 Node Query 缓存,采用 LRU算法,由该节点的所有 Shard 共享,只缓存 Filter Context 查询相关内容。通过静态配置,需要设置在每个 Data Node 上:
Node Level - indices.queries.cache.size: “10%”
Index Level - index.queries.cache.enabled: true
Shard Query Cache
分片 Refresh 时候,Shard Request Cache 会失效。如果 Shard 对应的数据频繁发生变化,该缓存的效率会很差。
缓存每个分片上的查询结果,只会缓存设置了 size=0 的查询结果。不会缓存 hits,但是会缓存 Aggregations 和 Suggestions,通过 LRU 算法,将整个 JSON 查询作为 Key,与 JSON 对象的顺序相关,通过静态配置:
indices.requests.cache.size: “1%”
Fielddata Cache
Segment 被合并后,会失效。
除了 Text 类型,默认都采用 doc_values。节约了内存。Text 类型的字段需要打开 Fileddata 才能对其进行聚合和排序,Text 经过分词,排序和聚合的效果不佳,建议不要轻易使用,通过静态配置:
可以控制 Indices.fielddata.cache.size,避免产生 GC(默认无限制)诊断内存状况
```json GET _cat/nodes?v
GET _nodes/stats/indices?pretty
GET _cat/nodes?v&h=name,queryCacheMemory,queryCacheEvictions,requestCacheMemory,requestCacheHitCount,request_cache.miss_count
GET _cat/nodes?h=name,port,segments.memory,segments.index_writer_memory,fielddata.memory_size,query_cache.memory_size,request_cache.memory_size&v
<a name="Vmozd"></a>
## 常见的内存问题
- Segments 个数过多,导致 full GC
- 现象:集群整体响应缓慢,也没有特别多的数据读写,但是发现节点在持续进行 Full GC
- 分析:查看 Elasticsearch 的内存使用,发现 segments.memory 占用很大空间
- 解决:通过 force merge,把 segments 合并成一个。
- 建议:对于不再写入和更新的索引,可以将其设置成制度。同时,进行 force merge 操作。如果问题依然存在,则需要考虑扩容。此外,对索引进行 force merge,还可以减少对 global_ordinals 数据结构的构建,减少对 fielddata cache 的开销。
---
- Field data cache 过大,导致 full GC
- 现象:集群整体响应缓慢,也没有特别多的数据读写,但是发现节点在持续进行 Full GC
- 分析:查看 Elasticsearch 的内存使用,发现 fielddata.memory.size 占用很大空间。同时,数据不存在写入和更新,也执行过 segments merge。
- 解决:将 fielddata.memory.size 设小,重启节点,堆内存恢复正常
- 建议:Field data cache 的构建比较重,ES 不会主动释放,所以这个值应该设置的保守一些,如果业务上确实有所需要,可以通过增加节点,扩容解决。
---
- 复杂的嵌套聚合,导致集群 full GC
- 现象:节点响应缓慢,持续进行 Full GC
- 分析:导出 Dump 分析。发现内存中由大量 bucket 对象,查看日志,发现复杂的嵌套聚合
- 解决:优化聚合
- 建议:在大量数据集上进行嵌套聚合查询,需要很大的堆内存来完成。如果业务场景确实需要。则需要增加硬件进行扩展。同时,为了避免这类查询影响整个集群,需要设置 Circuit Break 和 search.max_buckets 的数值。
<a name="jG73S"></a>
### Circuit Break
- Parent circuit breaker:设置所有的熔断器可以使用的内存的总量
- Fielddata circuit breaker:加载 fielddata 所需要的内存
- Request circuit breaker:防止每个请求级数据结构超过一定的内存(例如聚合计算的内存)
- In flight circuit breaker:Request 中的断路器
- Accounting request circuit breaker:请求结束后不能释放的对象所占用的内存
```json
GET /_nodes/stats/breaker?
Tripped 大于 0, 说明有过熔断
Limit size 与 estimated size 约接近,越可能引发熔断
千万不要触发了熔断,就盲目调大参数,有可能会导致集群出问 题,也不因该盲目调小,需要进行评估
建议将集群升级到 7.x,更好的 Circuit Breaker 实现机制
增加了 indices.breaker.total.use_real_memory 配置项,可以更加精准的分析内存状况,避免 OOM
运维工具
节点上出现了高内存占用,可以执行清除缓存的操作,这个操作会影响集群的性能,但是会避免你的集群出现 OOM 的问题。
POST _cache/clear
当你想移除一个节点,或者对一个机器进行维护。同时你又不希望集群的颜色变黄或者变红。
PUT _cluster/settings
{
"transient": {
"cluster.routing.allocation.exclude._ip":"192.168.9.220"
}
}
将分片从一个节点移动到另一个节点,使用场景:当一个数据节点上由过多的Hot Shards:可以通过手动分配分片到特定的节点解决。
POST _cluster/reroute
{
"commands": [
{
"move": {
"index": "index_name",
"shard": 0,
"from_node": "node_name_1",
"to_node": "node_name_2"
}
}
]
}
设置各类 Circuit Breaker。避免 OOM 的发生。
PUT _cluster/settings
{
"persistent": {
"indices.breaker.total.limit":"40%"
}
}
重启一个节点
POST _flush/synced