淺析分散式搜索引擎

来源:https://www.cnblogs.com/mujingyu/archive/2019/06/28/11104923.html
-Advertisement-
Play Games

1. 基礎知識 1.1 認識Lucene 維基百科的定義: Lucene是一套用於 全文檢索 和 搜索 的 開放源碼程式庫 ,由Apache軟體基金會支持和提供。Lucene提供了一個簡單卻強大的應用程式介面,能夠做全文索引和搜索,在Java開發環境里Lucene是一個成熟的免費開放源代碼工具;就其 ...


1. 基礎知識

1.1 認識Lucene

維基百科的定義:

Lucene是一套用於全文檢索搜索開放源碼程式庫,由Apache軟體基金會支持和提供。Lucene提供了一個簡單卻強大的應用程式介面,能夠做全文索引和搜索,在Java開發環境里Lucene是一個成熟的免費開放源代碼工具;就其本身而論,Lucene是現在並且是這幾年,最受歡迎的免費Java信息檢索程式庫。

Lucene官網:http://lucene.apache.org

1.2 倒排索引

在搜索引擎中,每個文檔都有一個對應的文檔 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,搜索系統查找倒排索引,從中讀出包含這個單詞的文檔,這些文檔就是提供給用戶的搜索結果。

要註意倒排索引的兩個重要細節:

  • 倒排索引中的所有詞項對應一個或多個文檔;
  • 倒排索引中的詞項根據字典順序升序排列

上面只是一個簡單的慄子,並沒有嚴格按照字典順序升序排列。

1.3 Lucene與Elasticsearch

Lucene是一個開源的全文檢索引擎工具包(類似於Java api),而Elasticsearch底層是基於這些包,對其進行了擴展,提供了比Lucene更為豐富的查詢語言,可以非常方便的通過Elasticsearch的HTTP介面與底層Lucene交互。

如果在應用程式中直接使用Lucene,你需要覆蓋大量的集成框架工作,而使用ElasticSearch就省下了這些集成工作。

一句話概括:Elasticsearch是Lucene面向企業搜索應用的擴展,極大的縮短研發周期。

剛剛入門Elasticsearch,只需稍微瞭解下Lucene,無需去真正學習它,就可以很好的完成全文索引的工作,很好的進行開發。等熟練使用es之後,可以反過頭來學習Lucene裡面底層的原理,也是一種提升。

因為Lucene是一個編程庫,您可以按原始介面來調用。但是Elasticsearch是在它基礎上擴展的應用程式,就可以直接拿來使用了。

舉個例子,你直接拿汽車(Elasticsearch)來開,開好車就行,無需瞭解裡面的發動機、各個組件(Lucene library)。後面你在去瞭解一些原理,對於修車等等會有幫助。

1.4 ES的核心面試題

(1)es的分散式架構原理是什麼(es是如何實現分散式的)?

(2)es寫入數據的工作原理是什麼?es查詢數據的工作原理是什麼?

(3)es在數據量很大的情況下(數十億級別)如何提高查詢性能?

(4)es生產集群的部署架構是什麼?每個索引的數據量大概有多少?每個索引大概有多少個分片?

2. 認識ES

Lucene 是最先進、功能最強大的搜索庫。如果直接基於 lucene 開發,非常複雜,即便寫一些簡單的功能,也要寫大量的 Java 代碼,需要深入理解原理。

elasticsearch 基於 lucene,簡稱es,隱藏了 lucene 的複雜性,提供了簡單易用的 restful api / Java api 介面(另外還有其他語言的 api 介面)。現在分散式搜索基本已經成為大部分互聯網行業的Java系統的標配,其中尤為流行的就是es,前幾年es沒火的時候,大家一般用solr。但是這兩年基本大部分企業和項目都開始轉向es了。

  • 分散式的文檔存儲引擎
  • 分散式的搜索引擎和分析引擎
  • 分散式,支持 PB 級數據

2.1 ES 的核心概念

Near Realtime

近實時,有兩層含義:

  • 從寫入數據到數據可以被搜索到有一個小延遲(大概是 1s)
  • 基於 es 執行搜索和分析可以達到秒級

Cluster 集群

集群包含多個節點,每個節點屬於哪個集群都是通過一個配置來決定的,對於中小型應用來說,剛開始一個集群就一個節點很正常。

Node 節點

