ES索引的最后一公里,減少最后的延遲

我經(jīng)常被問(wèn)到這樣的問(wèn)題:ES最大能撐多少Q(mào)PS ?
確實(shí)這比證明我爸是我爸的問(wèn)題更難解釋清楚,不過(guò)我通常會(huì)給出兩個(gè)選擇;1)你是否希望盡量高QPS而不需要管最大latency甚至是總體latency升的很高? 2)你是否希望最大的latency 盡量的低,甚至不允許發(fā)生大于XXX的latency?因此這個(gè)回答需要你對(duì)你需要實(shí)現(xiàn)的業(yè)務(wù)熟悉。

大促在即,最近幾個(gè)索引被盯上了,因?yàn)槌霈F(xiàn)了類(lèi)似99.9% 百分位延遲很大的問(wèn)題,大家都知道,在電商大并發(fā)的系統(tǒng)里,任何的延遲抖動(dòng)最后可能都會(huì)導(dǎo)致非??植赖谋浪?yīng),因此最近兩周都在仔細(xì)地琢磨這個(gè)問(wèn)題,想盡力去解決。

話(huà)不多說(shuō),出正題,當(dāng)前我們的索引遇到了下面的問(wèn)題:

  1. 索引A,百萬(wàn)級(jí),用terms + Time range + aggs, 偶爾延遲抖動(dòng)很厲害,最大延遲去到1s
  2. 索引B,億級(jí)別,只對(duì)id 做terms 查詢(xún),平均延遲幾ms,但是99.9% 百分位延遲有500ms+,重點(diǎn)調(diào)優(yōu)對(duì)象
  3. 這幾個(gè)索引的indxing rate 在幾百上千不等,在優(yōu)化這幾個(gè)索引期間,用定時(shí)任務(wù)做force merge,但是發(fā)現(xiàn)做force merge時(shí)總體延遲非常高,需要分析為什么會(huì)這樣。

問(wèn)題一:range, range 還是range 惹的禍

第一個(gè)問(wèn)題,由于terms 只會(huì)過(guò)濾出非常少量的結(jié)果,因此我猜測(cè)aggs 是非常穩(wěn)定的,首先排除,最大嫌疑肯定就是這個(gè)Time range 了,因此解決辦法也非常迅速,因?yàn)閠erms 結(jié)果集很少,那么就直接把Time 的range 放一個(gè)script在內(nèi)存算好了, 結(jié)果當(dāng)然是,延遲抖動(dòng)沒(méi)有了!
之前的文章也曾經(jīng)介紹過(guò),對(duì)于不做數(shù)學(xué)運(yùn)算,不做聚合的數(shù)字類(lèi)型應(yīng)該用keyword 來(lái)建索引,但是像類(lèi)似時(shí)間這種類(lèi)型,其實(shí)本質(zhì)上還是用了long來(lái)存,所以對(duì)它做range時(shí),之前說(shuō)的那些Elasticsearch 5.x 源碼分析(12)對(duì)類(lèi)似枚舉數(shù)據(jù)的搜索異常慢的一種猜測(cè) 問(wèn)題都還是會(huì)有,本質(zhì)都是用了Lucene 新的 Block K-d Tree 的數(shù)據(jù)結(jié)構(gòu)引起的,因此如果你做的range結(jié)果集非常大的話(huà)就還是有問(wèn)題,避免的方式無(wú)非就是減少這些bucket的數(shù)量,比如采用秒級(jí)時(shí)間戳落盤(pán),而不是毫秒,這樣查詢(xún)速度提升還是很明顯的。

那你應(yīng)該會(huì)問(wèn)了,新版本ES不是修了這個(gè)issue了么,是的,在新的Lucene 里這個(gè)查詢(xún)被一個(gè)IndexOrDocValuesQuery ("Points + Doc values") 包了一層,如果Lucene的新solution 還不清楚的再次翻一下上面的這個(gè)連接,下面我們從性能的角度再看這個(gè)問(wèn)題。


