1.基本概念
索引(Index)
ES將數據存儲於一個或多個索引中,索引是具有類似特性的文檔的集合。類比傳統的關系型數據庫領域來說,索引相當於SQL中的一個數據庫,或者一個數據存儲方案(schema)。索引由其名稱(必須為全小寫字符)進行標識,並通過引用此名稱完成文檔的創建、搜索、更新及刪除操作。一個ES集群中可以按需創建任意數目的索引。
類型(Type)
類型是索引內部的邏輯分區(category/partition),其意義完全取決於用戶需求。因此,一個索引內部可定義一個或多個類型(type)。一般來說,類型就是為那些擁有相同的域的文檔做的預定義。例如,在索引中,可以定義一個用於存儲用戶數據的類型,一個存儲日志數據的類型,以及一個存儲評論數據的類型。類比傳統的關系型數據庫領域來說,類型相當於 “表” 。
Elasticsearch 為何要在 7.X版本中 去除type的概念
- 背景說明
Elasticsearch是一個基於Apache Lucene(TM)的開源搜索引擎。無論在開源還是專有領域,Lucene可以被認為是迄今為止最先進、性能最好的、功能最全的搜索引擎庫。
Elasticsearch 是一種NoSQL數據庫(非關系型數據庫),和常規的關系型數據庫(比如:MySQL,Oralce等)的基本概念,對應關系如下:
Elasticsearch:index --> type --> doc --> field
MySQL: 數據庫 --> 數據表 --> 行 --> 列
因為關系型數據庫比非關系型數據庫的概念提出的早,而且很成熟,應用廣泛。
所以,后來很多NoSQL(包括:MongoDB,Elasticsearch等)都參考並延用了傳統關系型數據庫的基本概念。
一個客觀的現象和事實如下:
Elasticsearch 官網提出的近期版本對 type 概念的演變情況如下:
在 5.X 版本中,一個 index 下可以創建多個 type;
在 6.X 版本中,一個 index 下只能存在一個 type;
在 7.X 版本中,直接去除了 type 的概念,就是說 index 不再會有 type。
- 原因
因為 Elasticsearch 設計初期,是直接查考了關系型數據庫的設計模式,存在了 type(數據表)的概念。
但是,其搜索引擎是基於 Lucene 的,這種 “基因” 決定了 type 是多余的。 Lucene 的全文檢索功能之所以快,是因為 倒排索引 的存在。
而這種 倒排索引 的生成是基於 index 的,而並非 type。多個type 反而會減慢搜索的速度。
為了保持 Elasticsearch “一切為了搜索” 的宗旨,適當的做些改變(去除 type)也是無可厚非的,也是值得的。
為何不是在 6.X 版本開始就直接去除 type,而是要逐步去除type?
因為歷史原因,前期 Elasticsearch 支持一個 index 下存在多個 type的,而且,有很多項目在使用 Elasticsearch 作為數據庫。
如果直接去除 type 的概念,不僅是很多應用 Elasticsearch 的項目將面臨 業務、功能和代碼的大改,
而且對於 Elasticsearch 官方來說,也是一個巨大的挑戰(這個是傷筋動骨的大手術,很多涉及到 type 源碼是要修改的)。
所以,權衡利弊,采取逐步過渡的方式,最終,推遲到 7.X 版本才完成 “去除 type” 這個 革命性的變革。
文檔(Document)
文檔是索引和搜索的原子單位,它是包含了一個或多個域(Field)的容器,基於JSON格式進行表示。文檔由一個或多個域組成,每個域擁有一個名字及一個或多個值,有多個值的域通常稱為 “多值域” 。每個文檔可以存儲不同的域集,但同一類型下的文檔至應該有某種程度上的相似之處。
-
document路由原理
①路由算法:shard = hash(routing) % number_of_primary_shards
②決定一個document在哪個shard上,最重要的一個值就是routing值,默認是_id,也可手動指定,相同的routing值,每次過來,從hash函數中,產出的hash值一定是相同的
例:手動指定一個routing value,比如 put /index/type/id?routing=user_id
③這就是primary shard數量不可變的原因。
倒排索引(Inverted Index)
每一個文檔都對應一個ID。倒排索引會按照指定語法對每一個文檔進行分詞,然后維護一張表,列舉所有文檔中出現的terms以及它們出現的文檔ID和出現頻率。搜索時同樣會對關鍵詞進行同樣的分詞分析,然后查表得到結果。
什么是倒排索引?
假設有下列幾條數據:
1 |
|
ID是Elasticsearch自建的文檔id,那么Elasticsearch建立的索引如下:
1 |
|
Elasticsearch分別為每個field都建立了一個倒排索引,Kate, John, 24, Female這些叫term,而 [1,2] 就是Posting List。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。通過posting list這種索引方式可以很快進行查找,比如要找age=24的人。
Term Dictionary
Elasticsearch為了能快速找到某個term,將所有的term排序,二分法查找term,logN的查找效率,就像通過字典查找一樣,這就是Term Dictionary。類似於傳統數據庫的B-Tree的,但是Term Dictionary較B-Tree的查詢快。
Term Index
B-Tree通過減少磁盤尋道次數來提高查詢性能,Elasticsearch也是采用同樣的思路,直接通過內存查找term,不讀磁盤,但是如果term太多,term dictionary也會很大,放內存不現實,於是有了Term Index,就像字典里的索引頁一樣,A開頭的有哪些term,分別在哪頁,term index其實是一顆 (trie) 前綴樹:
這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然后從這個位置再往后順序查找。
所以term index不需要存下所有的term,而僅僅是他們的一些前綴與Term Dictionary的block之間的映射關系,再結合FST(Finite State Transducers)的壓縮技術,可以使term index緩存到內存中。從term index查到對應的term dictionary的block位置之后,再去磁盤上找term,大大減少了磁盤隨機讀的次數。
假設我們現在要將mop, moth, pop, star, stop, top (term index里的term前綴) 映射到序號:0,1,2,3,4,5 (term dictionary的block位置)。最簡單的做法就是定義個Map,大家找到自己的位置取值即可,但從內存占用少的角度考慮,FST更節省空間。
⭕️ 表示一種狀態
–> 表示狀態的變化過程,上面的字母/數字表示狀態變化和權重
將單詞分成單個字母通過⭕️和–>表示出來,0權重不顯示。如果⭕️后面出現分支,就標記權重,最后整條路徑上的權重加起來就是這個單詞對應的序號。
FST以字節的方式存儲所有的term,這種壓縮方式可以有效的縮減存儲空間,使得term index足以放進內存,但這種方式也會導致查找時需要更多的CPU資源。
壓縮技巧
Elasticsearch里除了上面說到用FST壓縮term index外,對posting list也有壓縮技巧。如果Elasticsearch需要對人的性別進行索引,如果有上千萬個人,而性別只分男/女,每個posting list都會有至少百萬個文檔id。Elasticsearch采用一定的壓縮算法對這些文檔id進行壓縮:
增量編碼壓縮,將大數變小數,按字節存儲
首先,Elasticsearch要求posting list是有序的(為了提高搜索的性能),這樣做的好處是方便壓縮,看下面這個圖例:
原理就是通過增量,將原來的大數變成小數僅存儲增量值,再精打細算按bit排好隊,最后通過字節存儲,而不是用int(4個字節)來存儲。
Roaring bitmaps
Roaring bitmaps基於bitmap。Bitmap是一種數據結構,假設某個posting list:[1,3,4,7,10],其對應的bitmap就是:[1,0,1,1,0,0,1,0,0,1]。
用0/1表示某個值是否存在,存在的值對應的bit值是1,即一個字節 (8位) 可以代表8個文檔id,舊版本 (5.0之前) 的Lucene就是用這樣的方式來壓縮的,但這樣的壓縮方式仍然不夠高效,如果有1億個文檔,那么需要12.5MB的存儲空間,這僅僅是對應一個索引字段。於是衍生出了Roaring bitmaps這樣更高效的數據結構。
將posting list按照65535為界限分塊 (block) ,比如第一塊所包含的文檔id范圍在0~65535之間,第二塊的id范圍是65536~131071,以此類推。再用<商,余數>的組合表示每一組id,這樣每組里的id范圍都在0~65535內了。
Bitmap的缺點是存儲空間隨着文檔個數線性增長,Roaring bitmaps利用了某些指數特性來規避這一點:
”為什么是以65535為界限?”
65535=2^16-1,正好是2個字節能表示的最大數,一個short的存儲單位,注意到上圖里的最后一行“If a block has more than 4096 values, encode as a bit set, and otherwise as a simple array using 2 bytes per value”,如果是較大的 (block) 塊,用 bitset 存,小塊用一個 short[] 存儲。
那為什么用4096來區分采用數組還是bitmap的閥值呢?
這個是從內存大小考慮的,當block塊里元素超過4096后,用bitmap更省空間: 采用bitmap需要的空間是恆定的: 65536/8 = 8192bytes 而如果采用short[],所需的空間是: 2*N(N為數組元素個數) N=4096剛好是邊界:
聯合索引
上述都是單field索引,如果是多個field索引的聯合查詢,倒排索引如何滿足快速查詢的要求呢?
利用跳表(Skip list)的數據結構快速做“與”運算,或者利用上面提到的bitset按位“與”。先看看跳表的數據結構:
將一個有序鏈表level0,挑出其中幾個元素到level1及level2,每個level越往上,選出來的指針元素越少,查找時依次從高level往低查找,比如55,先找到level2的31,再找到level1的47,最后找到55,一共3次查找,查找效率和2叉樹的效率相當,但也是用了一定的空間冗余來換取的。
假設有下面三個posting list需要聯合索引:
如果使用跳表,對最短的posting list中的每個id,逐個在另外兩個posting list中查找看是否存在,最后得到交集的結果。如果使用bitset,就很直觀了,直接按位與,得到的結果就是最后的交集。
節點(Node)
一個運行中的 Elasticsearch 實例稱為一個節點,而集群是由一個或者多個擁有相同 cluster.name 配置的節點組成, 它們共同承擔數據和負載的壓力。
ES集群中的節點有三種不同的類型:
- 主節點:負責管理集群范圍內的所有變更,例如增加、刪除索引,或者增加、刪除節點等。 主節點並不需要涉及到文檔級別的變更和搜索等操作。可以通過屬性node.master進行設置。
- 數據節點:存儲數據和其對應的倒排索引。默認每一個節點都是數據節點(包括主節點),可以通過node.data屬性進行設置。
- 協調節點:如果node.master和node.data屬性均為false,則此節點稱為協調節點,用來響應客戶請求,均衡每個節點的負載。
分片(Shard)
一個索引中的數據保存在多個分片中,相當於水平分表。一個分片便是一個Lucene 的實例,它本身就是一個完整的搜索引擎。我們的文檔被存儲和索引到分片內,但是應用程序是直接與索引而不是與分片進行交互。
ES實際上就是利用分片來實現分布式。分片是數據的容器,文檔保存在分片內,分片又被分配到集群內的各個節點里。 當你的集群規模擴大或者縮小時, ES會自動的在各節點中遷移分片,使得數據仍然均勻分布在集群里。
一個分片可以是主分片或者副本分片。 索引內任意一個文檔都歸屬於一個主分片,所以主分片的數目決定着索引能夠保存的最大數據量。一個副本分片只是一個主分片的拷貝。 副本分片作為硬件故障時保護數據不丟失的冗余備份,並為搜索和返回文檔等讀操作提供服務。
如果當前插入大量數據,那么會對es集群造成一定的壓力,所以在插入大量數據前,也就是在建立索引的時候,我們最好把副本數設置為0;等數據建立完索引之后,在手動的將副本數更改到2,這樣可以提高數據的索引效率
在索引建立的時候就已經確定了主分片數,但是副本分片數可以隨時修改。默認情況下,一個索引會有5個主分片,而其副本可以有任意數量。
主分片和副本分片的狀態決定了集群的健康狀態。每一個節點上都只會保存主分片或者其對應的一個副本分片,相同的副本分片不會存在於同一個節點中。如果集群中只有一個節點,則副本分片將不會被分配,此時集群健康狀態為yellow,存在丟失數據的風險。
3個節點,3個主分片,1份副本
增加一份副本
實際上,每一個分片還會進一步拆分為分段(Segment)。這是ES寫入文檔所采用的機制決定的。
2.寫數據
原理解析預備知識
- index:類似數據庫,是存儲、索引數據的地方。
- shard:index 由 shard 組成,一個 primary shard,其他是 replica shard。
- segment:shard 包含 segment,segment 中是倒排索引,它是不可變的;segment 內的文檔數量的上限是
2^31
。 - 倒排索引:倒排索引是 Lucene 中用於使數據可搜索的數據結構。
- translog:記錄文檔索引和刪除操作的日志。Lucene 在每次 commit 之后把數據持久化到磁盤,但是 commit 操作代價很大,所以不能在每次數據變更之后執行 commit。Elasticsearch 為防止宕機造成數據丟失,每次寫入數據時會同步寫到
buffer
和translog
,在 flush 操作時把數據持久化。 - commit point:列出所有已知 segment 的文件。
當用戶向一個節點提交了一個索引新文檔的請求,節點會計算新文檔應該加入到哪個分片(shard)中。每個節點都存儲有每個分片存儲在哪個節點的信息,因此協調節點會將請求發送給對應的節點。注意這個請求會發送給主分片,等主分片完成索引,會並行將請求發送到其所有副本分片,保證每個分片都持有最新數據。
每次寫入新文檔時,都會先寫入內存 buffer
(這時數據是搜索不到的),同時將數據寫入 translog
日志文件。
- es每隔1秒(可配置)或者
buffer
快滿時執行一次刷新(refresh)操作,將buffer
數據 refresh 到os cache
即操作系統緩存。這時數據就可以被搜索到了: - buffer 的文檔被寫入到一個新的 segment 中(這1秒時間內寫入內存的新文檔都會被寫入一個文件系統緩存(filesystem cache)中,並構成一個分段(segment));
- segment 被打開以供搜索(此時這個segment里的文檔可以被搜索到,但是尚未寫入硬盤,即如果此時發生斷電,則這些文檔可能會丟失);
- 內存 buffer 清空。
- 不斷有新的文檔寫入,則這一過程將不斷重復執行。每隔一秒將生成一個新的segment,當
translog
越來越大達到一定長度的時候,就會觸發 flush 操作(flush 完成了 Lucene 的commit
操作) - 第一步將
buffer
中現有數據refresh
到os cache
中去,清空buffer
; - 然后,將一個
commit point
寫入磁盤文件,同時強行將os cache
中目前所有的數據都 fsync 到磁盤文件中去; - 最后清空現有
translog
日志文件並生成新的translog。
執行fsync后segment寫入磁盤,清空內存和translog
由上面的流程可以看出,在兩次fsync操作之間,存儲在內存和文件系統緩存中的文檔是不安全的,一旦出現斷電這些文檔就會丟失。所以ES引入了translog來記錄兩次fsync之間所有的操作,這樣機器從故障中恢復或者重新啟動,ES便可以根據translog進行還原。
當然,translog本身也是文件,存在於內存當中,如果發生斷電一樣會丟失。因此,ES會在每隔5秒時間或是一次寫入請求完成后將translog寫入磁盤。可以認為一個對文檔的操作一旦寫入磁盤便是安全的可以復原的,因此只有在當前操作記錄被寫入磁盤,ES才會將操作成功的結果返回發送此操作請求的客戶端。
3.segment 合並
buffer
每 refresh
一次,就會產生一個 segment
(默認情況下是 1 秒鍾產生一個),這樣 segment
會越來越多,此時會定期執行 merge。
- 將多個
segment
合並成一個,並將新的segment
寫入磁盤; - 新增一個
commit point
,標識所有新的segment
; - 新的
segment
被打開供搜索使用; - 刪除舊的
segment
。
對於一個分片進行查詢請求,將會輪流查詢分片中的所有segment,這將降低搜索的效率。因此ES會自動啟動合並segment的工作,將一部分相似大小的segment合並成一個新的大segment。合並的過程實際上是創建了一個新的segment,當新segment被寫入磁盤,所有被合並的舊segment被清除。
合並segment
合並完成后刪除舊segment,新segment可供搜索
整個過程如圖:
4.刪除和更新
由於 segment
是不可變的,索引刪除的時候既不能把文檔從 segment
刪除,也不能修改 segment
反映文檔的更新。
- 刪除操作,會生成一個
.del
文件,commit point
會包含這個.del
文件。.del
文件將文檔標識為deleted
狀態,在結果返回前從結果集中刪除。 - 更新操作,會將原來的文檔標識為
deleted
狀態,然后新寫入一條數據。查詢時兩個文檔有可能都被索引到,但是被標記為刪除的文檔會被從結果集刪除。
5.查詢
查詢的過程大體上分為查詢(query)和取回(fetch)兩個階段。這個節點的任務是廣播查詢請求到所有相關分片,並將它們的響應整合成全局排序后的結果集合,這個結果集合會返回給客戶端。
查詢階段
當一個節點接收到一個搜索請求,則這個節點就變成了協調節點。
圖10、查詢過程分布式搜索
第一步是廣播請求到索引中每一個節點的分片拷貝。 查詢請求可以被某個主分片或某個副本分片處理,協調節點將在之后的請求中輪詢所有的分片拷貝來分攤負載。
每個分片將會在本地構建一個優先級隊列。如果客戶端要求返回結果排序中從第from名開始的數量為size的結果集,則每個節點都需要生成一個from+size大小的結果集,因此優先級隊列的大小也是from+size。分片僅會返回一個輕量級的結果給協調節點,包含結果集中的每一個文檔的ID和進行排序所需要的信息。
協調節點會將所有分片的結果匯總,並進行全局排序,得到最終的查詢排序結果。此時查詢階段結束。
取回階段
查詢過程得到的是一個排序結果,標記出哪些文檔是符合搜索要求的,此時仍然需要獲取這些文檔返回客戶端。
協調節點會確定實際需要返回的文檔,並向含有該文檔的分片發送get請求;分片獲取文檔返回給協調節點;協調節點將結果返回給客戶端。
圖11、分布式搜索的取回階段
相關性計算
在搜索過程中對文檔進行排序,需要對每一個文檔進行打分,判別文檔與搜索條件的相關程度。在舊版本的ES中默認采用TF/IDF(term frequency/inverse document frequency)算法對文檔進行打分。
查詢的時候操作系統會將磁盤文件里的數據自動緩存到 filesystem cache
。Elasticsearch 嚴重依賴於底層的 filesystem cache
,如果給 filesystem cache
很大,可以容納所有的 index
、segment
等文件,那么搜索的時候就基本都是走內存的,性能會非常高;反之,搜索速度並不會很快。
6.filter執行原理
當一個filter搜索請求打到Elasticsearch的時候,ES會進行下面的操作:
(1)在倒排索引中查找搜索串,獲取document list
以date來舉例:
word doc1 doc2 doc3 2019-01-01 * * 2019-02-02 * * 2019-03-03 * * *
filter: 2019-02-02
在倒排索引中尋找,我們發現2019-02-02對應的document list是doc2、doc3
(2)為每個在倒排索引中搜索到的結果,構建一個bitset
這一步是非常重要的,使用找到的doc list,構建一個bitset,即一個二進制的數組,數組的每個元素都是0或1,用來標識一個doc對一個filter條件是否匹配,如果匹配的話值就是1,不匹配值就是0。
所以上面的filter的bitset的結果就是:
[0,1,1]
doc1:不匹配這個filter的
doc2和doc3:匹配這個filter的
注:盡可能用簡單的數據結構去實現復雜的功能,可以節省內存空間,提升性能。
(3)遍歷每個過濾條件對應的bitset,優先從最稀疏的開始搜索,查找滿足所有條件的document
由於一次性可以在一個search請求中發出多個filter條件,那么就會產生多個bitset,遍歷每個filter條件對應的bitset優先從最稀疏的開始遍歷
[0,0,0,0,0,0,0,1] 比較稀疏的bitset [1,0,1,1,0,1,0,1]
這里主要是因為先遍歷比較稀疏的bitset,就可以先過濾掉盡可能多的數據
(4)caching bitset
caching bitset會跟蹤query,在最近256個query中超過一定次數的過濾條件,緩存其bitset。對於小segment(<1000 或<3%),不緩存bitset。這樣下次如果在有這個條件過來的時候,就不用重新掃描倒排索引,反復生成bitset,可以大幅度提升性能。
說明:
1、在最近的256個filter中,有某個filter超過了一定次數,這個次數不固定,那么elasticsearch就會緩存這個filter對應的bitset
2、filter針對小的segment獲取到的結果,是可以不緩存的,segment記錄數小於1000,或者segment大小小於index總大小的3%。因為此時segment數據量很小,哪怕是掃描也是很快的;segment會在后台自動合並,小segment很快會跟其它小segment合並成大segment,此時緩存就沒有什么意思了,segment很快會消失。
filter比query好的原因除了不計算相關度分數以外還有這個caching bitset。所以filter性能會很高。
(5)filter大部分的情況下,是在query之前執行的,可以盡可能過濾掉多的數據
query: 會計算每個doc的相關度分數,還會根據這個相關度分數去做排序
filter: 只是簡單過濾出想要的數據,不計算相關度分數,也不排序
(6)如果document有新增和修改,那么caching bitset會被自動更新
這個過程是ES內部做的,比如之前的bitset是[0,0,0,1]。那么現在插入一條數據或是更新了一條數據doc5,而且doc5也在緩存的bitset[0,0,0,1]的filter查詢條件中,那么ES會自動更新這個bitset,變為[0,0,0,1,1]
(7)以后只要有相同的filter條件的查詢請求打過來,就會直接使用這個過濾條件對應的bitset
這樣查詢性能就會很高,一些熱的filter查詢,就會被cache住。