Node 是集群中的一個節點,每個節點有一個唯一的名稱,這個名稱預設是隨機分配的。預設節點會去加入一個名稱為 elasticsearch 的集群。如果直接啟動一堆節點,那麼它們會自動組成一個 elasticsearch 集群,當然一個節點也可以組成 elasticsearch集群。

Document & field

文檔是 es 中最小的數據單元,一個 document 可以是一條客戶數據、一條商品分類數據、一條訂單數據,通常用 json 數據結構來表示。每個 index 下的 type,都可以存儲多條 document。一個 document 裡面有多個 field,每個 field 就是一個數據欄位。

{
    "product_id": "1",
    "product_name": "iPhone X",
    "product_desc": "蘋果手機",
    "category_id": "2",
    "category_name": "電子產品"
}

Index

索引包含了一堆有相似結構的文檔數據,比如商品索引。一個索引包含很多 document,一個索引就代表了一類相似或者相同的 ducument。

Type

類型,每個索引里可以有一個或者多個 type,type 是 index 的一個邏輯分類,比如商品 index 下有多個 type:日化商品 type、電器商品 type、生鮮商品 type。每個 type 下的 document 的 field 可能不太一樣。

shard

單台機器無法存儲大量數據,es 可以將一個索引中的數據切分為多個 shard,分佈在多台伺服器上存儲。有了 shard 就可以橫向擴展,存儲更多數據,讓搜索和分析等操作分佈到多台伺服器上去執行,提升吞吐量和性能。每個 shard 都是一個 Lucene index。

replica

任何一個伺服器隨時可能故障或宕機,此時 shard 可能就會丟失,因此可以為每個 shard 創建多個 replica 副本。replica 可以在 shard 故障時提供備用服務,保證數據不丟失,多個 replica 還可以提升搜索操作的吞吐量和性能。primary shard(建立索引時一次設置,不能修改,預設 5 個),replica shard(隨時修改數量,預設 1 個),預設每個索引 10 個 shard,5 個 primary shard,5個 replica shard,最小的高可用配置,是 2 台伺服器。

這麼說吧,shard 分為 primary shard 和 replica shard。而 primary shard 一般簡稱為 shard,而 replica shard 一般簡稱為 replica。

ES 核心概念 vs DB 核心概念

es db
index 資料庫
type 數據表
docuemnt 一行數據

以上是一個簡單的類比。

3. ES架構原理

elasticsearch設計的理念就是分散式搜索引擎,底層其實還是基於lucene的。核心思想就是在多台機器上啟動多個es進程實例,組成了一個es集群。

es中存儲數據的基本單位是索引,比如現在要在es中存儲一些訂單數據,此時就應該在es中創建一個索引,order_idx,所有的訂單數據就都寫到這個索引裡面去,一個索引相當於mysql里的一張表。

index -> type -> mapping -> document -> field

以下是對上述名詞概念的一種類比,僅僅是一種類比描述:

index: mysql 里的一張表。

type:沒法跟mysql里去對比,一個index里可以有多個type,每個type的欄位都是差不多的,但是有一些略微的差別。

假設有一個訂單的index,裡面專門是放訂單數據。

在 mysql 中建表,有些訂單是實物商品的訂單,比如一件衣服、一雙鞋子;有些訂單是虛擬商品的訂單,比如游戲點卡,話費充值。這兩種訂單大部分欄位是一樣的,但是少部分欄位可能存在略微的一些差別。

所以就需要在訂單 index 里創建兩個 type,一個是實物商品訂單 type,一個是虛擬商品訂單 type,這兩個 type 大部分欄位是一樣的,少部分欄位是不一樣的。

很多情況下,一個 index 里可能就一個 type,但是確實如果說是一個 index 里有多個 type 的情況(註意mapping types這個概念在 ElasticSearch 7.X 已被完全移除,詳細說明可以參考官方文檔)。

可以認為 index 是一個類別的表,具體的每個 type 代表了 mysql 中的一個表。每個 type 有一個 mapping,如果你認為一個 type 是具體的一個表,index 就代表多個 type 同屬於的一個類型,而 mapping 就是這個 type 的表結構定義

場景類比:在 mysql 中創建一個表的時候,需要定義表結構、表結構中的欄位、每個欄位的類型。因此 index 里的一個 type 裡面寫的一條數據,這條數據叫做一條 document,一條 document 就代表了 mysql 中某個表裡的一行,每個 document 有多個 field,每個 field 就代表了這個 document 中的一個欄位的值。

