一、導讀
本篇是白日夢的第三篇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
通過自己設定超時時間來縮短超時時間預設的超時時間。