https://www.elastic.co/blog/better-query-planning-for-range-queries-in-elasticsearch

這張圖是從ES的一篇博客中截取,綠色的線(xiàn)指的是如果你的terms查詢(xún)總是召回0.1% 的結(jié)果集,那么這個(gè)查詢(xún)的延遲 一般是很穩(wěn)定的。紫色的線(xiàn)就代表這個(gè)range 查詢(xún)隨著range 的結(jié)果集增大而延遲增大,這個(gè)很容易理解,那么再看這個(gè)藍(lán)色的線(xiàn),也就是Lucene的這個(gè)solution,它的意思就是在range 的結(jié)果集在0.1%以?xún)?nèi)時(shí),則還是走了這個(gè)field的索引,并且把取得的id集合和terms的id集合做conjunction處理,但是如果range 的結(jié)果集大于0.1%時(shí),怎放棄走range field的索引,而是直接在terms的結(jié)果集基礎(chǔ)上逐個(gè)對(duì)doc Value 進(jìn)行判斷。

上面這個(gè)圖當(dāng)然是個(gè)極度理想化的圖,因?yàn)檫@個(gè)IndexOrDocValuesQuery永遠(yuǎn)都是得到一個(gè)最低延遲的查詢(xún),因此實(shí)際情況很可能是下面這個(gè)圖


首先我們不可能每次的結(jié)果集占比都是非常穩(wěn)定,IndexOrDocValuesQuery是個(gè)只智能分配的過(guò)程,比如如果這個(gè)閾值取1%,而range在這個(gè)1%結(jié)果集的延遲還是低于對(duì)terms的1%結(jié)果集做doc Value查詢(xún)時(shí),那么從藍(lán)線(xiàn)得出,我們這次的tradeoff 是虧了的。但我們?nèi)匀挥X(jué)得這個(gè)交易還是劃得來(lái)的,因?yàn)槲覀冎粨p失了毫秒級(jí)而已。

這個(gè)問(wèn)題我在ES5.5 發(fā)現(xiàn)已經(jīng)修復(fù)了,那么為什么還是會(huì)帶來(lái)這么厲害的延遲抖動(dòng)呢,問(wèn)題就在于,range 的索引不是常駐內(nèi)存的。也就是說(shuō)上面的紫線(xiàn)的結(jié)果,在高并發(fā)或者甚至地并發(fā)隨著時(shí)間的推移,它總會(huì)被GC掉!因此重新取這個(gè)cache是無(wú)法避免的。
所以如果你想盡量減少這個(gè)開(kāi)銷(xiāo),那么你只能把Query cache調(diào)大來(lái)緩解,而最致命的是下面這個(gè)Lucene的issue

Cache costly subqueries asynchronously

如果你把range查詢(xún)放在filter里,那么Lucene總是希望嘗試去cache這個(gè)查詢(xún),因此,如果cache丟失了,它下次又會(huì)嘗試去查詢(xún)并且cache住。這個(gè)問(wèn)題至少在Lucene 7.2 還是沒(méi)有很好解決。

問(wèn)題一解決辦法:
對(duì)于你能判斷的做完terms 后的結(jié)果集是恒定并且很少的話(huà),盡量避免掉對(duì)大結(jié)果集的字段做range查詢(xún),放在內(nèi)存做是個(gè)非常不錯(cuò)的選擇。


問(wèn)題二:如果想低延遲,盡量把整個(gè)index cache住

這個(gè)問(wèn)題我們從頭到尾再捋一捋

  • 機(jī)器 24C/64G/800G, HEAP 30G
  • index 130G+/3shards/ 50G一個(gè)分片
  • Query: id terms + time field doc values

下面是其中的一個(gè)查詢(xún)例子:

{
    "size": 50,
    "query": {
      "bool": {
        "filter": [
          {
            "terms": {
              "_id": [
                11111111111,
                22222222222,
                33333333333,
                 ...
              ]
            }
          },
          {
            "script": {
              "script": {
                "lang": "painless",
                "params": {
                  "now": 1523849100000
                },
                "inline": "def now = params.now; if (now == null) { def d = new Date(); now = d.getTime(); } now+=28800000; return doc['sell_time'].value > now"
              }
            }
          }
        ]
      }
    }
}

乍一看大家都傻眼了,可以說(shuō)這就是一個(gè)簡(jiǎn)單的get id的操作,老實(shí)說(shuō)Redis可能輕松就上幾W QPS了,這個(gè)ES最長(zhǎng)延遲竟然去到500ms+ 甚至有1s的,都不太好意思去交代。

但是只要仔細(xì)去分析,還是挺容易發(fā)現(xiàn)問(wèn)題的,我們用Lucene的語(yǔ)言去解讀這個(gè)查詢(xún):

  • _id terms , 我們的id字段,為了盡量避免對(duì)long 做term,我們嘗試把 id改成 _id,這個(gè)過(guò)程會(huì)讀取 Lucene FST 前綴表 (常駐HEAP),后綴表(不常駐HEAP,但是理論應(yīng)該常駐OS cache),找到一堆id集合

Notice:
這里忘了說(shuō)經(jīng)一下就是我們之前的業(yè)務(wù)的id字段都是用long值和類(lèi)型來(lái)保存,但是根據(jù)經(jīng)驗(yàn)來(lái)看,如果不是id用一些非常復(fù)雜的邏輯拼湊的話(huà)應(yīng)該還是不用去太多顧慮這個(gè)的加載問(wèn)題,(我不過(guò)我們確實(shí)碰到過(guò)一些業(yè)務(wù)方,比如財(cái)務(wù),由于是hive庫(kù)表,確實(shí)唯一id字段,都會(huì)用非常復(fù)雜的拼湊邏輯來(lái)拼湊一個(gè)字符串來(lái)作為id,這種的話(huà)id 的查找確實(shí)比較蛋疼),由于一般的id都是有序增加,就算是用number保存查詢(xún)也是比較快的

  • 對(duì)這些id集合逐個(gè)讀取sell_time的 doc Value 表 (不常駐 HEAP, 如果很大可能會(huì)有OS cache miss)
  • 最后拿到最終的id結(jié)果集,去正向 .fdt .fnm .fdx表中去撈數(shù)據(jù) (不常住HEAP,如果很大可能會(huì)有OS cache miss)

接下來(lái)逐個(gè)去分析,先看FST表,Lucene的FST表大致是一下類(lèi)似下面這樣的結(jié)構(gòu)圖


摘自http://blog.csdn.net/ronalod

這里簡(jiǎn)單介紹兩句,Lucene對(duì)有terms 倒排表,是分開(kāi)前綴表和后綴表兩部分組成,為了不會(huì)撐爆HEAP,Lucene會(huì)智能推算出一個(gè)前綴表來(lái)常駐HEAP,比如我們做一個(gè) term :abcd, 那么其實(shí)會(huì)先查找FST表,找到ab,或者abc, 然后再?gòu)暮缶Y表找到abcd,進(jìn)而在doc 里查到倒排索引表,如果從數(shù)據(jù)結(jié)構(gòu)來(lái)看,大致入下圖

摘自http://blog.csdn.net/ronalod

根據(jù)熱數(shù)據(jù)特性,計(jì)算是tim文件不是常駐 HEAP,那么其實(shí)原則上它會(huì)失效的可能也是很低的,就是說(shuō)根據(jù)一個(gè)前綴,一下找到一堆的id的可能性是非常大的,因此這部分不太可能造成大延遲。