創建一個es索引的時候,這個索引可以拆分成多個 shard,每個 shard 存儲部分數據。拆分多個 shard 是有好處:

支持橫向擴展

比如整個數據量是 3T,3 個 shard,每個 shard 可以分別拆分成 1T 的數據,若現在數據量增加到 4T 該怎麼擴展?很簡單,重新建一個有 4 個 shard 的索引,將數據導進去;

提高性能

數據分佈在多個 shard,即多台伺服器上,所有的操作都會在多台機器上並行分散式執行,提高了吞吐量和性能。

另外 shard 的數據實際是有多個備份,也就是說每個 shard 都有一個 primary shard,這個 primary shard 專門負責寫入數據,同時這個還有幾個副本 replica shardprimary shard 寫入數據之後,會將數據同步到自己的所有副本 replica shard 上去(primary shard和其副本的replica shard不會在同一臺機器上)。

因此通過上述的主副shard方案,每個shard的數據在多台機器上都有備份,如果某個機器宕機了,沒關係,還有別的數據副本在別的機器上,這就可以保證高可用了。

es集群會自動從眾多節點中選舉某一個節點為master節點,這個master節點其實就是乾一些管理的工作,比如維護索引元數據拉,負責切換primary shard和replica shard身份等。

當master節點宕機了,那麼es集群會重新選舉一個節點成為新的master節點。

當非master節點宕機了,那麼會由master節點,讓那個宕機節點上的primary shard的身份轉移到其他機器上的replica shard。隨後當修複了那個宕機機器並重啟之後,master 節點會控制將缺失的 replica shard 分配過去,同步後續修改的數據之類的,讓集群恢復正常。

簡單說,當某個非 master 節點宕機了,那麼此節點上的 primary shard 就消失了,那麼 master 會檢測到該節點掛了,隨即讓那個 primary shard 對應的 replica shard(在其他機器上)切換為 primary shard。如果宕機的機器修複了,修複後的節點也不再是 primary shard,而是 replica shard。

上述就是elasticsearch作為一個分散式搜索引擎最基本的一個架構設計。

4. 寫入&查詢數據的工作原理

如果只把es當作一個黑盒,只會使用api讀寫數據是不能滿足實際開發需求的,因為一旦出了問題,根本無從下手解決。

4.1 寫數據過程

  • 客戶端選擇一個node發送請求過去,這個node就是coordinating node(協調節點)。

  • coordinating node對document進行路由,將請求轉發給對應的node(有primary shard)。

  • 實際的node上的primary shard處理請求,然後將數據同步到有對應replica shard的node上。

  • coordinating node,如果發現primary node和所有replica node都搞定之後,就返迴響應結果給客戶端。

4.2 讀數據過程

數據寫入某個document的時候,es會自動會給這個document自動分配一個全局唯一的id,稱為doc id,協同節點也是根據doc id進行hash路由到對應的primary shard上面去。也可以手動指定doc id,比如用訂單id,用戶id。

讀數據的過程大致是通過doc id查詢,es會根據doc id進行hash,判斷出來當時把doc id分配到了哪個shard上面去,再從那個shard去查詢:

  • 客戶端發送請求到任意一個node,當前node成為coordinate node(協調節點)。

  • coordinate nodedoc id 進行哈希路由,將請求轉發到對應的 node,此時會使用 round-robin 隨機輪詢演算法,在 primary shard 以及其所有 replica 中隨機選擇一個,讓讀請求負載均衡。

  • 接收請求的 node 返回 document 給 coordinate node

  • coordinate node 返回 document 給客戶端。

4.3 搜索數據過程

es 最強大的是做全文檢索,就是比如你有三條數據:

java真好玩兒啊
java好難學啊
j2ee特別牛

es可以根據 "java" 關鍵詞來搜索,將包含 "java"關鍵詞的 document 給搜索出來。es 就會給你返回:"java真好玩兒啊","java好難學啊"。

  • 客戶端發送請求到一個 coordinate node
  • 協調節點將搜索請求轉發到所有的 shard 對應的 primary shardreplica shard
  • query phase:每個 shard 將自己的搜索結果(其實就是一些 doc id)返回給協調節點,由協調節點進行數據的合併、排序、分頁等操作,產出最終結果。
  • fetch phase:接著由協調節點根據 doc id 去各個節點上拉取實際document 數據,最終返回給客戶端。

