- Published on
Elasticsearch 使用规范与索引管理最佳实践
- Authors
- Name
- Liant
Elasticsearch 使用规范
基于现有资源制订的使用规范,假设生产项目集群有5个节点。
构架索引要求
- 索引对外提供服务使用别名,命名需要加上版本号。
别名search_online_a_index
,实名search_online_a_index_v2
POST /_aliases
{
"actions": [
{
"add": {
"index": "search_online_a_index_v2",
"alias": "search_online_a_index"
}
}
]
}
规则: search
为搜索引擎项目提供更新服务, online_a
具体品牌业务, index_v2
当前版本号
使用别名好处较多: 如要修改字段类型时,先构建一个索引,然后索引数据,数据索引完成之后,可以几乎不影响正常业务情况下无缝切换。
创建索引时,在mapping中指定
dynamic=false
。避免新增字段时,自动解析字段类型导致类型不准确.比如第一次解析的值是
1
,被解析为integer
,第二次值是2.1
,这种情况下自动解析为是double
。那么造成的后果就是类型不匹配。最后的结果只能修改mapping重新索引数据。如果索引的字段的值是数字类的,且确定不使用范围查询,需要全部设置为keyword类型。
避免mapping字段数过多,一般不超过100个。
创建索引时,设置慢日志。
"search.slowlog.threshold.query.warn": "5s", # 搜索慢日志 warn级别 "search.slowlog.threshold.query.info": "1s", # 搜索慢日志 info级别 "search.slowlog.threshold.fetch.warn": "1s", # fetch慢日志 warn级别 "search.slowlog.threshold.fetch.info": "800ms", # fetch慢日志 info级别
Elasticsearch索引不支持连表,但支持join类型,即nested和parent-child,尽量避免使用。
Elasticsearch的索引默认支持mapping嵌套,正常情况下慎用嵌套类型,如果使用嵌套类型,建议深度不要超过2,嵌套类型文档内部更新无法原子更新。
通常情况下,nested比parent-child查询快5-10倍,但parent-child更新文档比较方便。
需要批量更新时,走批量更新队列服务,这样不影响单个数据更新。
需要批量更新时,尽量在业务服务不繁忙时操作。
不直连Elasticsearch集群,使用搜索引擎项目进行查询,避免排查问题时增加难度。
字段名称禁止使用以
_
开始的索引名称,Elasticsearch内部使用的字段名称使用_
开始。索引命名全为小写。建议全部由字母、数字组成,字母开头。不是用特殊字符。
使用搜索引擎搭建的同步服务,示例:别名search_index,实际名字search_index_v2
索引名称字符长度不超过255位,字段名称字符长度不要超过32位。
日志类型建议以月分片(根据日志体量来,可以以天/周/月为单位)。
查询要求
设置查询超时,使用request_cache。
curl -XPOST http://localhost:9200/search_index/_search?request_cache=true -d ' { "size" : 0, "timeout" : "60s", "terminate_after" : 1000000, "query" : { "match_all" : {} }, "aggs" : { "terms" : { "terms" : { "field" : "keyname" } } } }'
必须指定
_source
中的返回字段。避免占用不必要的资源,比如网络传输,程序内存等。
避免深度分页,超过
from+size>10000
时,考虑其他搜索接口search_after
,scroll
。另外,调整
max_result_window
过大容易出现OOM
。查询时,谨慎使用
script
,wildcard
模糊查询,cardinality
基数查询。这几个查询类型都是消耗资源的查询
查询时,注意字段类型匹配。
mapping中integer的字段,在查询时不要输入汉字等明显不能转为整数的字符。
如果需要使用聚合等,建议异步计算。
如果聚合耗时,建议凌晨时分做聚合计算。保存结果数据。
数据量大的时候,查询频繁,建立考虑独立的集群。
其他应该考虑
关于集群
规模较大的集群配置专有主节点,避免脑裂问题
在集群规模较大时,建议配置专有主节点只负责集群管理,不存储数据,不承担数据读写压力。
JVM内存
给JVM配置机器一半的内存,但是不建议超过32G。
JVM 堆内存较大时,内存垃圾回收暂停时间比较长,建议配置 ZGC
或 G1
垃圾回收算法。
尽可能使用SSD
对于文档检索类查询性能要求较高的场景,建议考虑 SSD 作为存储,同时按照 1:10 的比例配置内存和硬盘。
对于日志分析类查询并发要求较低的场景,可以考虑采用机械硬盘作为存储,同时按照 1:50 的比例配置内存和硬盘。
配置查询聚合节点
查询聚合节点可以发送粒子查询请求到其他节点,收集和合并结果,以及响应发出查询的客户端。通过给查询聚合节点配置更高规格的 CPU 和内存,可以加快查询运算速度、提升缓存命中率。
某客户使用 25 台 8 核 CPU32G 内存节点 Elasticsearch 集群,查询 QPS 在 4000 左右。增加 6 台 16 核 CPU32G 内存节点作为查询聚合节点,观察服务器 CPU、JVM 堆内存使用情况,并调整缓存、分片、副本参数,查询 QPS 达到 12000。
创建索引
创建索引的mapping文件
PUT /search_index_v1
{
"aliases": { # 别名一般在建立索引之后手动添加
"search_index": {} # 别名1,可以有多个别名.有多个别名时,写入只能使用实名.否则es会有歧义错误
},
"settings": {
"index": {
"refresh_interval": "1s", # 数据写入之后,需要等待刷新,刷新之后才可以被搜索到
"number_of_shards": "5", # 分片
"number_of_replicas": "1", # 副本
"search.slowlog.threshold.query.warn": "5s", # 搜索慢日志 warn级别
"search.slowlog.threshold.query.info": "1s", # 搜索慢日志 info级别
"search.slowlog.threshold.fetch.warn": "1s", # fetch慢日志 warn级别
"search.slowlog.threshold.fetch.info": "800ms", # fetch慢日志 info级别
"indexing.slowlog.threshold.index.warn": "12s", # index慢日志 warn级别
"indexing.slowlog.threshold.index.info": "5s" # index慢日志 info级别
}
},
"mappings": {
"_source": { # 开启_source,可以避免二次查询,相对的占用存储空间.
"enabled": true
},
"properties": {
"xxx_id": {
"type": "keyword"
},
"timestamp": {
"type": "long"
},
"xxx_status": {
"type": "integer"
},
"xxx_content": {
"type": "text"
}
}
}
}
主要介绍两个重要设置 settings
与 mappings
。
settings 设置
{
"settings": {
"index": {
"number_of_shards": 5,
"number_of_replicas": 0
}
}
}
分片(Shard)数量
生产环境目前是5台机器,按照目前数据量来说5个分片比较合理。
分片(Shard)存储大小
一般来说,单个 Shard 的建议合适的大小是 20G 到 50G,对于普通搜索类数据,最好控制在 20G,而对于时间序列类数据(如日志)最好控制在 50G。
副本(replica)数不大于3
Elasticsearch默认副本数是1,即存在1个primary和1个replica共两份数据,主备形式同步,为了提升数据的安全性,避免数据丢失,可以设置副本数为2,即存在1个primary和两个replica共三份数据。但是没有必要设置过大的副本数,Elasticsearch的复制会对集群性能产生影响,特别是refresh_interval设置比较小的集群。
需要重建索引时,设置number_of_replicas=0
(即副本数为0)可以提升写入文档速度,数据索引完成之后,可以调整副本数量,主分片是不可以的,需要一开始就确定好。
mappings 设置
Elasticsearch的索引最多默认支持1000个字段,Elasticsearch的字段数越多,对于集群缓存的消耗越严重,通常建议不要超过 100 个字段。
不使用多type索引
Elasticsearch7.x 之后将不再支持多type索引。多type索引共用_id,其version往往容易产生冲突。
几个重要的设置项
dynamic 设置字段自动解析
{ "settings": {...}, "mappings": { "dynamic": false, "properties": {...} } }
_source 是否保留源文档,通常为true.避免二次查询。
{ "settings": {...}, "mappings": { "_source": { "enabled": true }, "properties": {...} } }
_all 将多个字段合并为一个字段提供检索,7.x 被遗弃了
{ "settings": {...}, "mappings": { "my_type":{ "_all": { "enabled": false }, "properties": {...} } } }
null_value 查询null是必要,另外可以和exist结合起来使用
{ "settings": {...}, "mappings": { "properties": { "status_code": { "type": "keyword", "null_value": "NULL" } } } }
ignore_malformed 默认false,添加文档时忽略该字段的异常数据
{ "mappings": { "my_type": { "properties": { "number_one": { "type": "integer", "ignore_malformed": true }, "number_two": { "type": "integer" } } } } }
keyword 表示最大的字段值长度,超出这个长度的字段将不会被索引,但是会存储。
{ "settings": {...}, "mappings": { "properties": { "keyword": { "type": "keyword", "ignore_above": 256 } } } }
索引管理
索引mapping新增字段
处理步骤
- 更新mapping
- 通过
全量数据索引数据服务
索引数据
这种方式不调整索引名称,不影响业务。
尽量在业务使用不频繁的情况下操作
索引mapping修改字段类型
Elasticsearch是不支持mapping字段的类型修改的,只能重建索引才可以。
方案一 reindex
适用于任何调整: 删除字段,修改字段类型,新增字段别名(fields)
使用调整过的mapping创建新版本的索引
使用
reindex
的方式重新索引数据索引数据之后,切换别名即可提供服务
__reindex_介绍
该方法要在mapping中启用 _source
POST _reindex?slices=5&refresh&wait_for_completion=false
{
"conflicts": "proceed",
"source": {
"index": "search_index_v1", // 源索引
"size": 5000, // 每批次大小
"query": { // 查询条件,可做条件过滤
"term": {
"user": "kimchy"
}
}
},
"dest": {
"index": "search_index_v2", // 目标索引
"pipeline": "mytx_pipeline_20220530" // 使用pipeline处理数据
},
"script": {
"source": "ctx._source.tag = ctx._source.remove(\"flag\")"
}
}
reindex数据比bulk要快5~10倍
方案2 _update_by_query
适用于任何调整: 新增字段别名(fields),删除字段
在原索引中更新mapping
使用
_update_by_query
的方式重新索引数据
_update_by_query 介绍
POST /search_index_v1/_update_by_query?conflicts=proceed&wait_for_completion=false
{
"script": {
"inline": "ctx._source.commission2 = []", // 做一些字段处理,如清空数据等
"lang": "painless"
},
"query": { // 搜索条件
"term": {
"id": {
"value": "6256"
}
}
}
}
_update_by_query 用于重新索引数据,新增字段别名,删除数据等。
全量数据索引数据服务
方案3 使用适用于任何调整: 调整字段类型,新增字段别名(fields),删除字段
使用新的mapping新增新索引
将数据id投递到
全量数据索引数据服务
消息队列,消费者消费消息,bulk写入Elasticsearch索引数据之后,切换别名即可提供服务
索引数据清理
- _delete_by_query
在kibana中执行
POST /search_index_v1/_delete_by_query
{
"query": {
"term": {
"project": 5
}
}
}
谨慎查询条件,在删除之前使用查询确认下要删除的数据是否正确。
索引数据
索引方案:
- Elasticsearch建模,确定mapping中的字段类型,在Elasticsearch中手动创建索引。
- 具体项目中,将要更新的数据id投递到消息队列中,消费者从队列中读取数据id,查询数据库,最后索引到Elasticsearch中。
- 搜索引擎项目中新建一个控制器,提供具体索引http访问接口。
索引服务规则
规则 1 : 单一项目更新数据到同一个索引(通过MQ同步ES数据)。
- 至少两个队列sync,通过 routing_key 投递数据
- 定义服务:
新增数据服务(insert)
- 队列名称: 索引别名.项目.具体业务.操作.que后缀(如
search_index.insert.que
) - routing_key名称: 索引别名.项目.具体业务.操作.rk后缀(如
search_index.insert.rk
)
- 队列名称: 索引别名.项目.具体业务.操作.que后缀(如
更新数据服务(update)
- 队列名称: 索引别名.项目.具体业务.操作.que后缀(如
search_index.update.que
) - routing_key名称: 索引别名.项目.具体业务.操作.rk后缀(如
search_index.update.rk
)
- 队列名称: 索引别名.项目.具体业务.操作.que后缀(如
索引数据服务(index,不区分update与insert)
- 队列名称: 索引别名.项目.具体业务.操作.que后缀(如
search_index.index.que
) - routing_key名称: 索引别名.项目.具体业务.操作.rk后缀(如
search_index.index.rk
)
- 队列名称: 索引别名.项目.具体业务.操作.que后缀(如
全量数据索引数据服务(whole,不区分update与insert) 尽量使用批量投递.但也不可过长,建议20个数据id
- 队列名称: 索引别名.项目.具体业务.操作.que后缀(如
search_index.whole.que
) - routing_key名称: 索引别名.项目.具体业务.操作.rk后缀(如
search_index.whole.rk
)
- 队列名称: 索引别名.项目.具体业务.操作.que后缀(如
其中
全量数据索引数据服务
必需添加。另外如果不需要区分
新增数据服务
与更新数据服务
,则可以选择索引数据服务
。规则 2 : 多个项目都需要更新数据到同一个索引。
对每一个项目都使用 规则1
多个队列同时写入一个索引中,应该尽量避免同时批量更新数据
坑点:先创建了数据,在更新mapping,再次更新数据(如果文档数据没有变化,Elasticsearch会认为是空操作,不做任何处理),会导致mapping不起作用.
解决办法:使用Elasticsearch的新建接口创建文档;如果是新增了字段,将改字段清空,在修改文档。
查询数据
对外提供服务一缕走搜索引擎项目,搜索引擎项目查询服务需要配置索引的别名。
检索分类说明

查询建议
match操作不走缓存
exists
的 Elasticsearch提供了exists查询,用以返回字段存在值的记录,默认情况下只有字段的值为null
或者[]
的时候,Elasticsearch才会认为字段不存在- 对于字符串类型字段,当字段没有出现、字段值为null的情况下,则该字段不存在;字段值为空则计算为字段存在;
- 对于数字类型字段,当字段没有出现、字段值为null的情况下,则该字段不存在;
- 对于布尔类型字段,当字段没有出现、字段值为null的情况下,则该字段不存在;
- 对于数组类型字段,只要字段没有出现、字段值为null、字段值为空数组、字段值数组的所有元素都为null,则该字段不存在;
- 对于对象字段,只要字段没有出现、字段值是空对象、字段值为null,则该字段不存在;
不需要评分时,使用
bool
的filter
可以极大的提高检索速率避免使用脚本查询
script
、通配符查询wildcard
以及正则查询regexp
如果查询较为复杂,会有性能问题,结果会导致单个查询占用整个集群资源,导致服务不可用
避免使用深度分页,越是往后越是消耗资源。
- 使用search_after时 from设置为0,-1或者直接不加from
- 使用search_after进行分页相比
from+size
的方式要更加高效,而且在不断有新数据入库的时候仅仅使用1. from和size分页会有重复的情况 - 相比使用scroll分页,search_after可以进行实时的查询
- search_after不能跳跃分页,只能顺序一页一页往下查询
易出现的问题
1. mapping字段不支持修改,需要谨慎
需要修改,只能重建索引
2. 分页
# 默认情况下,结果最多是10000,当返回结果不够的时候设置参数 `max_result_window`
curl -XPUT 127.0.0.1:9200/search_index/_settings -d '{
"index.max_result_window":1000000
}'
注意:深度分页会占用大量的资源,配合
search_after
与scroll
使用另外
max_result_window
调整过大容易出现OOM
2. 查询类型不匹配
当mapping中的类型为integer,long,byte等数字类型时,如果输入的不是数字类型,查询时会有类型转换错误。
3. 使用script查询
script查询时会占用大量的资源。如果要使用的话,位置最好放到DSL最后,因为DSL是从前往后的执行过滤。
null
值
4. 查询 如果要查询 null值 需要在设计mapping的时候指定字段的 null_value。
5. Elasticsearch的搜索引擎结果并不一定准确,这个需要知晓
这个是数据分片之后排序的通病。
案例一 新建索引
模拟一个场景:商品需要检索服务。
索引建模,确定商品索引需要那些字段。 在kibana执行
PUT /shop_index_v1 { "aliases": { "shop_index": {} }, "settings": { "refresh_interval": "1s", "number_of_shards": 5, "number_of_replicas": 0, // 新建时不使用,数据索引完成之后开启 "search.slowlog.threshold.query.warn": "5s", "search.slowlog.threshold.query.info": "1s", "search.slowlog.threshold.fetch.warn": "1s", "search.slowlog.threshold.fetch.info": "800ms", "indexing.slowlog.threshold.index.warn": "12s", "indexing.slowlog.threshold.index.info": "5s" }, "mappings": { "_source": { "enabled": true }, "dynamic": "false", "properties": { "accordexpand": { "type": "text", "analyzer": "ik_max_word" }, "activity": { "type": "keyword" }, "additional_money": { "type": "float" }, "apply_year": { "type": "keyword" }, "assess_time": { "type": "integer" }, "bank": { "type": "short" }, "brand_text": { "type": "keyword", "fields": { "standard": { "type": "text", "analyzer": "standard" } } } } } }
搜索引擎项目中创建索引 (结合自己的后端项目,以下是PHP项目举例)
目前是在搜索引擎项目创建mapping文件:
app/Common/Mapping/ShopMapping.php
然后在搜索引擎项目(Swoft框架)中使用command功能新建命令脚本:
app/Console/Command/ShopMapping.php
# 创建index # 应该读取别名,检查Elasticsearch中是否已有使用,如果有的话需要在版本号后+1,否则就是_v1 php bin/swoft shop:createIndex # 更新index # 应该读取别名,更新mapping。当有多个别名时,需要手动指定真实名称 php bin/swoft shop:updateIndex
另外在配置文件中配置索引别名
在生产环境执行命令,创建索引。真实名字应该为:
search_index_v1
索引数据
同样在
ShopCommand.php
文件中定义消费者服务,因为只需要更新数据即可。# 索引数据服务(index,不区分update与insert) # 实时更新数据,新增数据等。默认是写入索引别名中 php bin/swoft shop:indexData # 全量数据索引数据服务 # 批量更新数据,新增字段等场景使用到 # 索引需要指向最新的索引版本,默认向最新版本的索引(应该在命令行中预留参数,便于更新到其他索引中)中更新数据数据。 php bin/swoft shop:wholeData # 将数据投递到消息队列中,可带常用的查询参数,导入指定的数据到全量队列 php bin/swoft shop:publishData
提供查询服务
在搜索引擎项目中新增接口文件:
app/Http/Controller/ShopController.php
/** * 商品索引 * @RequestMapping("/shop-index/_search") * @return Response * @throws Exception */ public function _search() { $request = context()->getRequest(); $response = context()->getResponse(); $authorization = $request->getHeaderLine('authorization'); $body = $request->getRawBody(); $url = ElasticsearchService::build_search_proxy_url(env('SHOP_INDEX')); $params = $request->getQueryParams(); if (count($params) > 0) { $url .= '?' . http_build_query($params); } list($content, $info) = ESHelper::proxy($url, $authorization, $body); return ESHelper::mergeResponse($response, $content, $info); }
重启搜索引擎项目后,可以提供商品的搜索服务。
另外,需要单独配置读取权限Elasticsearch账户给具体的品牌业务项目。http的base认证。
案例二 重建索引
模拟一个场景进行讲解: 商品需要修改字段类型,没有新增字段
需要修改类型,则需要新建索引,在索引数据。
在案例一的前提下,更新mapping文件之后。
执行
php bin/swoft shop:createIndex
,创建新索引,真实名字应该为:search_index_v2
使用reindex功能更新数据(在业务使用频率低时处理)。
在kibana执行
POST _reindex?slices=5&refresh&wait_for_completion=false { "conflicts": "proceed", "source": { "index": "search_index_v1", // 源索引 "size": 5000, // 每批次大小 "query": { // 所有数据 "match_all": { } } }, "dest": { "index": "search_index_v2", // 目标索引 } }
生产任务,后台执行,需要记住任务id.
任务已经执行完成之后切换别名
在kibana中执行
POST /_aliases { "actions": [ { "remove": { "index": "search_index_v1", "alias": "search_index" } }, { "add": { "index": "search_index_v2", "alias": "search_index" } } ] }
需要移除别名,不然更新服务会有索引歧义错误。
之后看业务具体情况关闭索引,最后删除数据。