Elasticsearch原理解析與效能調優
阿新 • • 發佈:2020-11-09
## 基本概念
### 定義
- 一個分散式的實時文件儲存,*每個欄位* 可以被索引與搜尋
- 一個分散式實時分析搜尋引擎
- 能勝任上百個服務節點的擴充套件,並支援 PB 級別的結構化或者非結構化資料
### 用途
- 全文檢索
- 結構化搜尋
- 分析
### VS傳統資料庫
- 傳統資料庫
- 提供精確匹配
- ES
- 提供精確匹配
- 全文檢索
- 處理同義詞
- 給文件相關性評分
- 生成分析與聚合資料
- 實時
### 專有名詞
- 索引(名詞)
類似於資料庫
- 索引(動詞)
類似於insert。例如索引一個文件到一個索引
- 倒排索引
預設每個屬性都會有一個倒排索引,可以設定屬性不被索引,它只能被覆蓋,不能被修改
- 型別
類似表,同一索引的不同型別,可以擁有不同的欄位,但應該擁有大部分相似的欄位。它可以包含大小寫,不能包含句號,不能以下劃線開頭,長度限制為256.
- Id
文件的id,可以在生成文件時指定或自動生成,自動生成的ID,在大部分情況下多個節點的時候唯一。如果在建立文件時,ID衝突,伺服器會返回409
- 文件
類似於記錄,文件只能被替換,而不能被修改,文件的欄位型別需要一致,否則無法進行精確匹配
- 精確值欄位
對於數字,日期,布林和一個not_analyzed欄位,進行查詢,會適用精確匹配
- 全文搜尋欄位
否則,進行相關性搜尋
```shell
PUT /megacorp/employee/1
{
"first_name" : "John",
"last_name" : "Smith",
"age" : 25,
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
```
例如
- megacorp是索引
- employee是型別
- 1是文件id
- json內容是文件
## 互動
### RESTful API
```shell
curl -X '://:/?' -d ''
```
| `VERB` | 適當的 HTTP *方法* 或 *謂詞* : `GET`、 `POST`、 `PUT`、 `HEAD` 或者 `DELETE`。 |
| -------------- | ------------------------------------------------------------ |
| `PROTOCOL` | `http` 或者 `https`(如果你在 Elasticsearch 前面有一個 `https` 代理) |
| `HOST` | Elasticsearch 叢集中任意節點的主機名,或者用 `localhost` 代表本地機器上的節點。 |
| `PORT` | 執行 Elasticsearch HTTP 服務的埠號,預設是 `9200` 。 |
| `PATH` | API 的終端路徑(例如 `_count` 將返回叢集中文件數量)。Path 可能包含多個元件,例如:`_cluster/stats` 和 `_nodes/stats/jvm` 。 |
| `QUERY_STRING` | 任意可選的查詢字串引數 (例如 `?pretty` 將格式化地輸出 JSON 返回值,使其更容易閱讀) |
| `BODY` | 一個 JSON 格式的請求體 (如果請求需要的話) |
#### 例子
request:
```shell
curl -XGET 'http://localhost:9200/_count?pretty' -d '
{
"query": {
"match_all": {}
}
}
'
```
response:
```shell
{
"count" : 0,
"_shards" : {
"total" : 5,
"successful" : 5,
"failed" : 0
}
}
```
### 檢索文件功能
- 獲取一個文件
```shell
GET /megacorp/employee/1
```
- 簡單查詢
```shell
GET /megacorp/employee/_search #查詢前十條記錄
```
```shell
GET /megacorp/employee/_search?q=last_name:Smith #查詢smith的前十條記錄
```
- 表示式查詢
```shell
GET /megacorp/employee/_search
{
"query" : {
"match" : {
"last_name" : "Smith"
}
}
}
```
```shell
GET /megacorp/employee/_search
{
"query" : {
"bool": {
"must": {
"match" : {
"last_name" : "smith"
}
},
"filter": {
"range" : {
"age" : { "gt" : 30 }
}
}
}
}
}
```
- 全文檢索
```shell
GET /megacorp/employee/_search
{
"query" : {
"match" : {
"about" : "rock climbing"
}
}
}
```
- 短語查詢
```shell
GET /megacorp/employee/_search
{
"query" : {
"match_phrase" : {
"about" : "rock climbing"
}
}
}
```
- 分析(類似於聚合group by)
request:
```shell
GET /megacorp/employee/_search
{
"aggs" : {
"all_interests" : {
"terms" : { "field" : "interests" },
"aggs" : {
"avg_age" : {
"avg" : { "field" : "age" }
}
}
}
}
}
```
response:
```shell
...
"all_interests": {
"buckets": [
{
"key": "music",
"doc_count": 2,
"avg_age": {
"value": 28.5
}
},
{
"key": "forestry",
"doc_count": 1,
"avg_age": {
"value": 35
}
},
{
"key": "sports",
"doc_count": 1,
"avg_age": {
"value": 25
}
}
]
}
```
- 修改文件
```shell
PUT /website/blog/123
{
"title": "My first blog entry",
"text": "I am starting to get the hang of this...",
"date": "2014/01/02"
}
```
舊文件不會馬上刪掉
新文件會被索引
文件的version會加一
- 刪除文件
```
DELETE /megacorp/employee/123
```
文件的version依然會加一
## 分散式特性
### ES自動執行的分散式動作
- 分配文件到不同的容器 或 *分片* 中,文件可以儲存在一個或多個節點中
- 按叢集節點來均衡分配這些分片,從而對索引和搜尋過程進行負載均衡
- 複製每個分片以支援資料冗餘,從而防止硬體故障導致的資料丟失
- 將叢集中任一節點的請求路由到存有相關資料的節點
- 叢集擴容時無縫整合新節點,重新分配分片以便從離群節點恢復
### 水平擴充套件VS垂直擴充套件
ES對水平擴充套件是友好的,通過購置更多的機器,可以更好的使用ES的分散式功能
### 叢集
叢集擁有一個或多個節點,當有節點加入或者退出叢集時,叢集會重新平均分配所有資料的分佈
#### 主節點功能
- 增加/刪除索引
- 增加/刪除節點
不涉及文件的變更和搜尋,因此單一的主節點不會成為叢集的效能瓶頸
> 索引分片的元資料在每個ES節點都有儲存,每個節點在接到請求後,都知道到哪臺ES node找到資料,通過轉發請求到ES node所在的機器
>
> 一個分片的最大文件數:(2^31-128)
>
> 一個索引的主分片數在建立時被確定,且無法修改:因為文件的儲存是用shard = hash(routing) % number_of_primary_shards來確定文件的位置的。routing預設是id,也可以自定義
>
> 分片的副本數可以隨時修改
#### 建立索引
```shell
PUT /blogs
{
"settings" : {
"number_of_shards" : 3,
"number_of_replicas" : 1
}
}
```
![ES-cluster](http://upload-images.jianshu.io/upload_images/24077087-db0af4d6f7d05f3c.PNG)
#### 故障轉移
##### 增加一臺機器到叢集
![ES-addonenode](http://upload-images.jianshu.io/upload_images/24077087-86f5fd13b9486a64.PNG)
##### 刪除節點
![ES-delonenode](http://upload-images.jianshu.io/upload_images/24077087-b94f5f7404434653.PNG)
#### 分散式寫入衝突
![ES-datalose](http://upload-images.jianshu.io/upload_images/24077087-de70030d5aabecf2.PNG)
對於多個client的寫入ES,有可能造成寫入衝突,導致資料的丟失
在一些場景下,資料丟失是可以接受的
但是在某些場景下,是不允許的。
##### 悲觀控制併發
傳統資料庫的控制方式。通過對記錄加鎖,來實現併發的序列執行
##### 樂觀併發控制
ES採用樂觀控制
所謂樂觀控制,就是伺服器假設大部分情況下,是不會發生衝突的,如果發生衝突,則拒絕修改,客戶端可以需要通過重新獲取並重試進行處理。
過程如下圖
![ES-versionConflict](http://upload-images.jianshu.io/upload_images/24077087-80b48eff19aca62e.PNG)
#### 分散式文件儲存
##### 確定文件位於那個shard
```
shard = hash(routing) % number_of_primary_shards
```
> API支援帶routing引數,來自定義路由,來確保相關文件路由到同一個分片
##### 以ID新建,寫入和刪除文件
![ES-writedocbyid](http://upload-images.jianshu.io/upload_images/24077087-6149cdabc7fca3f5.PNG)
###### 一致性保證
- none: 主分片活躍,允許寫入
- all: 在所有分片活躍,允許寫入
- quorum: 半數以上節點活躍,允許寫入
> 如果暫時沒有足夠的分片活躍,ES會等待,預設等待1分鐘,可以通過引數timeout改變這個值,如果超時,則失敗返回
>
> 新索引預設有 `1` 個副本分片,這意味著為滿足 `規定數量` *應該* 需要兩個活動的分片副本。 但是,這些預設的設定會阻止我們在單一節點上做任何事情。為了避免這個問題,要求只有當 `number_of_replicas` 大於1的時候,規定數量才會執行。
##### 以ID檢索文件
與上圖類似
##### 以ID更新文件
與上圖類似,但在更新完文件後,會重建索引
> 在區域性更新文件的時候,主分片會以整份文件來同步給副本,來保證資料的完整性
##### 通過條件獲取多個文件
![ES-mget](http://upload-images.jianshu.io/upload_images/24077087-66f17cc9cd81d201.PNG)
## 搜尋
### 返回特殊欄位
```
GET /_search
```
```shell
{
"hits" : {
"total" : 14,
"hits" : [
{
"_index": "us",
"_type": "tweet",
"_id": "7",
"_score": 1,
"_source": {
"date": "2014-09-17",
"name": "John Smith",
"tweet": "The Query DSL is really powerful and flexible",
"user_id": 2
}
},
... 9 RESULTS REMOVED ...
],
"max_score" : 1
},
"took" : 4,
"_shards" : {
"failed" : 0,
"successful" : 10,
"total" : 10
},
"timed_out" : false
}
```
- took
執行的毫秒數
- _shards
查詢分片的狀態,例如有幾個分片是失敗的,幾個是成功的
- timeout
可以通過在查詢設定超時,如果查詢超過時間,則只返回已經成功獲得的資料,剩餘的資料將丟棄
- _index
資料來源的Lucene索引,文件的每一個欄位,都擁有一個不同的Lucene索引
> 在查詢中可以指定Lucene的索引,預設是不指定的,所以會查詢該文件的所有索引,並彙總結果。如果指定,則會限定僅在指定的Lucene索引中查詢資料
### 分頁
```
GET /_search?size=5&from=5
```
> 此方式只適用於淺分頁,如果查詢過深,會導致嚴重的效能問題。
>
> 因為例如查詢size為5,from=10000。那麼ES會從各分片中都查詢10005條記錄,如果有100個shard,那麼就會有100\*10005條記錄,ES再對這100\*10005排序,並僅返回5條記錄
### 深分頁
使用遊標scroll
它在ES中建立了一個有有效期的快照,提供給scroll進行資料的深度查詢
### 倒排索引
對一下文件進行倒排:
1. The quick brown fox jumped over the lazy dog
2. Quick brown foxes leap over lazy dogs in summer
得到:
```
Term Doc_1 Doc_2
-------------------------
Quick | | X
The | X |
brown | X | X
dog | X |
dogs | | X
fox | X |
foxes | | X
in | | X
jumped | X |
lazy | X | X
leap | | X
over | X | X
quick | X |
summer | | X
the | X |
------------------------
```
#### 倒排面臨的挑戰
- Quick跟quick,使用者有可能認為它們是相同的,也有可能認為是不同的
- dog和dogs非常接近,在相關性搜尋時,它們應該都被搜尋到
- jump和leap是同義詞,在相關性搜尋時,它們應該都被搜尋到
### 理解相關性
相關性的分數是一個模糊的概念。沒有精確值,沒有唯一正確的答案。是一種根據各種規則對文件進行的一種量化的估計。
它評分的準則如下:
- 檢索詞頻率
檢索詞在該欄位出現的頻率越高,分數越高
- 反向文件頻率
檢索詞在索引出現的頻率越高,分數越低
- 欄位長度準則
欄位的長度越長,分數越低
### 相關性破壞
在使用全文檢索某個關鍵字的時候,會出現,相關度低的文件的得分高於相關度高的文件的得分。
例如檢索詞*milk*。索引內有兩個主分片,milk在P1出現了5次,在P2出現了6次。由於P1和P2的詞分佈不一樣。
P1的詞量比P2的詞量高,那麼milk算在P1出現佔比小,導致在P1得相關性得分高,而在P2,佔比da,導致在P2的相關性得分低。
#### 原因
是因為區域性資料分佈不均勻導致的
#### 解決方法
- 插入更多的文件
- 使用?search_type=dfs_query_then_fetch進行全域性評分。但會有嚴重的效能問題。不推薦使用。
### 查詢過濾bitset
每次使用檢索詞查詢,都會為檢索詞建立一個bitset,bitset包含了匹配的文件的序號。在熱搜尋的檢索詞,ES會對這些bitset有針對的進行快取,而不用在再次查詢的時候,重新查詢倒排索引。
對於多個查詢可以有下圖
![ES-bitset](http://upload-images.jianshu.io/upload_images/24077087-38e3b4fb045bccff.PNG)
> 當倒排索引重建的時候,bitset在快取會自動失效
#### 快取的策略
- 最近256次被使用的bitset,會被快取
- 段內記錄小於1w的,不會被快取
## 索引管理
### 建立索引
可以顯式建立,也可以隱式建立。
在大叢集下,索引的建立,涉及元資料的同步,有可能導致叢集負載的大量增加。此時需要禁用索引的隱式建立
```
action.auto_create_index: false
```
### 刪除索引
刪除索引,會涉及大量資料的刪除,如果使用者意外地試圖通過一條命令,把所有索引刪掉,這可能導致可怕的後果
通過禁用此操作,可以設定如下
```
action.destructive_requires_name: true
```
### 分析器
每個索引都可以設定自己的分析器,分析器的用途主要是在全文索引上面,通過對不同的語言,使用不同的分詞,不同的詞轉換來構造倒排索引和計算相關性。
## 分片
### 倒排索引的不變性
#### 好處
- 一旦被讀入系統快取,就會一直留在那裡,直到LRU演算法把不常用的倒排索引剔除。這對ES的讀取效能提供了非常大的提升
#### 不好
- 新的文件加入,不能增量更新,只能重建索引並替換
### 如何保證新資料能實時能查詢到
用更多的索引。
對於新的文件,不馬上重建索引,而是通過新增額外的索引。在查詢資料時,通過輪詢所有的索引,併合並結果返回。
![ES-shar](http://upload-images.jianshu.io/upload_images/24077087-0068a03f06a5cbb6.PNG)
> ES並不是嚴格意義上的實時,準確來說是準實時,由於data從插入到建立倒排索引這段時間,新資料是不能訪問的
## 聚合
像資料庫的group by。只是語法不一樣。功能相通
## 應用層效能調優
### 調大 refresh interval
預設重新整理時間是1s,每次重新整理都會有一次磁碟寫入,並建立一個新的段。通過設定更大的重新整理時間,可以讓磁碟寫入的次數更低,寫入的段更大。減少段合併的次數。
### 禁止OS把ES置換出去
OS的核心會在記憶體緊張的時候,把程序置換到外村。而對於效能跟記憶體強相關的ES來說,置換到外存是致命的。通過設定程序在核心的引數,禁止置換,可以避免OS的這種動作
### 預留大量的檔案系統快取給ES
由於ES大部分資料的不變性,使得ES的大部分磁碟操作,都可以通過檔案系統的快取來加快速度。一旦ES的倒排索引和資料快取到系統,如果沒有其他程序的干擾,而且是比較頻繁訪問的資料,則會一直駐留在系統快取,使得ES的大部分操作都是走記憶體的。一般來說,分配一半的記憶體給檔案系統,是合適的。
### 使用自動生成ID
如果指定ID,ES會在叢集內檢查是否ID已經存在,這對大叢集來說,是昂貴的。如果ID是自動生成的,ES會跳過檢查,直接插入文件
### 更好的硬體
- 更大的記憶體
- SSD
- 本地磁碟
### 不要使用join關聯查詢
ES不適合做關聯查詢,會導致嚴重的效能問題。
如果業務一定要join,可以把關聯的資料都寫到一個索引內,或者通過應用程式來做關聯的動作。
### 強制merge只讀索引
merge成一個單一的段,會得到更好的效能
### 增加副本
有更多的機器,通過提高副本數,可以提高讀效率
### 不要返回大資料
ES不適合這場景
### 避免稀疏
不要把不相關的資訊存入同一個索引
### 資料預熱
對於熱點資料,可以通過一個客戶端請求ES,讓資料先佔據filesystem cache。
### 冷熱資料分離
冷熱資料部署在不同的機器,可以讓熱資料在快取內不會被冷資料沖走
## 核心層效能調優
### 限流
如果ES出現高負載的請求,ES的協調節點會累積大量的請求在記憶體在等待處理,隨著請求數的增加,協調節點的記憶體佔用會越來越大,最後導致OOM。
通過限流,可以有效緩解。
### 大查詢
如果客戶端發來了一個複雜的查詢,使得需要返回的資料異常的大,這也會導致OOM問題。
通過修改核心,讓如果請求的記憶體佔用超過系統可以承受的範圍,則截斷來解決
### FST過大引發OOM
FST是對倒排索引在記憶體的索引,它通過字首狀態機的方法,快速的定位檢索詞在倒排索引的磁碟位置,達到減少磁碟訪問次數而加快檢索速度的目的。
但由於FST是常駐記憶體的,如果倒排索引達到一定規模時,FST必然會引起OOM問題。而且FST是存放在JVM堆內記憶體的。堆內記憶體的上限時32G。
而10 TB的資料就需要10G到15G的記憶體來存放FST。
- 通過把FST的儲存放到堆外記憶體
- 通過LRU演算法來管理FST,對不常用的FST置換出記憶體
- 修改ES訪問FST的邏輯,使得ES可以從堆內直接訪問堆外的FST
- 在堆內增加FST的cache,加快命中速度
Ref:
- https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html
- https://zhuanlan.zhihu.com/p/99718374
- https://blog.csdn.net/wangnan9279/article/details/7