寫請求是寫入 primary shard,然後同步給所有的 replica shard;讀請求可以從 primary shard 或 replica shard 讀取,採用的是隨機輪詢演算法。

4.4 寫數據底層原理

es里的寫流程,有4個底層的核心概念:refreshflushtranslogmerge

1)數據先寫入記憶體buffer,在寫入buffer的同時將數據寫入translog日誌文件,註意:此時數據還沒有被成功es索引記錄,因此無法搜索到對應數據;

2)如果buffer快滿了或者到一定時間,es就會將buffer數據refresh到一個新的segment file中,但是此時數據不是直接進入segment file的磁碟文件,而是先進入os cache的。這個過程就是refresh

每隔1秒鐘,es將buffer中的數據寫入一個新的segment file,因此每秒鐘會產生一個新的磁碟文件segment file,這個segment file中就存儲最近1秒內buffer中寫入的數據。

如果buffer裡面此時沒有數據,那當然不會執行refresh操作咯,每秒創建換一個空的segment file,如果buffer裡面有數據,預設1秒鐘執行一次refresh操作,刷入一個新的segment file中。

操作系統中,磁碟文件其實都有一個操作系統緩存os cache,因此數據寫入磁碟文件之前,會先進入操作系統級別的記憶體緩存os cache中。

一旦buffer中的數據被refresh操作,刷入os cache中,就代表這個數據就可以被搜索到了。

這就是為什麼es被稱為準實時(NRT,near real-time):因為寫入的數據預設每隔1秒refresh一次,也就是數據每隔一秒才能被 es 搜索到,之後才能被看到,所以稱為準實時。

es可以通過restful api或者java api,手動執行一次refresh操作,也就是手動將buffer中的數據刷入os cache中,讓數據立馬就可以被搜索到。

只要數據被輸入os cache中,buffer就會被清空,並且數據在translog日誌文件裡面持久化到磁碟了一份,此時就可以讓這個segment file的數據對外提供搜索了。

3)重覆1~2步驟,新的數據不斷進入buffertranslog,不斷將buffer數據寫入一個又一個新的segment file中去,每次refresh完,buffer就會被清空,同時translog保留一份日誌數據。隨著這個過程推進,translog文件會不斷變大。當translog文件達到一定程度時,就會執行commit操作。

4)commit操作發生第一步,就是將buffer中現有數據refreshos cache中去,清空buffer

5)將一個 commit point 寫入磁碟文件,裡面標識著這個 commit point 對應的所有 segment file,同時強行將 os cache 中目前所有的數據都 fsync 到磁碟文件中去。

8)將現有的translog清空,然後再次重啟啟用一個translog,此時commit操作完成。

預設每隔30分鐘會自動執行一次commit,但是如果translog文件過大,也會觸發commit。整個commit的過程,叫做flush操作。我們可以手動執行flush操作,就是將所有os cache數據刷到磁碟文件中去。

我們也可以通過es的api,手動執行flush操作,手動將os cache中的數據fsync強刷到磁碟上去,記錄一個commit point,清空translog日誌文件。

translog日誌文件的作用是什麼?

在你執行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是準實時的,因此數據寫入1秒後才可以搜索到。
  • es可能會丟失數據:有5秒的數據停留在buffer、translog的os cache、segment file的os cache中,也就是這5秒的數據不在磁碟上,此時如果宕機,會導致5秒的數據丟失。

如果你希望一定不能丟失數據,可以查官方文檔設置個參數。使得每次寫入一條數據,都是寫入buffer,同時寫入磁碟上的translog,但是這會導致寫性能、寫入吞吐量會下降一個數量級。本來一秒鐘可以寫2000條,現在一秒鐘可能只能寫200條。

總結一下:

  • 數據先寫入記憶體 buffer,然後每隔 1s,將數據 refresh 到 os cache,到了 os cache 數據就能被搜索到(所以我們才說 es 是準實時的,因為從寫入到能被搜索到中間有 1s 的延遲)。

  • 每隔 5s,將數據寫入 translog 文件(這樣如果機器宕機,記憶體數據全沒,最多會有 5s 的數據丟失)。
  • translog大到一定程度,或者預設每隔 30mins,會觸發 commit 操作,將緩衝區的數據都 flush 到 segment file 磁碟文件中。

數據寫入 segment file 之後,同時就建立好了倒排索引。

