數(shù)據(jù)庫(kù)表數(shù)據(jù)量大讀寫(xiě)緩慢如何優(yōu)化(3)【Elasticsearch的使用】
在上一篇文章里有提到Elasticsearch能在短時(shí)間內(nèi)搜索、分析大量數(shù)據(jù),并作為查詢數(shù)據(jù)的存儲(chǔ)系統(tǒng)。坦白的說(shuō),Elasticsearch確實(shí)是個(gè)好東西,畢竟它在分布式開(kāi)源搜索和分析引擎中處于領(lǐng)先地位。不過(guò)他也存在不少的坑,以至于網(wǎng)上也一直能看到有人抱怨它有多么多么不好用。
如何使用Elasticsearch設(shè)計(jì)表結(jié)構(gòu)?
我們知道Elasticsearch(以下簡(jiǎn)稱“ES”)是基于索引的設(shè)計(jì),它沒(méi)辦法像MySQL那樣使用join查詢,所以,查詢數(shù)據(jù)時(shí)我們需要把每條主數(shù)據(jù)及關(guān)聯(lián)子表的數(shù)據(jù)全部整合在一條記錄中。
比如MySQL中有一個(gè)訂單數(shù)據(jù),使用ES查詢時(shí),我們會(huì)把每條主數(shù)據(jù)及關(guān)聯(lián)子表數(shù)據(jù)全部整合在下表中:

從上表中,我們發(fā)現(xiàn):使用ES存儲(chǔ)數(shù)據(jù)時(shí)并不會(huì)設(shè)計(jì)多個(gè)表,而是將所有表的相關(guān)字段數(shù)據(jù)匯集在一個(gè)document中,即一個(gè)完整的文檔結(jié)構(gòu),類似下面的json:
到這里,是不是很疑惑:為什么我們把所有的表匯聚在一個(gè)document中,而不是設(shè)計(jì)成多個(gè)表?為什么ES不需要關(guān)聯(lián)查詢?這就涉及到ES的存儲(chǔ)結(jié)構(gòu)原理相關(guān)知識(shí),下面來(lái)看看:
ES的存儲(chǔ)結(jié)構(gòu)
ES是一個(gè)分布式查詢系統(tǒng),它的每一個(gè)節(jié)點(diǎn)都是一個(gè)基于Lucene的查詢引擎。下面通過(guò)Lucene和MySQL的概念對(duì)比,你就理解Lucene了。
(1)Lucene和MySQL概念對(duì)比
Lucene是一個(gè)索引系統(tǒng),通過(guò)從易到難的方式,我們把Lucene與MySQL的一些概念簡(jiǎn)單做映射:

通過(guò)表中相關(guān)概念的對(duì)比,相信大家已經(jīng)了解了Lucene中每個(gè)概念的作用,這部分內(nèi)容也是對(duì)上面內(nèi)容的一個(gè)補(bǔ)充。
到這里你可能還有一個(gè)疑問(wèn):Lucene的索引index到底是什么?我們繼續(xù)討論。
(2)無(wú)結(jié)構(gòu)文檔的倒排索引(index)
實(shí)際上,Lucene使用的是倒排索引的結(jié)構(gòu),具體是什么意思呢?先舉個(gè)例子,你就能更好地理解了。
假如我們有一個(gè)無(wú)結(jié)構(gòu)的文檔,如下表所示:

簡(jiǎn)單倒排索引后,顯示結(jié)果如下表所示:

我們發(fā)現(xiàn):無(wú)結(jié)構(gòu)的文檔經(jīng)過(guò)簡(jiǎn)單的倒排索引后,字典表主要存放關(guān)鍵字,而倒排表存放該關(guān)鍵字所在的文檔id。
通過(guò)以上簡(jiǎn)單的例子,我們已經(jīng)明白倒排索引的結(jié)構(gòu)了,但是表數(shù)據(jù)往往是有結(jié)構(gòu)的,并不是一篇篇文章。如果一個(gè)文檔有結(jié)構(gòu)呢,我們?cè)撛趺崔k?
(3)有結(jié)構(gòu)文檔的倒排索引(Index)
再來(lái)舉一個(gè)更復(fù)雜的例子,比如每個(gè) Doc 都有多個(gè) Field,F(xiàn)ield 有不同的值(包含不同的 Term),倒排索引的結(jié)構(gòu)參考如下圖所示:

