一,es原理
es 無非就是寫入數據,搜索數據。你要是不明白你發起一個寫入和搜索請求的時候,es 在干什么,那你真的是......
對 es 基本就是個黑盒,你還能干啥?你唯一能干的就是用 es 的 api 讀寫數據了。要是出點什么問題,你啥都不知道,那還能指望你什么呢?
二,es 寫數據過程
- 客戶端選擇一個 node 發送請求過去,這個 node 就是 `coordinating node`(協調節點)。
- `coordinating node` 對 document 進行**路由**,將請求轉發給對應的 node(有 primary shard)。
- 實際的 node 上的 `primary shard` 處理請求,然后將數據同步到 `replica node`。
- `coordinating node` 如果發現 `primary node` 和所有 `replica node` 都搞定之后,就返回響應結果給客戶端。
三, es 讀數據過程
可以通過 `doc id` 來查詢,會根據 `doc id` 進行 hash,判斷出來當時把 `doc id` 分配到了哪個 shard 上面去,從那個 shard 去查詢。
- 客戶端發送請求到**任意**一個 node,成為 `coordinate node`。
- `coordinate node` 對 `doc id` 進行哈希路由,將請求轉發到對應的 node,此時會使用 `round-robin` **隨機輪詢算法**,在 `primary shard` 以及其所有 replica 中隨機選擇一個,讓讀請求負載均衡。
- 接收請求的 node 返回 document 給 `coordinate node`。
- `coordinate node` 返回 document 給客戶端。
四, es 搜索數據過程
es 最強大的是做全文檢索,就是比如你有三條數據:
```
java真好玩兒啊
java好難學啊
j2ee特別牛
```
你根據 `java` 關鍵詞來搜索,將包含 `java`的 `document` 給搜索出來。es 就會給你返回:java真好玩兒啊,java好難學啊。
- 客戶端發送請求到一個 `coordinate node`。
- 協調節點將搜索請求轉發到**所有**的 shard 對應的 `primary shard` 或 `replica shard`,都可以。
- query phase:每個 shard 將自己的搜索結果(其實就是一些 `doc id`)返回給協調節點,由協調節點進行數據的合並、排序、分頁等操作,產出最終結果。
- fetch phase:接着由協調節點根據 `doc id` 去各個節點上**拉取實際**的 `document` 數據,最終返回給客戶端。
> 寫請求是寫入 primary shard,然后同步給所有的 replica shard;讀請求可以從 primary shard 或 replica shard 讀取,采用的是隨機輪詢算法。
五, 寫數據底層原理
先寫入內存 buffer,在 buffer 里的時候數據是搜索不到的;同時將數據寫入 translog 日志文件。
如果 buffer 快滿了,或者到一定時間,就會將內存 buffer 數據 `refresh` 到一個新的 `segment file` 中,但是此時數據不是直接進入 `segment file` 磁盤文件,而是先進入 `os cache` 。這個過程就是 `refresh`。
每隔 1 秒鍾,es 將 buffer 中的數據寫入一個**新的** `segment file`,每秒鍾會產生一個**新的磁盤文件** `segment file`,這個 `segment file` 中就存儲最近 1 秒內 buffer 中寫入的數據。
但是如果 buffer 里面此時沒有數據,那當然不會執行 refresh 操作,如果 buffer 里面有數據,默認 1 秒鍾執行一次 refresh 操作,刷入一個新的 segment file 中。
操作系統里面,磁盤文件其實都有一個東西,叫做 `os cache`,即操作系統緩存,就是說數據寫入磁盤文件之前,會先進入 `os cache`,先進入操作系統級別的一個內存緩存中去。只要 `buffer` 中的數據被 refresh 操作刷入 `os cache`中,這個數據就可以被搜索到了。
六,為什么叫 es 是**准實時**的?
`NRT`,全稱 `near real-time`。默認是每隔 1 秒 refresh 一次的,所以 es 是准實時的,因為寫入的數據 1 秒之后才能被看到。可以通過 es 的 `restful api` 或者 `java api`,**手動**執行一次 refresh 操作,就是手動將 buffer 中的數據刷入 `os cache`中,讓數據立馬就可以被搜索到。只要數據被輸入 `os cache` 中,buffer 就會被清空了,因為不需要保留 buffer 了,數據在 translog 里面已經持久化到磁盤去一份了。
重復上面的步驟,新的數據不斷進入 buffer 和 translog,不斷將 `buffer` 數據寫入一個又一個新的 `segment file` 中去,每次 `refresh` 完 buffer 清空,translog 保留。隨着這個過程推進,translog 會變得越來越大。當 translog 達到一定長度的時候,就會觸發 `commit` 操作。
commit 操作發生第一步,就是將 buffer 中現有數據 `refresh` 到 `os cache` 中去,清空 buffer。然后,將一個 `commit point` 寫入磁盤文件,里面標識着這個 `commit point` 對應的所有 `segment file`,同時強行將 `os cache` 中目前所有的數據都 `fsync` 到磁盤文件中去。最后**清空** 現有 translog 日志文件,重啟一個 translog,此時 commit 操作完成。
這個 commit 操作叫做 `flush`。默認 30 分鍾自動執行一次 `flush`,但如果 translog 過大,也會觸發 `flush`。flush 操作就對應着 commit 的全過程,我們可以通過 es api,手動執行 flush 操作,手動將 os cache 中的數據 fsync 強刷到磁盤上去。
t ranslog 日志文件的作用是什么?你執行 commit 操作之前,數據要么是停留在 buffer 中,要么是停留在 os cache 中,無論是 buffer 還是 os cache 都是內存,一旦這台機器死了,內存中的數據就全丟了。所以需要將數據對應的操作寫入一個專門的日志文件 `translog` 中,一旦此時機器宕機,再次重啟的時候,es 會自動讀取 translog 日志文件中的數據,恢復到內存 buffer 和 os cache 中去。
translog 其實也是先寫入 os cache 的,默認每隔 5 秒刷一次到磁盤中去,所以默認情況下,可能有 5 秒的數據會僅僅停留在 buffer 或者 translog 文件的 os cache 中,如果此時機器掛了,會**丟失** 5 秒鍾的數據。但是這樣性能比較好,最多丟 5 秒的數據。也可以將 translog 設置成每次寫操作必須是直接 `fsync` 到磁盤,但是性能會差很多。
實際上你在這里,如果沒有問你 es 丟數據的問題,你可以在這里給炫一把,你說,其實 es 第一是准實時的,數據寫入 1 秒后可以搜索到;可能會丟失數據的。有 5 秒的數據,停留在 buffer、translog os cache、segment file os cache 中,而不在磁盤上,此時如果宕機,會導致 5 秒的**數據丟失**。
七,總結
數據先寫入內存 buffer,然后每隔 1s,將數據 refresh 到 os cache,到了 os cache 數據就能被搜索到(所以我們才說 es 從寫入到能被搜索到,中間有 1s 的延遲)。每隔 5s,將數據寫入 translog 文件(這樣如果機器宕機,內存數據全沒,最多會有 5s 的數據丟失),translog 大到一定程度,或者默認每隔 30mins,會觸發 commit 操作,將緩沖區的數據都 flush 到 segment file 磁盤文件中。
數據寫入 segment file 之后,同時就建立好了倒排索引。
八,刪除/更新數據底層原理
如果是刪除操作,commit 的時候會生成一個 `.del` 文件,里面將某個 doc 標識為 `deleted` 狀態,那么搜索的時候根據 `.del` 文件就知道這個 doc 是否被刪除了。
如果是更新操作,就是將原來的 doc 標識為 `deleted` 狀態,然后新寫入一條數據。
buffer 每 refresh 一次,就會產生一個 `segment file`,所以默認情況下是 1 秒鍾一個 `segment file`,這樣下來 `segment file` 會越來越多,此時會定期執行 merge。每次 merge 的時候,會將多個 `segment file` 合並成一個,同時這里會將標識為 `deleted` 的 doc 給**物理刪除掉**,然后將新的 `segment file` 寫入磁盤,這里會寫一個 `commit point`,標識所有新的 `segment file`,然后打開 `segment file` 供搜索使用,同時刪除舊的 `segment file`。
九, 底層 lucene
簡單來說,lucene 就是一個 jar 包,里面包含了封裝好的各種建立倒排索引的算法代碼。我們用 Java 開發的時候,引入 lucene jar,然后基於 lucene 的 api 去開發就可以了。
通過 lucene,我們可以將已有的數據建立索引,lucene 會在本地磁盤上面,給我們組織索引的數據結構。
倒排索引
在搜索引擎中,每個文檔都有一個對應的文檔 ID,文檔內容被表示為一系列關鍵詞的集合。例如,文檔 1 經過分詞,提取了 20 個關鍵詞,每個關鍵詞都會記錄它在文檔中出現的次數和出現位置。
那么,倒排索引就是**關鍵詞到文檔** ID 的映射,每個關鍵詞都對應着一系列的文件,這些文件中都出現了關鍵詞。
舉個栗子。
有以下文檔:
| DocId | Doc |
|---|---|
| 1 | 谷歌地圖之父跳槽 Facebook |
| 2 | 谷歌地圖之父加盟 Facebook |
| 3 | 谷歌地圖創始人拉斯離開谷歌加盟 Facebook |
| 4 | 谷歌地圖之父跳槽 Facebook 與 Wave 項目取消有關 |
| 5 | 谷歌地圖之父拉斯加盟社交網站 Facebook |
對文檔進行分詞之后,得到以下**倒排索引**。
| WordId | Word | DocIds |
|---|---|---|
| 1 | 谷歌 | 1,2,3,4,5 |
| 2 | 地圖 | 1,2,3,4,5 |
| 3 | 之父 | 1,2,4,5 |
| 4 | 跳槽 | 1,4 |
| 5 | Facebook | 1,2,3,4,5 |
| 6 | 加盟 | 2,3,5 |
| 7 | 創始人 | 3 |
| 8 | 拉斯 | 3,5 |
| 9 | 離開 | 3 |
| 10 | 與 | 4 |
| .. | .. | .. |
另外,實用的倒排索引還可以記錄更多的信息,比如文檔頻率信息,表示在文檔集合中有多少個文檔包含某個單詞。
那么,有了倒排索引,搜索引擎可以很方便地響應用戶的查詢。比如用戶輸入查詢 `Facebook`,搜索系統查找倒排索引,從中讀出包含這個單詞的文檔,這些文檔就是提供給用戶的搜索結果。
要注意倒排索引的兩個重要細節:
- 倒排索引中的所有詞項對應一個或多個文檔;
- 倒排索引中的詞項**根據字典順序升序排列**
> 上面只是一個簡單的栗子,並沒有嚴格按照字典順序升序排列。