ElasticSearchd的自我介绍
主要功能
基础概念
ES和传统数据库的对比
Elastic Stack生态和场景方案
日志收集系统
-
基本的日志系统 -
增加数据源,和使用MQ -
Metric收集和APM性能监控
多数据中心方案
ElasticSearch和Kibana安装
ElasticSearch
-
首先下载ElasticSearch包 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wChCMAhW-1653812907262)(ES.assets\image-20210704235849426.png)] -
然后进行解压 tar -zxvf elasticsearch-7.9.0-linux-x86_64.tar.gz
-
修改配置文件 # 现在elasticsearch-7.9.0目录下创建data和logs文件夹,如果已经存在则无需创建
# 进入config文件夹
vim elasticsearch.yml
# 修改内容包括
cluster.name: elasticsearch
node.name: es-node0
path.data: /usr/local/elasticsearch-7.6.2/data
path.logs: /usr/local/elasticsearch-7.6.2/logs
http.port: 9200
network.host: 0.0.0.0
cluster.initial_master_nodes: ["es-node0"]
#然后再来修改另一个文件夹
vim jvm.options
#需要把运行内存从1G改为128m,本地运行不需要这么大
-Xms128m
-Xmx128m
-
创建用户 ES启动不能以root用户来启动 useradd user-es
# 授权
chown -R user-es:user-es /usr/local/software/elasticsearch-7.9.0
-
修改系统限制 vi /etc/security/limits.conf
vi /etc/sysctl.conf
保存退出后可通过下面命令进行刷新 sysctl -p
elasticsearch提供了9300、9200两个端口,一个是共有的、一个是私有的
通过./elasticsearch启动
通过http://ip:9200/访问
kibana
安装访问注意需要打开防火墙,不然会访问不到
# 查看防火墙装状态
systemctl status firewalld
# 关闭防火墙
systemctl stop firewalld
# 永久关闭防火墙
systemctl disable firewalld
# 重启防火墙
systemctl enable firewalld
ES 普通查询和聚合查询
批量导入数据
curl -H "Content-Type: application/json" -XPOST "{ip}:{port}/bank/_bulk?pretty&refresh" --data-binary "@/{path}/accounts.json"
其中accounts.json为导入的数据文件,下面为其中一条数据例子
{"create":{}}
{ "process": { "parent": { "name": "powershell.exe", "entity_id": "{42FC7E13-C11D-5C05-0000-0010C6E90401}", "executable": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" }, "name": "cmd.exe", "pid": 2012, "entity_id": "{42FC7E13-CB3E-5C05-0000-0010A0125101}", "command_line": "\"C:\\WINDOWS\\system32\\cmd.exe\" /c \"for /R c: %%f in (*.docx) do copy %%f c:\\temp\\\"", "executable": "C:\\Windows\\System32\\cmd.exe", "ppid": 7036 }, "logon_id": 217055, "@timestamp": 131883571822010000, "event": { "category": "process", "type": "creation" }, "user": { "full_name": "bob", "domain": "ART-DESKTOP", "id": "ART-DESKTOP\\bob" } }
查看索引状态
curl "{ip}:{port}/_cat/indices?v=true" | grep {index的name}
查询语法
查询所有
GET /bank/_search
{
"query": { "match_all": {} },
"sort": [
{ "account_number": "asc" }
]
}
查询结果
返回字段解释
- took:ES运行查询所花费的事件(以毫秒为单位)
- time_out:搜索请求是否超时
- _shards:搜索了多少个碎片,以及成功,失败或跳过了多少个碎片的细目分类
- 找到的最相关文档的分数
- hits.total.value:找到了多少个匹配的文档
- hits.sort:文档的排序位置(不按相关性得分排序时)
- hits.source:文档的相关性得分(使用match_all时不适用)
分页查询(from+size)
GET /bank/_search
{
"query": { "match_all": {} },
"sort": [
{ "account_number": "asc" }
],
"from": 10,
"size": 10
}
从第10条开始往后查询10条
指定字段查询:match
GET /bank/_search
{
"query": { "match": { "address": "mill lane" } }
}
在字段中搜索特定字词,可以使用match,上面就是查询address字段中包含mill或lane的数据
TIP:由于ES底层是按照分词索引的,所以上述查询结果是address字段中包含mill或者lane的数据
查询段落匹配:match_phrase
和上面的match相反,如果希望查询的条件是address字段中包含mill lane,则可以使用match_phrase
GET /bank/_search
{
"query": { "match_phrase": { "address": "mill lane" } }
}
多条件查询:bool
使用bool可以组合多个查询条件
例如:搜素40岁客户的账户,但不包括男性
GET /bank/_search
{
"query": {
"bool": {
"must": [
{ "match": { "age": "40" } }
],
"must_not": [
{ "match": { "sex": "男" } }
]
}
}
}
TIP:must,should,must_not,filter都是bool查询的子句
查询条件:query or filter
在bool中可以同时具备query/must 和filter
著作权归https://pdai.tech所有。
链接:https://www.pdai.tech/md/db/nosql-es/elasticsearch-x-usage.html
GET /bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"state": "ND"
}
}
],
"filter": [
{
"term": {
"age": "40"
}
},
{
"range": {
"balance": {
"gte": 20000,
"lte": 30000
}
}
}
]
}
}
}
那么query和filter的区别是什么?
query上下文的条件是用来给文档打分的,匹配越好_source越高,而filter的条件只产生两种结果:符合和不符合,后者被过滤掉
聚合查询:Aggregation
ES的Aggregation(聚合运算),其实也就是SQL中的group by
简单聚合
比如统计出account每个州的统计数量,使用aggs关键字对state字段聚合,被聚合的字段无需分词统计,所以使用state.keywork对整个字段统计
GET /bank/_search
{
"size": 0,
"aggs": {
"group_by_state": {
"terms": {
"field": "state.keyword"
}
}
}
}
结果
当size为0时,代表不会返回具体数据,所以hits为空,而doc_count表示每个bucket中每个州的数据条数
嵌套聚合
ES可以处理聚合条件的嵌套
比如,计算每个州的平均结余,设计的是在对state分组的基础上,嵌套计算avg(balance):
GET /bank/_search
{
"size": 0,
"aggs": {
"group_by_state": {
"terms": {
"field": "state.keyword"
},
"aggs": {
"average_balance": {
"avg": {
"field": "balance"
}
}
}
}
}
}
结果
对聚合结果排序
GET /bank/_search
{
"size": 0,
"aggs": {
"group_by_state": {
"terms": {
"field": "state.keyword",
"order": {
"average_balance": "desc"
}
},
"aggs": {
"average_balance": {
"avg": {
"field": "balance"
}
}
}
}
}
}
索引管理
在增加文档时,如果ES还没有该文档所对应的index,那么就会动态创建一个index,但如果我们需要对这个建立索引的过程做更多的控制,比如想要确保索引有数量适中的主分片,并且在我们索引任何数据之前,分析器和映射器已经被建立好了,这时有如下两种方案,禁止自动创建索引/手动创建索引
禁止自动创建索引
可以在config/elasticsearch.yml的每个节点下添加下面的配置
action.auto_create_index: false
索引的格式
PUT /my_index
{
"settings": { ... any settings ... },
"mappings": {
"properties": { ... any properties ... }
}
}
- settings:用来设置分片,副本等配置信息
- mappings:字段映射,类型等
- properties:由于type在后续版本中会被Deprecated,所以无需被type嵌套
索引的管理操作
创建索引
创建一个user索引text-index-users,其中包含三个属性,name,age,remarks,存储在一个分片一个副本上
PUT /text-index-user
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"name":{
"type": "text",
"fields": {
"keyword":{
"type":"keyword",
"ignore_above":256
}
}
},
"age":{
"type": "long"
},
"remarks":{
"type": "text"
}
}
}
}
修改索引
可以通过该查看索引状态
curl 'localhost:9200/_cat/indices?v' | grep user
索引的状态是yellow的,因为测试的环境是单点环境,无法创建副本,但是在上述number_of_replicas 配置中设置了副本数是1; 所以在这个时候我们需要修改索引的配置
现在来修改副本数
PUT /text-index-user/_settings
{
"settings":{
"number_of_replicas":0
}
}
再次查看索引状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-snuVPNbg-1653812907266)(ES.assets\image-20210708003122794.png)]
打开/关闭索引
删除索引
DELETE /test-index-users
kibana下怎么管理索引
索引模板(Index Template)
index template是啥?
索引模板其实就是一种告诉ES在创建索引时如何配置索引的方法
使用方法
在创建索引之前可以先配置模板,这样在创建索引(手动创建索引或通过对文档建立索引)时,模板设置将用作创建索引的基础
模板类型
分为两种:索引模板和组件模板
- 组件模板是可重用的构建块,用于配置映射,设置和别名;它们不会直接应用于一组索引
- 索引模板可以包含组件模板的集合,也可以直接指定设置,映射和别名
索引模板中的优先级
- 可组合模板优先于旧模板。如果没有可组合模板匹配给定索引,则旧版模板可能仍匹配并被应用。
- 如果使用显式设置创建索引并且该索引也与索引模板匹配,则创建索引请求中的设置将优先于索引模板及其组件模板中指定的设置。
- 果新数据流或索引与多个索引模板匹配,则使用优先级最高的索引模板。
内置索引模板
ES具有内置索引模板,每个模板的优先级为100,适用以下索引模式
logs-*-* metrics-*-* synthetics-*-*
案例
创建两个索引组件模板:
PUT _component_template/component_template1
{
"template": {
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
}
}
}
}
}
PUT _component_template/runtime_component_template
{
"template": {
"mappings": {
"runtime": {
"day_of_week": {
"type": "keyword",
"script": {
"source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))"
}
}
}
}
}
}
创建使用组件模板的索引模板
PUT _index_template/template_1
{
"index_patterns": ["bar*"],
"template": {
"settings": {
"number_of_shards": 1
},
"mappings": {
"_source": {
"enabled": true
},
"properties": {
"host_name": {
"type": "keyword"
},
"created_at": {
"type": "date",
"format": "EEE MMM dd HH:mm:ss Z yyyy"
}
}
},
"aliases": {
"mydata": { }
}
},
"priority": 500,
"composed_of": ["component_template1", "runtime_component_template"],
"version": 3,
"_meta": {
"description": "my custom"
}
}
创建一个匹配bar* 的索引bar-test
PUT /bar-test
然后获取mapping
GET /bar-test/_mapping
这个以后用到再说吧
DSL查询之复合查询
复合查询,也就是多种条件组合的查询,在ES中提供了5种符合查询,bool query(布尔查询),boosting query(提高查询),constans_source(固定分数查询),dis_max(最佳匹配查询),function_score(函数查询)
bool query(布尔查询)
通过布尔逻辑将较小的查询组合成较大的查询
概念特点:
- 子查询可以任意顺序出现
- 可以嵌套多个查询,包括bool查询
- 如果bool查询中没有must条件,那么shouold中必须至少满足一条才会返回结果
bool查询包含四种操作符,分别是must,should,must_not,filter,他们都是一种数组,数组里面是对应的判断条件
- must:必须匹配,贡献算分
- must_not:过滤子句,必须不能匹配,但不贡献算分
- should:选择性匹配,至少满足一条,贡献算分
- filter:过滤子句,必须匹配,但不贡献算分
案例
POST _search
{
"query": {
"bool" : {
"must" : {
"term" : { "user.id" : "kimchy" }
},
"filter": {
"term" : { "tags" : "production" }
},
"must_not" : {
"range" : {
"age" : { "gte" : 10, "lte" : 20 }
}
},
"should" : [
{ "term" : { "tags" : "env1" } },
{ "term" : { "tags" : "deployed" } }
],
"minimum_should_match" : 1,
"boost" : 1.0
}
}
}
tip:在filter元素下指定的查询对评分没有影响,评分返回为0,分数仅受已指定查询的影响
GET _search
{
"query": {
"bool": {
"filter": {
"term": {
"status": "active"
}
}
}
}
}
tip:该案例查询为所有文档分配0分,因为没有指定评分查询
GET _search
{
"query": {
"bool": {
"must": {
"match_all": {}
},
"filter": {
"term": {
"status": "active"
}
}
}
}
}
tip:该案例查询具有match_all查询,该查询为所有文档指定1.0分
GET /_search
{
"query": {
"bool": {
"should": [
{ "match": { "name.first": { "query": "shay", "_name": "first" } } },
{ "match": { "name.last": { "query": "banon", "_name": "last" } } }
],
"filter": {
"terms": {
"name.last": [ "banon", "kimchy" ],
"_name": "test"
}
}
}
}
}
tip:每个query条件都有一个_name属性,可以用来追踪搜索出的数据到底match了哪个条件
boosting query(提高查询)
不同于bool查询,bool查询只要一个子查询条件不匹配,那么搜索的数据就不会出现,而boosting query则是降低显示的权重/优先级(即score)
比如搜索逻辑是name = ‘apple’ and type = ‘fruit’,对于只满足部分条件的数据,不是不显示,而是降低显示的优先级(即score)
GET /test-dsl-boosting/_search
{
"query": {
"boosting": {
"positive": {
"term": {
"content": {
"value": "apple"
}
}
},
"negative": {
"term": {
"content": {
"value": "pie"
}
}
},
"negative_boost": 0.5
}
}
}
constant_score(固定分数查询)
查询某个条件时,固定的返回指定的score;显然当不需要计算score时,只需filter条件即可,以为filter 会忽略score
GET /test-dsl-constant/_search
{
"query": {
"constant_score": {
"filter": {
"term": { "content": "apple" }
},
"boost": 1.2
}
}
}
tip:此时查询返回的数据的score就都是1.2
dis_max(最佳匹配查询)
分离最大化查询(Dijunction Max Query)指的是:将任何与任意查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回
案例
用户输入词组 “Brown fox” 然后点击搜索按钮。事先,我们并不知道用户的搜索项是会在 title 还是在 body 字段中被找到,但是,用户很有可能是想搜索相关的词组。用肉眼判断,文档 2 的匹配度更高,因为它同时包括要查找的两个词:
GET /test-dsl-dis-max/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
要知道为何doc1更匹配需要具体看ES是如何进行评分的
可以通过这样查询去看每个词的命中分数
不使用bool查询,可以使用dis_max即分离最大化查询,分离的意思是或(or),这与可以把结合(conjunction)理解成与(and)相对应,分离最大化查询指的的:将任何与任意查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回
GET /test-dsl-dis-max/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
],
"tie_breaker": 0
}
}
}
dis_max条件的计算分数
分数 = 第一匹配条件+ tie_breaker * 第二个匹配的条件分数…
GET /test-dsl-dis-max/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
],
"tie_breaker": 0
}
}
}
这样就通过dix_max将doc2置前了,如果这里缺省tie_breaker字段的话,默认就是0,还可以设置它的比例(在0到1之间)来控制排名(值为1时和should查询是一致的)
function_score(函数查询)
简而言之,其实就是自定义function的方式来计算_score
ES中的自定义function
- script_score:使用自定义的脚本来完全控制分值计算逻辑,如果需要以下预定义函数之外的功能,可以根据需要通过脚本进行实现
- weight:对每份文档适用一个简单的提升,且该提升不会被归约:当weight为2时,结果为2*_score
- random_score:使用一致性随机分值计算来对每个用户采用不同的结果排序方式,对相同用户仍然使用相同的排序方式
- field_value_factor:使用文档中某个字段的值来改变_score,比如将受欢迎程度或者投票数量考虑在内
- 衰减函数(Decat Fuction) - linear, exp, gauss
案例
random_score为例
GET /_search
{
"query": {
"function_score": {
"query": {"match_all": {}},
"boost": "5",
"random_score": {},
"boost_mode": "multiply"
}
}
}
还可以使用function的组合(functions)
GET /_search
{
"query": {
"function_score": {
"query": {"match_all": {}},
"boost": "5",
"functions": [
{
"filter": {"match":{"test": "bar"}},
"random_score": {},
"weight": 23
},
{
"filter": {"match":{"test":"cat"}},
"weight": 42
}
],
"max_boost": 42,
"score_mode": "max",
"boost_mode": "multiply"
, "min_score": 42
}
}
}
script_score的使用方式
GET /_search
{
"query": {
"function_score": {
"query": {
"match": { "message": "elasticsearch" }
},
"script_score": {
"script": {
"source": "Math.log(2 + doc['my-int'].value)"
}
}
}
}
}
DSL查询之全文搜索
Match类型
GET /test-dsl-match/_search
{
"query": {
"match": {
"title": "QUICK!"
}
}
}
ES执行上面这个match查询的步骤:
-
检查字段类型 title字段是一个string类型(analyzed)已分析的全文字段,这意味着查询的字符串本身也应该被分析 -
分析查询字符串 将查询的字符串QUIOCK!传入标准分析器中,输出结果时单个项quick,因为只有一个单词项,所以match查询执行的单个底层term查询 -
查询匹配文档 用term查询在倒排索引中查找quick然后获取一组包含该项的文档 -
为每个文档评分 用term查询计算每个文档相关度评分_score,这是种将词频(即词quick在相关文档的title字段中出现的频率)和反向文档频率(即词quick在所有文档的title字段中出现的频率),以及字段的长度(即字段越短相关度越高)相结合的计算方式
Match多个词深入
本质
GET /test-dsl-match/_search
{
"query": {
"match": {
"title": "BROWN DOG"
}
}
}
因为match查询必须查找两个词([“brown”,“dog”]),它在内部实际上先执行两次term查询,然后将两次查询的结果合并作为最终结果输出,为了做到这点,它将两个term包入一个Bool查询中,所以上面的查询语句,实际上和下面的查询相同
GET /test-dsl-match/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"title": "brown"
}
},
{
"term": {
"title": "dog"
}
}
]
}
}
}
逻辑
上面等同于should(任意一个满足),是因为match还有一个operator参数,默认是or,所以对应的是should
所以上面的查询还可以是下面的这样
GET /test-dsl-match/_search
{
"query": {
"match": {
"title": {
"query": "BROWN DOG",
"operator": "or"
}
}
}
}
那么如果需要and操作呢?
GET /test-dsl-match/_search
{
"query": {
"match": {
"title": {
"query": "BROWN DOG",
"operator": "and"
}
}
}
}
这又可以这么来写
GET /test-dsl-match/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"title": "brown"
}
},
{
"term": {
"title": "dog"
}
}
]
}
}
}
控制match的匹配精度
如果用户给定三个查询词,想找到只包含其中两个的文档,该怎么做?使用or 或and好像都不合适
match查询支持minimum_should_match最小匹配参数,这就可以指定匹配的词项数用来表示一个文档是否相关,可以将其设置为某个具体数字,也可以将其设置为一个百分数(常用)
GET /test-dsl-match/_search
{
"query": {
"match": {
"title": {
"query": "quick brown dog",
"minimum_should_match": "75%"
}
}
}
}
tip:当给定百分比时,minimum_should_match会做合适的事情,比如在之前的三词项例子中,75%会自动被截断成66%,即三个词里面两个词,无论这个词被设置成这个,至少包含一个词项的文档才会认为是匹配的
上面的查询也等同于下面
GET /test-dsl-match/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "quick" }},
{ "match": { "title": "brown" }},
{ "match": { "title": "dog" }}
],
"minimum_should_match": 2
}
}
}
其他match类型
-
match_pharse GET /test-dsl-match/_search
{
"query": {
"match_phrase": {
"title": {
"query": "quick brown"
}
}
}
}
match_phrase本质是连续的term的查询 -
match_pharse_prefix ES在match_phrase基础上提供了一种可以查询最后一个词项是前缀的方法 GET /test-dsl-match/_search
{
"query": {
"match_phrase_prefix": {
"title": {
"query": "quick brown f"
}
}
}
}
TIP:prefix的意思并不是整个text的开始匹配,而是最后一个词项满足term的prefix查询 -
match_bool_prefix 除了match_phrase_prefix,ES还提供了match_bool_prefix查询 GET /test-dsl-match/_search
{
"query": {
"match_bool_prefix": {
"title": {
"query": "quick brown f"
}
}
}
}
那么这种方式和match_phrase_prefix有啥区别呢? match_bool_prefix本质上可以转换为: GET /test-dsl-match/_search
{
"query": {
"bool" : {
"should": [
{ "term": { "title": "quick" }},
{ "term": { "title": "brown" }},
{ "prefix": { "title": "f"}}
]
}
}
}
所以match_bool_prefix查询中quick,brown是无序的 -
multi_match 如果希望一次对多个字段进行查询,可以使用以下方式 {
"query": {
"multi_match" : {
"query": "Will Smith",
"fields": [ "title", "*_name" ]
}
}
}
*表示前缀匹配字段
query string类型
此查询使用语法根据运算符(and,or)来分析和拆分提供的查询字符串NOT,然后查询在返回匹配的文档之前独立分析每个拆分的文本
可以使用query_string查询创建一个复杂的搜索,其中包括通配符,跨多个字段的搜索等等,尽管用途广泛,单查询是严格的,如果查询字符串中包含任何无效语法,则返回错误
GET /test-dsl-match/_search
{
"query": {
"query_string": {
"query": "(lazy dog) OR (brown dog)",
"default_field": "title"
}
}
}
-
query_string_simple 该查询使用一种简单的语法来解析提供的查询字符串并将其拆分为基于特殊运算符的术语,然后查询在返回匹配的文档之前独立分析每个术语,尽管其语法比query_string查询更受限制,但simple_query_string查询不会针对无效语法返回错误,而是,它将忽略查询字符串的任何无效部分 GET /test-dsl-match/_search
{
"query": {
"simple_query_string" : {
"query": "\"over the\" + (lazy | quick) + dog",
"fields": ["title"],
"default_operator": "and"
}
}
}
Interval类型
Intervals是时间间隔的意思,本质上将多个规则按照顺序匹配
GET /test-dsl-match/_search
{
"query": {
"intervals" : {
"title" : {
"all_of" : {
"ordered" : true,
"intervals" : [
{
"match" : {
"query" : "quick",
"max_gaps" : 0,
"ordered" : true
}
},
{
"any_of" : {
"intervals" : [
{ "match" : { "query" : "jump over" } },
{ "match" : { "query" : "quick dog" } }
]
}
}
]
}
}
}
}
}
tip:因为interval之间是可以组合的,所以它可能会表现的很复杂
DSL查询之Term详解
字段是否存在:exist
由于多种原因,文档字段的索引值可能不存在
- 源JSON中的字段是null或[]
- 该字段已“index”:false在映射中设置
- 字段值的长度超过了ignore_above了映射中的设置
- 字段值格式错误,并且ignore_malformed已在映射中定义
所以exist表示查找是否存在字段
GET /test-dsl-term-level/_search
{
"query": {
"exists": {
"field": "remarks"
}
}
}
id查询:ids
ids既对id查找
GET /test-dsl-term-level/_search
{
"query": {
"ids": {
"values": [3, 1]
}
}
}
前缀:prefix
通过前缀查找某个字段
GET /test-dsl-term-level/_search
{
"query": {
"prefix": {
"name": {
"value": "Jan"
}
}
}
}
分词匹配:term
根据分词查询
GET /test-dsl-term-level/_search
{
"query": {
"term": {
"programming_languages": {
"value": "php"
}
}
}
}
多个分词匹配
按照单个分词term匹配,它们是or的关系
GET /test-dsl-term-level/_search
{
"query": {
"terms": {
"programming_languages": ["php","c++"]
}
}
}
按某个数字字段分词匹配:term set
这种查询方式的初衷是用文档中的数字字段动态匹配查询满足term的个数
GET /test-dsl-term-level/_search
{
"query": {
"terms_set":{
"programming_languages":{
"terms":["java","php"],
"minimum_should_match_field":"required_matches"
}
}
}
}
TIP: minimum_should_match_field -> 意思就是最小匹配度,这里的意思就是拿required_matches的值作为最小匹配度,这里就是最少要匹配到2个term的意思
通配符:wildcard
通配符匹配,比如*
GET /test-dsl-term-level/_search
{
"query": {
"wildcard": {
"name": {
"value": "D*ai",
"boost": 1.0,
"rewrite":"constant_score" //(可选,字符串)用于重写查询的方法
}
}
}
}
范围:range
常被用在数字或者日期范围的查询
GET /test-dsl-term-level/_search
{
"query": {
"range": {
"required_matches": {
"gte": 3,
"lte": 4
}
}
}
}
正则:regexp
以"Jan"开头的name字段
GET /test-dsl-term-level/_search
{
"query": {
"regexp": {
"name":{
"value": "Ja.*"
}
}
}
}
模糊匹配:fuzzy
官方文档对模糊匹配:编辑距离是将一个术语转换为另一个术语所需的一个字符更改的次数,这个更改可以包括以下:
- 更改字符(box -> fox)
- 删除字符(black -> lack)
- 插入字符(sic -> sick)
- 转置两个相邻字符(act -> cat)
GET /test-dsl-term-level/_search
{
"query": {
"fuzzy":{
"remarks": {
"value": "hell"
}
}
}
}
聚合查询之Bucket聚合详解
ES中的聚合其实就是相对于sql中得分group by
SELECT COUNT(color)
FROM table
GROUP BY color
ES中桶在概念上类似于SQL的分组(Group by),而指标类似于count()、sum()、MAX()等统计方法
- 桶(Buckets):满足特定条件的文档的集合
- 指标(Metrics):对桶内的文档进行统计计算
ES包含三种聚合(Aggregation方式)
- 桶聚合(Bucket Aggregration)
- 指标聚合(Metric Aggregration)
- 管道聚合(Pipline Aggregration) -> 聚合管道化,简单而言就是上个聚合的结果成为下一个聚合的输入
理解Bucket聚合
标准的聚合
GET /test-agg-cars/_search
{
"size": 0,
"aggs": {
"popular_colors": {
"terms": {
"field": "color.keyword"
}
}
}
}
- 聚合操作被置于顶层更参数aggs之下
- 然后,可以为聚合指定一个我们想要的名称,例如本例中华:popular_colors
- 最后定义单个桶的类型terms
多个聚合
同时计算两种桶的结果:对color和对make
GET /test-agg-cars/_search
{
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color.keyword"
}
},
"make_by" : {
"terms" : {
"field" : "make.keyword"
}
}
}
}
聚合的嵌套
新的聚合层让我们可以将avg度量嵌套置于terms桶内,实际上,这就为每个颜色生成了平均价格
GET /test-agg-cars/_search
{
"size": 0,
"aggs": {
"colors": {
"terms": {
"field": "color.keyword"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
动态脚本的聚合
ES支持一些基于脚本(生成运行时的字段)的复杂的动态聚合
GET /test-agg-cars/_search
{
"runtime_mappings": {
"make.length": {
"type": "long",
"script": "emit(doc['make.keyword'].value.length())"
}
},
"size" : 0,
"aggs": {
"make_length": {
"histogram": {
"interval": 1,
"field": "make.length"
}
}
}
}
前置过滤条件:filter
在当前文档集上下文中定义与指定过滤器(Filter)匹配的所有文档的单个存储桶,通常,这将用于将当前聚合上下文缩小到一组特定的文档
GET /test-agg-cars/_search
{
"size": 0,
"aggs": {
"make_by": {
"filter": {"term":{"type":"honda"}},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
就是说先匹配filter中term命中的文档,再对命中的文档进行聚合
对filter进行分组聚合:filters
场景:对不同日志类型的日志进行分组,这时就需要filters
GET /test-agg-logs/_search
{
"size": 0,
"aggs": {
"messages": {
"filters": {
"other_bucket_key": "other_messages",
"filters": {
"infos": {"match":{"body":"info"}},
"warnings": {"match":{"body":"waring"}}
}
}
}
}
}
对number类型聚合:Range
基于多桶值源的聚合,使用户能够定义一组范围,每个范围代表一个桶,在聚合过程中,将从每个存储区范围中检查每个文档中提取的值,并存储相关/匹配的文档
TIP:这种聚合包括from值,但不包括to每个范围的值、
GET /test-agg-cars/_search
{
"size": 0,
"aggs": {
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{
"to": 20000
},
{"from": 20000,"to":40000},
{
"from": 40000
}
]
}
}
}
}
IP类型聚合:IP Range
专用于IP值的范围聚合
GET /ip_addresses/_search
{
"size": 0,
"aggs": {
"ip_ranges": {
"ip_range": {
"field": "ip",
"ranges": [
{
"to": "10.0.0.5"
},
{
"from": "10.0.0.5"
}
]
}
}
}
}
CIDR Mask分组(属于IP)
GET /ip_addresses/_search
{
"size": 0,
"aggs": {
"ip_ranges": {
"ip_range": {
"field": "ip",
"ranges": [
{ "mask": "10.0.0.0/25" },
{ "mask": "10.0.0.127/25" }
]
}
}
}
}
增加key显示(属于IP)
GET /ip_addresses/_search
{
"size": 0,
"aggs": {
"ip_ranges": {
"ip_range": {
"field": "ip",
"ranges": [
{ "to": "10.0.0.5" },
{ "from": "10.0.0.5" }
],
"keyed": true // here
}
}
}
}
自定义key显示(属于IP)
GET /ip_addresses/_search
{
"size": 0,
"aggs": {
"ip_ranges": {
"ip_range": {
"field": "ip",
"ranges": [
{ "key": "infinity", "to": "10.0.0.5" },
{ "key": "and-beyond", "from": "10.0.0.5" }
],
"keyed": true
}
}
}
}
对日期类型聚合:Date Range
专用于日期值的范围聚合
GET /test-agg-cars/_search
{
"size": 0,
"aggs": {
"range": {
"date_range": {
"field": "sold",
"format": "yyyy-MM-dd",
"ranges": [
{
"from": "2014-01-01",
"to": "2014-12-31"
}
]
}
}
}
}
TIP:此聚合与Range聚合之间的主要区别在于from和to值可以在Date Math表达式中表示,并且可以指定日期格式,通过该日期格式将返回from and to 响应字段,这里还需注意,此聚合包括from值,但不包括to每个范围的值
柱状图功能:Histrogram
GET /test-agg-cars/_search
{
"size" : 0,
"aggs":{
"price":{
"histogram":{
"field": "price.keyword",
"interval": 20000
},
"aggs":{
"revenue": {
"sum": {
"field" : "price"
}
}
}
}
}
}
上面的查询是围绕price聚合构建的,它包含一个histogram桶,它要求字段的类型必须是数值型的同时需要设定分组的间隔范围,间隔设置为20000意味着我们将会得到如[0-19999,20000-39999,…]这样的区间
接着,在直方图内定义嵌套的度量,这个sum度量,它会对落入某一具体售价区间的文档中price字段的值进行求和,这可以为我们提供每个售价区间的收入,从而可以发现到底是普通家用车赚钱还是奢侈车赚钱
- 为任何聚合输出的分类和统计结果创建条形图,而不只是直方图桶,比如为最受欢迎的10种汽车以及它们的平均售价,标准差这些信息创建一个条形图,这里会用到terms桶和extended_stats度量
GET /test-agg-cars/_search
{
"size": 0,
"aggs": {
"makes": {
"terms": {
"field": "make.keyword",
"size": 10
},
"aggs": {
"stats": {
"extended_stats": {
"field": "price"
}
}
}
}
}
}
上面例子会按受欢迎程度返回制造商列表以及它们各自的统计信息,也就是stats种的信息,包括平均值,总值,最大最小值等
聚合查询之Metric聚合详解
理解
- 从分类看:Metric聚合分析分为单值分析和多值分析两类
- 从功能看:根据具体应用场景设计了一些分析api,比如地理位置,百分数等等
单值分析:标准stat类型
avg平均值
POST /exams/_search?size=0
{
"aggs": {
"avg_grade": { "avg": { "field": "grade" } }
}
}
max最大值
POST /sales/_search?size=0
{
"aggs": {
"max_price": { "max": { "field": "price" } }
}
}
min最小值
POST /sales/_search?size=0
{
"aggs": {
"min_price": { "min": { "field": "price" } }
}
}
sum和
POST /sales/_search?size=0
{
"query": {
"constant_score": {
"filter": {
"match": { "type": "hat" }
}
}
},
"aggs": {
"hat_prices": { "sum": { "field": "price" } }
}
}
value_count数量
POST /sales/_search?size=0
{
"aggs" : {
"types_count" : { "value_count" : { "field" : "type" } }
}
}
单值分析:其他类型
weighted_avg:带权重的avg
POST /exams/_search
{
"size": 0,
"aggs": {
"weighted_grade": {
"weighted_avg": {
"value": {
"field": "grade"
},
"weight": {
"field": "weight"
}
}
}
}
}
cardinality基数(distinct去重)
POST /sales/_search?size=0
{
"aggs": {
"type_count": {
"cardinality": {
"field": "type"
}
}
}
}
median_absolute_deviation中位值
GET reviews/_search
{
"size": 0,
"aggs": {
"review_average": {
"avg": {
"field": "rating"
}
},
"review_variability": {
"median_absolute_deviation": {
"field": "rating"
}
}
}
}
非单值分析:stats型
stats包含avg,max,sum和count
POST /exams/_search?size=0
{
"aggs": {
"grades_stats": { "stats": { "field": "grade" } }
}
}
matrix_stats针对矩阵模型
以下例子使用矩阵统计量来描述收入与贫困之间的关系
GET /_search
{
"aggs": {
"statistics": {
"matrix_stats": {
"fields": [ "poverty", "income" ]
}
}
}
}
extended_stats
根据从汇总文档中提取的数值计算统计信息
GET /exams/_search
{
"size": 0,
"aggs": {
"grades_stats": { "extended_stats": { "field": "grade" } }
}
}
上面的汇总计算了所有文档的成绩统计信息,聚合类型为extended_stats,并且字段设置定义将在其上计算统计信息的文档的数字字段
string_stats针对字符串
用于计算从聚合文档中提取的字符串值的统计信息,这些值可以从特定的关键字字段中检索出来
POST /my-index-000001/_search?size=0
{
"aggs": {
"message_stats": { "string_stats": { "field": "message.keyword" } }
}
}
非单值分析:百分数型
percentiles百分数范围
针对从聚合文档中提取的数值计算一个或多个百分位数
GET latency/_search
{
"size": 0,
"aggs": {
"load_time_outlier": {
"percentiles": {
"field": "load_time"
}
}
}
}
tip:默认情况下,百分位度量标准将生成一定范围的百分位:【1,5,25,50,75,95,99】
percentile_ranks百分数排行
根据从汇总文档中提取的数值计算一个多个百分位等级
GET latency/_search
{
"size": 0,
"aggs": {
"load_time_ranks": {
"percentile_ranks": {
"field": "load_time",
"values": [ 500, 600 ]
}
}
}
}
上面结果表示90.01%的页面加载在500ms内完成,而100%的页面加载在600ms内完成
非单值分析:地理位置类型
geo_bounds
POST /museums/_search?size=0
{
"query": {
"match": { "name": "musée" }
},
"aggs": {
"viewport": {
"geo_bounds": {
"field": "location",
"wrap_longitude": true
}
}
}
}
上面汇总展示了如何针对具有商店业务类型的所有文档计算位置字段的边界框
geo_centtroid
POST /museums/_search
{
"aggs": {
"centroid": {
"geo_centroid": {
"field":"location"
}
}
}
}
上面汇总显示了如何针对具有犯罪类型的盗窃文件计算位置字段的质心
geo_line
POST /test/_search?filter_path=aggregations
{
"aggs": {
"line": {
"geo_line": {
"point": {"field": "my_location"},
"sort": {"field": "@timestamp"}
}
}
}
}
将存储桶中的所有geo_point值聚合到由所选排序字段排序的LineString
非单值分析:Top型
top_hits分桶后的top hits
POST /sales/_search?size=0
{
"aggs": {
"top_tags": {
"terms": {
"field": "type",
"size": 3
},
"aggs": {
"top_sales_hits": {
"top_hits": {
"sort": [
{
"date": {
"order": "desc"
}
}
],
"_source": {
"includes": [ "date", "price" ]
},
"size": 1
}
}
}
}
}
}
top_metrics
POST /test/_search?filter_path=aggregations
{
"aggs": {
"tm": {
"top_metrics": {
"metrics":{
"field":"m"
},
"sort":{"s":"desc"}
}
}
}
}
聚合查询之Pipline聚合详解
管道聚合(Pipeline Aggregration):简单而言其实就是让上一步的聚合结果成为下一个聚合的输入,这就是管道
举例解释管道
- 比如HttpServletRequest处理的过滤器中
当一个request过来的时候,需要对这个request做一系列的加工,使用责任链模式可以对每个加工组件化,减少耦合,也可以使用在当一个request过来的时候,需要找到合适的加工方式,当一个加工方式不适合这个request的时候,传递到下一个加工方法,该加工方式再尝试对request加工
ES的管道解说
第一个维度:管道聚合有很多不同类型,每种类型都与其他聚合计算不同的信息,但是可以将这些类型分为两类
- 父级:父级聚合的输出提供了一组管道聚合,它可以计算新的存储桶或新的聚合以添加到现有的存桶中。
- 兄弟级:同级聚合的输出提供的管道聚合,并且能够计算与该同级聚合处于同一级别的新聚合
第二个维度:根据功能设计的意图
比如前置聚合可能是Bucket聚合,后置聚合可能是基于Metric聚合,那么它就可以成为一类管道
- Bucket聚合 -》 Metric聚合:Bucket聚合的结果,成为下一步metric聚合的输入
Average bucket聚合
POST _search
{
"size": 0,
"aggs": {
"sales_per_month": {
"date_histogram": {
"field": "date",
"calendar_interval": "month"
},
"aggs": {
"sales": {
"sum": {
"field": "price"
}
}
}
},
"avg_monthly_sales": {
// tag::avg-bucket-agg-syntax[]
"avg_bucket": {
"buckets_path": "sales_per_month>sales",
"gap_policy": "skip",
"format": "#,##0.00;(#,##0.00)"
}
// end::avg-bucket-agg-syntax[]
}
}
}
- 嵌套的bucket聚合:聚合出按月价格的直方图、
- Metric聚合:对上面的聚合再求平均值
字段类型
- bucket_path:指定聚合的名称
- gap_policy:当管道聚合遇到不存在的值,有点类似于term等聚合的(missing)时所采取的策略,可选择值为:skip,insert_zeros
- skip:此选项将丢失的数据视为bucket不存在,它将跳过桶并使用下一个可用值继续计算
- format:用于格式化聚合桶的输出(key)
Stats bucket聚合
POST /sales/_search
{
"size": 0,
"aggs": {
"sales_per_month": {
"date_histogram": {
"field": "date",
"calendar_interval": "month"
},
"aggs": {
"sales": {
"sum": {
"field": "price"
}
}
}
},
"stats_monthly_sales": {
"stats_bucket": {
"buckets_path": "sales_per_month>sales"
}
}
}
}
原理篇 - ES原理的初步认知
先提出以下疑问:
- 为什么我的搜索
*foo-bar* 无法匹配 foo-bar ? - 为什么增加更多的文件会压缩索引(Index)?
- 为什么ElasticSearch占用很多内存?
云上的集群
云里面的每个白色正方型盒子代表一个节点-Node
在一个或者多个节点之间,多个绿色小方块在一起形成一个ElaticSearch的索引
在一个索引下,分布在多个节点里的绿色小方块称为分片–Shard
一个ElasticSearch的Shard本质上是一个Lucene Index
Lucene是一个Full Text搜索库(也有很多其她形式的搜索库),ES是建立在Lucene之上的
图解Lucene
Segment
在Lucene里面有很多小的segment,我们可以把它们看成Lucene内部的mini-index
- Segment内部(有许多数据结构)
- Inverted Index
- Stored Fields
- Document Values
- Cache
Inverted Index
Inverted Index主要包括两部分
- 一个有序的数据字典Dictionary(包括单词Term和它出现的频率)
- 与单词Term对应的Postings(即存在这个单词的文件)
tip:当我们搜索时,首先会将搜索的内容分解,然后在字典里面找到对应的Term,从而查到与搜索相关的文件内容
- 自动补全(AutoCompletion-Prefix)
查找以字母c开头的字符,可以简单的通过二分查找(Binary Search)在Inverted Index表中找到例如“choice”,"comign"这样的词(Term)
查找所有包含“our”字符的单词,那么系统会扫描整个Inverted Index,这是非常昂贵的
在这种情况下,如果想要做优化,那么我们面对的问题是如何生成合适的Term
对于此类的问题,可能有以下几种可行的解决方案
* suffix -> xiffus *
如果我们想以后缀为搜索条件,可以为Term做反向处理
(60.6384, 6.5017) -> u4u8gyykk
对于GEO位置信息,可以将它转换为GEO Hash
123 -> {1-hundreds, 12-tens, 123}
对于简单的数字,可以为他生成多重形式的Term
如何解决拼写错误
使用Python库为单词生成一个包含错误拼写信息的树形状态机,解决拼写错误的问题
Stored Field 字段查找
当想要查找包含某个特定标题内容的文件时,Inverted Index就不能很好的解决整个问题,所以Lucene提供了另一种数据结构Stored Field来解决这个问题,本质上,Stored是一个简单的键值对key-value,默认情况下,ES会存储整个文件的JSON source
Document Values 为了排序,聚合
以上结构仍然无法解决诸如:排序,聚合,facet,因为我们可能要读取大量不需要的信息
而Document Values这种结构解决了此种问题,这种结构本质上是一个列式的存储,他高度优化了具有相同类型的数据的存储结构
为了提高效率,ES可以将索引下某一个Document Value全部读取到内存中进行操作,这大大提升访问速度,但是也同时会消耗大量的内存空间,但总而言之,这些数据结构Inverted Index,Stored Fields,Document Values及其缓存,都在segment内部
小总结
ES的cluster中有很多Node -》 每个Node里面又有很多的shard -》 一个或多个Node中的shard组合在一起形成一个ElasticSearch的索引 -》每个shard 相当于Lucene Index,一个ElasticSearch的shard本质上就是一个Lucene Index -》 每个Lucene里面又有很多的小segment,segment可以看成是Lucene内部的mini-index -》每个segment内部又有着许多的内部结构,举例:Inverted Index、Stored Fields、Document Values、Cache等,Es的搜索其实是基于这些数据结构的
ES搜索时会发生什么(重点理解!!!!)
搜索时,Lucene会搜索所有的segment然后将每个segment的搜索结果返回,最后合并呈现给客户
Lucene的一些特性使得这个过程非常重要
- Segments是不可变的(immutable)
- Delete?当删除发生时,Lucene做的只是将其标志位置为删除,但文件还是会在它原来的地方,不会发生改变
- Update?所以对于更新来说,本质上它做的工作是:先删除,然后重新索引(Re-index)
- 随处可见的压缩
- Lucene非常擅长压缩数据,基本上所有教科书上的压缩方式,都能在Lucene中找到
- 缓存所有的所有
- Lucene也会将所有的信息做缓存,这大大提高了它的查询效率
缓存的故事
当ES索引一个文件时,会为文件建立相应的缓存,并且会定期(每秒)刷新这些数据,然后这些文件就可以被搜索到
随着时间的增加,会有很多的segment,所以ES会将这些segment合并,在这个过程中,segment会最终被删除掉
这也就是为什么增加文件可能会使索引所占空间变小,它会引起merge,从而可能会有更多的压缩
例子:
有两个segment将会merge
这两个segment最终会被删除,然后合并成一个新的segment
这时这个新的segment在缓存中处于cold状态,但是大多数segment仍然保持不变,处于warm状态。
以上场景经常在Lucene Index内部发生的。
在shard中搜索
ES从shard中搜索的过程与Lucene Segment中搜索的过程类似
tip:与在Lucene Segement中搜索不同的是,shard可能是分布在不同Node上,所以在搜索与返回结果时,所有的信息都会通过网络传输
注意:一次搜索查找两个shard == 2次分别搜索shard
对日志文件处理
当想要搜索特定日期产生的日志时,通过根据时间戳对日志文件进行分块和索引,会极大提高搜索效率
当想要删除旧的数据时,只需删除老的索引即可
在上图中,每个index有两个shards
如何scale
tip:shard不会进行更进一步的拆分,但是shard可能会被转移到不同的节点上
所以当集群节点压力增长到一定的程度时,就可能会考虑增加新的节点,这就会要求我们对所有数据进行重新索引,这是我们所不太希望的,所以需要在规划的时候就考虑清楚,如何去平衡足够多的节点与不足节点之间的关系
节点分配与shard优化
- 为更重要的数据索引节点,分配性能更好的机器
- 确保每个shard都有副本信息replica
每个节点,每个都存留一份路由表,所以当请求到任何一个节点时,ES都有能力将请求转发到期望节点的shard进一步处理
演示:一个真实的请求
这里的query有一个类型filtered,以及一个multi_match的查询
根据作者进行聚合,得到top10的hits的top10作者的信息
这个请求可能会被分发到集群里的任意一个节点
这个节点会成为当前请求的协调者,它会根据索引信息,判断请求会被路由到哪个核心节点,以及哪个副本是可用的等等
ES会将Query转换成Lucene Query
TIP:但queries不会被缓存,所以如果相同的Query重复执行,应用程序自己需要做缓存
- filters可以在任何时候使用
- query只有在需要score的时候才使用
搜索结束之后,结果会沿着下行的路径向上逐层返回!
原理篇 - ES原理的补充和整体架构
ES的整体架构
- 一个ES index在集群模式下,有多个Node(节点)组成,每个节点就是ES的Instance(实例)
- 每个节点都有多个shard(分片),p0,p1是主分片,R0,R2是副本分片
- 每个分片上对应着就是一个Lucene Index(底层索引文件)
- Lucene Index是一个统称
- 由多个Segment(段文件,也就是所谓的倒排索引)组成,每个段文件存储着就是Doc文档
- commit point 记录了所有segments的信息
Lucene索引结构
Lucene的索引结构中有哪些文件呢?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0BxOoq8v-1653812907296)(ES.assets\image-20210716230241459.png)]
文件的关系:
Lucene处理流程(可以直观看到ES搜索的内部流程)
创建索引的过程
- 准备待索引的原文档,数据来源可能是文件、数据库、网络等
- 对文档的内容进行分词组件处理,形成一系列的Term
- 索引组件对文档和Term处理,形成字典和倒排表
搜索索引的过程
- 对查询语句进行分词处理,形成一系列Term
- 根据倒排索引表查找出包含Term的文档,并进行合并形成符合结果的文档集
- 比对查询语句与各个文档相关性得分,并按照得分高低返回
ElasticSearch分析器
分析:包含下main的过程
- 首先,将一块文本分成适合于倒排索引的独立的词条
- 然后,将这些词条同意化为标准格式以提高它们的可搜索性,或者recall
分析器执行上面的工作,分析器实际上是将三个功能封装到了一个包里
- 字符过滤器:首先,字符串按顺序通过每个字符过滤器,它们的任务是在分词前整理字符串,一个字符过滤器可以用来去掉HTML,或者将&转化成and
- 分词器:其次,字符串被分词器分为单个的词条,一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条
- Token过来器:最后,词条按顺序通过每个token过滤器,这个过程可能会改变词条(例如,小写化Quick),删除词条(例如,像a, and , the等无用词),或者增加词条(例如,像jump和leap这种同义词)
TIP:Elasticsearch提供了开箱即用的字符过滤器,分词器和token过滤器,这些可以组合起来形成自定义的分析器以用于不同的目的
内置分析器
测试文本
"Set the shape to semi-transparent by calling set_trans(5)"
标准分析器是Elasticsearch默认使用的分析器,它是分析各种语言文本最常用的选择,它根据unicode联盟定义的单词边界划分文本,删除绝大部分标点,最后,将词条小写,它会产生
set, the, shape, to, semi, transparent, by, calling, set_trans, 5
简单分析器在任何不是字符的地方分隔文本,将词条小写,它会产生
set, the, shape, to, semi, transparent, by, calling, set, trans
空格分析器在空格的地方划分文本,它会产生
Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
特定语言分析器用于很多语言,它们可以考虑指定语言的特点,例如,英语分析器附带了一组英语无用词(常用单词,例如and或者the,它们对相关性没有多少影响),它们会被删除,由于理解英语语法的规则,这个分词器可以提取英语的词干
英语分词器会产生下面的词条
set, shape, semi, transpar, call, set_tran, 5
TIP:这里注意,当使用英语分析器时,transparent,calling和set_trans已经变为词根格式
什么时候使用分析器
当我们索引一个文档,它的全文域被分析成词条以用来创建倒排索引,但是,当我们在全文域搜索的时候,我们需要将查询字符串通过相同的分析过程,以保证我们搜索的词条格式与索引中的词条格式一致
全文查询,理解每个域是如何定义的,因此它们可以做正确的事:
- 当查询一个全文域时,会对查询字符串应用相同的分析器,以产生正确的搜索词条列表
- 当查询一个精准值域时,不会分析查询字符串,而是搜索你指定的精确值
比如:
那么为什么会返回这样的结果呢?
- date域包含一个精确值:单独的词条2014-09-15
- _all域是一个全文域,所以分词进程将日期转化为三个词条:2014,09,05
当我们在_all查询2014时,它匹配所有的12条推文,因为它们都含有2014
GET /_search?q=2014 # 12 results
当我们在_all域查询2014-09-15,它首先分析字符串,产生匹配2014,09,15中任意词条的查询,这也会匹配所有12条推文,因为它们都含有2014
GET /_search?q=2014-09-15 # 12 results !
当我们在date域查询2014-09-15时,它会寻找精确日期,只找到一个推文:
GET /_search?q=date:2014-09-15 # 1 result
当我们在date域查询2014时,它找不到任何文档,因为没有文档含有这个精确日期
GET /_search?q=date:2014 # 0 results !
原理篇 - ES原理之索引文档流程详解
文档索引步骤顺序
新建单个文档所需的步骤顺序:
- 客户端向Node1发送新建索引或删除删除请求
- 节点使用文档的_id确定文档属于分片0,请求会被转发到Node3,因为分片0的主分片目前被分配在Node3上
- Node3在主分片上面执行请求,如果成功,它将请求并行转发到Node1和Node2的副分片上,一旦所有副本分片都报告成功,Node3将向协调节点报告成功,协调节点向客户端报告成功
使用bulk修改多个文档步骤顺序
- 客户端向Node1发送bulk请求
- Node1为每个节点创建一个批量请求,并将这些请求并行转发到每个包含主分片的节点主机
- 主分片一个接一个按顺序执行每个操作,当每个操作成功时,主分片并行转发新文档(或删除)到副本分片,然后执行下一个操作,一旦所有的副本分片报告所有操作成功,该节点将向协调节点报告成功,协调节点将这些响应收集整理并返回给客户端
文档索引过程详解
整体的索引流程
- 协调节点默认使用文档ID参与计算(也支持通过routing),以便为路由提供合适的分片
shard = hash(document_id) % (num_of_primary_shards)
- 当分片所在节点接收到来自协调节点的请求后,会将请求写入到Memory Buffer,然后定时(默认是每隔一秒)写入到Filesystem Cache,这个从Momery Buffer 到Filesystem Cache的过程叫做refresh
- 当然在某些情况下,存在Momery Buffer和Fielsystem Cache的数据可能会丢失,ES是通过translog的机制来保证数据的可靠性的,其实现机制是接收到请求后,同时也会写入到translog中,当Filesystem Cache中的数据写入到磁盘时,才会清楚掉,这个过程叫做flush
- 在flush过程中,内存中的缓存将被清楚,内容被写入一个新段,段的fsync将创建一个新的提交点,并将内容刷新到磁盘,旧的tanslog将被删除并开始一个新的translog,flush触发的时机时定时触发(默认30分钟)或者translog变得太大(默认为512m)时
分步骤看数据持久化过程
通过分步骤看数据持久化过程:write -> refresh -> flush -> merge
一个新文档过来,会存储在in-memory buffer内存缓存区中,顺便会记录到Transog(ElasiticSearch增加了一个tanslog,或者叫事务日志,在每一次对ElasticSearch进行操作时进行了日志记录),这时候还没有到segment,是搜索不到这个新文档的,数据只有被refresh后,才可以被搜索到
refresh默认1秒,执行一次上图流程,ES是支持修改这个值的,通过index.refresh_interval设置refresh(冲刷)间隔时间
refresh的流程大致如下:
- in-memory buffer中的文档写入到新的segment中,但segment是存储在文件系统的缓存中的,此时文档可以被搜索到
- 最后清空in-memory buffer,注意:Translog没有被清空,为了将segment数据写到磁盘
- 文档经过refresh后,segment暂时写到文件系统缓存,这样避免了性能IO操作,又可以使文档搜索到,refresh默认1秒执行一次,性能损耗太大,一般建以稍微延长这个refresh时间间隔,比如5秒,因此,ES其实就是准实时,并非真正的实时
每隔一段时间,例如translog变得越来越大了,索引被刷新flush:一个新的translog被创建,并且一个全量提交被执行
上一个过程中,segment在文件系统中缓存,会有意外故障文档丢失,那么,为了保证文档不会丢失,需要将文档写入磁盘,那么文档从文件缓存写入磁盘的过程就是flush,写入磁盘后,清空translog
具体过程如下:
- 所有在内存缓冲区的文档都被写入一个新的段
- 缓冲区被清空
- 一个Commit Point被写入硬盘
- 文件系统缓存通过fsync被刷新(flush)
- 老的translog被删除
由于自动刷新流程每秒会创建一个新的段,这样会导致短时间内的段数量暴增,而段数量太多会带来较大麻烦,每一个段都会消耗文件句柄,内存和cpu运行周期,更重要的是,每个搜索请求都必须轮流检查每个段,所以段越多,搜索也就越慢
ElasticSearch通过在后台进行Merge Segment来解决这个问题,小的段会被合并到大的段,然后这些大的段再被合并到更大的段
当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用,合并进程选择一小部分大小相似的段,并在后台将它们合并到更大的段中,这并不会中断索引和搜索
一旦合并结束,老的段会被删除
- 新的段被刷新(flush)到了硬盘,**写入一个包含新段且排除旧的和较小的段的新提交点
- 新的段被打开用来搜索
- 老的段被删除
合并大的段需要消耗大量的I/O和cpu资源,如果任其发展会影响搜索性能,ES在默认情况下会对合并流程进行资源限制,所以搜索仍然有足够的资源很好的执行
深入ElasticSearch索引和文档的实现机制
考虑和分析一个分布式系统的写操作时,一般需要从下面几个方面考虑:
- 可靠性:也可以说是持久性,数据写入系统成功后,数据不会被回滚或丢失、
- 一致性:数据写入成功后,再次查询时必须保证读取到最新版本的数据,不能读取到旧数据
- 原子性:一个写入或者更新操作,要么完全成功,要么完全失败,不允许出现中间状态
- 隔离性:多个写入操作相互不影响
- 实时性:写入后是否立刻被查询到
- 性能:写入性能,吞吐量到底怎么样
TIP:ES作为分布式系统,在写入时也必须满足上述的特点
Lucene的写
ElasticSearch内部使用了Lucene完成索引创建和搜索功能,Lucene中写操作主要时通过IndexWriter类实现,IndexWriter提供三个接口:
public long addDocument();
public long updateDocuments();
public long deleteDocuments();
通过这三个接口可以完成单个文档的写入,更新和删除功能,包括分词,倒排创建,正排创建等所有与搜索相关的流程,只要Doc通过IndexWriter写入后,后面就可以通过IndexSearcher搜索了,但这样就出现了一些疑问
- 上述操作是单机的,而不是我们所知道的分布式
- 文档写入Lucene后并不是立即可查询的,需要生成完整的Segment后才可被搜索,如何保证实时性?
- Lucene生成的segment是在内存中,如果机器宕机或掉电后,内存中的Segment会丢失,如何保证数据可靠性?
- Lucene不支持部分文档更新,但是这又是一个强需求,如何支持部分更新?
ElasticSearch的写
ES采用多shard方式,通过配置rounting规则将数据分成多个数据子集,每个数据子集提供独立的索引和搜索功能,当写入文档时,根据rounting规则,将文档发送给特定shard中建立索引,这样就实现分布式了
ES整体架构采用的是一主多副的方式
每个Index由多个shard组成,每个shard有一个主节点和多个副本节点,副本个数可配,每次写入的时候,写入请求会先根据rounting规则选择发送给哪个shard,Index request中可以设置使用哪个Field的值作为路由参数,如果没有设置,则使用mapping中的配置,如果mapping也没有配置,则使用_id作为路由参数,然后通过rounting的Hash值选择出shard(在OperationRounting类中),最后从集群的Meta中找出该shard的primary节点
请求接着会发送给primary Shard,在primary shard上执行成功,再从primary shard上将请求同时发送给多个Replilca Shard,请求在多个Replica Shard上执行成功并返回给Primary shard后,写入请求执行成功,返回结果给客户端
这种模式下,写入操作的延时就等于latency = Latency(primary write) + Max(Replicas write),只要有副本在,写入延时最小也是两次单shard的写入时延总和,写入效率会较低,但是这样的好处也很明显,避免写入后,单机或磁盘故障导致数据丢失,在数据重要性和性能方面,一般都是优先选择数据,除非一些允许丢失数据的特殊场景
采用多个副本后,避免了单机或磁盘故障发生时,对已经持久化后的数据造成损害,但是ES为了减少磁盘IO保证读写性能,一般是每隔一段时间(比如5分钟)才会把Lucene的Segment写入磁盘持久化,对于写入内存,但还未Flush到磁盘的Lucene数据,如果发生机器宕机或者掉电,那么内存中的数据也会丢失,这时候如何保证?
对于上面的问题,ES学习了数据库中的处理方式,增加commitLog模块,ES中称为TransLog
在每一个shard中,写入流程分为两部分,先写入Lucene,再写入TransLog
写请求到达shard后,先写Lucene文件,创建好索引,此时索引还在内存黎曼,接着去写TransLog,写完TransLog后,刷新TransLog数据到磁盘上,写入磁盘成功后,请求返回用户,这里有几个关键点:
- 和数据库不同,数据库是先写commitlog,然后再写内存,而ES是先写内存,最后才写translog,可能的原因是Lucene的内存写入会有很复杂的逻辑,很容易失败,比如分词,字段长度超过限制等,比较重,为了避免TransLog中有大量无效记录,减少recover的复杂度和提高速度,所以就把写Lucene放在了最前面
- 写Lucene内存后,并不是可被搜索到的,需要通过Refresh把内存的转成完整的Segment后,然后再次reopen后才能被搜索到,一般这个时间设置为1秒,导致写入ES的文档,最快要1秒才可被搜索到,所以ES在搜索方面是NRT(Near Real Time)近实时的系统
- 当ES作为Nosql的数据库时,查询方式是getById,这种查询可以直接从TransLog中查询,这时候就成了RT(Real Time)实时系统
- 每隔一段比较长的时间,比如30分钟后,Lucene会把内存中生成的新Segment刷新到磁盘上,刷新后索引文件已经持久化了,历史的TransLog就没有用了,会清空掉旧的TransLog
ES的Update流程
Lucene中不支持部分字段的update,所以需要在ElasticSearch中实现该功能,具体流程如下:
- 收到Update请求后,从Segment或者TransLog中读取同id的完整Doc,记录版本号为v1
- 将版本v1的全量Doc和请求中的部分字段doc合并为一个完整的doc,同时更新内存中的versionMap,获取完整Doc后,update请求就变成Index请求,加锁
- 再次从versionMap中读取该id的最大版本号v2,如果versionMap中么有,则从Segment或TransLog中读取,这里基本都会从versionMap中获取到
- 检查版本是否冲突(v1 == v2),如果冲突,则回退到开始的”update doc“阶段,重新执行,如果不冲突,则执行最新的add请求
- 在index doc阶段,首先将version +1 得到v3,再将Doc加入到Luncene中去,Lucene中会先删除id下的已存在doc id,然后再增加新Doc,写如Lucene成功后,将当前v3更新到versiobnMap中
- 释放锁,部分更新的流程就结束了
ES写入请求类型
ES中写入请求主要包括这几个:Index(create), Update, Delete和Bulk,其中前三个是单文档操作,后一个是多文档操作,其中Bulk中可以包括index,update,delete
TIP:在6.0.0以及之后的版本中,前3个文档操作的实现基本和Bulk操作一致,甚至有些就是通过调用Bulk的接口实现的
Bulk请求的写入流程
红色:client Node、绿色:Primary Node、蓝色:Replica Node
-
Ingest Pipeline 可以在这一步对原始文档做一些处理,比如HTML解析,自定义处理,具体的处理逻辑可以通过插件来实现,在ES中,由于Ingest Pipeline会比较消耗CPU等资源,可以设置专门的Ingest Node,专门用来处理Ingest Pipepline逻辑,如果当前Node不能执行Ingest Pipeline,则会将请求发给另一台可以执行Ingest Pipeline 的Node -
Auto Create Index 判断当前Index是否存在,如果不存在,则需要自动创建Index,这里需要和master交互,也可以通过配置关闭自动创建Index的功能 -
set rounting 设置路由条件,如果Request中指定了路由条件,则直接使用Request 中的Rounting,否则使用Mapping中配置的,如果Mapping中无配置,则使用默认的_id字段值 在这一步中,如果没有指定id字段,则会自动生一个唯一的_id字段,目前使用的是UUID -
Construct BulkShardRequest 由于Bulk Request中会包含多个(Index/Update/Delete)请求,这些请求会根据rounting可能会落在多个shard上执行,这一步会按Shard挑选Single write Request,同一个Shard中的请求聚集在一起,构建Bulk Shard Request,每个Bulk Shard Requesrt对应一个Shard -
Send Request To Primary 这一步会将每一个BulkShardRequest请求发送给相应Shard 的Primary Node
-
Index or Uodate or Delete 循环执行每个Single write Request ,对于每个Request,根据操作类型(CREATE/INDEX/UPDATE/DELETE)选择不同的处理逻辑,其中,create/Index是直接增加Doc,Delete是根据_id删除Doc,Update会稍微复杂些,下面以Update为例: -
Translate Update to Index or Delete 这一步是update操作特有的步骤,在这里,会将update请求转为Index或者Delete请求,首先,会通过GetRequest查询到已经存在的同_id Doc(如果有)的完整字段和值(依赖_score字段),然后和请求中Doc合并,同时,这里会获取到读到的DOC版本号,记做V1 -
Parse DOC 这里会解析DOC各个字段,生成ParsedDocument对象,同时会生成uid Term,在ES中,_uid = type # _id,对用户,_id 可见,而ES中存储的是uid,这一部分生成的ParsedDocument中也有ES的系统字段,大部分会根据当前内容填充,部分未知的会在后面继续填充ParsedDocument -
Update Mapping ES中有个自动更新Mapping的功能,在这一步生效,会先挑选出Mapping中为包含的新Field,然后判断是否运行自动更新Mapping,如果允许,则更新Mapping -
Get Sequence Id And Version 由于当前是Primary Shard,则会从SequenceNumber Service获取一个SequenceID和Version,SequenceId在Shard级别每次递增1,SequenceID在写入DOC成功后,会用来初始化LocalCheckpoint,Version则是根据当前DOC的最大Version递增1 -
Add Doc To Lucene 这一步开始的时候会给特定的_uid加锁,然后判断该uid对应的version是否等于之前Translate update to index步骤里后去的version,如果不相等,则说明刚才读取doc后,该doc发生了变化,出现了版本冲突,这时候会抛出一个versionConfilct的异常,该异常会在primary Node最开始处捕获,重新从Translate update to index or delete开始执行 如果version相等,则继续执行,如果已经存在同id的doc,则会调用Lucene的UpdateDocument(uid,doc)接口,先根据uid删除Doc,然后再Index新的Doc,如果是首次写入,则直接调用Lucene的AddDocment接口完成Doc的Index,AddDocment也是通过UpdateDocument实现的 这一步中的问题是,如何保证Delete-Then-Add的原子性,怎么避免中间状态时被Refresh?答案是在开始Delete之前,会加一个Refresh Lock,禁止被Refresh,只有等Add完后释放了Refresh Lock后才能被Refresh,这样就保证了Delete-Then-Add的原子性 Lucene的UpdateDocument接口中只是处理多个Field,会遍历每个Field逐个处理,处理顺序是invert index,store field,doc values,point demension -
Write TransLog 写完Lucene的Segment后,会以keyvalue的形式写TransLog,key是_id,value是doc的内容,当查询的时候,如果请求是getDocById,则是可以直接根据 _id 从TransLog中读取到,满足NoSQL场景下的实时性 需要注意的是,这里只是写入到内存的TransLog,是否Sync到磁盘的逻辑还在后面,这一步的最后,会标记当前SequenceID已经成功执行,接着会更新当前Shard的LocalCheckPoint -
Renew Bulk Request 这里会从新构造Bulk Request,原因是前面已经将UpdateRequest翻译成了Index或Delete请求,则后续所有Replica中只需执行Index或Delete请求就可以了,不需要再执行Update逻辑,一是保证Replica中逻辑更简单,性能更好,而是保证同一个请求再Primary 和Replica中的执行结果一样 -
Flush TransLog 这里会根据TransLog的策略,选择不同的执行方式,要么是立即Flush到磁盘,要么是等到以后再Flush,Flush的频率越高,可靠性越高,对写入性能的影响越大 -
send Requests To Replicas 这里会将刚才构造的新的Bulk Request并行发送给多个Replica,然后等待Replica的返回,这里需要等待所有的Replica返回后(可能有成功,可能有失败),Primary Node才会返回用户,如果某个Replica失败了,则Primary 会给master发送一个Remove Shard请求,要求master将该Replica Shard从可用节点中移除 这里,同时会将SequenceID,PrimaryTerm,GlobalCheckPoint等传递给Replica 发送给Replica的i请求中,Action Name 等于原始ActionName+【R】,这里的R表示Replica,通过这个[R]的不同,可以找到处理Replica请求的Handler -
Receive Response From Replicas Replicas中请求都处理完后,会更新Primary Node 的LocalCheckPoint
-
Index or Delete 根据请求类型是Index还是Delete,选择不同的执行逻辑,这里没有Update,是因为在PrimaryNode中已经将Update转换成了Index或Delete请求了 -
Parse doc(同primary Node相同) -
Update mapping(同primary Node相同) -
Get Sequence Id and version Primary Node中会生成Sequence Id和Version,然后放入Replica Request中,这里只需要从Request中获取到就行 -
Add Doc To Lucene 由于已经在Primary Node中将部分update请求转换成了Index或Delete请求,这里只需处理Index和Delete两种请求,不再处理Update请求了,比primary node会简单一些。 -
write TransLog -
Flush TransLog 以上都和Primary中 Node逻辑一致
最后小总结
分布式系统中的六大特性
-
可靠性:由于Lucene的设计中不考虑可靠性,在ES中通过Replica和TransLog两套机制保证数据的可靠性 -
一致性:Lucene中的Flush锁只保证Update接口里面的Delete和Add中间不会Fluesh,但是Add完成后仍然有可能立即发生Flush,导致Segment可读,这样就没法保证Primary和所有其他Replica可以同一时间Flush,就会出现查询不稳定的情况,这里只能实现最终一致性 -
原子性:add和delete都是直接调用lucene的接口,是原子的,当部分更新时,使用version和锁保证更新是原子的 -
隔离性:仍然采用version和局部锁来保证更新的是特定版本的数据 -
实时性:使用定期Refresh Segment到内存,并且Reopen Segment方式保证搜索可以在较短时间(比如1秒)内被搜索到,通过将未被刷新到硬盘数据记入TransLog,保证对未提交数据可以通过ID实时访问到 -
性能:性能是一个系统性工程,所有环节都要考虑对性能的影响,在ES中,在很多地方的设计都考虑到了性能, 一是不需要所有Replica都返回才能返回给用户,只需要返回特定数目就行 二是生成的Segment先在内存中提供服务,等一段时间后才刷新到磁盘,Segment在内存这段时间的可靠性由TransLog保证 三是TransLog可以配置为周期行的Flush,但这个会给可靠性带来伤害 四是每个线程持有一个Segment,多线程时相互不影响,相互独立,性能更好 五是系统的写入流程对版本依赖较重,读取频率较高,因此采用了versionMap,减少热点数据的多次磁盘IO开销,Lucene中针对性能做了大量的优化
原理篇 - ES原理之读取文档流程详解
文档查询步骤顺序
以下是从主分片或者副本分片检索文档的步骤顺序:
- 客户端向Node 1发送获取请求
- 节点使用文档 _id 来确定文档属于分片0,分片0的副本分片存在于所有的三个节点上,在这种情况下,它将请求转发到Node 2
- Node 2将文档返回Node 1,然后将文档返回给客户段
在处理读请求时,协调节点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡
在文档被检索时,已经被检索的文档可能已经存在于主分片上但是还没有复制到副本分片,在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档,一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的
使用mget取回多个文档的步骤顺序:
- 客户端向Node 1发送mget请求
- Node 1为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的主分片或者副本分片的节点上,一旦收到所有答复,Node 1构建相应并将其返回给客户端
文档读取过程详解
所有的搜索系统一般都是两阶段查询,第一阶段查询到匹配的DocID,第二阶段再查询DocID对应的完整文档,这种在ES中称为query_then_fetch
-
在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或副本分片)。每个分片在本地执行搜索并构建一个匹配文档的大小为from + size的优先队列。 PS:在2.搜索的时候是会查询FileSystem Cache的,但是有部分数据还在Memory Buffer,所以搜索是近实时的 -
每个分片返回各自优先队列中所有文档的ID和排序值给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表 -
接下来就是取回阶段,协调节点辨别出哪些文档需要被取回并向相关的分片提交多个GET请求,每个分片加载并丰富文档,如果有需要的话,接着返回文档给协调节点,一旦所有文档都被取回了,协调节点返回结果给客户端
深入了解ElasticSearch读取文档的实现机制
读操作
一致性指的是写入成功后,下一次读操作一定要能读取到最新的数据,对于搜索,这个要求会低一些,可以有一些延迟,但是对于NoSQL数据库,则一般要求最好是强一致性
结果匹配上,NoSQL作为数据库,查询过程中只有不符合两种情况,而搜索里面还有是否相关,类似于NoSQL的结果只能是0或1,而搜索里面可能会有0.1,0.5,0.9等部分匹配或更相关的情况
结果召回上,搜索一般只需要召回最满足条件的Top N结果即可,而NoSQL一般需要返回满足条件的所有结果
搜索系统一般都是两阶段查询,第一阶段查询到对应的Doc ID,也就是PK;第二阶段再通过DOC ID去查询完整文档,而NoSQL数据库一般是一阶段就返回结果,在ES中两种都支持。
目前NoSQL的查询,聚合,分析和统计等功能上都要比搜索弱的
Lucene的读
ES使用Lucene作为搜索引擎,通过Lucene完成特定字段的搜索等功能,在Lucene中这个功能是通过IndexSearcher的下列接口实现的:
public TopDocs search(Query query, int n);
public Document doc(int docID);
public int count(Query query);
......(其他)
第一个Search接口实现搜索功能,返回最满足Query的N个结果,第二个接口通过doc id查询Doc内容,第三个count接口通过Query获取到命中数
这三个概念是搜索中最基本的三个功能点,对于大部分ES中的查询都是比较复杂的,直接使用这个接口是无法满足需求的,比如分布式问题,这些就都留给了ES解决
ElasticSearch的读
ES中每个Shard都会有多个Replica,主要是为了保证数据可靠性,除此之外,还可以增加都能力,因为写的时候虽然要写大部分Replica Shard,但是查询的时候只需查询Primary 和Replica中的任何一个就可以了
在上图中,该Shard有1个Primary和2个Replica Node,当查询的时候,从三个节点中根据Request中的preference参数选择一个节点查询,preference可以设置 _local, _primary, _replica以及其它选项,如果选择了primary,则每次查询都是直接查询primary,可以保证每次查询都是最新的,如果设置了其他参数,那么可能会查询到R1或者R2,这时候就有可能查询不到最新的数据
ES中的查询是如何支持分布式的
ES中通过分区实现分布式,数据写入的时候根据 _rounting 规则将数据写入某一个shard中,这样就能将海量数据分布在多个shard以及多台机器上,已达到分布式的目标,这样就导致了查询的时候,潜在数据会在当前index的所有shard中,所以ES查询的时候需要查询所有Shard,同一个Shard的primary和replica选择一个即可,查询请求会分发给所有shard,每个shard中都是一个独立的查询引擎,比如需要返回top 10 的结果,那么每个shard都会查询并且返回top 10的结果,然后在client Node里面会接收所有shard的结果,然后通过优先级队列二次排序,选择出TOP 10的结果返回给用户
这里有一个问题就是请求膨胀,用户的一个搜索请求在ES内部会变成shard个请求,这里有个优化点,虽然是shard个请求,但是shard个数不一定要是当前index中的shard个数,只要是当前查询相关的shard即可,这个需要基于业务和请求内容优化,通过这种方式可以优化请求膨胀数
ES中的查询主要分为两类:GET请求:通过ID查询特定DOC;Search请求:通过Quert查询匹配Doc
PS:上图中内存中的Segment是指刚Refresh Segment,但是还没持久化到磁盘的新Segment,而非从磁盘加载到内存中的Segment
对于Search类请求,查询的时候是一起查询内存和磁盘上的Segment,最后将结果合并返回,这种查询是近实时的,主要是由于内存中的Index数据需要一段时间后才会刷新成为Segment
对于Get类请求,查询的时候是先查询内存中的TransLog,如果成功就立即返回,如果没找到再查询磁盘上的TransLog,如果还没有则再去查询磁盘上的Segment,这种查询是实时的,这种查询顺序可以保证查询到的DOC是最新版本的DOC,这个功能也是为了保证NoSQL场景下的实时要求
所有的搜索系统一般都是两阶段查询,第一阶段查询匹配到的doc ID,第二阶段再查询Doc ID对应的完整文档,这种在ES中称为query_then_fetch,还有一种是一阶段查询的时候就返回完整DOC,在ES中称为query_and_fetch,一般第二种适用于只需要查询一个shard的请求
题外话:
除了一阶段,二阶段外,还有一种三阶段查询的情况,搜索里面有一种算分逻辑是根据TF(Term Frequency)和DF(Document Frequency)计算基础分,但是ES中查询的时候,是在每个shard中独立查询的,每个shard中的TF和DF都是独立的,虽然在写入的时候通过 _rounting保证DOC分布均衡,但是没法保证TF和DF均衡,那么就会导致局部的TF和DF不准的情况出现,这个时候基于TF,DF的算分就不准了,为了解决这一问题,ES引入了DFS查询,比如 DFS_query_then_fetch,会先收集所有shard中的TF和DF值,然后将这些值代入请求中,再次执行query_then_fetch,这样算分的时候TF和DF就是准确的,类似的有DFS_query_and_fetch,这种查询的优势是算分更加精确,但是效率会变差,另一种选择是用BM25代替TF/DF模型
新版本ES中,用户无法指定DFS_query_and_fetch和query_and_fetch,这两种只能被ES系统改写
ElasticSearch查询流程
-
Get Remove Cluster Shard 判断是否需要跨集群访问,如果需要,则获取要访问的shard列表 -
Get Search Shard Iterator 获取当前cluster中要访问的shard,和上一步中的Remove Cluster Shard合并,构建出最终要访问的完整的shard列表 这一步中,会根据Request请求中的参数从Primary Node和Replica Node中选择出一个要访问的Shard -
For Every Shard:Perform 遍历每个Shard ,对每个shard执行后面逻辑 -
Send Request To Query Shard 将查询阶段请求发送给相应的Shard -
Merge Docs 上一步将请求发送给多个Shard后,这一步就是异步等待返回结果,然后对结果合并,这里的合并策略是维护一个Top N大小的优先级队列,每当收到一个shard的返回,就把结果放入优先队列做一次排序,直到所有的shard都返回 翻页逻辑也是在这里,如果需要取Top 30 - TOP 40的结果,这个意思是所有shard查询结果中的30-40的结果,那么在每个shard中无法确定最终的结果,每个shard需要返回top 40的结果给client Node,然后client Node中在merge Doc的时候,计算出top 40的结果,最后再去除top 30,剩余的10个结果就是需要的top 30 -top 40的结果 上述翻页逻辑有一个明显的缺点就是每次Shard返回的数据中包括了已经翻过的历史结果,如果翻页很深,则在这里需要排序的Docs会很多,比如Shard有1000,取第9990到10000的结果,那么这次查询,Shard总共需要返回1000 * 10000,也就是一千万Doc,这种情况很容易导致OOM。 另一种翻页方式是使用search-after,这种方式会更轻量级,如果每次只需要返回10条结构,则每个shard只需要返回search_after之后的10个结果即可,返回的总数据量只是和shard个数以及本次需要的个数有关,和历史已读取的个数无关,推荐使用这种方式 -
Send Request To Fetch Shard 选出TOP N 个DOC ID 发送这些DOC ID 所在的shard执行fetch phase ,最后返回top N 的DOC的内容
第一阶段查询的步骤:
ES详解 - 优化:ElasticSearch性能优化详解(重点!重点!)S
硬件优化
在系统层面能够影响应用性能的一般包括三个因素:CPU,内存和IO,可以从这三方面进行性能优化
一般来说,CPU繁忙的原因有以下几个:
- 线程中有无线循环,无阻塞,正则匹配或者单纯的计算
- 发生了频繁的GC
- 多线程的上下文切换
大多数ES部署往往对CPU要求不高,因此,相对于其他资源,具体配置多少个(CPU)不是那么关键,应该选择具有多个内核的现代处理器,常见的集群使用2到8个核的机器,如果要在更快的CPU和更多核数之间选择,选更多核数更好,多个内核提供的额外并发远胜于稍微快一点的时钟频率
如果中哪一种资源是最先被耗尽的,它可能是内存,排序和聚合都很消耗内存,所以有足够的堆空间来应付它们是很重要的,即使堆空间是比较小的时候,也能为操作系统文件缓存提供额外的内存,因为Lucene使用的许多数据结构是基于磁盘的格式,ES利用操作系统缓存能产生很大的效果
64G内存的机器是非常理想的,但是32G和16G机器也很常见,少于8G会适得其反(因为最终可能需要很多的小机器),大于64G也会有问题
由于ES构建基于Lucene,而Lucene设计强大之处在于Lucene能够很好的利用操作系统来缓存索引数据,以提供快速的查询性能,Lucene的索引文件Segment是存储在单文件中的,并且不可变,对于OS来说,能够很友好的将索引文件保持在Cache中,以便快速访问,因此,我们很有必要将一半的物理内存留给Lucene,另一半的物理内存留给ES(JVM heap)
- 当机器内存小于64G时,遵循通用的原则,50%给ES,50%留给Lucene
当机器的内存大于64时,遵循以下原则:
禁止swap,一旦允许内存和磁盘的交换,会引起致命的性能问题,可以通过在elasticsearch.yml中 bootstrap.memory_lock:true,以保持JVM锁定内存,保证ES的性能
索引优化设置
索引优化主要是在ES的插入层面优化,ES本身索引速度还是很快的,可以根据不同的需求,针对索引优化
批量提交
当有大量数据提交的时候,建以采用批量提交(Bulk操作);此外使用bulk请求时,每个请求不超过几十M,因为太大会导致内存使用过大
例如在做ELK过程中,Logstash indexer提交数据到ES中,batch size就可以作为一个优化功能点,但是优化size大小需要根据文档大小和服务器性能而定
像Logstash 中提交文档大小通过20MB,Logstash会将一个批量请求切分为多个批量请求
如果在提交过程中,遇到EsRejectedExecutionException异常的话,则说明集群的索引性能已经达到极限了,这种情况下,要么提高服务器集群的资源,要么根据业务规则,减少数据收集速度,比如只收集warn,error级别以上的日志
增加Refresh时间间隔
为了提高索引性能,Es在写入数据的时候,采用延迟写入的策略,即数据先写入内存,当超过默认1秒(index.refresh_interval)会进行一次写入操作,就是将内存中segment数据刷新到磁盘中,此时我们才能将数据搜索出来,所以这就是为什么ES提供的是近实时搜索功能,而不是实时搜索功能
如果系统对数据延迟性要求不高的话,我们可以通过延长refresh时间间隔,可以有效的减少segment合并压力,提高索引速度,比如在做全链路跟踪的过程中,我们就将index.refresh_interval设置为30S,减少refresh次数,再比如,在进行全量索引时,可以将refresh次数临时关闭,即index.refresh_interval设置为-1,数据导入成功后再打开到正常模式,比如30S
TIP:在加载大量数据时可以暂时不用refresh和replicas,index.refresh_interval设置为-1,index.number_of_replicas设置为0
修改index_buffer_size的设置
索引缓存的设置可以控制多少内存分配给索引进程,这时一个全局配置,会应用于一个节点上所有不同的分片上
indices.memory.index_buffer_size: 10%
indices.memory.min_index_buffer_size: 48mb
- indices.memory.index_buffer_size接收一个百分比或者一个表示字节大小的值,默认是10%,意味着分配给节点的总内存的10%用来做索引缓冲的大小,这个数值被分到不同的分片(shards)上,如果设置的百分比,还可以设置min_index_buffer_size(默认48mb)和max_index_buffer_size(默认没有上限)
transLog相关的设置
一是控制数据从内存到硬盘的操作频率,以减少IO,可将sync_interval的时间设置大一些,默认为5S
index.translog.sync_interval: 5s
也可以控制transLog数据块的大小,达到threshold大小时,才会fluish到Lucene索引文件,默认为512m
index.translog.flush_threshold_size: 512mb
注意 _id字段的使用
_id 字段的使用,应尽可能避免自定义 _id ,以避免针对ID的版本控制,建以使用ES的默认ID生成策略或者使用数字类型ID作为主键
注意 _all 字段以及 _source字段的使用
_all 字段以及 _source字段使用,应该注意场景和需要, _all 字段包含了所有的索引字段,方便做全文检索,如果没有这种需求,可以禁用, _source存储了原始的document内存,如果没有获取原始文档数据的需求,可以通过设置includes, excludes属性来定义放入 _source的字段
合理的配置使用index属性
合理的配置使用index属性,analyzed和not_analyzed,根据业务需求来控制字段是否分词或不分词,只有groupby需求的字段,配置时就设置成not-analyzed,以提高查询或聚类的效率
减少副本数量
ES默认副本数量为3个,虽然这样会提高集群的可用性,增加搜索的并发数,但是同时也会影响写入索引的效率
在索引过程中,需要把更新的文档发到副本节点上,等副本节点生效后再进行返回结束,使用ES做业务搜索的时候,建以副本数目还是设置为3个,但是像内部ELK日志系统,分布式跟踪系统中,完全可以将副本数目设置为1个
查询方面优化
路由优化
当查询文档的时候,ES如何直到一个文档存放在哪一个分片中呢,它是通过以下公式计算出来的
shard = hash(rounting) % number_of_primary_shards
rounting默认值是文档的id,也可以采用自定义值,比如用户ID
在查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为2个步骤
- 分发:请求到达协调节点后,协调节点将查询请求分发到每个分片上
- 聚合:协调节点搜索每个分片上的查询结果,再将查询的结果进行排序,之后给用户返回结果
查询的时候,可以直接根据rounting信息定位到某个分片查询,不需要查询所有的分片,经过协调节点排序
像上面自定义的用户查询,如果rounting设置为userid的话,可以直接查询出数据来,效率会提升很多
Filter VS Query
尽可能使用过滤上下文(Filter)替代查询上下文(Query)
- Query:此文档与此查询子句的匹配程度如何?
- Filter:此文档和查询子句匹配?
ES针对Filter查询只需回答是 、否就可以,不需要像Query查询一样计算相关性分数,同时Filter 结果可以缓存
深度翻页
ES应该尽量避免深度翻页
正常翻页查询都是从from开始size条数据,这样就需要在每个分片中查询打分排名在前面的fron+size条数据,协同节点收集每个分配的前from +siez条数据,协同节点一共会收到N * (from + size)条数据,然后进行排序,再将其中from 到 from +size 条数据返回出去,如果from +size很大的话,导致参加排序的数量会同步扩大很多,最终导致CPU资源消耗增大
ES深度翻页可以使用ES的 scorll 和scroll-scan高效滚动的方式来解决这样的问题
也可以结合实际业务特点,文档id大小如果和文档创建时间是一致有序的话,可以以文档id作为分页的偏移量,并将其作为分页查询的一个条件
脚本(script)合理使用
脚本使用主要有3中形式,内联动态编译方式,_script索引库中存储和文件脚本存储的形式;一般脚本的使用场景是粗排,尽量用第二种方式先将脚本存储在 _script索引库中,起到提前编译,然后通过引用脚本id,并结合params参数使用,既可以达到模型(逻辑)和数据进行分离,同时又便于脚本模块的扩展与维护
Cache的设置及使用
-
query cache:ES查询的时候,使用filter查询会使用query cache,如果业务场景中的过滤查询比较多,建议将query cache设置大一些,以提高查询速度 indices.queries.cache.size:10%(默认),可设置成百分比,也可设置成具体值,如256mb 当然也可以禁用查询缓存(默认是开启的),通过index.queries.cache.enabled:fales设置 -
FieldDataCache:在聚类或排序时,field data cache会使用频繁,因此,设置字段数据缓存的大小,在聚类或排序场景较多的情景下很有必要,可通过indices.fielddata.cache.size:30%或具体值10GB来设置,但是如果场景或数据变更比较频繁,设置cache并不是好的做法,因为缓存加载的开销也是特别大的 -
ShardRequestCache:查询请求发起后,每个分片会将结果返回给协调节点(Coordinating Node),由协调节点将结果整合,如果有需求,可以设置开启,通过设置index.requests.cache,enable:true来开启,不过,shard request cache只缓存hits.tootal,aggregations,suggestions类型的数据,并不会缓存hits的内容,也可以通过设置indices.requests.cache.size:1%(默认)来控制缓存空间大小
常见的一些查询优化经验
- query_string 或 multi_match的查询字段越多,查询越慢,可以在mapping阶段,利用copy_to属性将多字段的值索引到一个新字段,multi_match时,用新的字段查询
- 日期字段的查询,尤其是用now的查询实际上是不存在缓存的,因此,从业务的角度来考虑是否一定要用now,毕竟利用query cache是能够大大提高查询效率的
- 查询结果集的大小不能随意设置成大的离谱的值,如query.setsize不能设置成Integer.MAX_VALUE,因为ES内部需要建立一个数据结构来放指定大小的结果集数据
- 避免层级过深的聚合查询,层级过深的aggregation,会导致内存,CPU消耗,建以在服务层通过程序来组转业务,也可以通过pipeline的方式来优化
- 复用预索引数据方式来提高AGG性能:
比如通过 terms aggregations替代 range aggregations,如果要根据年龄来分组,分组目标是:少年(14岁以下),青年(14-18),中年(29-50),老年(51以上),可以在索引的时候设置一个age_group字段,预先将数据进行分类,从而不用按age来做range aggregations,通过age_group字段就可以了
通过开启慢查询配置定位慢查询
不论是数据库还是搜索引擎,对于问题的排查,开启慢查询日志是十分必要的,ES开启慢查询的方式有很多种,但是最常用的是调用模板API进行全局设置
PUT /_template/{TEMPLATE_NAME}
{
"template":"{INDEX_PATTERN}",
"settings" : {
"index.indexing.slowlog.level": "INFO",
"index.indexing.slowlog.threshold.index.warn": "10s",
"index.indexing.slowlog.threshold.index.info": "5s",
"index.indexing.slowlog.threshold.index.debug": "2s",
"index.indexing.slowlog.threshold.index.trace": "500ms",
"index.indexing.slowlog.source": "1000",
"index.search.slowlog.level": "INFO",
"index.search.slowlog.threshold.query.warn": "10s",
"index.search.slowlog.threshold.query.info": "5s",
"index.search.slowlog.threshold.query.debug": "2s",
"index.search.slowlog.threshold.query.trace": "500ms",
"index.search.slowlog.threshold.fetch.warn": "1s",
"index.search.slowlog.threshold.fetch.info": "800ms",
"index.search.slowlog.threshold.fetch.debug": "500ms",
"index.search.slowlog.threshold.fetch.trace": "200ms"
},
"version" : 1
}
PUT {INDEX_PAATERN}/_settings
{
"index.indexing.slowlog.level": "INFO",
"index.indexing.slowlog.threshold.index.warn": "10s",
"index.indexing.slowlog.threshold.index.info": "5s",
"index.indexing.slowlog.threshold.index.debug": "2s",
"index.indexing.slowlog.threshold.index.trace": "500ms",
"index.indexing.slowlog.source": "1000",
"index.search.slowlog.level": "INFO",
"index.search.slowlog.threshold.query.warn": "10s",
"index.search.slowlog.threshold.query.info": "5s",
"index.search.slowlog.threshold.query.debug": "2s",
"index.search.slowlog.threshold.query.trace": "500ms",
"index.search.slowlog.threshold.fetch.warn": "1s",
"index.search.slowlog.threshold.fetch.info": "800ms",
"index.search.slowlog.threshold.fetch.debug": "500ms",
"index.search.slowlog.threshold.fetch.trace": "200ms"
}
这样,在日志目录下的慢查询日志就会有输出记录必要的信息了
{CLUSTER_NAME}_index_indexing_slowlog.log
{CLUSTER_NAME}_index_search_slowlog.log
数据结构优化
基于ES的使用场景,文档数据结构尽量和使用场景进行结合,去掉没用及不合理的数据
尽量减少不需要的字段
如果ES用于业务搜索服务,一些不需要用于搜索的字段最好不存到ES中,这样即节省空间,同时在相同数据量下,也能提高搜索性能
避免使用动态值作字段,动态递增的mapping,会导致集群崩溃,同样,也需要控制字段的数量,业务中不适用的字段,就不要索引,控制索引的字段数量,mapping深度,索引字段的类型,对于ES的性能优化是重中之重
ES对于字段数,mapping深度的一些默认设置:
index.mapping.nested_objects.limit: 10000
index.mapping.total_fields.limit: 1000
index.mapping.depth.limit: 20
Nested Object VS Parent/Child
尽量避免使用nested或parent/child的字段,能不用就不用;nested query慢,parent/child query更慢,比nested query慢上百倍,因此能在mapping设计阶段搞定的(大宽表设计或采用比较smart 的数据结构,就不要用父子关系的mapping)
如果一定要使用nested fields,保证nested fields字段不能过多,目前ES默认限制是50,因此针对一个document,每一个nsted field都会产生一个独立的document,这就使得doc数量剧增,影响查询效率,尤其是join的效率
index.mapping.nested_fields.limit: 50
选择静态映射,非必要时,禁止动态映射
尽量避免使用动态映射,这样有可能会导致集群崩溃,此外,动态映射有可能会带来不可控制的数据类型,进而有可能导致在查询端出现相关异常,影响业务
tip:ES作为搜索引擎时,主要承载query的匹配和排序功能,那数据的存储类型基于这两种功能的用途分为两类,一是需要匹配的字段,用来建立倒排索引对query匹配用,另一类字段是用作粗排用到的特征字段,如点击数,评论数等等
document模型设计
对于mysql,我们经常有一些复杂的关联查询,在es中复杂的关联查询尽量别用,一旦使用了性能一般较差
最好是先在java系统里完成关联,将关联好的数据直接写入es中,搜索的时候,就不需要利用es的搜索语法来完成join之类的关联搜索了
document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。es 能支持的操作就那么多,不要考虑用 es 做一些它不好操作的事情。如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。
集群架构设计
合理的部署ES有助于提高服务的整体可用性
主节点,数据节点和协调节点分离
ES集群在架构拓扑时,采用主节点,数据节点和负载均衡节点分离的架构,在5.x版本以后,又可以将数据节点再细分为Hot-Warm的架构模式
ES的配置文件中有两个参数,node.master和node.data,这两个参数搭配使用时,能够帮助提高服务器性能
配置node.master:true和node.data:false,该node服务器只作为一个主节点,但不存储任何索引数据,推荐每个集群运行3个专用的master节点来提供最好的弹性,使用时,还需要将discovery.zen.minimum_master_nodes setting参数设置为2,以免出现脑裂(split_brain)的情况,用三个专用的master节点,专门负责处理集群的管理以及加强状态的整体稳定性,因为这3个master节点不包含数据也不会实际参与搜索以及索引操作,在JVM上它们不用做相同的事,例如繁重的索引或者耗时,资源耗费很大搜索,因此不太可能因为垃圾回收而导致停顿,因此,master节点的CPU,内存以及磁盘设置可以比data节点少很多
配置node.master:false和node.data:false,该node服务器只作为一个数据节点,只用于存储索引数据,使该node服务器功能单一,只用于数据存储和数据查询,降低其资源消耗率
在ES 5.x版本之后,data节点又可以再细分为Hot-Warm架构,即分为热节点(hot node)和暖节点(warm node)
hot节点主要使索引节点(写入节点),同时会保存近期的一些频繁被查询的索引,由于进行索引非常耗费CPU和IO,即属于IO和CPU密集型操作,建以使用SSD的磁盘类型,保持良好的写入性能,推荐部署最少化的3个Hot节点来保证高可用,根据近期需要收集以及查询的数据量,可以增加服务器数量来获取的想要的性能
将节点设置为Hot类型需要elasticsearch.yml如下的设置
node.attr.box_type: hot
如果是针对执行的index操作,可以通过settings设置,index.rounting.allocation.require.box_type:hot将索引写入hot节点
这种类型的节点是为了处理大量的,而且不经常访问的只读索引索引而设计的,由于这些索引是只读的,warm节点倾向于挂载大量磁盘(普通磁盘)来替代SSD,内存,CPU的配置跟hot节点保持一致即可,节点数量一般也是大于等于3个
将节点设置为warm类型需要elasticsearch.yml如下设置
node.attr.box_type: warm
同时,也可以再elasticsearch.yml中设置index.code:best_compression保证warm节点的压缩配置
当索引不再被频繁查询时,可以通过index.rounting.allocation.require.box_type:warm,从而保证索引不写入hot节点,以便将SSD资源用在刀刃上,一旦设置了这个属性,ES会自动将索引合并到warm节点
协调节点用于分布式里的协调,将各分片或节点返回的数据整合后返回,该节点不会被选做主节点,也不会存储任何索引数据,该服务器主要用于查询负载均衡,在查询的时候,通常会涉及到从多个node服务器上查询数据,并将请求分发到多个指定的node服务器,并对各个node服务器返回的数据统一进行一个汇总处理,最终返回给客户端。在ES集群中,所有的节点都有可能是协调节点,但是,可以通过设置node.master,node.data,node.ingest都为false来设置专门的协调节点,需要较好的CPU和较高的内存
- node.master:false和node.data:true,该node服务器只作为一个数据系欸但,只用于存储索引数据,使该node服务器功能单一,只用于存储和查询数据,降低其资源消耗率
- node.master:true和node.data:false,该node服务器只作为一个主节点,但不存储任何索引数据,该node服务器将使用自身空闲的资源,来协调各种创建索引请求或者查询请求,并将这些请求合理分发到相关node服务器上
- node.master:false和node.data:false,该node服务器既不会被选作主节点,也不会存储任何索引数据,该服务器主要用于查询负载均衡,在查询的时候,通常会涉及到从多个服务器上查询数据,并将请求分发到多个指定的node服务器,并对各个node服务器返回的结果进行一个汇总处理,最终返回给客户端
关闭data节点服务器中的http功能
针对ES集群中的所有数据节点,不用开启http服务,将其中的配置参数这样设置,http.enabled;false,同时也不要安装head,bigdesk,marvel等监控插件,这样保证data节点服务器只需要处理创建、更新、删除、查询索引数据等操作
http功能可以在非数据节点服务器上开启,上述相关的监控插件也安装到这些服务器上,用于监控ES集群状态等数据信息,这样做一来出于安全考虑,而来处于服务性能考虑
一台服务器上最好只部署一个node
一台物理服务器上可以启动多个node服务器节点(通过设置不同的启动port),但一台服务器上的CPU,内存,硬盘等资源毕竟有限,从服务性能考虑,不建议一台服务器上启动多个node节点
集群分片设置
ES 一旦创建好索引后,就无法调整分片的设置,而在 ES 中,一个分片实际上对应一个 lucene 索引,而 lucene 索引的读写会占用很多的系统资源,因此,分片数不能设置过大;所以,在创建索引时,合理配置分片数是非常重要的。一般来说,我们遵循一些原则:
控制每个分片占用的硬盘容量不超过 ES 的最大 JVM 的堆空间设置(一般设置不超过 32 G,参考上面的 JVM 内存设置原则),因此,如果索引的总容量在 500 G 左右,那分片大小在 16 个左右即可;当然,最好同时考虑原则 2。 考虑一下 node 数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了 1 个以上的副本,同样有可能会导致数据丢失,集群无法恢复。所以,一般都设置分片数不超过节点数的 3 倍。
|