也就是說(shuō):有結(jié)構(gòu)的文檔經(jīng)過(guò)倒排索引后,字段中的每個(gè)值都是一個(gè)關(guān)鍵字,存放在左邊的 Term Dictionary(詞匯表)中,且每個(gè)關(guān)鍵字都有對(duì)應(yīng)地址指向所在文檔。
以上例子只是一個(gè)參考,實(shí)際上不管是字典表還是倒排表都是非常復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。了解了 ES 的存儲(chǔ)數(shù)據(jù)結(jié)構(gòu),我們就能更好地理解 ES 的表結(jié)構(gòu)設(shè)計(jì)思路了。
講到這,我們先討論下:ES 的 Document 如何定義結(jié)構(gòu)和字段格式(類似 MySQL 的表結(jié)構(gòu))?
(4)ES的document怎么定義結(jié)構(gòu)和字段格式
前面我們討論了 ES 的存儲(chǔ)結(jié)構(gòu),從它是基于索引的設(shè)計(jì)來(lái)看,我們知道,設(shè)計(jì) ES Document 結(jié)構(gòu)時(shí),并不需要像 MySQL 那樣關(guān)聯(lián)表,而是會(huì)把所有相關(guān)數(shù)據(jù)匯集在 1 個(gè) Document 中,接下來(lái)我們看個(gè)例子。
我直接將剛剛 order 的 JSON 文檔轉(zhuǎn)成一個(gè) ES 定義文檔命令(這里需要注意:SQL 中的子表數(shù)據(jù),在 ES 中需要以嵌入式對(duì)象的格式存儲(chǔ)),代碼示例如下:
我們已經(jīng)了解了 ES 表結(jié)構(gòu)的設(shè)計(jì),在實(shí)際業(yè)務(wù)中,我們往往會(huì)遇到這種情況:主數(shù)據(jù)修改了表結(jié)構(gòu),ES 也要求修改文檔結(jié)構(gòu),這時(shí)我們?cè)撛趺崔k?這就涉及下面要討論的第 2 個(gè)問(wèn)題——如何修改表結(jié)構(gòu)。
Elasticsearch如何修改表結(jié)構(gòu)?
在實(shí)際業(yè)務(wù)中,如果想增加新的字段,ES支持直接添加,但如果想修改字段類型或者改名,ES官方文檔里是這樣寫(xiě)的(有興趣的小伙伴可以練練英文,沒(méi)興趣的可以直接跳過(guò)):
Except for supported mapping parameters, you can’t change the mapping or field type of an existing field. Changing an existing field could invalidate data that’s already Indexed.
If you need to change the mapping of a field in other indices, create a new index with the correct mapping and reIndex your data into that index.
Renaming a field would invalidate data already indexed under the old field name. Instead, add an alias field to create an alternate field name.
因?yàn)樾薷淖侄蔚念愋蜁?huì)導(dǎo)致索引失效,所以 ES 不支持我們修改原來(lái)字段的類型。
如果你想修改字段的映射,首先需要新建一個(gè)索引,然后使用 ES 的 reindex 功能將舊索引拷貝到新索引中。那么什么是reindex呢?reindex是ES自帶的API,在實(shí)際代碼匯總,你看下調(diào)用示例就能明白它的功用了。
不過(guò),直接重命名字段時(shí),我們使用reindex功能會(huì)導(dǎo)致原來(lái)保存的舊的字段名的索引數(shù)據(jù)失效,這種情況如何解決?此時(shí)我們可以使用alias索引功能,代碼示例如下:
說(shuō)到修改表結(jié)構(gòu),使用普通MySQL時(shí),并不建議直接修改字段類型,改名或刪除字段。因?yàn)槊看胃掳姹緯r(shí),我們都要做好版本回滾的打算,為此設(shè)計(jì)每個(gè)版本對(duì)應(yīng)數(shù)據(jù)庫(kù)時(shí),我們會(huì)盡量兼容前面版本的代碼。
因ES的結(jié)構(gòu)基于MySQL而設(shè)計(jì),兩者之間存在對(duì)應(yīng)關(guān)系,所以也不建議直接修改ES的表結(jié)構(gòu)。
那如果我們真有修改的需求呢?一般而言,我們會(huì)先保留舊的字段,然后直接添加并使用新的字段,知道新版本的代碼全部穩(wěn)定工作后,我們?cè)僬覚C(jī)會(huì)清理舊的不用的字段,即分成2個(gè)版本完成修改需求。
討論完如何修改表結(jié)構(gòu),我們繼續(xù)討論最后一個(gè)要點(diǎn):ES的那些坑。ES的坑有哪些?
坑一:ES是準(zhǔn)實(shí)時(shí)的?
當(dāng)更新數(shù)據(jù)至ES且返回成功提示(注意這一瞬間),你會(huì)發(fā)現(xiàn)ES查詢返回的數(shù)據(jù)仍然不是最新的,背后的原因究竟是什么?這就要求我們對(duì)數(shù)據(jù)索引的整個(gè)過(guò)程有所了解,且待我們一步步揭開(kāi)真實(shí)的面紗。
數(shù)據(jù)索引整個(gè)過(guò)程因涉及 ES 的分片,Lucene Index、Segment、 Document 的三者之間關(guān)系等知識(shí)點(diǎn),所以我們有必要先把這部分內(nèi)容串起來(lái)說(shuō)明。
ES 的一個(gè)分片(這里跳過(guò) ES 分片相關(guān)介紹)就是一個(gè) Lucene Index,每一個(gè) Lucene Index 由多個(gè) Segment 構(gòu)成,即 Lucene Index 的子集就是 Segment,如下圖所示:
關(guān)于 Lucene Index、Segment、 Document 三者之間的關(guān)系,你看完下面這張圖就一目了然了,如下圖所示:

