Elasticsearch如何做到数十亿数据查询毫秒级响应?
如果面試的時(shí)候碰到這樣一個(gè)面試題:ES 在數(shù)據(jù)量很大的情況下(數(shù)十億級(jí)別)如何提高查詢效率?
這個(gè)問題說白了,就是看你有沒有實(shí)際用過 ES,因?yàn)樯?#xff1f;其實(shí) ES 性能并沒有你想象中那么好的。
很多時(shí)候數(shù)據(jù)量大了,特別是有幾億條數(shù)據(jù)的時(shí)候,可能你會(huì)懵逼的發(fā)現(xiàn),跑個(gè)搜索怎么一下 5~10s?后面反而就快了,可能就幾百毫秒。
說實(shí)話,ES 性能優(yōu)化是沒有銀彈的。啥意思呢?就是不要期待著隨手調(diào)一個(gè)參數(shù),就可以萬能的應(yīng)對(duì)所有的性能慢的場(chǎng)景。
也許有的場(chǎng)景是你換個(gè)參數(shù),或者調(diào)整一下語法,就可以搞定,但是絕對(duì)不是所有場(chǎng)景都可以這樣。
Filesystem Cache
你往 ES 里寫的數(shù)據(jù),實(shí)際上都寫到磁盤文件里去了,查詢的時(shí)候,操作系統(tǒng)會(huì)將磁盤文件里的數(shù)據(jù)自動(dòng)緩存到 Filesystem Cache 里面去。
es 的搜索引擎嚴(yán)重依賴于底層的 filesystem cache,你如果給 filesystem cache 更多的內(nèi)存,盡量讓內(nèi)存可以容納所有的 idx segment file 索引數(shù)據(jù)文件,那么你搜索的時(shí)候就基本都是走內(nèi)存的,性能會(huì)非常高。
性能差距究竟可以有多大?我們之前很多的測(cè)試和壓測(cè),如果走磁盤一般肯定上秒,搜索性能絕對(duì)是秒級(jí)別的,1秒、5秒、10秒。但如果是走 filesystem cache,是走純內(nèi)存的,那么一般來說性能比走磁盤要高一個(gè)數(shù)量級(jí),基本上就是毫秒級(jí)的,從幾毫秒到幾百毫秒不等。
這里有個(gè)真實(shí)的案例。某個(gè)公司 es 節(jié)點(diǎn)有 3 臺(tái)機(jī)器,每臺(tái)機(jī)器看起來內(nèi)存很多,64G,總內(nèi)存就是 64 * 3 = 192G。每臺(tái)機(jī)器給 es jvm heap 是 32G,那么剩下來留給 filesystem cache 的就是每臺(tái)機(jī)器才 32G,總共集群里給 filesystem cache 的就是 32 * 3 = 96G 內(nèi)存。而此時(shí),整個(gè)磁盤上索引數(shù)據(jù)文件,在 3 臺(tái)機(jī)器上一共占用了 1T 的磁盤容量,es 數(shù)據(jù)量是 1T,那么每臺(tái)機(jī)器的數(shù)據(jù)量是 300G。這樣性能好嗎? filesystem cache 的內(nèi)存才 100G,十分之一的數(shù)據(jù)可以放內(nèi)存,其他的都在磁盤,然后你執(zhí)行搜索操作,大部分操作都是走磁盤,性能肯定差。
歸根結(jié)底,你要讓 es 性能要好,最佳的情況下,就是你的機(jī)器的內(nèi)存,至少可以容納你的總數(shù)據(jù)量的一半。
根據(jù)我們自己的生產(chǎn)環(huán)境實(shí)踐經(jīng)驗(yàn),最佳的情況下,是僅僅在 es 中就存少量的數(shù)據(jù),就是你要用來搜索的那些索引,如果內(nèi)存留給 filesystem cache 的是 100G,那么你就將索引數(shù)據(jù)控制在 100G 以內(nèi),這樣的話,你的數(shù)據(jù)幾乎全部走內(nèi)存來搜索,性能非常之高,一般可以在 1 秒以內(nèi)。
比如說你現(xiàn)在有一行數(shù)據(jù)。id,name,age .... 30 個(gè)字段。但是你現(xiàn)在搜索,只需要根據(jù) id,name,age 三個(gè)字段來搜索。如果你傻乎乎往 es 里寫入一行數(shù)據(jù)所有的字段,就會(huì)導(dǎo)致說 90% 的數(shù)據(jù)是不用來搜索的,結(jié)果硬是占據(jù)了 es 機(jī)器上的 filesystem cache 的空間,單條數(shù)據(jù)的數(shù)據(jù)量越大,就會(huì)導(dǎo)致 filesystem cahce 能緩存的數(shù)據(jù)就越少。其實(shí),僅僅寫入 es 中要用來檢索的少數(shù)幾個(gè)字段就可以了,比如說就寫入 es id,name,age 三個(gè)字段,然后你可以把其他的字段數(shù)據(jù)存在 mysql/hbase 里,我們一般是建議用 es + hbase 這么一個(gè)架構(gòu)。
hbase 的特點(diǎn)是適用于海量數(shù)據(jù)的在線存儲(chǔ),就是對(duì) hbase 可以寫入海量數(shù)據(jù),但是不要做復(fù)雜的搜索,做很簡(jiǎn)單的一些根據(jù) id 或者范圍進(jìn)行查詢的這么一個(gè)操作就可以了。從 es 中根據(jù) name 和 age 去搜索,拿到的結(jié)果可能就 20 個(gè) doc id,然后根據(jù) doc id 到 hbase 里去查詢每個(gè) doc id 對(duì)應(yīng)的完整的數(shù)據(jù),給查出來,再返回給前端。
寫入 es 的數(shù)據(jù)最好小于等于,或者是略微大于 es 的 filesystem cache 的內(nèi)存容量。然后你從 es 檢索可能就花費(fèi) 20ms,然后再根據(jù) es 返回的 id 去 hbase 里查詢,查 20 條數(shù)據(jù),可能也就耗費(fèi)個(gè) 30ms,可能你原來那么玩兒,1T 數(shù)據(jù)都放 es,會(huì)每次查詢都是 5~10s,現(xiàn)在可能性能就會(huì)很高,每次查詢就是 50ms。
數(shù)據(jù)預(yù)熱
假如說,哪怕是你就按照上述的方案去做了,es 集群中每個(gè)機(jī)器寫入的數(shù)據(jù)量還是超過了 filesystem cache 一倍,比如說你寫入一臺(tái)機(jī)器 60G 數(shù)據(jù),結(jié)果 filesystem cache 就 30G,還是有 30G 數(shù)據(jù)留在了磁盤上。
其實(shí)可以做數(shù)據(jù)預(yù)熱。
舉個(gè)例子,拿微博來說,你可以把一些大V,平時(shí)看的人很多的數(shù)據(jù),你自己提前后臺(tái)搞個(gè)系統(tǒng),每隔一會(huì)兒,自己的后臺(tái)系統(tǒng)去搜索一下熱數(shù)據(jù),刷到 filesystem cache 里去,后面用戶實(shí)際上來看這個(gè)熱數(shù)據(jù)的時(shí)候,他們就是直接從內(nèi)存里搜索了,很快。
或者是電商,你可以將平時(shí)查看最多的一些商品,比如說 iphone 8,熱數(shù)據(jù)提前后臺(tái)搞個(gè)程序,每隔 1 分鐘自己主動(dòng)訪問一次,刷到 filesystem cache 里去。
對(duì)于那些你覺得比較熱的、經(jīng)常會(huì)有人訪問的數(shù)據(jù),最好做一個(gè)專門的緩存預(yù)熱子系統(tǒng),就是對(duì)熱數(shù)據(jù)每隔一段時(shí)間,就提前訪問一下,讓數(shù)據(jù)進(jìn)入 filesystem cache 里面去。這樣下次別人訪問的時(shí)候,性能一定會(huì)好很多。
冷熱分離
es 可以做類似于 mysql 的水平拆分,就是說將大量的訪問很少、頻率很低的數(shù)據(jù),單獨(dú)寫一個(gè)索引,然后將訪問很頻繁的熱數(shù)據(jù)單獨(dú)寫一個(gè)索引。最好是將冷數(shù)據(jù)寫入一個(gè)索引中,然后熱數(shù)據(jù)寫入另外一個(gè)索引中,這樣可以確保熱數(shù)據(jù)在被預(yù)熱之后,盡量都讓他們留在 filesystem os cache 里,別讓冷數(shù)據(jù)給沖刷掉。
你看,假設(shè)你有 6 臺(tái)機(jī)器,2 個(gè)索引,一個(gè)放冷數(shù)據(jù),一個(gè)放熱數(shù)據(jù),每個(gè)索引 3 個(gè) shard。3 臺(tái)機(jī)器放熱數(shù)據(jù) index,另外 3 臺(tái)機(jī)器放冷數(shù)據(jù) index。然后這樣的話,你大量的時(shí)間是在訪問熱數(shù)據(jù) index,熱數(shù)據(jù)可能就占總數(shù)據(jù)量的 10%,此時(shí)數(shù)據(jù)量很少,幾乎全都保留在 filesystem cache 里面了,就可以確保熱數(shù)據(jù)的訪問性能是很高的。但是對(duì)于冷數(shù)據(jù)而言,是在別的 index 里的,跟熱數(shù)據(jù) index 不在相同的機(jī)器上,大家互相之間都沒什么聯(lián)系了。如果有人訪問冷數(shù)據(jù),可能大量數(shù)據(jù)是在磁盤上的,此時(shí)性能差點(diǎn),就 10% 的人去訪問冷數(shù)據(jù),90% 的人在訪問熱數(shù)據(jù),也無所謂了。
document模型設(shè)計(jì)
對(duì)于 MySQL,我們經(jīng)常有一些復(fù)雜的關(guān)聯(lián)查詢。在 es 里該怎么玩兒,es 里面的復(fù)雜的關(guān)聯(lián)查詢盡量別用,一旦用了性能一般都不太好。
最好是先在 Java 系統(tǒng)里就完成關(guān)聯(lián),將關(guān)聯(lián)好的數(shù)據(jù)直接寫入 es 中。搜索的時(shí)候,就不需要利用 es 的搜索語法來完成 join 之類的關(guān)聯(lián)搜索了。
document 模型設(shè)計(jì)是非常重要的,很多操作,不要在搜索的時(shí)候才想去執(zhí)行各種復(fù)雜的亂七八糟的操作。es 能支持的操作就那么多,不要考慮用 es 做一些它不好操作的事情。如果真的有那種操作,盡量在 document 模型設(shè)計(jì)的時(shí)候,寫入的時(shí)候就完成。另外對(duì)于一些太復(fù)雜的操作,比如 join/nested/parent-child 搜索都要盡量避免,性能都很差的。
分頁性能優(yōu)化
es 的分頁是較坑的,為啥呢?舉個(gè)例子吧,假如你每頁是 10 條數(shù)據(jù),你現(xiàn)在要查詢第 100 頁,實(shí)際上是會(huì)把每個(gè) shard 上存儲(chǔ)的前 1000 條數(shù)據(jù)都查到一個(gè)協(xié)調(diào)節(jié)點(diǎn)上,如果你有個(gè) 5 個(gè) shard,那么就有 5000 條數(shù)據(jù),接著協(xié)調(diào)節(jié)點(diǎn)對(duì)這 5000 條數(shù)據(jù)進(jìn)行一些合并、處理,再獲取到最終第 100 頁的 10 條數(shù)據(jù)。
分布式的,你要查第 100 頁的 10 條數(shù)據(jù),不可能說從 5 個(gè) shard,每個(gè) shard 就查 2 條數(shù)據(jù),最后到協(xié)調(diào)節(jié)點(diǎn)合并成 10 條數(shù)據(jù)吧?你必須得從每個(gè) shard 都查 1000 條數(shù)據(jù)過來,然后根據(jù)你的需求進(jìn)行排序、篩選等等操作,最后再次分頁,拿到里面第 100 頁的數(shù)據(jù)。你翻頁的時(shí)候,翻的越深,每個(gè) shard 返回的數(shù)據(jù)就越多,而且協(xié)調(diào)節(jié)點(diǎn)處理的時(shí)間越長(zhǎng),非常坑爹。所以用 es 做分頁的時(shí)候,你會(huì)發(fā)現(xiàn)越翻到后面,就越是慢。
我們之前也是遇到過這個(gè)問題,用 es 作分頁,前幾頁就幾十毫秒,翻到 10 頁或者幾十頁的時(shí)候,基本上就要 5~10 秒才能查出來一頁數(shù)據(jù)了。
有什么解決方案嗎?
不允許深度分頁(默認(rèn)深度分頁性能很差)
跟產(chǎn)品經(jīng)理說,你系統(tǒng)不允許翻那么深的頁,默認(rèn)翻的越深,性能就越差。
類似于 app 里的推薦商品不斷下拉出來一頁一頁的
類似于微博中,下拉刷微博,刷出來一頁一頁的,你可以用 scroll api,關(guān)于如何使用,自行上網(wǎng)搜索。
scroll 會(huì)一次性給你生成所有數(shù)據(jù)的一個(gè)快照,然后每次滑動(dòng)向后翻頁就是通過游標(biāo) scroll_id 移動(dòng),獲取下一頁下一頁這樣子,性能會(huì)比上面說的那種分頁性能要高很多很多,基本上都是毫秒級(jí)的。
但是,唯一的一點(diǎn)就是,這個(gè)適合于那種類似微博下拉翻頁的,不能隨意跳到任何一頁的場(chǎng)景。也就是說,你不能先進(jìn)入第 10 頁,然后去第 120 頁,然后又回到第 58 頁,不能隨意亂跳頁。所以現(xiàn)在很多產(chǎn)品,都是不允許你隨意翻頁的,app,也有一些網(wǎng)站,做的就是你只能往下拉,一頁一頁的翻。
初始化時(shí)必須指定 scroll 參數(shù),告訴 es 要保存此次搜索的上下文多長(zhǎng)時(shí)間。你需要確保用戶不會(huì)持續(xù)不斷翻頁翻幾個(gè)小時(shí),否則可能因?yàn)槌瑫r(shí)而失敗。
除了用 scroll api,你也可以用 search_after 來做,search_after 的思想是使用前一頁的結(jié)果來幫助檢索下一頁的數(shù)據(jù),顯然,這種方式也不允許你隨意翻頁,你只能一頁頁往后翻。初始化時(shí),需要使用一個(gè)唯一值的字段作為 sort 字段。
節(jié)點(diǎn)副本調(diào)優(yōu)
ES的性能跟硬件資源有很大關(guān)系,當(dāng)ES集群?jiǎn)为?dú)部署到物理機(jī)器上時(shí),集群內(nèi)部的節(jié)點(diǎn)并不是獨(dú)占整臺(tái)物理機(jī)資源,在集群運(yùn)行的時(shí)候同一物理機(jī)上的節(jié)點(diǎn)仍會(huì)出現(xiàn)資源搶占的問題。所以在這種情況下,為了讓ES單個(gè)節(jié)點(diǎn)能夠使用最大程度的機(jī)器資源,采用每個(gè)ES節(jié)點(diǎn)部署在單獨(dú)一臺(tái)物理機(jī)上方式。
但緊接著,問題又來了,如果單個(gè)節(jié)點(diǎn)出現(xiàn)瓶頸了呢?我們應(yīng)該怎么再優(yōu)化呢?ES查詢的原理,當(dāng)請(qǐng)求打到某號(hào)分片的時(shí)候,如果沒有指定分片類型(preference參數(shù))查詢,請(qǐng)求會(huì)負(fù)載到對(duì)應(yīng)分片號(hào)的各個(gè)節(jié)點(diǎn)上。而集群默認(rèn)副本配置是一主一副,針對(duì)于此,我們想到了擴(kuò)容副本的方式,由默認(rèn)的一主一副變?yōu)橐恢鞫?#xff0c;同時(shí)增加相應(yīng)物理機(jī)。
整個(gè)架設(shè)方式通過VIP來負(fù)載均衡外部請(qǐng)求,第一層gateway節(jié)點(diǎn)實(shí)質(zhì)為ES中client node,相當(dāng)于一個(gè)智能負(fù)載均衡器,充當(dāng)著分發(fā)請(qǐng)求的角色。第二層為data node,負(fù)責(zé)存儲(chǔ)數(shù)據(jù)以及執(zhí)行數(shù)據(jù)的相關(guān)操作。整個(gè)集群有一套主分片,二套副分片(一主二副),從網(wǎng)關(guān)節(jié)點(diǎn)轉(zhuǎn)發(fā)過來的請(qǐng)求,會(huì)在打到數(shù)據(jù)節(jié)點(diǎn)之前通過輪詢的方式進(jìn)行均衡。集群增加一套副本并擴(kuò)容機(jī)器的方式,增加了集群吞吐量,從而提升了整個(gè)集群查詢性能。如下是京東到家訂單中心實(shí)際性能對(duì)比圖
當(dāng)然分片數(shù)量和分片副本數(shù)量并不是越多越好,在此階段中,對(duì)選擇適當(dāng)?shù)姆制瑪?shù)量做了近一步探索。分片數(shù)可以理解為Mysql中的分庫(kù)分表,而當(dāng)前訂單中心ES查詢主要分為兩類:單ID查詢以及分頁查詢。分片數(shù)越大,集群橫向擴(kuò)容規(guī)模也更大,根據(jù)分片路由的單ID查詢吞吐量也能大大提升,但對(duì)于聚合的分頁查詢性能則將降低。分片數(shù)越小,集群橫向擴(kuò)容規(guī)模更小,單ID的查詢性能也將下降,但對(duì)于分頁查詢,性能將會(huì)得到提升。所以如何均衡分片數(shù)量和現(xiàn)有查詢業(yè)務(wù)可通過多次測(cè)試數(shù)據(jù)進(jìn)行合理選擇。
總結(jié)
以上是生活随笔為你收集整理的Elasticsearch如何做到数十亿数据查询毫秒级响应?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Tensorflow 入门教程
- 下一篇: 推荐算法三视角:矩阵,图,时间线