4.5 刪除/更新數據底層原理

如果是刪除操作,commit 的時候會生成一個 .del 文件,裡面將某個 doc 標識為 deleted 狀態,那麼搜索的時候根據 .del 文件就知道這個 doc 是否被刪除了。

如果是更新操作,就是將原來的 doc 標識為 deleted 狀態,然後新寫入一條數據。

segment file 的 merge 操作:

buffer 每 refresh 一次,就會產生一個 segment file,所以預設情況下是 1 秒鐘一個 segment file,這樣下來 segment file 文件數量會越來越多,此時es會定期執行 merge。

每次 merge 的時候,會將多個 segment file 合併成一個,同時這裡會將標識為 deleted 的 doc 給物理刪除掉,然後將新的 segment file 寫入磁碟,這裡會寫一個 commit point,標識所有新的 segment file,然後打開 segment file 供搜索使用,同時刪除舊的 segment file

總的來說,就是當segment file多到一定程度的時候,es就會自動觸發merge操作,將多個segment file給merge成一個segment file。

5. 提高查詢性能

es在數據量很大的情況下(數十億級別)如何提高查詢效率/性能?

es性能其實並沒有你想象中那麼好的。很多時候數據量大了,特別是有幾億條數據的時候,可能跑個搜索怎麼需要5秒~10秒。第一次搜索的時候,是5~10秒,後面反而就快了,可能就幾百毫秒。

es性能優化是沒有什麼銀彈的,不要期待著隨手調一個參數,就可以萬能的應對所有的性能慢的場景。也許有的場景是換個參數,或者調整一下語法,就可以搞定,但是絕對不是用一個配置參數通用所有場景的。

在這個海量數據的場景下,如何提升es搜索的性能,需要生產環境實踐經驗積累。

5.1 filesystem cache(性能優化的殺手鐧)

利用操作系統的緩存 os cache進行性能優化點。

es里寫的數據,實際上最終都會寫到磁碟文件里去,磁碟文件里的數據操作系統會自動將裡面的數據緩存到os cache裡面去。

因為es的搜索引擎嚴重依賴於底層的filesystem cache,所以如果給filesystem cache更多的記憶體,儘量讓記憶體可以容納所有的indxsegment file索引數據文件,那麼es搜索的時候就基本都是走記憶體的,性能會非常高。

很多測試和壓測可以看出性能差距巨大:如果走磁碟一般肯定超秒,搜索性能絕對是秒級別的,1秒,5秒,10秒。但如果是走filesystem cache,也就是是走純記憶體,那麼一般來說性能比走磁碟要高一個數量級,基本上就是毫秒級的,從幾毫秒到幾百毫秒不等。

案例:某個公司 es 節點有 3 台機器,每台機器看起來記憶體很多,64G,總記憶體就是 64 * 3 = 192G。每台機器給 es jvm heap 是 32G,那麼剩下來留給 filesystem cache 的就是每台機器才 32G,總共集群里給 filesystem cache 的就是 32 * 3 = 96G 記憶體。而此時,整個磁碟上索引數據文件,在 3 台機器上一共占用了 1T 的磁碟容量,es 數據量是 1T,那麼每台機器的數據量是 300G。這樣性能好嗎? filesystem cache 的記憶體才 100G,十分之一的數據可以放記憶體,其他的都在磁碟,然後你執行搜索操作,大部分操作都是走磁碟,性能肯定差。

歸根結底,讓 es 查詢性能很高,最佳實踐是讓es進程占用系統的記憶體儘量大,至少可以容納你的總數據量的一半。

根據我們自己的生產環境實踐經驗,最佳的情況下,是僅僅在 es 中就存少量的數據,就是只存用來搜索的那些索引,如果記憶體留給 filesystem cache 的是 100G,那麼就將索引數據量控制在 100G 以內,這樣的話,數據幾乎全部走記憶體來搜索,性能非常之高,一般可以在 1 秒以內。

比如說你現在有一行數據。id,name,age等 30 個欄位。實際需求只需要根據 id,name,age 這三個欄位來搜索數據,但是如果往 es 里寫入一行數據所有的欄位,就會導致 90% 的數據不用來搜索卻硬是占據了 es 機器上的 filesystem cache 的大部分空間,單條數據的數據量越大,就會導致 filesystem cahce 能緩存的數據就越少。