通過(guò)上面這個(gè)圖,我們知道一個(gè) Lucene Index 可以存放多個(gè) Segment,而每個(gè) Segment 又可以存放多個(gè) Document。
掌握了以上基礎(chǔ)知識(shí)點(diǎn),接下來(lái)就進(jìn)入正題——數(shù)據(jù)索引的過(guò)程詳解。
第一步:當(dāng)新的 Document 被創(chuàng)建,數(shù)據(jù)首先會(huì)存放到新的 Segment 中,同時(shí)舊的 Document 會(huì)被刪除,并在原來(lái)的 Segment 上標(biāo)記一個(gè)刪除標(biāo)識(shí)。當(dāng) Document 被更新,舊版 Document 會(huì)被標(biāo)識(shí)為刪除,并將新版 Document 存放新的 Segment 中。
第二步:Shard 收到寫(xiě)請(qǐng)求時(shí),請(qǐng)求會(huì)被寫(xiě)入 Translog 中,然后 Document 被存放 memory buffer (注意:memory buffer 的數(shù)據(jù)并不能被搜索到)中,最終 Translog 保存所有修改記錄。
第三步:每隔 1 秒(默認(rèn)設(shè)置),refresh 操作被執(zhí)行一次,且 memory buffer 中的數(shù)據(jù)會(huì)被寫(xiě)入一個(gè) Segment 并存放 filesystem cache 中,這時(shí)新的數(shù)據(jù)就可以被搜索到了,如下圖所示:

