先了解相關讀寫原理
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 強刷到磁盤上去。
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 秒后可以搜索到;可能會丟失數據的。有 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 之后,同時就建立好了倒排索引。
一個segment是一個完備的lucene倒排索引,而倒排索引是通過詞典 (Term Dictionary)到文檔列表(Postings List)的映射關系,快速做查詢的。由于詞典的size會很大,全部裝載到heap里不現實,因此Lucene為詞典做了一層前綴索引(Term Index),這個索引在Lucene4.0以后采用的數據結構是FST (Finite State Transducer)。這種數據結構占用空間很小,Lucene打開索引的時候將其全量裝載到內存中,加快磁盤上詞典查詢速度的同時減少隨機磁盤訪問次數。
每個segment都有會一些索引數據駐留在heap里。因此segment越多,瓜分掉的heap也越多,并且這部分heap是無法被GC掉的
刪除/更新數據底層原理
如果是刪除操作,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 。
通用建議
不要返回大結果集
Elasticsearch被設計為搜索引擎,使其非常適合檢索與查詢匹配的最前面的文檔。但是,對于屬于數據庫域的工作負載(如檢索匹配特定查詢的所有文檔),效果不佳。如果需要執行此操作,請確保使用Scroll API。
避免大文檔
假設默認的http.max_content_length設置為100MB,Elasticsearch將拒絕索引任何大于此值的文檔。您可能決定增加該特定設置,但是Lucene仍然有大約2GB的限制。
即使不考慮硬性限制,大型文檔通常也不實用。大型文檔給網絡,內存使用和磁盤帶來了更大的壓力,即使對于不要求_source的搜索請求也是如此,因為Elasticsearch在所有情況下都需要獲取文檔的_id,并且由于 文件系統緩存的工作方式,大型文檔獲取此字段的成本更高。索引此文檔使用的內存量是該文檔原始大小的數倍。近似搜索(例如phrase查詢)和高亮顯示開銷也變得更加昂貴,因為它們的成本直接取決于原始文檔的大小。
重新考慮信息單位有時是有用的。例如,您想使書籍可搜索,并不一定意味著文檔應包含整本書。最好將章節或段落用作文檔,這些文檔中具有一個屬性,以標識它們屬于哪本書。這不僅避免了大文檔的問題,還使搜索體驗更好。例如,如果用戶搜索foo和bar這兩個詞,則匹配的結果跨不同章節可能體驗非常差,而匹配結果落入同一段落中就可能很不錯。
秘密訣竅
混合精確搜索和提取詞干
在構建搜索應用程序時,通常必須使用詞干,比如對于“skiing”的查詢需要匹配包含“ ski”或“ skis”的文檔。但是,如果用戶想專門搜索“skiing”怎么辦?執行此操作的典型方法是使用 multi-field,以便以兩種不同的方式對相同的內容建立索引:
curl -X PUT "localhost:9200/index?pretty" -H 'Content-Type: application/json' -d'{ "settings": { "analysis": { "analyzer": { "english_exact": { "tokenizer": "standard", "filter": [ "lowercase" ] } } } }, "mappings": { "properties": { "body": { "type": "text", "analyzer": "english", "fields": { "exact": { "type": "text", "analyzer": "english_exact" } } } } }}'curl -X PUT "localhost:9200/index/_doc/1?pretty" -H 'Content-Type: application/json' -d'{ "body": "Ski resort"}'curl -X PUT "localhost:9200/index/_doc/2?pretty" -H 'Content-Type: application/json' -d'{ "body": "A pair of skis"}'curl -X POST "localhost:9200/index/_refresh?pretty"
兩個記錄都返回的搜索:
curl -X GET "localhost:9200/index/_search?pretty" -H 'Content-Type: application/json' -d'{ "query": { "simple_query_string": { "fields": [ "body" ], "query": "ski" } }}'
返回第一個記錄的搜索:
curl -X GET "localhost:9200/index/_search?pretty" -H 'Content-Type: application/json' -d'{"query": {"simple_query_string": {"fields": [ "body.exact" ],"query": "ski" } }}'
獲得一致的評分
當要獲得良好的評分功能時,Elasticsearch使用分片和副本進行操作會增加挑戰。
分數不可復制
假設同一位用戶連續兩次執行相同的請求,并且文檔兩次都沒有以相同的順序返回,這是非常糟糕的體驗,不是嗎?不幸的是,如果您有副本(index.number_of_replicas大于0),則可能會發生這種情況。原因是Elasticsearch以循環方式選擇查詢應訪問的分片,因此,如果您連續運行兩次相同的查詢,很有可能會訪問同一分片的不同副本。
現在為什么會出現問題?索引統計是分數的重要組成部分。而且由于刪除的文檔,同一分片的副本之間的索引統計可能會有所不同。您可能知道刪除或更新文檔時,不會立即將舊文檔從索引中刪除,而是將其標記為已刪除,并且僅在下次合并該舊文檔所屬的segment時才從磁盤中刪除它。但是,出于實際原因,這些已刪除的文檔將用于索引統計。因此,假設主分片剛剛完成了一個大型合并,刪除了許多已刪除的文檔,那么它的索引統計信息可能與副本(仍然有大量已刪除文檔)完全不同,因此得分也有所不同。
解決此問題的推薦方法是,使用一個標識所登入的用戶的字符串(例如,用戶ID或會話ID)作為首選項。這樣可以確保給定用戶的所有查詢始終會打到相同的分片,因此各查詢的得分更加一致。
解決此問題的另一個好處是:當兩個文檔的分數相同時,默認情況下將按其內部Lucene文檔ID(與_id無關)對它們進行排序。但是,這些doc ID在同一分片的副本之間可能會有所不同。因此,通過始終訪問相同的分片,得分相同的文檔更獲得一致的排序。
相關性看起來不對
如果您發現具有相同內容的兩個文檔獲得不同的分數,或者完全匹配的內容沒有排在第一位,則該問題可能與分片有關。默認情況下,Elasticsearch使每個分片負責產生自己的分數。但是,由于索引統計信息是得分的重要貢獻者,因此只有在分片具有相似的索引統計信息時,此方法才有效。假設是由于默認情況下文檔均勻地路由到分片,因此索引統計信息應該非常相似,并且評分將按預期進行。但是,如果您:
在寫入索引時路由,
查詢多個索引,
或索引中的數據太少
那么很有可能所有與搜索請求有關的分片都沒有相似的索引統計信息,并且相關性可能很差。
如果數據集較小,則解決此問題的最簡單方法是將所有內容編入具有單個分片(index.number_of_shards:1)的索引,這是默認設置。然后,所有文檔的索引統計信息都將相同,并且得分也將保持一致。
否則,解決此問題的推薦方法是使用dfs_query_then_fetch搜索類型。這將使Elasticsearch對所有涉及的分片執行初始往返,要求他們提供與查詢有關的索引統計信息,然后協調節點將合并這些統計信息,并在請求分片執行查詢階段時將合并的統計信息與請求一起發送,這樣分片就可以使用這些全局統計信息而不是它們自己的統計信息來進行評分。
在大多數情況下,這種額外的往返開銷應該很少。但是,如果您的查詢包含大量字段/term或模糊查詢,請注意,僅收集統計信息可能并不便利,因為必須在term詞典中查找所有term才能查找到統計信息。
將靜態相關性信號納入評分
許多域具有已知的與相關性相關的靜態信號。例如,PageRank和URL長度是Web搜索的兩個常用功能,以便獨立于查詢來調整網頁的分數。
有兩個主要查詢,可以將靜態分數貢獻與文本相關性結合起來,例如。用BM25計算得出: - script_score query - rank_feature query、
例如,假設您有一個希望與BM25得分結合使用的pagerank字段,以使最終得分等于score = bm25_score + pagerank /(10 + pagerank)。
使用script_score查詢,查詢將如下所示:
curl -X GET "localhost:9200/index/_search?pretty" -H 'Content-Type: application/json' -d'{"query": {"script_score": {"query": {"match": { "body": "elasticsearch" } },"script": {"source": "_score * saturation(doc[u0027pageranku0027].value, 10)" } } }}'
盡管這兩個選項都將返回相似的分數,但需要權衡取舍:script_score提供了很大的靈活性,使您可以根據需要將文本相關性分數與靜態信號結合起來。另一方面,rank_feature查詢僅提供了幾種將靜態信號混合到評分中的方法。但是,它依賴于rank_feature和rank_features字段,它們以一種特殊的方式索引值,從而使rank_feature查詢可以跳過非競爭性文檔并更快地獲得查詢的頂部匹配項。
寫入優化
加大translog flush間隔,目的是降低iops、writeblock。
從ES 2.x開始,在默認設置下,translog的持久化策略為:每個請求都“flush”。對應配置項如下:index.translog.durability: request
這是影響 ES 寫入速度的最大因素。但是只有這樣,寫操作才有可能是可靠的。如果系統可以接受一定概率的數據丟失(例如,數據寫入主分片成功,尚未復制到副分片時,主機斷電。由于數據既沒有刷到Lucene,translog也沒有刷盤,恢復時translog中沒有這個數據,數據丟失),則調整translog持久化策略為周期性和一定大小的時候“flush”,例如:index.translog.durability: async
設置為async表示translog的刷盤策略按sync_interval配置指定的時間周期進行。
index.translog.sync_interval: 120s
加大index refresh間隔,除了降低I/O,更重要的是降低了segment merge頻率。
每次索引的refresh會產生一個新的Lucene段,這會導致頻繁的segment merge行為使更改對搜索可見的操作(稱為刷新)非常昂貴,并且在正在進行索引活動的情況下經常進行調用會損害索引速度。
默認情況下,Elasticsearch會定期每秒刷新一次索引,但僅在最近30秒內已收到一個或多個搜索請求的索引上刷新。
如果您沒有或只有很少的搜索流量(例如,每5分鐘少于一個搜索請求)并且想要優化索引速度,則這是最佳配置。此行為旨在在不執行搜索時在默認情況下自動優化批量索引。為了選擇退出此行為,請顯式設置刷新間隔。
另一方面,如果您的索引遇到常規搜索請求,則此默認行為表示Elasticsearch將每1秒刷新一次索引。如果您有能力增加從索引到文檔可見之間的時間,則可以將index.refresh_interval增加到更大的值,例如30s,可能有助于提高索引速度。
調整bulk請求。
批量請求將比單文檔索引請求產生更好的性能。為了知道批量請求的最佳大小,您應該在具有單個分片的單節點上運行基準測試。首先嘗試一次索引100個文檔,然后索引200個,再索引400個,依此類推。在每次基準測試運行中,批量請求中的文檔數量加倍。當索引速度開始趨于平穩時,您便知道已達到批量請求數據的最佳大小。如果得分相同,寧可少也不要多。當大量請求同時發送時,請注意太大的批量請求可能會使集群處于內存壓力下,因此,建議即使每個請求看起來執行得更好,也要避免每個請求超過幾十兆字節。
如果 CPU 沒有壓滿,則應該提高寫入端的并發數量。但是要注意 bulk線程池隊列的reject情況,出現reject代表ES的bulk隊列已滿,客戶端請求被拒絕,此時客戶端會收到429錯誤(TOO_MANY_REQUESTS),客戶端對此的處理策略應該是延遲重試。不可忽略這個異常,否則寫入系統的數據會少于預期。即使客戶端正確處理了429錯誤,我們仍然應該盡量避免產生reject。因此,在評估極限的寫入能力時,客戶端的極限寫入并發量應該控制在不產生reject前提下的最大值為宜。
bulk線程池和隊列
建立索引的過程屬于計算密集型任務,應該使用固定大小的線程池配置,來不及處理的任務放入隊列。線程池最大線程數量應配置為CPU核心數+1,這也是bulk線程池的默認設置,可以避免過多的上下文切換。隊列大小可以適當增加,但一定要嚴格控制大小,過大的隊列導致較高的GC壓力,并可能導致FGC頻繁發生。
升級硬件
如果索引是受I / O約束的,則應研究為文件系統高速緩存提供更多內存(請參見上文)或購買速度更快的驅動器。特別是,已知SSD驅動器的性能要比旋轉磁盤好。始終使用本地存儲,應避免使用NFS或SMB等遠程文件系統。還請注意虛擬存儲,例如Amazon的Elastic Block Storage。虛擬存儲在Elasticsearch上可以很好地工作,并且很有吸引力,因為它安裝起來如此之快且簡單,但是與專用本地存儲相比,它在本質上在持續運行方面還很慢。如果在EBS上建立索引,請確保使用預配置的IOPS,否則操作可能會很快受到限制。
通過配置RAID 0陣列,跨多個SSD劃分索引。請記住,由于任何一個SSD的故障都會破壞索引,因此會增加故障的風險。但是,通常這是一個正確的權衡:優化單個分片以實現最佳性能,然后在不同節點之間添加副本,以便為任何節點故障提供冗余。您還可以使用快照和還原來備份索引以提供進一步保障。
優化磁盤間的任務均勻情況,將shard盡量均勻分布到物理主機的各個磁盤。
如果部署方案是為path.data配置多個路徑來使用多塊磁盤,則ES在分配shard時,落到各磁盤上的 shard 可能并不均勻,這種不均勻可能會導致某些磁盤繁忙,利用率在較長時間內持續達到100%。這種不均勻達到一定程度會對寫入性能產生負面影響。ES在處理多路徑時,優先將shard分配到可用空間百分比最多的磁盤上,因此短時間內創建的shard可能被集中分配到這個磁盤上,即使可用空間是99%和98%的差別。后來ES在2.x版本中開始解決這個問題:預估一下shard 會使用的空間,從磁盤可用空間中減去這部分,直到現在6.x版也是這種處理方式。但是實現也存在一些問題:
從可用空間減去預估大小這種機制只存在于一次索引創建的過程中,下一次的索引創建,磁盤可用空間并不是上次做完減法以后的結果。這也可以理解,畢竟預估是不準的,一直減下去空間很快就減沒了。但是最終的效果是,這種機制并沒有從根本上解決問題,即使沒有完美的解決方案,這種機制的效果也不夠好。如果單一的機制不能解決所有的場景,那么至少應該為不同場景準備多種選擇。為此,我們為ES增加了兩種策略。·
簡單輪詢:在系統初始階段,簡單輪詢的效果是最均勻的。·
基于可用空間的動態加權輪詢:以可用空間作為權重,在磁盤之間加權輪詢。
您應該通過禁用交換來確保操作系統不會交換出Java進程。
大多數操作系統嘗試為文件系統高速緩存使用盡可能多的內存,并急切換出未使用的應用程序內存。這可能會導致部分JVM堆甚至其可執行頁面換出到磁盤上。
交換對性能,節點穩定性非常不利,應不惜一切代價避免交換。它可能導致垃圾收集持續數分鐘而不是毫秒,并且可能導致節點響應緩慢甚至斷開與集群的連接。在彈性分布式系統中,讓操作系統殺死該節點更為有效。
文件系統緩存將用于緩沖I / O操作。您應確保至少將運行Elasticsearch的計算機的一半內存分配給文件系統緩存。
-
性能優化
+關注
關注
0文章
18瀏覽量
7429 -
Elasticsearch
+關注
關注
0文章
28瀏覽量
2827
發布評論請先 登錄
相關推薦
評論