因此,寫入 es 中要用來檢索的少數幾個欄位就可以了,比如寫入 es 的是 id,name,age 這三個欄位,其他的欄位數據存在 mysql/hbase 里,我們一般是建議用 es + hbase的組合架構。

hbase 的特點是適用於海量數據的線上存儲,就是對 hbase 可以寫入海量數據,但是不要做複雜的搜索,做很簡單的一些根據 id 或者範圍進行查詢的這麼一個操作就可以了。從 es 中根據 name 和 age 去搜索,拿到的結果可能就 20 個 doc id,然後根據 doc id 到 hbase 里去查詢每個 doc id 對應的完整的數據,給查出來,再返回給前端。

寫入 es 的數據最好小於等於(略微大於一點也可) es 的 filesystem cache 的記憶體容量。粗略估算從 es 檢索可能需要耗時 20ms,然後再根據 es 返回的 id 去 hbase 里查詢,查 20 條數據,可能需要耗時 30ms,這樣每次查詢就是 50ms。而原來的一次查詢就是耗時 5~10s,這種性能提升是非常顯著的。

5.1 數據預熱

實際生產中還有一種情況:即使按照上述的方案去做了,因為物理設備的限制,導致 es 集群中每個機器寫入的數據量還是超過了 filesystem cache 一倍,比如有 60G 數據需要寫入一臺機器,結果 filesystem cache撐死只有 30G,那麼剩下的 30G 數據只存留在了磁碟上。

對於這種情況,最佳解決方案就是做數據預熱

舉例微博熱搜數據,自己搭建一個後臺系統,這個系統就是定時去搜索當前用戶的熱數據或者可能上熱搜的數據,提前刷到 filesystem cache 里去,之後用戶實際上來看這個熱數據的時候,他們就是直接從記憶體里搜索了。

電商業務也是如此,可以將用戶平時熱搜的商品,比如說新品上市手機或數位設備等熱搜商品數據提前在後臺搞個程式,每隔 1 分鐘自己主動訪問一次,刷到 filesystem cache 里去。

對於那些比較熱的、經常會有人訪問的數據,最好做一個專門的緩存預熱子系統,就是對熱數據每隔一段時間,就提前訪問一下,讓數據進入 filesystem cache 裡面去。這樣下次別人訪問的時候,性能一定會好很多。

5.3 冷熱分離

es 可以做類似於 mysql 的水平拆分,也就是將大量的訪問很少、頻率很低的數據,單獨寫一個索引,然後將訪問很頻繁的熱數據單獨寫一個索引。最好是將冷數據寫入一個索引中,然後熱數據寫入另外一個索引中,這樣可以確保熱數據在被預熱之後,儘量都讓它們留在 filesystem os cache 里,別讓冷數據給沖刷掉

假設有 6 台機器,2 個索引,一個放冷數據,一個放熱數據,每個索引 3 個 shard。3 台機器放熱數據 index,另外 3 台機器放冷數據 index。90%的用戶群體大量時間是在訪問熱數據 index,熱數據可能就占總數據量的 10%,由於這部分頻繁搜索的數據量很少,於是幾乎全都保留在 filesystem cache 裡面了,就可以確保熱數據的訪問性能是很高的。

對於冷數據而言,是在另一個 index 里的,跟熱數據 index 不在相同的機器上,兩種數據存放的機器之間不存在聯繫。如果有人訪問冷數據,可能大量數據是在磁碟上的,此時性能差點,因為冷數據本身就是很少人訪問(10%左右的用戶群體),這些用戶隨便怎麼搜索冷數據也不會把熱數據從記憶體中"擠出來"。

5.4 Document 模型設計

對於 MySQL,我們經常有一些複雜的關聯查詢。但在 es 里儘量不要用複雜的關聯查詢,一旦用了性能一般都不會太好。

document 模型設計是非常重要的,很多操作,不要在搜索的時候才想去執行各種複雜的亂七八糟的操作。es 能支持的操作就那麼多,不要考慮用 es 做一些它不好操作的事情。如果真的有那種操作,儘量在 document 模型設計的時候,寫入的時候就完成。另外對於一些太複雜的操作,比如 join/nested/parent-child 搜索都要儘量避免,性能都很差的。

因此,在搜索/查詢的時候,要執行一些業務強相關的特別複雜的操作:

  • 在寫入數據的之前就設計好模型,加幾個欄位,把處理好的數據寫入加的欄位裡面。

  • 用 Java 程式封裝的複雜關聯操作,es僅用來做搜索數據的操作。