通過(guò)以上數(shù)據(jù)索引過(guò)程的說(shuō)明,我們發(fā)現(xiàn) ES 并不是實(shí)時(shí)的,而是有 1 秒延時(shí),因延時(shí)問(wèn)題的解決方案我們?cè)谏弦黄杏懻撨^(guò),提示用戶查詢的數(shù)據(jù)會(huì)有一定延時(shí)即可。
坑二:ES 宕機(jī)恢復(fù)后,數(shù)據(jù)丟失
在數(shù)據(jù)索引的過(guò)程這部分內(nèi)容,我們提及了每隔 1 秒(根據(jù)配置),memory buffer 中的數(shù)據(jù)會(huì)被寫(xiě)入 Segment 中,此時(shí)這部分?jǐn)?shù)據(jù)可被用戶搜索到,但沒(méi)有被持久化,一旦系統(tǒng)宕機(jī)了,數(shù)據(jù)就會(huì)丟失。
比如下圖中灰色的桶,目前它可被搜索到,但還沒(méi)有持久化,一旦 ES 宕機(jī),數(shù)據(jù)將會(huì)丟失。
如何防止數(shù)據(jù)丟失呢?使用 Lucene 中的 commit 操作就能輕松解決這個(gè)問(wèn)題。
commit 具體操作:先將多個(gè) Segment 合并保存到磁盤(pán)中,再將灰色的桶變成上圖中綠色的桶。
不過(guò),使用 commit 操作存在一點(diǎn)不足:耗 IO,從而引發(fā) ES 在 commit 之前宕機(jī)的問(wèn)題。一旦系統(tǒng)在 translog fsync 之前宕機(jī),數(shù)據(jù)也會(huì)直接丟失,如何保證 ES 數(shù)據(jù)的完整性便成了亟待解決的問(wèn)題。
遇到這種情況,我們采用 translog 解決就行,因?yàn)?Translog 中的數(shù)據(jù)不會(huì)直接保存在磁盤(pán)中,只有 fsync 后才保存,這里我分享兩種 Translog 解決方案。
第一種:將 Index.translog.durability 設(shè)置成 request ,如果我們發(fā)現(xiàn)系統(tǒng)運(yùn)行得不錯(cuò),采用這種方式即可;第二種:將 Index.translog.durability 設(shè)置成 fsync,每次 ES 宕機(jī)啟動(dòng)后,先將主數(shù)據(jù)和 ES 數(shù)據(jù)進(jìn)行對(duì)比,再將 ES 缺失的數(shù)據(jù)找出來(lái)。強(qiáng)調(diào)一個(gè)知識(shí)點(diǎn):Translog 何時(shí)會(huì) fsync ?當(dāng) Index.translog.durability 設(shè)置成 request 后,每個(gè)請(qǐng)求都會(huì) fsync,不過(guò)這樣影響 ES 性能。這時(shí)我們可以把 Index.translog.durability 設(shè)置成 fsync,那么每隔 Index.translog.sync_interval 后,每個(gè)請(qǐng)求才會(huì) fsync 一次。
坑三:分頁(yè)越深,查詢效率越慢
ES 分頁(yè)這個(gè)坑的出現(xiàn),與 ES 的讀操作請(qǐng)求的處理流程密切關(guān)聯(lián),為此我們有必要先深度剖析下 ES 的讀操作請(qǐng)求的處理流程,如下圖所示:
關(guān)于 ES 的讀操作流程主要分為兩個(gè)階段:Query Phase、Fetch Phase。
Query Phase: 協(xié)調(diào)的節(jié)點(diǎn)先把請(qǐng)求分發(fā)到所有分片,然后每個(gè)分片在本地查詢建一個(gè)結(jié)果集隊(duì)列,并將命令中的 Document id 以及搜索分?jǐn)?shù)存放隊(duì)列中,再返回給協(xié)調(diào)節(jié)點(diǎn),最后協(xié)調(diào)節(jié)點(diǎn)會(huì)建一個(gè)全局隊(duì)列,歸并收到的所有結(jié)果集并進(jìn)行全局排序。Query Phase 需要注意:在 ES 查詢過(guò)程中,如果 search 帶了 from 和 size 參數(shù),Elasticsearch 集群需要給協(xié)調(diào)節(jié)點(diǎn)返回 shards number * (from + size) 條數(shù)據(jù),然后在單機(jī)上進(jìn)行排序,最后給客戶端返回 size 大小的數(shù)據(jù)。比如客戶端請(qǐng)求 10 條數(shù)據(jù)(比如 3 個(gè)分片),那么每個(gè)分片則會(huì)返回 10 條數(shù)據(jù),協(xié)調(diào)節(jié)點(diǎn)最后會(huì)歸并 30 條數(shù)據(jù),但最終只返回 10 條數(shù)據(jù)給客戶端。
Fetch Phase: 協(xié)調(diào)節(jié)點(diǎn)先根據(jù)結(jié)果集里的 Document id 向所有分片獲取完整的 Document,然后所有分片返回完整的 Document 給協(xié)調(diào)節(jié)點(diǎn),最后協(xié)調(diào)節(jié)點(diǎn)將結(jié)果返回給客戶端。(關(guān)于什么是協(xié)調(diào)節(jié)點(diǎn),我們先忽略它。)在整個(gè) ES 的讀操作流程中,Elasticsearch 集群實(shí)際上需要給協(xié)調(diào)節(jié)點(diǎn)返回 shards number * (from + size) 條數(shù)據(jù),然后在單機(jī)上進(jìn)行排序,最后返回給客戶端這個(gè) size 大小的數(shù)據(jù)。
比如有 5 個(gè)分片,我們需要查詢排序序號(hào)從 10000 到 10010(from=10000,size=10)的結(jié)果,每個(gè)分片到底返回多少數(shù)據(jù)給協(xié)調(diào)節(jié)點(diǎn)計(jì)算呢?告訴你不是 10 條,是 10010 條。也就是說(shuō),協(xié)調(diào)節(jié)點(diǎn)需要在內(nèi)存中計(jì)算 10010*5=50050 條記錄,所以在系統(tǒng)使用中,如果用戶分頁(yè)越深查詢速度會(huì)越慢,也就是說(shuō)并不是分頁(yè)越多越好。
那如何更好地解決 ES 分頁(yè)問(wèn)題呢?為了控制性能,我們主要使用 ES 中的 max_result_window 配置,這個(gè)數(shù)據(jù)默認(rèn)為 10000,當(dāng) from+size > max_result_window ,ES 將返回錯(cuò)誤。
由此可見(jiàn),在系統(tǒng)設(shè)計(jì)時(shí),我們一般需要控制用戶翻頁(yè)不能太深,而這在現(xiàn)實(shí)場(chǎng)景中用戶也能接受,這也是我之前方案采用的設(shè)計(jì)方式。要是用戶確實(shí)有深度翻頁(yè)的需求,我們?cè)偈褂?ES 中search_after 的功能也能解決,不過(guò)就是無(wú)法實(shí)現(xiàn)跳頁(yè)了。
我們舉一個(gè)例子,查詢按照訂單總金額分頁(yè),上一頁(yè)最后一條 order 的總金額 total_amount 是 10,那么下一頁(yè)的查詢示例代碼如下:
? ? 這個(gè) search_after 里的值,就是上次查詢結(jié)果的排序字段的結(jié)果值。 小結(jié) 本章關(guān)于使用 Elasticsearch 需要注意的要點(diǎn)我們就討論完了,下一篇我們開(kāi)始討論分表分庫(kù)。
更多內(nèi)容歡迎關(guān)注公眾號(hào)“服務(wù)端技術(shù)精選”。掃描二維碼推送至手機(jī)訪問(wèn)。
版權(quán)聲明:本文由財(cái)神資訊-領(lǐng)先的體育資訊互動(dòng)媒體轉(zhuǎn)載發(fā)布,如需刪除請(qǐng)聯(lián)系。