那么在往下看,doc values 保存在dvd 中,并且 id 都是順序保存的,保存就是一堆的key/value 結(jié)構(gòu),只不過(guò) key 都是通過(guò)Lucene壓縮的數(shù)據(jù)結(jié)構(gòu),如果最后壓縮的文件很少,那么對(duì)于一堆的id集合來(lái)說(shuō),page cache missing 應(yīng)該會(huì)存在,但是頻繁missing應(yīng)該不至于。所以這個(gè)地方也是個(gè)關(guān)注點(diǎn),cache missing應(yīng)該還是會(huì)有延遲增長(zhǎng)的,特別是在大segment merge完后特別明顯。

摘自http://blog.csdn.net/ronalod

最后就是fetch 的過(guò)程了,這個(gè)過(guò)程就沒(méi)什么好說(shuō)了,通過(guò)id get doc,最最最壞的情況應(yīng)該就是在大segments 做完merge,所有文件都沒(méi)cache, 然后突然來(lái)一批稀疏id的時(shí)候去load 文件,這種造成最大的延遲的可能性是最大的。

那再回顧這個(gè)索引, 單機(jī)占用空間50G+, 而我們只有64G內(nèi)存,30G分配到HEAP,也就是 OS cache 其實(shí)30G不到,那么要緩存50G+ 的文件,應(yīng)該cache 頻繁切換的可能還是有的,這里還沒(méi)算上 1K TPS 的indexing,還有后臺(tái)的merge 線(xiàn)程所造成的大文件切換。

問(wèn)題二解決辦法:
那么分析到這里的話(huà),99.9% 百分位延遲的問(wèn)題似乎就講得通了,因此我們的措施就是增大內(nèi)存到96G,減少索引容量,清理掉沒(méi)用的數(shù)據(jù),并且要把refresh 的時(shí)間把握好,盡量讓Lucene來(lái)生成一些不大不小的segments,一方面避免后臺(tái)頻繁merge,一方面也使得切換大文件時(shí)OS 能盡快地cache segments文件。


問(wèn)題三:做force merge 延遲非常高

由于我們show hand買(mǎi)中問(wèn)題二的root cause,那么問(wèn)題三自然就很容易推斷了:
首先,force merge 會(huì)強(qiáng)行merge一些比較大的segments,例如 2G + 2G -> 4G ,這些在切指針時(shí)就會(huì)造成這4G的文件全部missing,OS加載需要一點(diǎn)時(shí)間。
其次,F(xiàn)ST 表需要重新算,doc values需要重新算,元數(shù)據(jù)表需要重新算,這些都是額外常駐在HEAP的,因此大索引的話(huà)做force merge 其實(shí) HEAP的老生代很容易沾滿(mǎn)并且頻繁觸發(fā)GC, 甚至最壞的時(shí)候會(huì)有full GC,在第三個(gè)問(wèn)題排查的時(shí)候我們的節(jié)點(diǎn)發(fā)生了最高超過(guò)6s的GC。
所以最后的總結(jié)就是在生產(chǎn)的環(huán)境謹(jǐn)慎對(duì)待force merge。


結(jié)論:

  • 謹(jǐn)慎對(duì)待數(shù)字類(lèi)型的查詢(xún),long, date 等,date字段應(yīng)該盡可能round up到秒,分鐘更佳,long值不做運(yùn)算的統(tǒng)一改到keyword,這樣FST表可以常駐HEAP
  • 如果對(duì)最大延遲有要求,合理分配索引大小和機(jī)器內(nèi)存,如果OS cache能夠完全cover index size則基本可以消除掉page cache missing帶來(lái)的大延遲
  • 合理規(guī)劃好索引segments 的merge ,大白天的謹(jǐn)慎操作force merge

參考文獻(xiàn):

Lucene底層原理和優(yōu)化經(jīng)驗(yàn)分享(1)-Lucene簡(jiǎn)介和索引原理
Lucene底層原理和優(yōu)化經(jīng)驗(yàn)分享(2)-Lucene優(yōu)化經(jīng)驗(yàn)總結(jié)
Frame of Reference and Roaring Bitmaps
Better Query Planning for Range Queries in Elasticsearch
Latency spike after big merge
Cache costly subqueries asynchronously
Solr Wiki

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容