5.5 分頁性能優化

5.5.1 分頁的坑

舉個例子,假如每頁是 10 條數據,用戶現在要查詢第 100 頁,es實際上是會把每個 shard 上存儲的前 1000 條數據都查到一個協調節點上,如果這個 index 有個 5 個 shard,那麼就有 5000 條數據,接著協調節點對這 5000 條數據進行一些合併、處理,再獲取到最終第 100 頁的 10 條數據。

分散式情況下,要查第 100 頁的 10 條數據,不可能是從 5 個 shard,每個 shard 就查 2 條數據,最後到協調節點合併成 10 條數據。

es必須得從每個 shard 都查 1000 條數據過來,然後根據用戶搜索的需求進行排序、篩選等等操作,最後再次分頁,拿到裡面第 100 頁的數據。當搜索翻頁的時候,翻的越深,每個 shard 返回的數據就越多,而且協調節點處理的時間越長,非常坑爹。所以用 es 做分頁的時候,明顯會發現越翻到後面越翻越慢。

實際開發測試中遇到過這個問題,用 es 作分頁,前幾頁就幾十毫秒,翻到 10 頁或者幾十頁的時候,基本上就要 5~10 秒才能查出來一頁數據了。

5.5.2 解決方案

1)不允許深度分頁(預設深度分頁性能很差)

一般公司要求,系統不允許翻很深的頁,因為預設翻的越深,性能就越差。

2)類似於 APP 里的推薦商品不斷下拉出來一頁一頁的

類似於微博中,下拉刷微博,刷出來一頁一頁的,你可以用 scroll api,關於如何使用,可自行搜索。

scroll 會一次性給你生成所有數據的一個快照,然後每次滑動向後翻頁就是通過游標 scroll_id 移動,獲取下一頁下一頁這樣子,性能會比上面說的那種分頁性能要高很多,基本上都是毫秒級的。

但唯一需要註意的是:這個適合於那種類似微博下拉翻頁的,不能隨意跳到任何一頁的場景

也就是說,用戶不能先進入第 10 頁,再跳到第 120 頁,然後又跳到第 58 頁,不能隨意亂跳頁。因此現在很多移動產品都是不允許用戶隨意翻頁,也有一些網站做的就是你只能往下拉,一頁一頁的翻。

初始化時必須指定 scroll 參數,告訴 es 要保存此次搜索的上下文多長時間。你需要確保用戶不會持續不斷翻頁翻幾個小時,否則可能因為超時而失敗。

除了用 scroll api,也可以用 search_after 來做,search_after 的思想是使用前一頁的結果來幫助檢索下一頁的數據,顯然,這種方式也不允許用戶隨意翻頁,用戶只能一頁頁往後翻。初始化時,需要使用一個唯一值的欄位作為 sort 欄位。

6. 生產部署

ES 生產集群的部署架構是什麼?每個索引的數據量大概有多少?每個索引大概有多少個分片?

這樣的問題不是技術能力問題,就是單純考察面試者是否在真正的生產環境里使用過es,屬於面試必問問題。

這裡有個基本版本部署配置:

  • es 生產集群,部署了 5 台機器,每台機器是 6 核 64G 的,集群總記憶體是 320G。
  • es 集群的日增量數據大概是 2000 萬條,每天日增量數據大概是 500MB,每月增量數據大概是 6 億,也就是15G。目前系統已經運行了幾個月,現在 es 集群里數據總量大概是 100G 左右。
  • 目前線上有 5 個索引(這個結合業務來看,看有哪些數據可以放 es 里),每個索引的數據量大概是 20G,所以這個數據量之內,每個索引分配的是 8 個 shard,比預設的 5 個 shard 多了 3 個 shard。

7. 更深的問題

分散式搜索技術如果挖深了可以問得極其深,比較嚴格的面試官可能會挖到es底層:相關度評分演算法(TF/IDF演算法)、deep paging、上千萬數據批處理、跨機房多集群同步、搜索效果優化等很多的實際生產問題。

分散式消息隊列也是如此:Kafka的主從複製的底層原理、leader選舉的演算法、增加partition以後的rebalance演算法,如何優化 Kafka 寫入的吞吐量。

TF/IDF演算法

