白日梦的ES笔记三:万字长文 Elasticsearch基础概念统一扫盲
公众号首发、欢迎关注
一、导读
本篇是白日梦的第三篇ES笔记,前面已经跟大家分享过两篇ES笔记了,分别是:
其实这个专题相对来说质量还是比较不错的,看过前面两篇文章之后基本上大家可以上手使用ES了,包括对一些花里花哨的查询相关的写法也有所了解。然后这一篇文章会和大家调过头来重新巩固一下基础概念上的扫盲。
二、彩蛋福利:账号借用
三、ES的Index、Shard及扩容机制
首先你看下这个表格(ES6):
Elasticsearch | 关系型数据库 |
---|---|
Document | 行 |
type(ES7中被取消) | 表 |
index | Database |
在ES中的Index的地位相当于是MySQL中的database。所以你让ES帮你存储数据你总得先创建一个Index吧,如果你手动的定制创建Index,你还可以为Index指定shard。
那什么是shard呢?下文马上说。
下面是对Index操作的Case:
# 创建索引
PUT my_index
{
# 设置index的shard信息
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2
},
# 设置index中各个字段的类型,属性(下文细讲)
"mapping":{
...
}
}
# 修改索引
PUT /my_index/_settings
{
# 只能改number_of_replicas,不能改number_of_shards
"number_of_replicas":3
}
# 删除索引
DELETE /my_index
DELETE /my_index1,my_index2
DELETE /my_*
DELETE /_all # 删掉所有索引
# 如果不想让ES可以一下子删除所有索引,可以通过配置文件设置
elasticsearch.yml
action.destructive_requires_name:true
shard分为primary shard和replica shard ,其中的primary shard可以接受读/写请求,replica shard可以接受读请求,起到一个负载的作用。默认情况下我创建的索引都有: number_of_shards = 5 和number_of_replicas = 1
。表示一共有五个primary shard,并且每个primary 都有一个副本。也就是 5+5*1 =10个shard。
但是当你启动单台ES实例时,架构其实是下面这样:
你会发现,其实系统中就有5个shard。不存在上面计算的10个shard。原因是因为ES要求Primary Shard 和它的备份 replica shard不能同时存在于一个Node上。所以你单个Node启动后,就只有5个primary shard。并且这时你去看集群的状态,会发现整个集群处于yellow状态,表示集群整体可用,但是存在replica shard不可用的情况。
然后你会不会好奇,假设我有2个Node(两个ES实例)组成的ES集群,你怎样做,才能让系统中的Shard是如何负载均衡分布在两个Node上呢?
回答:其实你不用操心,ES自己会帮你完成的。当你增加或减少节点时,ES会自动的进行rebalance,使数据平均分散在不同的节点中。
举个例子:假设你真的又启动了一个Node,这个Node会自动的加入到上面那个ES中去,自动组成一个有两个Node的集群,如果你依然使用的默认配置即:number_of_shards = 5 和 number_of_replicas = 1
。这时ES会自动将系统rebalance成下图这样:
此时你再去看集群的状态,会发现为green。表示集群中所有shard都可用。
Node2中会存在5个replica shard,他们是Node1中的Primary的备份。每个shard相当于是一个luncene实例,拥有完整的检索数据、处理请求的能力。所以shard的数量越多,一定意义上意味着ES的吞吐量就越大。
但是你需要注意的是,primary shard的数量是不能改变的,但是它的副本的数量可以改变。
至于为什么primary shard的数量是不能改变的,下文中的路由原理会说的。
所以当你想对现在有的ES集群进行扩容的时,就存在两种选择:
1、纵向扩容:你不改变集群的总shard数,然后去买配置更高,存储更大的机器跑这些shard。
2、横向扩容:你扩大replica shard的数量,然后去多购置几个配置低的机器,你只需要写好配置文件,再启动Node,它自己会加入到现有的集群中。因为每个shard的都能对外提供服务嘛,所以你这样扩容系统的性能肯定有提升。
根据现在云服务器实例的市场行情来看,方案二会更省钱一些。
当然了如果你想让ES集群有最好的性能,还是使用默认的配置:number_of_shards = 5 和number_of_replicas = 1
,这时你需要10台机器。每个集群上都启动一个ES实例,让这10个实例组建集群。就像下图这样:
这时每个shard都独享操作系统的所有资源,性能自然会最好。
四、ES支持的核心数据类型
参考官网 https://www.elastic.co/guide/en/elasticsearch/reference/6.2/mapping-types.html
4.1、数字类型
long、integer、short、byte、double、float、half_float、scaled_float
示例:
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"number_of_bytes": {
"type": "integer"
},
"time_in_seconds": {
"type": "float"
},
"price": {
"type": "scaled_float",
"scaling_factor": 100
}
}
}
}
}
4.2、日期类型
date
示例:
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"birthday ": {
"type": "date"
}
}
}
}
}
PUT my_index/_doc/1
{ "date": "2015-01-01" }
4.3、boolean类型
string类型的字符串可以被ES解释成boolean。
boolean
示例:
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"is_published": {
"type": "boolean"
}
}
}
}
}
4.4、二进制类型
binary
示例
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"name": {
"type": "text"
},
"blob": {
"type": "binary"
}
}
}
}
}
PUT my_index/_doc/1
{
"name": "Some binary blob",
"blob": "U29tZSBiaW5hcnkgYmxvYg=="
}
4.5、范围
integer_range、float_range、long_range、double_range、date_range
示例
PUT range_index
{
"settings": {
"number_of_shards": 2
},
"mappings": {
"_doc": {
"properties": {
"expected_attendees": {
"type": "integer_range"
},
"time_frame": {
"type": "date_range",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}
}
}
}
}
PUT range_index/_doc/1?refresh
{
"expected_attendees" : {
"gte" : 10,
"lte" : 20
},
"time_frame" : {
"gte" : "2015-10-31 12:00:00",
"lte" : "2015-11-01"
}
}
4.6、复杂数据类型
对象类型,嵌套对象类型
示例:
PUT my_index/_doc/1
{
"region": "US",
"manager": {
"age": 30,
"name": {
"first": "John",
"last": "Smith"
}
}
}
在ES内部这些值被转换成这种样式
{
"region": "US",
"manager.age": 30,
"manager.name.first": "John",
"manager.name.last": "Smith"
}
4.7、Geo-type
ES支持地理上的定位点。
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"location": {
"type": "geo_point"
}
}
}
}
}
PUT my_index/_doc/1
{
"text": "Geo-point as an object",
"location": {
"lat": 41.12,
"lon": -71.34
}
}
PUT my_index/_doc/4
{
"text": "Geo-point as an array",
"location": [ -71.34, 41.12 ]
}
五、精确匹配与全文检索
精确匹配和全文检索是ES提供的两种检索方式,都不难理解。
5.1、精确匹配:exact value
搜索时输入的value必须和目标完全一致才算作命中。
"query": {
# match_phrase 短语精确匹配的关键字
# 只有name字段 完全等于 “白日梦”的doc 才算命中然后返回
"match_phrase": {
"name": "白日梦"
}
}
5.2、全文检索:full text
全文检索时存在各种优化处理如下:
- 缩写转换: cn == china
- 格式转换 liked == like == likes
- 大小写转换 Tom == tom
- 同义词转换 like == love
示例
GET /_search
{
"query": {
# match是全文检索的关键字
# 白日梦可以被分词器分成:白、白日、白日梦
# 所以当你使用:白、白日、白日梦、我是白日梦、白日梦是我 等等词条检索,都可以检索出结果
"match" : {
"name" : "白日梦"
}
}
}
六、倒排索引 & 正排索引
6.1、倒排索引 inverted index
其实正排索引和倒排索引都是人们取的名字而已。主要是你理解它是什么东西就好了。
正排索引:以doc为维度,记录doc中出现了哪些词。
倒排索引:以把doc打碎成一个个的词条,以词语为维度。记录它在哪些doc中出现过。
倒排索引要做的事就是将一篇文章通过分词器打散成很多词,然后记录各个词分别在哪篇doc中出现过。用户在使用的时候输入一串搜索串,这串字符串同样会使用一样的分词器打散成很多词。再拿着这些词去方才建立的倒排索引中匹配。同时结合相关性得分找到。
假设我们存在这样两句话。
doc1 : hello world you and me
doc2 : hi world how are you
建立倒排索引就是这样
词条 | doc1(*表示出现过) | doc2(-表示不曾出现过) |
---|---|---|
hello | * | - |
world | * | * |
you | * | * |
and | * | - |
me | * | - |
hi | - | * |
how | - | * |
are | - | * |
这时,我们拿着hello world you 来检索,同样需要先经过分词器分词,然后可以得到分出来的三个单词:hello、world、you,然后拿着这三个单词去上面的倒排索引表中找,你可以看到:
-
hello在doc1中出现过。
-
world在doc1、doc2中出现过。
-
you在doc1、doc2中出现过。
最终doc1、doc2都会被检索出,但是doc1命中了更多的词,因此doc1得分会更高,排名越靠前。
6.2、正排索引 doc value
doc value 是指所有不分词的document的field。
在建立索引的时候,一方面会建立倒排索引,以供搜索用。一方面会建立正排索引,也就是doc values,以供排序,聚合,过滤等操作使用。
正排索引大概长这样:
document | name | age |
---|---|---|
doc1 | 张三 | 12 |
doc2 | 李四 | 34 |
os cache会缓存正排索引,以提高访问doc value
的速度。当OS Cache中内存大小不够存放整个正排索引时,doc value
中的值会被写入到磁盘中。
关于性能方面的问题:ES官方建议,大量使用OS Cache来进行缓存和提升性能。不建议使用jvm内存来缓存数据,那样会导致一定的gc开销,甚至可能导致oom问题。所以官方的建议给JVM更小的内存,给OS Cache更大的内存。假如我们的机器64g,只需要给JVM 16g即可。
6.3、禁用doc value
假设我们不使用聚合、排序等操作,为了节省空间,在创建mappings
时,可以选择禁用doc value
,不创建正排索引。
PUT /index
{
"mappings":{
"my_type":{
"properties":{
"my_field":{
"type":"text",
"doc_values":false # 禁用doc value
}
}
}
}
}
七、简述相关性评分
relevance score 相关度评分算法, 直白说就是算出一个索引中的文本和搜索文本之间的相似程度。
Elasticsearch使用的是 TF-IDF算法 (term-frequency / inverser document frequency)。
- term-frequency: 表示你搜索的词条在当前doc中出现的次数,出现的次数越多越相关。
- inverse document frequency : 表示搜索文本中的各个词条在整个index中所有的document中出现的次数,出现的次数越多越不相关。
- field-length: field长度越长,越不相关。
八、分词器
ES官网分词器模块 https://www.elastic.co/guide/en/elasticsearch/reference/6.2/analysis.html
8.1、什么是分词器?
我们使用分词器可以将一段话拆分成一个一个的单词,甚至可以进一步对分出来的单词进行词性的转换、时态的转换、单复数的转换的操作。
为什么使用分词器呢?
你想一个doc那么长,成千上万字。为了对它进行特征的提取,分析。就得把它还原成组成它的词条。这样会提高检索时的召回率,让更多的doc被检索到。
8.2、分词器的组成
character filter:
在一段文本在分词前先进行预处理,比如过滤html标签, 将特殊符号转换成123..这种 阿拉伯数字等特殊符号的转换。
tokenizer:
进行分词、拆解句子、记录词条的位置(在当前doc中占第几个位置term position)及顺序。
token filter:
进行同义词的转换,去除同义词,单复数的转换等等。
ES内置的分词器:
- standard analyzer(默认)
- simple analyzer
- whitespace
- language analyzer(特定语言的分词器,English)
另外比较受欢迎的中文分词器为IK分词器,这个分词器的插件包、安装方式我都整理成文档了,公众号后台回复:es即可领取。
8.3、修改Index使用的分词器
PUT /my_index
{
"settings":{
"analysis":{
"analyzer":{
"es_std":{
# 指定分词器的类型是:standard
"type":"standard",
# 指定分词器的停用词:_english_
"stopwords":"_english_"
}
}
}
}
}
九、mapping
9.1、认识mapping
看到这里你肯定知道了,我们想往ES中写数据是需要一个index的。其实我们在往ES中PUT数据之前是可以手动创建Mapping,这里的mapping其实好比你搞一个java类,做一次对数据结构的抽象,比如name 的类型是String,age的类型是Integer。
就好比下面这样:
PUT my_index
{
# 指定index的primary shard数量以及 replicas的数量
“settings”:{
"number_of_shards":1,
"number_of_repicas":0
},
# 关键字,我们手动自定my_index中的mapping
"mappings": {
"my_index": { # index的名称
"properties": { # 关键字,mapping的属性,字段
"my_field1": { # 相当于Java中的 String my_field1
"type": "text",
"analyzer":"english"# 指定分词器,说明这个字段需要分词建立倒排索引
},
"my_field2": { # 相当于Golang中的 var my_field2 float
"type": "float",
# 指定是否要分词。analyzed表示要,not_analyzed表示不要
"index":"not_analyzed"
},
"my_field3": {
"type": "scaled_float",
"scaling_factor": 100
}
}
}
}
}
1、mapping json中包含了诸如
properties
、matadata(_id,_source,_type)
、settings(analyzer)
以及其他的settings。2、我们把上面的json中的properties部分称为:root object
3、自己创建mapping一般是为了更好的控制各个字段的数据类型,包括使用到的分词器。
4、另外注意:field的mapping只能新增,不能修改。
你也可以在往ES中PUT数据之前不创建任何Mapping,ES会自动为我们生成mapping。就像下面这样,自动生成的mapping信息叫做dynamic mapping,下文中我们还会详细讲这个dynamic
PUT my_index/_doc/1
{
"title": "This is a document"
}
9.2、查看mapping
# 查看某个index下的某个type的mapping
GET /index/_mapping/type
# 查看某个index的mapping
GET /index/_mapping
9.3、dynamic mapping (动态mapping)
就像下面这样,我们直接往ES中PUT数据,ES在为我们创建index时就会自动生成dynamic mapping。其实用大白话讲就是ES自动推断你往它里面存的json串的类型。比如下面的"first_name"会被dynamic mapping成string 类型的。
PUT my_index/_doc/1
{
"first_name": "John"
}
ES使用_type
来描述doc字段的类型,原来我们直接往ES中存储数据,并没有指定字段的类型,原因是ES存在动态类型推断(ES支持的类型上文中我们也一起看过了,如果不记得阔以再去看一下哈)。默认的mapping中定义了每个field对应的数据类型以及如何进行分词。
null --> no field add
true flase --> boolean
123 --> long
123.123 --> double
1999-11-11 --> date
"hello world" --> string
Object --> object
9.4、定制dynamic mapping 策略
- ture: 语法陌生字段就进行dynamic mapping。
- false: 遇到陌生字段就忽略。
- strict: 遇到默认字段就报错。
示例
PUT /my_index/
{
"mappings":{
"dynamic":"strict"
}
}
- 禁用ES的日期探测的Demo
# 创建mapping并制定:禁用ES的日期探测
PUT my_index
{
"mappings": {
"_doc": {
"date_detection": false
}
}
}
# 添加一条doc
PUT my_index/_doc/1
{
"create": "1985/12/22"
}
# 查看doc,结果如下
GET my_index/_doc/1
{
"_index": "my_index",
"_type": "_doc",
"_id": "1",
"_version": 1,
"found": true,
"_source": {
"create": "1985/12/22"
}
}
# 查看mapping
GET my_index/_mapping
# 结果如下:
{
"my_index": {
"mappings": {
"_doc": {
"date_detection": false,
"properties": {
"create": {
# 被任务是text类型
"type": "text",
# ES会自动帮你创建的下面的field部分
# 即 create是text类型,create.ketword是keyword类型
# keyword类型不会分词,默认保留前256字符
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
}
}
- 定制日期发现规则
PUT my_index
{
"mappings": {
"_doc": {
"dynamic_date_formats": ["MM/dd/yyyy"]
}
}
}
PUT my_index/_doc/1
{
"create_date": "09/25/2015"
}
- 定制数字类型的探测规则
PUT my_index
{
"mappings": {
"_doc": {
"numeric_detection": true
}
}
}
PUT my_index/_doc/1
{
"my_float": "1.0",
"my_integer": "1"
}
定制type field
ES中type相当于MySQL的数据表嘛,ES中可以给现存的type添加field。但是不能修改,否则就会报错。
type在高版本的ES7中被废弃了,Index的概念依然保留着。
# 创建index:twitter
PUT twitter
{
"mappings": {
# user为type
"user": {
"properties": {
"name": {
# 会被全部检索
"type": "text" ,
# 指定当前field使用 english分词器
"analyzer":"english"
},
"user_name": { "type": "keyword" },
"email": { "type": "keyword" }
}
},
"tweet": {
"properties": {
"content": { "type": "text" },
"user_name": { "type": "keyword" },
# "tweeted_at": { "type": "date" },
"tweeted_at": {
"type": "date"
# 通过index设置为当前field tweeted_at不能被分词
"index": "not_analyzeed"
}
}
}
}
}
9.5、mapping复杂数据类型在底层的存储格式
Object类型
# object类型的json
{
"address":{
"province":"shandong",
"city":"qingdao"
},
"name":"bairimeng",
"age":"12"
}
# ES会将上面的json转换成如下的格式存储
{
"name" : [bairimeng],
"age" : [12],
"address.province" : [shandong]
"address.city" : [qingdao]
}
Object数组类型
# Object数组类型
{
"address":[
{"age":"12","name":"张三"},
{"age":"12","name":"张三"},
{"age":"12","name":"张三"}
]
}
# ES会将上面的json转换成如下的格式存储
{
"address.age" : [12,12,12],
"address.name" : [张三,张三,张三]
}
9.6、ES7中废弃了type的概念
在一开始我们将ElasticSearch的index比作MySQL中的database,将type比作table,其实这种类比是错误的。因为在MySQL中不同表之间的列在物理上是没有关系的,各自占有自己的空间。
但是在ES中不是这样,可能type=Student中的name列和type=Teacher中的name列会被lucene认为是同一个field。导致Lucene处理效率下降。
所以在ES7中直接就将type概念废弃了。
不过你也不用担心,大部分企业都倾向于使用低版本的ES,比好比你现在用的依然是java8 而不是JDK14。
9.7、认识一些mate-field(元数据字段)
这里说的元数据字段指定的是,当你检索doc时,除了返回的doc本身的数据之外,其他的出现在检索结果中的数据,我们是需要了解这些字段都是什么含义的。如下:
_index , _type , _id , _source , _version
_id
它是document的唯一标识信息。上图中我手动指定了id等于1。如果不指定的话,ES会自动为我们生成一个长20个字符的id,ES会保证集群中的生成的doc id不会发生冲突。 有这种场景,比如你的数据是从MySQL这种数据库中倒入进ES的,那其实完全可以使用MySQL中的数据行的ID作为doc id。
_index
你可以简单粗暴的将es的index的地位理解成MYSQL中的数据库。这里的元数据_index被用来标识当前的doc存在于哪个index中。index的命名规范,名称小写,不能用下划线开头,不能包含逗号。
ES支持跨域index进行检索
详情见官网 https://www.elastic.co/guide/en/elasticsearch/reference/6.2/mapping-index-field.html
_type
这个字段用来标识doc的类型。但它其实是一个逻辑上的划分。
field中的value在顶层的lucene建立索引的时候,全部使用的opaque bytes类型,不区分类型的lucene是没有type概念的。
为了方便我们区分出不通doc的类型,于是在document中加了一个_type
属性。
ES会通过_type
进行type的过滤和筛选,一个index中是存放的多个type实际上是存放在一起的,因此一个index下,不可能存在多个重名的type。
_version `_version`是doc的版本号,可以用来做并发控制,当一个doc被创建时它的`_version`是1,之后对它的每一次修改,都会使这个版本号+1,哪怕是你将这个doc删除了,这个doc的版本号也会增加1。
_source
通过这个字段可以定制我们想要返回字段。比如说一个type = user类型的doc中存在100个字段,但是可能前端并不是真的需要这100个字段,于是我们使用_source去除一些字段,注意和filter是不一样的,因为filter不会影响相关性得分。
你可用像下面这样禁用_source
PUT tweets
{
"mappings": {
"_doc": {
"_source": {
"enabled": false
}
}
}
}
_all
首先它也是一个元数据,当我们往ES中插入一条document时。ES会自动的将这个doc中的多个field的值串联成一个字符串,然后用这个作为_all
字段的值并建立索引。当用户发起检索却没有指定从哪个字段查询时,默认就会在这个_all
中进行匹配。
_field_names
举个例子说明这个属性怎么用:
首先往index=my_index的索引下灌两条数据
# Example documents
PUT my_index/_doc/1
{
"title": "This is a document"
}
PUT my_index/_doc/2?refresh=true
{
"title": "This is another document",
"body": "This document has a body"
}
然后像下面这样使用_field_names
检索,并且指定了字段=“title”。此时ES会将所有包含title字段,且title字段值不为空的doc检索出来。
GET my_index/_search
{
"query": {
"terms": {
"_field_names": [ "title" ]
}
}
}
禁用_field_names
:
PUT tweets
{
"mappings": {
"_doc": {
"_field_names": {
"enabled": false
}
}
}
}
_routing
下面路由导航中细说。
_uid
在ES6.0中被弃用。
9.8、copy_to
在上一篇文章中跟大家介绍过可以像下面这样跨越多个字段搜索
# dis_max
GET /your_index/your_type/_search
{
# 基于 tie_breaker 优化dis_max
# tie_breaker可以使dis_max考虑其它field的得分影响
"query": {
# 直接取下面多个query中得分最高的query当成最终得分
# 这也是best field策略
"dis_max": {
"queries":[
{"match":{"name":"关注"}},
{"match":{"content":"白日梦"}}
],
"tie_breaker":0.4
}
}
}
# best_field
# 使用multi_match query简化写法如下:
GET /your_index/your_type/_search
{
"query": {
"multi_match":{
"query":"关注 白日梦",
# 指定检索的策略 best_fields(因为dis_max就是best field策略)
"type":"best_fields",
# content^2 表示增加权重,相当于:boost2
"fields":["name","content^2"],
"tie_breaker":0.4,
"minimum_should_match":3
}
}
}
# most_field
GET /your_index/your_type/_search
{
# most_fields策略、优先返回命中更多关键词的doc
# 如下从title、name、content中搜索包含“赐我白日梦”的doc
"query": {
"multi_match":{
"query":"赐我白日梦",
# 指定检索的策略most_fields
"type":"most_fields",
"fields":["title","name","content"]
}
}
}
针对跨越多个字段的检索除了上面的most_field和best_field之外,还可以使用copy_to预处理。
这个copy_to实际上是在允许我们自定义一个_all字段, ES会将多个字段的值复制到一个_all中,然后再次检索时目标字段就使用我们通过copy_to创建出来的_all新字段中。
示例:
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"first_name": {
"type": "text",
# 把当前的first_name copy进full_name字段中
"copy_to": "full_name"
},
"last_name": {
"type": "text",
# 把当前的last_name copy进full_name字段中
"copy_to": "full_name"
},
"full_name": {
"type": "text"
}
}
}
}
}
PUT my_index/_doc/1
{
"first_name": "John",
"last_name": "Smith"
}
GET my_index/_search
{
"query": {
"match": {
"full_name": {
"query": "John Smith",
"operator": "and"
}
}
}
}
9.9、Arrays 和 Multi-field
更多内容参见官网 https://www.elastic.co/guide/en/elasticsearch/reference/6.2/mapping-types.html
十、图解: master的选举、容错以及数据的恢复。
如上图为初始状态图
假如图上的第一个节点是master节点,并且它挂掉了。那它挂掉后,整个cluster的status会变成red,表示存在数据丢失了集群不可用。然后集群会按照下面的步骤恢复:
第一步:完成master的选举,自动在剩下的节点中选出一个节点当成master节点。
第二步:选出master节点后,这个新的master节点会将P0在第三个节点中存在一个replica shard提升为primary shard,此时cluster 的 status = yellow,表示集群中的数据是可以被访问的但是存在部分replica shard不可用。
第三步:重新启动因为故障宕机的node,并且将右边两个节点中的数据拷贝到第一个节点中,进行数据的恢复。
十一、ES如何解决并发冲突
ES内部的多线程异步并发修改时,通过_version
版本号进行并发控制,每次创建一个document,它的_version
内部版本号都是1,以后对这个doc的修改,删除都会使这个版本号增1。
ES的内部需在Primary shard 和 replica shard之间同步数据,这就意味着多个修改请求其实是乱序的不一定按照先后顺序执行。
相关语法:
PUT /index/type/2?version=1{
"name":"XXX"
}
上面的命令中URL中的存在?version=1
,此时,如果存在其他客户端将id=2的这条记录修改过,导致id=2的版本号不等于1了,那么这条PUT语句将会失败并有相应的错误提示。这样也就规避了并发修改异常。
拓展:
ES也允许你使用自己的维护的版本号来进行并发控制,用法如下:
PUT /index/type/2?version=1&version_type=external
对比两者的不同:
-
使用es提供的_version进行版本控制的话,需要你的PUT命令中提供的version == es的维护的version。
-
添加参数
version_type=external
之后,假设当前ES中维护的doc版本号是1, 那么只有当用户提供的版本号大于1时,PUT才会成功。
十二、路由原理
什么是数据路由?
一个index被分成了多个shard,文档被随机的存在某一个分片 上。客户端一个请求随机打向index中的一个分片,但是请求的doc可能不存在于这个分片上,于是接受请求的shard会将请求路由到真正存储数据的shard上,这个过程叫做数据路由。
其中接受到客户端请求的节点称为coordinate node(协调节点),比如现在是客户端想修改服务端的一条消息,shard A接受到请求了,那么A就是 coordnate node协调节点。数据存储在B primary shard 上,那么协调节点就会将请求路由到B primary shard中,B处理完成后再向 B replica shard同步数据,数据同步完成后,B primary shard响应 coordinate node, 最后协调节点响应客户端结果。
假如说你每个primary shard有多个存活的replica shard,默认情况下coordinate node会将请求使用round-robin的方式分散到replica shard和这个primary shard上(因为它们的数据是一样的)
就像下图这样:
路由算法,揭开primary_shard数量不可变的面纱
shard = hash(routing) % number_of_primary_shards
公式不复杂,可以将上面的routing当成doc的id。无论是用户执行的还是自动生成的,反正肯定是唯一的。既然是唯一的,那每次hash得到的结果也是一样的, 这样一个唯一的值对主分片的数进行取余数,得到的结果就会在 0~最大分片数 之间。
你看看上面的路由公式中后半部分使用的是 number_of_primary_shards ,这也是为什么ES规定,primary shard的数量不能改变,但是replica shard 可以改变的原因。
除了上面说的路由方式,你还可以像下面这样定制路由规则:比如PUT /index/type/id?routing=user_id
,可以保证这类doc一定被路由到指定的shard上,而且后续进行应用级负载均衡时会批量提升读取的性能。
像下面这种用法,可以保证你的doc一定被路由到一个shard上,
# 添加一个doc,并制定routing
PUT my_index/_doc/1?routing=user1&refresh=true
{
"title": "This is a document"
}
# 通过id+routing获取你想要的doc
GET my_index/_doc/1?routing=user1
十三、写一致性及原理
我们在发送任何一个增删改查时,都可以带上一个 consistency 参数,指明我们想要的写一致性是什么,如下
PUT /index/type/id?consistency=quorum
有哪些可选参数呢?
- one:当我们进行写操作时,只要存在一个primary_shard=active 就能写入成功。
- all:cluster中全部shard都为active时,可以写入成功。
- quorum(法定的):也是ES的默认值, 要求大部分的replica_shard存活时系统才可用。
quorum数量的计算公式: int((primary+number_of_replicas)/2)+1
算一算,假如我们的集群中存在三个node,replica=1,那么cluster中就存在3+3*1=6个shard。
int((3+1)/2)+1 = 3
看计算的结果,只有当quorum=3 即replica_shard=3时,集群才是可用的。
但是当我们的单机部署时,由于ES不允许同一个server的primary_shard和replica_shard共存,也就是说我们的replica数目为0,为什么ES依然可以用呢?这是因为ES提供了一种特殊的处理场景,也就是当number_of_replicas>1时,上述检查集群是否可用的机制才会生效。
quorum不全时 集群进入wait()状态。 默认1分钟。在等待期间,期望活跃的shard的数量可以增加,到最后都没有满足这个数量的话就会timeout。
我们在写入时也可以使用timeout参数, 比如: PUT /index/type/id?timeout=30
通过自己设置超时时间来缩短超时时间默认的超时时间。