從原理到應用,Elasticsearch詳解(下)

上篇閱讀:juejin.im/post/5ce660…html


原始文檔存儲(行式存儲)

fdt文件

文檔內容的物理存儲文件,由多個chunk組成,Lucene索引文檔時,先緩存文檔,緩存大於16KB時,就會把文檔壓縮存儲。
node

fdx文件

文檔內容的位置索引,由多個block組成:mysql

  • 1024個chunk歸爲一個blockgit

  • block記錄chunk的起始文檔ID,以及chunk在fdt中的位置
    github

fnm文件

文檔元數據信息,包括文檔字段的名稱、類型、數量等。算法

原始文檔的查詢


注意問題:lucene對原始文件的存放是行式存儲,而且爲了提升空間利用率,是多文檔一塊兒壓縮,所以取文檔時須要讀入和解壓額外文檔,所以取文檔過程很是依賴CPU以及隨機IO。sql

相關設置

壓縮方式的設置json

原始文檔的存儲對應_source字段,是默認開啓的,會佔用大量的磁盤空間,上面提到的chunk中的文檔壓縮,ES默認採用的是LZ4,若是想要提升壓縮率,能夠將設置改爲best_compression。bootstrap

index.codec: best_compression複製代碼

特定字段的內容存儲api

查詢的時候,若是想要獲取原始字段,須要在_source中獲取,由於全部的字段存儲在一塊兒,因此獲取完整的文檔內容與獲取其中某個字段,在資源消耗上幾乎相同,只是返回給客戶端的時候,減小了必定量的網絡IO。

ES提供了特定字段內容存儲的設置,在設置mappings的時候能夠開啓,默認是false。若是你的文檔內容很大,而其中某個字段的內容有須要常常獲取,能夠設置開啓,將該字段的內容單獨存儲。

PUT my_index{  "mappings": {    "_doc": {      "properties": {        "title": {          "type": "text",          "store": true         }      }    }  }}複製代碼


倒排索引

倒排索引中記錄的信息主要有:

  • 文檔編號:segment內部文檔編號從0開始,最大值爲int最大值,文檔寫入以後會分配這樣一個順序號

  • 字典:字段內容通過分詞、歸一化、還原詞根等操做以後,獲得的全部單詞

  • 單詞出現位置:

    ▫分詞字段默認開啓,提供對於短語查詢的支持

    ▫對於很是常見的詞,例如the,位置信息可能佔用很大空間,短語查詢須要讀取的數據量很大,查詢速度慢

  • 單詞出現次數:單詞在文檔中出現的次數,做爲評分的依據

  • 單詞結束字符到開始字符的偏移量:記錄在文檔中開始與結束字符的偏移量,提供高亮使用,默認是禁用的

  • 規範因子:對字段長度進行規範化的因子,給予較短字段更多權重

倒排索引的查找過程本質上是經過單詞找對應的文檔列表的過程,所以倒排索引中字典的設計決定了倒排索引的查詢速度,字典主要包括前綴索引(.tip文件)和後綴索引(.tim)文件。

字典前綴索引(.tip文件)

一個合格的詞典結構通常有如下特色:

-查詢速度快 -內存佔用小 -內存+磁盤相結合

Lucene採用的前綴索引數據結構爲FST,它的優勢有:

詞查找複雜度爲O(len(str))

  • 共享前綴、節省空間、內存佔用率低,壓縮率高,模糊查詢支持好

  • 內存存放前綴索引,磁盤存放後綴詞塊

  • 缺點:結構複雜、輸入要求有序、更新不易

字典後綴(.tim文件)

後綴詞塊主要保存了單詞後綴,以及對應的文檔列表的位置。

文檔列表(.doc文件)

lucene對文檔列表存儲進行了很好的壓縮,來保證緩存友好:

  • 差分壓縮:每一個ID只記錄跟前面的ID的差值

  • 每256個ID放入一個block中

  • block的頭信息存放block中每一個ID佔用的bit位數,由於通過上面的差分壓縮以後,文檔列表中的文檔ID都變得不大,佔用的bit位數變少

上圖通過壓縮以後將6個數字從原先的24bytes壓縮到7bytes。

文檔列表的合併

ES的一個重要的查詢場景是bool查詢,相似於mysql中的and操做,須要將兩個條件對應的文檔列表進行合併。爲了加快文檔列表的合併,lucene底層採用了跳錶的數據結構,合併過程當中,優先遍歷較短的鏈表,去較長的列表中進行查詢,若是存在,則該文檔符合條件。