比如要搜索"dota ti5",我希望第一個搜索的結果文檔叫做"dota2"而不是"英雄聯盟(LOL)",那麼lucene是如何做的呢?Lucene 預設的搜索演算法叫做TF/IDF 演算法。

TF-IDF演算法全稱為term frequency–inverse document frequency。TF就是term frequency的縮寫,意為詞頻。IDF則是inverse document frequency的縮寫,意為逆文檔頻率。該演算法在信息處理中通常用來抽取關鍵詞。

比如,對一個文章提取關鍵詞作為搜索詞,就可以採用TF-IDF演算法。

要找出一篇文章中的關鍵詞,通常的思路就是,就是找到出現次數最多的詞。如果某個詞很重要,它應該在這篇文章中多次出現。於是,我們進行"詞頻"(Term Frequency,縮寫為TF)統計。

但是通常,一篇中文的文章中,都會有很多沒有實際意義的詞,比如"的"、"是"、"了"等助詞類詞,稱為停用詞,稱它們為停用詞是因為在文本處理過程中如果遇到它們,則立即停止處理,將其扔掉。將這些詞扔掉減少了索引量,增加了檢索效率,並且通常都會提高檢索的效果。停用詞主要包括英文字元、數字、數學字元、標點符號及使用頻率特高的單漢字等。

當過濾掉所有的停用詞後,剩下的都是實際意義的詞,但也不能簡單的認為那個詞出現的次數多就是關鍵詞。比如在一篇如何組裝電腦的文章中,出現"CPU"、"主板"等關鍵詞和出現"說明書"的次數一樣多,但很顯然,"CPU"、"主板"等關鍵詞,更能確定這個文章的特性。也就是說,"CPU"、"主板"等關鍵詞比"說明書"這個關鍵詞更重要,需要排在前面。

所以我們就需要一個權重繫數,用來調整各個關鍵詞的重要性。如果一個詞很少見,但是它在某個文章中反覆出現多次,那麼可以認為這個詞反應了這個文章的特性,可以把它作為關鍵詞。在信息檢索中,這個權重非常重要,它決定了關鍵詞的重要度,這個權重叫做"逆文檔頻率"(Inverse Document Frequency,縮寫為IDF),它的大小與一個詞的常見程度成反比。

在知道了詞頻和權重之後,兩者相乘,就得到一個詞的TF-IDF值,某個詞對文章的重要性越高,它的TF-IDF值就越大。所以,排在最前面的幾個詞,就是這篇文章的關鍵詞。

因此TF-IDF演算法的主要工作就是計算出TF*IDF值最大的那幾個詞,作為文章的關鍵詞。

TF-IDF演算法的優點是簡單快速,結果比較符合實際情況。缺點是,單純以"詞頻"衡量一個詞的重要性,不夠全面,有時重要的詞可能出現次數並不多。而且,這種演算法無法體現詞的位置信息,出現位置靠前的詞與出現位置靠後的詞,都被視為重要性相同,這是不正確的。(一種解決方法是,對全文的第一段和每一段的第一句話,給予較大的權重。)

當通過TF-IDF演算法找出文章的關鍵字後,可以運用到一些具體的場景。比如:根據關鍵字找出相似的文章。

如果你認為lucene本身的演算法不夠好,那麼你可以考慮去實現其他的演算法。比如BM25和BM25F演算法。在lucene當中,如果你想更改原本的演算法,那麼你需要extends原來的Similarity類,然後將它配置到你的索引writer的配置中。如果你需要更好的索引。那麼你可能需要為lucene重寫很多代碼,包括權重包(Weight),Query包(查詢),Score包(打分)。

8. 擴展博文

一步一步跟我學習lucene

Lucene解析 - 基本概念

Elasticsearch技術研討

Lucene系列(一)快速入門

Elasticsearch權威指南

全文搜索引擎 Elasticsearch 入門教程

Elasticsearch 入門教程-bitiger知乎專欄

ElasticSearch入門教程-水晶命匣

深入詳解Elasticsearch

Elasticsearch深入理解

elasticsearch 核心知識篇

視頻教程:

【博學谷】搜索集大成者(lucene&solr&es)

https://www.bilibili.com/video/av45594844?from=search&seid=10490762564639410509

千鋒Java:ElasticSearch6入門教程

https://www.bilibili.com/video/av45558199?from=search&seid=10490762564639410509

龍果學院-es核心知識篇

https://www.bilibili.com/video/av28091206?from=search&seid=7590158700323939959


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...