公眾號首發、歡迎關注
一、導讀
本篇是白日夢的第三篇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
通過自己設置超時時間來縮短超時時間默認的超時時間。