倒排索引的查詢過程

  • 內存加載tip文件,經過FST匹配前綴找到後綴詞塊位置

  • 根據詞塊位置,讀取磁盤中tim文件中後綴塊並找到後綴和相應的倒排表位置信息

  • 根據倒排表位置去doc文件中加載倒排表

  • 藉助跳錶結構,對多個文檔列表進行合併

filter查詢的緩存

對於filter過濾查詢的結果,ES會進行緩存,緩存採用的數據結構是RoaringBitmap,在match查詢中配合filter能有效加快查詢速度。

  • 普通bitset的缺點:內存佔用大,RoaringBitmap有很好的壓縮特性

  • 分桶:解決文檔列表稀疏的狀況下,過多的0佔用內存,每65536個docid分到一個桶,桶內只記錄docid%65536

  • 桶內壓縮:4096做爲分界點,小余這個值用short數組,大於這個值用bitset,每一個short佔兩字節,4096個short佔用65536bit,因此超過4096個文檔id以後,是bitset更節省空間。


DocValues(正排索引&列式存儲)

倒排索引保存的是詞項到文檔的映射,也就是詞項存在於哪些文檔中,DocValues保存的是文檔到詞項的映射,也就是文檔中有哪些詞項。

相關設置

keyword字段默認開啓

ES6.0(lucene7.0)以前

DocValues採用的數據結構是bitset,bitset對於稀疏數據的支持很差:

  • 對於稀疏的字段來講,絕大部分的空間都被0填充,浪費空間

  • 因爲字段的值之間插入了0,可能原本連續的值被0間隔開來了,不利於數據的壓縮

  • 空間被一堆0佔用了,緩存中緩存的有效數據減小,查詢效率也會下降

查詢邏輯很簡單,相似於數組經過下標進行索引,由於每一個value都是固定長度,因此讀取文檔id爲N的value直接從N*固定長度位置開始讀取固定長度便可。

ES6.0(lucene7.0)

  • docid的存儲的經過分片加快映射到value的查詢速度

  • value存儲的時候再也不給空的值分配空間

由於value存儲的時候,空值再也不分配空間,因此查詢的時候不能經過上述經過文檔id直接映射到在bitset中的偏移量來獲取對應的value,須要經過獲取docid的位置來找到對應的value的位置。

因此對於DocValues的查找,關鍵在於DocIDSet中ID的查找,若是按照簡單的鏈表的查找邏輯,那麼DocID的查找速度將會很慢。lucene7借用了RoaringBitmap的分片的思想來加快DocIDSet的查找速度:

  • 分片容量爲2的16次方,最多能夠存儲65536個docid

  • 分片包含的信息:

    ▫分片ID

    ▫存儲的docid的個數(值不爲空的DocIDSet)

    ▫DocIDSet明細,或者標記分片類型(ALL或者NONE)

  • 根據分片的容量,將分片分爲四種不一樣的類型,不一樣類型的查找邏輯不通:

    ▫ALL:該分片內沒有不存在值的DocID

    ▫NONE:該分片內全部的DocID都不存在值

    ▫SPARSE:該分片內存在值的DocID的個數不超過4096,DocIDSet以short數組的形式存儲,查找的時候,遍歷數組,找到對應的ID的位置

    ▫DENSE:該分片內存在值的DocID的個數超過4096,DocIDSet以bitset的形式存儲,ID的偏移量也就是在該分片中的位置

最終DocIDSet的查找邏輯爲:

  • 計算DocID/65536,獲得所在的分片N

  • 計算前面N-1個分片的DocID的總數

  • 找到DocID在分片N內部的位置,從而找到所在位置以前的DocID個數M

  • 找到N+M位置的value即爲該DocID對應的value


數據查詢

查詢過程(query then fetch)

  • 協調節點將請求發送給對應分片

  • 分片查詢,返回from+size數量的文檔對應的id以及每一個id的得分

  • 彙總全部節點的結果,按照得分獲取指定區間的文檔id

  • 根據查詢需求,像對應分片發送多個get請求,獲取文檔的信息

  • 返回給客戶端

get查詢更快

默認根據id對文檔進行路由,因此指定id的查詢能夠定位到文檔所在的分片,只對某個分片進行查詢便可。固然非get查詢,只要寫入和查詢的時候指定routing,一樣能夠達到該效果。

主分片與副本分片

ES的分片有主備之分,可是對於查詢來講,主備分片的地位徹底相同,平等的接收查詢請求。這裏涉及到一個請求的負載均衡策略,6.0以前採用的是輪詢的策略,可是這種策略存在不足,輪詢方案只能保證查詢數據量在主備分片是均衡的,可是不能保證查詢壓力在主備分片上是均衡的,可能出現慢查詢都路由到了主分片上,致使主分片所在的機器壓力過大,影響了整個集羣對外提供服務的能力。

新版本中優化了該策略,採用了基於負載的請求路由,基於隊列的耗費時間自動調節隊列長度,負載高的節點的隊列長度將減小,讓其餘節點分攤更多的壓力,搜索和索引都將基於這種機制。

get查詢的實時性

ES數據寫入以後,要通過一個refresh操做以後,纔可以建立索引,進行查詢。可是get查詢很特殊,數據實時可查。

ES5.0以前translog能夠提供實時的CRUD,get查詢會首先檢查translog中有沒有最新的修改,而後再嘗試去segment中對id進行查找。5.0以後,爲了減小translog設計的負責性以便於再其餘更重要的方面對translog進行優化,因此取消了translog的實時查詢功能。

get查詢的實時性,經過每次get查詢的時候,若是發現該id還在內存中沒有建立索引,那麼首先會觸發refresh操做,來讓id可查。

查詢方式

兩種查詢上下文:

  • query:例如全文檢索,返回的是文檔匹配搜索條件的相關性,經常使用api:match

  • filter:例如時間區間的限定,回答的是是否,要麼是,要麼不是,不存在類似程度的概念,經常使用api:term、range

過濾(filter)的目標是減小那些須要進行評分查詢(scoring queries)的文檔數量。

分析器(analyzer)

當索引一個文檔時,它的全文域被分析成詞條以用來建立倒排索引。當進行分詞字段的搜索的時候,一樣須要將查詢字符串經過相同的分析過程,以保證搜索的詞條格式與索引中的詞條格式一致。當查詢一個不分詞字段時,不會分析查詢字符串,而是搜索指定的精確值。

能夠經過下面的命令查看分詞結果:

GET /_analyze{  "analyzer": "standard",  "text": "Text to analyze"}複製代碼

相關性

默認狀況下,返回結果是按相關性倒序排列的。每一個文檔都有相關性評分,用一個正浮點數字段score來表示。score的評分越高,相關性越高。

ES的類似度算法被定義爲檢索詞頻率/反向文檔頻率(TF/IDF),包括如下內容:

  • 檢索詞頻率:檢索詞在該字段出現的頻率,出現頻率越高,相關性也越高。字段中出現過5次要比只出現過1次的相關性高。

  • 反向文檔頻率:每一個檢索詞在索引中出現的頻率,頻率越高,相關性越低。檢索詞出如今多數文檔中會比出如今少數文檔中的權重更低。

  • 字段長度準則:字段的長度是多少,長度越長,相關性越低。 檢索詞出如今一個短的title要比一樣的詞出如今一個長的content字段權重更大。

查詢的時候能夠經過添加?explain參數,查看上述各個算法的評分結果。


ES在Ad Tracking的應用

日誌查詢工具

TalkingData 移動廣告監測產品Ad Tracking(簡稱ADT)的系統會接收媒體發過來的點擊數據以及SDK發過來的激活和各類效果點數據,這些數據的處理過程正確與否相當重要。例如,設備的一條激活數據爲啥沒有歸因到點擊,這類問題的排查在Ad Tracking中很常見,經過將數據流中的各個處理環節的重要日誌統一發送到ES,能夠很方便的進行查詢,技術支持的同事能夠經過拼寫簡單的查詢條件排查客戶的問題。

  • 索引按天建立:定時關閉歷史索引,釋放集羣資源

  • 別名查詢:數據量增大以後,能夠經過拆分索引減輕寫入壓力,拆分以後的索引採用相同的別名,查詢服務不須要修改代碼

  • 索引重要的設置:

{    "settings": {        "index": {            "refresh_interval": "120s",            "number_of_shards": "12",            "translog": {                "flush_threshold_size": "2048mb"            },            "merge": {                "scheduler": {                    "max_thread_count": "1"                }            },            "unassigned": {                "node_left": {                    "delayed_timeout": "180m"                }            }        }    }}複製代碼
  • 索引mapping的設置

{    "properties": {        "action_content": {            "type": "string",            "analyzer": "standard"        },        "time": {            "type": "long"        },        "trackid": {            "type": "string",            "index": "not_analyzed"        }    }}複製代碼
  • sql插件,經過拼sql的方式,比起拼json更簡單

點擊數據存儲(kv存儲場景)

Ad Tracking收集的點擊數據是與廣告投放直接相關的數據,應用安裝以後,SDK會上報激活事件,系統會去查找這個激活事件是否來自於以前用戶點擊的某個廣告,若是是,那麼該激活就是一個推廣量,也就是投放的廣告帶來的激活。激活後續的效果點數據也都會去查找點擊,從點擊中獲取廣告投放的一些信息,因此點擊查詢在Ad Tracking的業務中相當重要。

業務的前期,點擊數據是存儲在Mysql中的,隨着後續點擊量的暴增,因爲Mysql不能橫向擴展,因此須要更換爲新的存儲。因爲ES擁有橫向擴展和強悍的搜索能力,而且以前日誌查詢工具中也一直使用ES,因此決定使用ES來進行點擊的存儲。

重要的設置

  • "refresh_interval": "1s"

  • "translog.flush_threshold_size": "2048mb"

  • "merge.scheduler.max_thread_count": 1

  • "unassigned.node_left.delayed_timeout": "180m"

結合業務進行系統優化

結合業務按期關閉索引釋放資源:Ad Tracking的點擊數據具備有效期的概念,超過有效期的點擊,激活不會去歸因。點擊有效期最長一個月,因此理論上天天建立的索引在一個月以後才能關閉。可是用戶配置的點擊有效期大部分都是一天,這大部分點擊在集羣中保存30天是沒有意義的,並且會佔用大部分的系統資源。因此根據點擊的這個業務特色,將天天建立的索引拆分紅兩個,一個是有效期是一天的點擊,一個是超過一天的點擊,有效期一天的點擊的索引在一天以後就能夠關閉,從而保證集羣中打開的索引的數據量維持在一個較少的水平。

結合業務將熱點數據單獨索引:激活和效果點數據都須要去ES中查詢點擊,可是二者對於點擊的查詢場景是有差別的,由於效果點事件(例如登陸、註冊等)歸因的時候不是去直接查找點擊,而是查找激活進而找到點擊,效果點要找的點擊必定是以前激活歸因到的,因此激活歸因到的這部分點擊也就是熱點數據。激活歸因到點擊以後,將這部分點擊單獨存儲到單獨的索引中,因爲這部分點擊量少不少,因此效果點查詢的時候很快。

索引拆分:Ad Tracking的點擊數據按天進行存儲,可是隨着點擊量的增大,單天的索引大小持續增大,尤爲是晚上的時候,merge須要合併的segment數量以及大小都很大,形成了很高的IO壓力,致使集羣的寫入受限。後續採用了拆分索引的方案,天天的索引按照上午9點和下午5點兩個時間點將索引拆分紅三個,因爲索引之間的segment合併是相互獨立的,只會在一個索引內部進行segment的合併,因此在每一個小索引內部,segment合併的壓力就會減小。

其餘調優

分片的數量

經驗值:

  • 每一個節點的分片數量保持在低於每1GB堆內存對應集羣的分片在20-25之間。

  • 分片大小爲50GB一般被界定爲適用於各類用例的限制。

JVM設置

  • 堆內存設置:不要超過32G,在Java中,對象實例都分配在堆上,並經過一個指針進行引用。對於64位操做系統而言,默認使用64位指針,指針自己對於空間的佔用很大,Java使用一個叫做內存指針壓縮(compressed oops)的技術來解決這個問題,簡單理解,使用32位指針也能夠對對象進行引用,可是一旦堆內存超過32G,這個壓縮技術再也不生效,實際上失去了更多的內存。

  • 預留一半內存空間給lucene用,lucene會使用大量的堆外內存空間。

  • 若是你有一臺128G的機器,一半內存也是64G,超過了32G,能夠經過一臺機器上啓動多個ES實例來保證ES的堆內存小於32G。

  • ES的配置文件中加入bootstrap.mlockall: true,關閉內存交換。

經過_cat api獲取任務執行狀況

GET http://localhost:9201/_cat/thread_pool?v&h=host,search.active,search.rejected,search.completed

  • 完成(completed)

  • 進行中(active)

  • 被拒絕(rejected):須要特別注意,說明已經出現查詢請求被拒絕的狀況,多是線程池大小配置的過小,也多是集羣性能瓶頸,須要擴容。

小技巧

  • 重建索引或者批量想ES寫歷史數據的時候,寫以前先關閉副本,寫入完成以後,再開啓副本。

  • ES默認用文檔id進行路由,因此經過文檔id進行查詢會更快,由於能直接定位到文檔所在的分片,不然須要查詢全部的分片。

  • 使用ES本身生成的文檔id寫入更快,由於ES不須要驗證一次自定義的文檔id是否存在。

參考資料

https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html

https://github.com/Neway6655/neway6655.github.com/blob/master/_posts/2015-09-11-elasticsearch-study-notes.md

https://www.elastic.co/blog/frame-of-reference-and-roaring-bitmaps

https://blog.csdn.net/zteny/article/details/85245967

https://www.elastic.co/blog/minimize-index-storage-size-elasticsearch-6-0


做者:TalkingData戰鵬弘

相關文章
相關標籤/搜索