前言
我曾經(jīng)面試安踏的技術(shù)崗,當(dāng)時(shí)面試官問(wèn)了我一個(gè)問(wèn)題:如果你想使用某個(gè)新技術(shù)但是領(lǐng)導(dǎo)不愿意,你怎么辦?
對(duì)于該問(wèn)題我相信大家就算沒(méi)有面試被問(wèn)到過(guò),現(xiàn)實(shí)工作中同事之間的合作也會(huì)遇到。
因此從我的角度重新去回答這個(gè)問(wèn)題,有以下幾點(diǎn):
1.師出有名,在軟件工程里是針對(duì)問(wèn)題場(chǎng)景提供解決方案的,如果脫離的實(shí)際問(wèn)題(需求)去做技術(shù)選型,無(wú)疑是耍流氓。
大家可以回顧身邊的“架構(gòu)師”、“技術(shù)Leader”是不是拍拍腦袋做決定,問(wèn)他們?yōu)槭裁催@么做,可能連個(gè)冠冕堂皇的理由都給不出。
2.信任度,只有基于上面的條件,你才有理由建議引入新技術(shù)。領(lǐng)導(dǎo)愿不愿意引入新技術(shù)有很多原因:領(lǐng)導(dǎo)不了解這技術(shù)、領(lǐng)導(dǎo)偏保守、領(lǐng)導(dǎo)不是做技術(shù)的等。
那么我認(rèn)為這幾種都是信任度,這種 信任度分人和事,人就是引入技術(shù)的提出者,事就是提出引入的技術(shù)。
3.盡人事,任何問(wèn)題只是單純解決 事 都是簡(jiǎn)單的,以我以往的做法,把基本資料收集全并以通俗易懂的方式歸納與講解,最好能提供一些能量化的數(shù)據(jù),這樣更加有說(shuō)服力。
知識(shí)普及OK后,就可以嘗試寫(xiě)方案與做個(gè)Demo,方案最好可以提供多個(gè),可以分短期收益與長(zhǎng)期收益的。完成上面幾點(diǎn)可以說(shuō)已經(jīng)盡人事了,如果領(lǐng)導(dǎo)還不答應(yīng)那么的確有他的顧慮,就算無(wú)法落實(shí),到目前為止的收獲也不錯(cuò)。
4.復(fù)雜的是人,任何人都無(wú)法時(shí)刻站在理智與客觀的角度去看待問(wèn)題,事是由人去辦的,所以同一件事由不同的人說(shuō)出來(lái)的效果也不一樣。
因此得學(xué)會(huì)向上管理、保持與同事之間合作融洽度,盡早的建立合作信任。本篇文章更多敘述的事,因此人方面不過(guò)多深究,有興趣的我可以介紹一本書(shū)《知行 技術(shù)人的管理之路》。
本篇我的實(shí)踐做法與上述一樣,除了4無(wú)法體現(xiàn)。那么下文我分了4大模塊:業(yè)務(wù)背景介紹、基礎(chǔ)概念講解、方案的選用與技術(shù)細(xì)節(jié)。
該篇文章不包含代碼有8000多千字,花了我3天時(shí)間寫(xiě),可能需要您花10分鐘慢慢閱讀,我承諾大家正文里面細(xì)節(jié)滿滿。
曾有朋友建議我拆開(kāi)來(lái)寫(xiě),但是我的習(xí)慣還是希望以一篇文章,這樣更加系統(tǒng)化的展示給大家。當(dāng)然大家有什么建議也可以在下方留言給我。
部分源碼,我放到了https://github.com/SkyChenSky/Sikiro 的Sikiro.ES.Api里
—2—
背景
本公司多年以來(lái)用SQL Server作為主存儲(chǔ),隨著多年的業(yè)務(wù)發(fā)展,已經(jīng)到了數(shù)千萬(wàn)級(jí)的數(shù)據(jù)量。
而部分非核心業(yè)務(wù)原本應(yīng)該超億的量級(jí)了,但是因?yàn)閺奈锢肀淼脑O(shè)計(jì)優(yōu)化上進(jìn)行了數(shù)據(jù)壓縮,導(dǎo)致維持在一個(gè)比較穩(wěn)定的數(shù)量。壓縮數(shù)據(jù)雖然能減少存儲(chǔ)量,優(yōu)化提供一定的性能,但是同時(shí)帶來(lái)的損失了業(yè)務(wù)可擴(kuò)展性。
舉個(gè)例子:
我們平臺(tái)某個(gè)用戶擁有最后訪問(wèn)作品記錄和總的閱讀時(shí)長(zhǎng),但是沒(méi)有某個(gè)用戶的閱讀明細(xì),那么這樣的設(shè)計(jì)就會(huì)導(dǎo)致后續(xù)新增一個(gè)抽獎(jiǎng)業(yè)務(wù),需要在某個(gè)時(shí)間段內(nèi)閱讀了多長(zhǎng)時(shí)間或者章節(jié)數(shù)量的作品,才能參加與抽獎(jiǎng);
或者運(yùn)營(yíng)想通過(guò)閱讀記錄統(tǒng)計(jì)或者分析出,用戶的愛(ài)好 和 受歡迎的作品。現(xiàn)有的設(shè)計(jì)對(duì)以上兩種業(yè)務(wù)情況都是無(wú)法滿足的。
此外我們平臺(tái)還有作品搜索功能,like ‘%搜索%’查詢是不走索引的而走 全表掃描,一張表42W全表掃描,數(shù)據(jù)庫(kù)服務(wù)器配置可以的情況下還是可以的,但是存在 并發(fā)請(qǐng)求 時(shí)候,資源消耗就特別厲害了,特別是在偶爾被爬蟲(chóng)爬取數(shù)據(jù) (我們平臺(tái)API的并發(fā)峰值能達(dá)到8w/s,每天的接口在淡季請(qǐng)求次數(shù)達(dá)到了1億1千萬(wàn))。
關(guān)系型數(shù)據(jù)庫(kù)擁有 ACID特性,能通過(guò)金融級(jí)的事務(wù)達(dá)成數(shù)據(jù)的一致性,然而它卻沒(méi)有橫向擴(kuò)展性,只要在海量數(shù)據(jù)場(chǎng)景下,單實(shí)例,無(wú)論怎么在關(guān)系型數(shù)據(jù)庫(kù)做優(yōu)化,都是只是治標(biāo)。
而NoSQL的出現(xiàn)很好的彌補(bǔ)了關(guān)系型數(shù)據(jù)庫(kù)的短板,在馬丁福勒所著的《NoSQL精粹》對(duì)NoSQL進(jìn)行了分類(lèi):文檔型、圖形、列式,鍵值, 從我的角度其實(shí)可以把搜索引擎納入NoSQL范疇,因?yàn)樗拇_滿足的NoSQL的4大特性: 易擴(kuò)展、大數(shù)據(jù)量高性能、靈活的數(shù)據(jù)模型、高可用。
我看過(guò)一些同行的見(jiàn)解,把Elasticsearch歸為文檔型NoSQL,我個(gè)人是沒(méi)有給他下過(guò)于明確的定義,這個(gè)上面說(shuō)法大家見(jiàn)仁見(jiàn)智。
MongoDB作為文檔型數(shù)據(jù)庫(kù)也屬于我的技術(shù)選型范圍,它的讀寫(xiě)性能高且平衡、數(shù)據(jù)分片與橫向擴(kuò)展等都非常適合我們平臺(tái)部分場(chǎng)景,最后我還是選擇Elasticsearch。原因有三:
我們運(yùn)維相比于MongoDB更熟悉Elasticsearch。
我們接下來(lái)有一些統(tǒng)計(jì)報(bào)表類(lèi)的需求,Elastic Stack的各種工具能很好滿足我們的需求。
我們目前著手處理的場(chǎng)景以非實(shí)時(shí)、純讀為主的業(yè)務(wù),Elasticsearch近實(shí)時(shí)搜索已經(jīng)能滿足我們。
Elasticsearch優(yōu)缺點(diǎn)
百度百科 : Elasticsearch是一個(gè)基于Lucene的搜索服務(wù)器。它提供了一個(gè)分布式多用戶能力的全文搜索引擎,基于RESTful web接口。Elasticsearch由JAVA語(yǔ)言開(kāi)發(fā)的,是一種流行的企業(yè)級(jí)搜索引擎。Elasticsearch用于云計(jì)算中,能夠達(dá)到實(shí)時(shí)搜索,穩(wěn)定,可靠,快速,安裝使用方便。官方客戶端在Java、.NET(C#)、php、Python/ target=_blank class=infotextkey>Python、Apache Groovy、Ruby和許多其他語(yǔ)言中都是可用的。
對(duì)于滿足當(dāng)下的業(yè)務(wù)需求和未來(lái)支持海量數(shù)據(jù)的搜索,我選擇了Elasticsearch,其實(shí)原因主要以下幾點(diǎn):
優(yōu)點(diǎn)
描述
橫向可擴(kuò)展性
可單機(jī)、可集群,橫向擴(kuò)展非常簡(jiǎn)單方便,自動(dòng)整理數(shù)據(jù)分片
索引被分為多個(gè)分片(Shard),利用多臺(tái)服務(wù)器,使用了分而治之的思想提升處理效率
支持搜索多樣化
與傳統(tǒng)關(guān)系型數(shù)據(jù)庫(kù)相比,ES提供了全文檢索、同義詞處理、相關(guān)度排名、復(fù)雜數(shù)據(jù)分析、海量數(shù)據(jù)的近實(shí)時(shí)處理等功能
高可用
提供副本(Replica)機(jī)制,一個(gè)分片可以設(shè)置多個(gè)副本,假如某服務(wù)器宕機(jī)后,集群仍能正常工作。
開(kāi)箱即用
簡(jiǎn)易的運(yùn)維部署,提供基于Restful API,多種語(yǔ)言的SDK
那么我個(gè)人認(rèn)為Elasticsearch比較大的 缺點(diǎn) 只有 吃內(nèi)存,具體原因可以看下文內(nèi)存讀取部分。
—3—
Elasticsearch
我個(gè)人對(duì)于Elasticsearch快的原因主要總結(jié)三點(diǎn):
內(nèi)存讀取
多種索引
倒排索引
doc values
集群分片
內(nèi)存讀取
Elasticsearch是基于Lucene, 而Lucene被設(shè)計(jì)為可以利用操作系統(tǒng)底層機(jī)制來(lái)緩存內(nèi)存數(shù)據(jù)結(jié)構(gòu),換句話說(shuō)Elasticsearch是依賴于操作系統(tǒng)底層的 Filesystem Cache, 查詢時(shí),操作系統(tǒng)會(huì)將磁盤(pán)文件里的數(shù)據(jù)自動(dòng)緩存到 Filesystem Cache 里面去,因此要求Elasticsearch性能足夠高,那么就需要服務(wù)器的提供的足夠內(nèi)存給Filesystem Cache 覆蓋存儲(chǔ)的數(shù)據(jù)。
上一段最后一句話什么意思呢?
假如:Elasticsearch 節(jié)點(diǎn)有 3 臺(tái)服務(wù)器各64G內(nèi)存,3臺(tái)總內(nèi)存就是 64 * 3 = 192G。
每臺(tái)機(jī)器給 Elasticsearch jvm heap 是 32G,那么每服務(wù)器留給 Filesystem Cache 的就是 32G(50%),而集群里的 Filesystem Cache 的就是 32 * 3 = 96G 內(nèi)存。
此時(shí),在 3 臺(tái)Elasticsearch服務(wù)器共占用了 1T 的磁盤(pán)容量,那么每臺(tái)機(jī)器的數(shù)據(jù)量約等于 341G,意味著每臺(tái)服務(wù)器只有大概10分之1數(shù)據(jù)是緩存在內(nèi)存的,其余都得走硬盤(pán)。
說(shuō)到這里大家未必會(huì)有一個(gè)直觀得認(rèn)識(shí),因此我從《大型網(wǎng)站技術(shù)架構(gòu):核心原理與案例分析》第36頁(yè)摳了一張表格下來(lái):
操作
響應(yīng)時(shí)間
打開(kāi)一個(gè)網(wǎng)站
幾秒
在數(shù)據(jù)庫(kù)中查詢一條記錄(有索引)
十幾毫秒
機(jī)械磁盤(pán)一次尋址定位
4毫秒
從機(jī)械磁盤(pán)順序讀取1MB數(shù)據(jù)
2毫秒
從SSD磁盤(pán)順序讀取1MB數(shù)據(jù)
0.3毫秒
從遠(yuǎn)程分布式緩存redis讀取一個(gè)數(shù)據(jù)
0.5毫秒
從內(nèi)存中讀取1MB數(shù)據(jù)
十幾微秒
Java程序本地方法調(diào)用
幾微秒
網(wǎng)絡(luò)傳輸2KB數(shù)據(jù)
1微秒
從上圖加粗項(xiàng)看出,內(nèi)存讀取性能是機(jī)械磁盤(pán)的200倍,是SSD磁盤(pán)約等于30倍,假如讀一次Elasticsearch走內(nèi)存場(chǎng)景下耗時(shí)20毫秒,那么走機(jī)械硬盤(pán)就得4秒,走SSD磁盤(pán)可能約等于0.6秒。講到這里我相信大家對(duì)是否走內(nèi)存的性能差異有一個(gè)直觀的認(rèn)識(shí)。
對(duì)于Elasticsearch有很多種索引類(lèi)型,但是我認(rèn)為核心主要是倒排索引和doc values。
倒排索引
Lucene將寫(xiě)入索引的所有信息組織為倒排索引(inverted index)的結(jié)構(gòu)形式。倒排索引是一種將分詞映射到文檔的數(shù)據(jù)結(jié)構(gòu),可以認(rèn)為倒排索引是面向分詞的而不是面向文檔的。
假設(shè)在測(cè)試環(huán)境的Elasticsearch存放了有以下三個(gè)文檔:
Elasticsearch Server(文檔1)
Masterring Elasticsearch(文檔2)
Apache Solr 4 Cookbook(文檔3)
以上文檔索引建好后,簡(jiǎn)略顯示如下:
詞項(xiàng)
數(shù)量
文檔
4
1
<3>
Apache
1
<3>
Cookbook
1
<3>
Elasticsearch
2
<1><2>
Mastering
1
Server
1
Solr
1
<3>
如上表格所示,每個(gè)詞項(xiàng)指向該詞項(xiàng)所出現(xiàn)過(guò)的文檔位置,這種索引結(jié)構(gòu)允許快速、有效的搜索出數(shù)據(jù)。
—5—
doc values
對(duì)于分組、聚合、排序等某些功能來(lái)說(shuō),倒排索引的方式并不是最佳選擇,這類(lèi)功能操作的是文檔而不是詞項(xiàng),這個(gè)時(shí)候就得把倒排索引逆轉(zhuǎn)過(guò)來(lái)成正排索引,這么做會(huì)有兩個(gè)缺點(diǎn):
構(gòu)建時(shí)間長(zhǎng)
內(nèi)存占用大,易OutOfMemory,且影響垃圾回收
Lucene 4.0之后版本引入了doc values和額外的數(shù)據(jù)結(jié)構(gòu)來(lái)解決上面得問(wèn)題,目前有五種類(lèi)型的doc values:NUMERIC、BINARY、SORTED、SORTED_SET、SORTED_NUMERIC,針對(duì)每種類(lèi)型Lucene都有特定的壓縮方法。
doc values是列式存儲(chǔ)的正排索引,通過(guò)docID可以快速讀取到該doc的特定字段的值,列式存儲(chǔ)存儲(chǔ)對(duì)于聚合計(jì)算有非常高的性能。
—6—
集群分片
Elasticsearch可以簡(jiǎn)單、快速利用多節(jié)點(diǎn)服務(wù)器形成集群,以此分?jǐn)偡?wù)器的執(zhí)行壓力。
此外數(shù)據(jù)可以進(jìn)行分片存儲(chǔ),搜索時(shí)并發(fā)到不同服務(wù)器上的主分片進(jìn)行搜索。
這里可以簡(jiǎn)單講述下Elasticsearch查詢?cè)恚珽lasticsearch的查詢分兩個(gè)階段:分散階段與合并階段。
任意一個(gè)Elasticsearch節(jié)點(diǎn)都可以接受客戶端的請(qǐng)求。接受到請(qǐng)求后,就是分散階段,并行發(fā)送子查詢給其他節(jié)點(diǎn); 然后是合并階段,則從眾多分片中收集返回結(jié)果,然后對(duì)他們進(jìn)行合并、排序、取長(zhǎng)等后續(xù)操作。 最終將結(jié)果返回給客戶端。
機(jī)制如下圖:
—7—
分頁(yè)深度陷阱
基于以上查詢的原理,擴(kuò)展一個(gè)分頁(yè)深度的問(wèn)題。
現(xiàn)需要查頁(yè)長(zhǎng)為10、第100頁(yè)的數(shù)據(jù),實(shí)際上是會(huì)把每個(gè) Shard 上存儲(chǔ)的前 1000(10*100) 條數(shù)據(jù)都查到一個(gè)協(xié)調(diào)節(jié)點(diǎn)上。
如果有 5 個(gè) Shard,那么就有 5000 條數(shù)據(jù),接著協(xié)調(diào)節(jié)點(diǎn)對(duì)這 5000 條數(shù)據(jù)進(jìn)行一些合并、處理,再獲取到最終第 100 頁(yè)的 10 條數(shù)據(jù)。也就是實(shí)際上查的數(shù)據(jù)總量為pageSize*pageIndex*shard,頁(yè)數(shù)越深則查詢的越慢。
因此ElasticSearch也會(huì)有要求,每次查詢出來(lái)的數(shù)據(jù)總數(shù)不會(huì)返回超過(guò)10000條。
那么從業(yè)務(wù)上盡可能跟產(chǎn)品溝通避免分頁(yè)跳轉(zhuǎn),使用滾動(dòng)加載。而Elasticsearch使用的相關(guān)技術(shù)是search_after、scroll_id。
ElasticSearch與數(shù)據(jù)庫(kù)基本概念對(duì)比
ElasticSearch
RDBMS
Index
Document
Field
MApping
表結(jié)構(gòu)
在Elasticsearch 7.0版本之前(<7.0),有type的概念,而elasticsearch和關(guān)系型數(shù)據(jù)庫(kù)的關(guān)系是,index type="table,但是在Elasticsearch">=7.0)弱化了type默認(rèn)為_(kāi)doc,而官方會(huì)在8.0之后會(huì)徹底移除type。
—8—
服務(wù)器選型
在官方文檔(https://www.elastic.co/guide/cn/elasticsearch/guide/current/heap-sizing.html)里建議Elasticsearch JVM Heap最大為32G,同時(shí)不超過(guò)服務(wù)器內(nèi)存的一半, 也就是說(shuō)內(nèi)存分別為128G和64G的服務(wù)器,JVM Heap最大只需要設(shè)置32G; 而32G服務(wù)器,則建議JVM Heap最大16G,剩余的內(nèi)存將會(huì)給到Filesystem Cache充分使用。
如果不需要對(duì)分詞字符串做聚合計(jì)算(例如,不需要 fielddata )可以考慮降低JVM Heap。JVM Heap越小,會(huì)導(dǎo)致Elasticsearch的GC頻率更高,但Lucene就可以的使用更多的內(nèi)存,這樣性能就會(huì)更高。
對(duì)于我們公司的未來(lái)新增業(yè)務(wù)還會(huì)有收集用戶的訪問(wèn)記錄來(lái)統(tǒng)計(jì)PV(page view)、UV(user view),有一定的聚合計(jì)算,經(jīng)過(guò)多方便的考慮與討論,平衡成本與需求后選擇了騰訊云的三臺(tái)配置為CPU 16核、內(nèi)存64G,SSD云硬盤(pán)的服務(wù)器,并給與Elasticsearch 配置JVM Heap = 32G。
—9—
需求場(chǎng)景選擇
Elasticsearch在本公司系統(tǒng)的可使用場(chǎng)景非常多,但是作為第一次引入因慎重選擇,給與開(kāi)發(fā)與運(yùn)維一定的時(shí)間熟悉與觀察。
經(jīng)過(guò)商討,選擇了兩個(gè)業(yè)務(wù)場(chǎng)景,用戶閱讀作品的記錄明細(xì)與作品搜索,選擇這兩個(gè)業(yè)務(wù)場(chǎng)景原因如下:
寫(xiě)場(chǎng)景
我們平臺(tái)的用戶黏度比較高,閱讀作品是一個(gè)高頻率的調(diào)用,因此用戶閱讀作品的記錄明細(xì)可在短時(shí)間內(nèi)造成海量數(shù)據(jù)的場(chǎng)景。(現(xiàn)一個(gè)月已達(dá)到了70G的數(shù)據(jù)量,共1億1千萬(wàn)條)
讀場(chǎng)景
閱讀記錄需提供給未來(lái)新增的抽獎(jiǎng)業(yè)務(wù)使用,可從閱讀章節(jié)數(shù)、閱讀時(shí)長(zhǎng)等進(jìn)行搜索計(jì)算。
作品搜索原有實(shí)現(xiàn)是通過(guò)關(guān)系型數(shù)據(jù)庫(kù)like查詢,已是具有潛在的性能問(wèn)題與資源消耗的業(yè)務(wù)場(chǎng)景
對(duì)于上述兩個(gè)業(yè)務(wù),用戶閱讀作品的記錄明細(xì)與抽獎(jiǎng)業(yè)務(wù)屬于新增業(yè)務(wù),對(duì)于在投入成本相對(duì)較少,也無(wú)需過(guò)多的需要兼容舊業(yè)務(wù)的壓力。
而作品搜索業(yè)務(wù)屬于優(yōu)化改造,得保證兼容原有的用戶搜索習(xí)慣前提下,新增拼音搜索。同時(shí)最好以擴(kuò)展的方式,盡可能的減少代碼修改范圍,如果使用效果不好,隨時(shí)可以回滾到舊的實(shí)現(xiàn)方式。
—10—
設(shè)計(jì)方案
共性設(shè)計(jì)
我使用.Net 5 WebApi將Elasticsearch封裝成ES業(yè)務(wù)服務(wù)API,這樣的做法主要用來(lái)隱藏技術(shù)細(xì)節(jié)(時(shí)區(qū)、分詞器、類(lèi)型轉(zhuǎn)換等),暴露粗粒度的讀寫(xiě)接口。
這種做法在馬丁福勒所著的《NoSQL精粹》稱(chēng)把數(shù)據(jù)庫(kù)視為“應(yīng)用程序數(shù)據(jù)庫(kù)”,簡(jiǎn)單來(lái)說(shuō)就是只能通過(guò)應(yīng)用間接的訪問(wèn)存儲(chǔ),對(duì)于這個(gè)應(yīng)用由一個(gè)團(tuán)隊(duì)負(fù)責(zé)維護(hù)開(kāi)發(fā),也只有這個(gè)團(tuán)隊(duì)才知道存儲(chǔ)的結(jié)構(gòu)。
這樣通過(guò)封裝的API服務(wù)解耦了外部API服務(wù)與存儲(chǔ),調(diào)用方就無(wú)需過(guò)多關(guān)注存儲(chǔ)的特性,像Mongodb與Elasticsearch這種無(wú)模式的存儲(chǔ),無(wú)需優(yōu)先定義結(jié)構(gòu),換而言之就是對(duì)于存儲(chǔ)已有結(jié)構(gòu)可隨意修改擴(kuò)展,那么“應(yīng)用程序數(shù)據(jù)庫(kù)”的做法也避免了其他團(tuán)隊(duì)無(wú)意侵入的修改。
考慮到現(xiàn)在業(yè)務(wù)需求復(fù)雜度相對(duì)簡(jiǎn)單,MQ消費(fèi)端也一起集成到ES業(yè)務(wù)服務(wù),若后續(xù)MQ消費(fèi)業(yè)務(wù)持續(xù)增多,再考慮把MQ消費(fèi)業(yè)務(wù)抽離到一個(gè)(或多個(gè)的)消費(fèi)端進(jìn)程。
目前以 同步讀、同步寫(xiě)、異步寫(xiě) 的三種交互方式,進(jìn)行與其他服務(wù)通信。
—11—
閱讀記錄明細(xì)
本需求是完全新增,因此引入相對(duì)簡(jiǎn)單,只需要在【平臺(tái)API】使用【RabbitMQ】進(jìn)行解耦,使用異步方式寫(xiě)入Elasticsearch,使用隊(duì)列除了用來(lái)解耦,還對(duì)此用來(lái)緩沖高并發(fā)寫(xiě)壓力的情況。
對(duì)于后續(xù)新增的業(yè)務(wù)例如抽獎(jiǎng)服務(wù),則只需要通過(guò)RPC框架對(duì)接ES業(yè)務(wù)API,以同步讀取的方式查詢數(shù)據(jù)。
—12—
作品搜索
對(duì)于該業(yè)務(wù),我第一反應(yīng)采用CQRS的思想,原有的寫(xiě)入邏輯我無(wú)需過(guò)多的關(guān)注與了解,因此我只需要想辦法把關(guān)系型數(shù)據(jù)庫(kù)的數(shù)據(jù)同步到Elasticsearch,然后提供業(yè)務(wù)查詢API替換原有平臺(tái)API的數(shù)據(jù)源即可。
那么數(shù)據(jù)同步則一般都是分推和拉兩種方式。
推的實(shí)時(shí)性無(wú)疑是比拉要高,只需增量的推送做寫(xiě)入的數(shù)據(jù)(增、刪、改)即可,無(wú)論是從性能、資源利用、時(shí)效各方面來(lái)看都比拉更有效。
實(shí)施該方案,可以選擇Debezium和SQL Server開(kāi)啟CDC功能。
Debezium由RedHat開(kāi)源的,同時(shí)需要依賴于kafka的,一個(gè)將多種數(shù)據(jù)源實(shí)時(shí)變更數(shù)據(jù)捕獲,形成數(shù)據(jù)流輸出的開(kāi)源工具,同類(lèi)產(chǎn)品有Canal, DataBus, Maxwell。
CDC全稱(chēng)Change Data Capture,直接翻譯過(guò)來(lái)為變更數(shù)據(jù)捕獲,核心為監(jiān)測(cè)服務(wù)捕獲數(shù)據(jù)庫(kù)的寫(xiě)操作(插入,更新,刪除),將這些變更按發(fā)生的順序完整記錄下來(lái)。
我個(gè)人在我博客文章多次強(qiáng)調(diào)架構(gòu)設(shè)計(jì)的輸入核心為兩點(diǎn):滿足需求與組織架構(gòu),在滿足需求的前提應(yīng)優(yōu)先選擇簡(jiǎn)單、合適的方案。技術(shù)選型應(yīng)需要考慮自己的團(tuán)隊(duì)是否可以支撐。
在上述無(wú)論是額外加入Debezium和kafka,還是需要針對(duì)SQL Server開(kāi)啟CDC都超出了我們運(yùn)維所能承受的極限,引入新的中間件和技術(shù)是需要試錯(cuò)的,而試錯(cuò)是需要額外高的成本,在未知的情況下引入更多的未知,只會(huì)造成更大的成本和不可控。
拉無(wú)疑是最簡(jiǎn)單最合適的實(shí)現(xiàn)方式,只需要使用調(diào)度任務(wù)服務(wù),每隔段時(shí)間定時(shí)去從數(shù)據(jù)庫(kù)拉取數(shù)據(jù)寫(xiě)入到Elasticsearch就可。
然而拉取數(shù)據(jù),分全量同步與增量同步:
對(duì)于增量同步,只需要每次查詢數(shù)據(jù)源Select * From Table_A Where RowVersion > LastUpdateVersion,則可以過(guò)濾出需要同步的數(shù)據(jù)。
但是這個(gè)方式有點(diǎn)致命的缺點(diǎn),數(shù)據(jù)源已被刪除的數(shù)據(jù)是無(wú)法查詢出來(lái)的,如果把Elasticsearch反向去跟SQL Server數(shù)據(jù)做對(duì)比又是一件比較愚蠢的方式,因此只能放棄該方式。
而全量同步,只要每次從SQL Server數(shù)據(jù)源全量新增到Elasticsearch,并替換舊的Elasticsearch的Index,因此該方案得全刪全增。
但是這里又引申出新的問(wèn)題,如果先刪后增,那么在刪除后再新增的這段真空期怎么辦?
假如有5分鐘的真空期是沒(méi)有數(shù)據(jù),用戶就無(wú)法使用搜索功能。那么只能先增后刪,先新增到一個(gè)Index_Temp,全量新增完后,把原有Index改名成Index_Delete,然后再把Index_Temp改成Index,最后把Index_Delete刪除。
這么一套操作下來(lái),有沒(méi)有覺(jué)得很繁瑣很費(fèi)勁?Elasticsearch有一個(gè)叫別名(Aliases)的功能,別名可以一對(duì)多的指向多個(gè)Index,也可以以原子性的進(jìn)行別名指向Index的切換,具體實(shí)現(xiàn)可以看下文。
—13—
記錄細(xì)節(jié)
實(shí)體定義
優(yōu)先定義了個(gè)抽象類(lèi)ElasticsearchEntity進(jìn)行復(fù)用,對(duì)于實(shí)體定義有三個(gè)注意的細(xì)節(jié)點(diǎn):
1.對(duì)于ElasticsearchEntity我定義兩個(gè)屬性_id與Timestamp,Elasticsearch是無(wú)模式的(無(wú)需預(yù)定義結(jié)構(gòu)),如果實(shí)體本身沒(méi)有_id,寫(xiě)入到Elasticsearch會(huì)自動(dòng)生成一個(gè)_id,為了后續(xù)的使用便捷性,我仍然自主定義了一個(gè)。
2.基于上述的分頁(yè)深度的問(wèn)題,因此在后續(xù)涉及的業(yè)務(wù)盡可能會(huì)以search_after+滾動(dòng)加載的方式落實(shí)到我們的業(yè)務(wù)。
原本我們只需要使用DateTime類(lèi)型的字段用DateTime.Now記錄后,再使用search_after后會(huì)自動(dòng)把DateTime類(lèi)型字段轉(zhuǎn)換成毫秒級(jí)的Timestamp, 但是我在實(shí)現(xiàn)demo的時(shí)候,去制造數(shù)據(jù),在程序里以for循環(huán)new數(shù)據(jù)的時(shí)候,發(fā)現(xiàn)生成的速度會(huì)在微秒級(jí)之間,那么假設(shè)用毫秒級(jí)的Timestamp進(jìn)行search_after過(guò)濾,同一個(gè)毫秒有4、5條數(shù)據(jù),那么容易在使用滾動(dòng)加載時(shí)候少加載了幾條數(shù)據(jù),這樣就到導(dǎo)致數(shù)據(jù)返回不準(zhǔn)確了。
因此我擴(kuò)展了個(gè)[DateTime.Now.DateTimeToTimestampOfMicrosecond()]生成微秒級(jí)的Timestamp,以此盡可能減少出現(xiàn)漏加載數(shù)據(jù)的情況。
3.對(duì)于Elasticsearch的操作實(shí)體的日期時(shí)間類(lèi)型均以DateTimeOffset類(lèi)型聲明,因?yàn)镋lasticsearch存儲(chǔ)的是UTC時(shí)間,而且會(huì)因?yàn)镠ttp請(qǐng)求的日期格式不同導(dǎo)致存放的日期時(shí)間也會(huì)有所偏差,為了避免日期問(wèn)題使用DateTimeOffset類(lèi)型是一種保險(xiǎn)的做法。
而對(duì)于WebAPI 接口或者M(jìn)Q的Message接受的時(shí)間類(lèi)型可以使用DateTime類(lèi)型,DTO(傳輸對(duì)象)與DO(持久化對(duì)象)使用Mapster或者AutoMapper類(lèi)似的對(duì)象映射工具進(jìn)行轉(zhuǎn)換即可。
注意DateTimeOffset轉(zhuǎn)DateTime得定義轉(zhuǎn)換規(guī)則 [TypeAdapterConfig .NewConfig().MapWith(dateTimeOffset=> dateTimeOffset.LocalDateTime)]。
如此一來(lái),把Elasticsearch操作細(xì)節(jié)隱藏在WebAPI里,以友好、簡(jiǎn)單的接口暴露給開(kāi)發(fā)者使用,降低了開(kāi)發(fā)者對(duì)技術(shù)細(xì)節(jié)認(rèn)知負(fù)擔(dān)。
[ElasticsearchType(RelationName = "user_view_duration")] public class UserViewDuration : ElasticsearchEntity { ///
/// 作品ID ///[Number(NumberType.Long, Name = "entity_id")] public long EntityId { get; set; } ////// 作品類(lèi)型 ///[Number(NumberType.Long, Name = "entity_type")] public long EntityType { get; set; } ////// 章節(jié)ID ///[Number(NumberType.Long, Name = "charpter_id")] public long CharpterId { get; set; } ////// 用戶ID ///[Number(NumberType.Long, Name = "user_id")] public long UserId { get; set; } ////// 創(chuàng)建時(shí)間 ///[Date(Name = "create_datetime")] public DateTimeOffset CreateDateTime { get; set; } ////// 時(shí)長(zhǎng) ///[Number(NumberType.Long, Name = "duration")] public long Duration { get; set; } ////// IP ///[Ip(Name = "Ip")] public string Ip { get; set; } }br
public abstract class ElasticsearchEntity { private Guid? _id; public Guid Id { get { _id ??= Guid.NewGuid(); return _id.Value; } set => _id = value; } private long? _timestamp; [Number(NumberType.Long, Name = "timestamp")] public long Timestamp { get { _timestamp ??= DateTime.Now.DateTimeToTimestampOfMicrosecond(); return _timestamp.Value; } set => _timestamp = value; } }
br
—14—
異步寫(xiě)入
對(duì)于異步寫(xiě)入有兩個(gè)細(xì)節(jié)點(diǎn):
1.該數(shù)據(jù)從RabbtiMQ訂閱消費(fèi)寫(xiě)入到Elasticsearch,從下面代碼可以看出,我刻意以月的維度建立Index,格式為 userviewrecord-2021-12,這么做的目的是為了方便管理Index和資源利用,有需要的情況下會(huì)刪除舊的Index。
2.消息訂閱與WebAPI暫時(shí)集成到同一個(gè)進(jìn)程,這樣做主要是開(kāi)發(fā)、部署都方便,如果后續(xù)訂閱多了,在把消息訂閱相關(guān)的業(yè)務(wù)抽離到獨(dú)立的進(jìn)程。
按需演變,避免過(guò)度設(shè)計(jì)
訂閱消費(fèi)邏輯
public class UserViewDurationConsumer : BaseConsumer { private readonly ElasticClient _elasticClient; public UserViewDurationConsumer(ElasticClient elasticClient) { _elasticClient = elasticClient; } public override void Excute(UserViewDurationMessage msg) { var document = msg.MapTo (); var result = _elasticClient.Create(document, a => a.Index(typeof(Entity.UserViewDuration).GetRelationName() + "-" + msg.CreateDateTime.ToString("yyyy-MM"))).GetApiResult(); if (result.Failed) LoggerHelper.WriteToFile(result.Message); } }
br
/// 訂閱消費(fèi) ///public static class ConsumerExtension { public static IApplicationBuilder UseSubscribe (this IApplicationBuilder appBuilder, IHostApplicationLifetime lifetime) where T : EasyNetQEntity, new() where TConsumer : BaseConsumer { var bus = appBuilder.ApplicationServices.GetRequiredService (); var consumer = appBuilder.ApplicationServices.GetRequiredService (); lifetime.ApplicationStarted.Register(() => { bus.Subscribe (msg => consumer.Excute(msg)); }); lifetime.ApplicationStopped.Register(() => bus?.Dispose()); return appBuilder; } }br
訂閱與注入
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { ...... } public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime) { app.UseAllElasticApm(Configuration); app.UseHealthChecks("/health"); app.UseDeveloperExceptionPage(); app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "SF.ES.Api v1"); c.RoutePrefix = ""; }); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); app.UseSubscribe (lifetime); } }
br
查詢接口
查詢接口此處有兩個(gè)細(xì)節(jié)點(diǎn):
如果不確定月份,則使用通配符查詢userviewrecord-*,當(dāng)然有需要的也可以使用別名處理。
因?yàn)镋lasticsearch是記錄UTC時(shí)間,因此時(shí)間查詢得指定TimeZone。
[HttpGet] [Route("record")] public ApiResult > GetRecord([FromQuery] UserViewDurationRecordGetRequest request) { var dataList = new List (); string dateTime; if (request.BeginDateTime.HasValue && request.EndDateTime.HasValue) { var month = request.EndDateTime.Value.DifferMonth(request.BeginDateTime.Value); if(month <= 0 ) dateTime = request.BeginDateTime.Value.ToString("yyyy-MM"); else dateTime = "*"; } else dateTime = "*"; var mustQuerys = new List , QueryContainer>>(); if (request.UserId.HasValue) mustQuerys.Add(a => a.Term(t => t.Field(f => f.UserId).Value(request.UserId.Value))); if (request.EntityType.HasValue) mustQuerys.Add(a => a.Term(t => t.Field(f => f.EntityType).Value(request.EntityType))); if (request.EntityId.HasValue) mustQuerys.Add(a => a.Term(t => t.Field(f => f.EntityId).Value(request.EntityId.Value))); if (request.CharpterId.HasValue) mustQuerys.Add(a => a.Term(t => t.Field(f => f.CharpterId).Value(request.CharpterId.Value))); if (request.BeginDateTime.HasValue) mustQuerys.Add(a => a.DateRange(dr => dr.Field(f => f.CreateDateTime).GreaterThanOrEquals(request.BeginDateTime.Value).TimeZone(EsConst.TimeZone))); if (request.EndDateTime.HasValue) mustQuerys.Add(a => a.DateRange(dr => dr.Field(f => f.CreateDateTime).LessThanOrEquals(request.EndDateTime.Value).TimeZone(EsConst.TimeZone))); var searchResult = _elasticClient.Search (a => a.Index(typeof(UserViewDuration).GetRelationName() + "-" + dateTime) .Size(request.Size) .Query(q => q.Bool(b => b.Must(mustQuerys))) .SearchAfterTimestamp(request.Timestamp) .Sort(s => s.Field(f => f.Timestamp, SortOrder.Descending))); var apiResult = searchResult.GetApiResult >(); if (apiResult.Success) dataList.AddRange(apiResult.Data); return ApiResult >.IsSuccess(dataList); }
br
—15—
作品搜索
實(shí)體定義
SearchKey是原有SQL Server的數(shù)據(jù),現(xiàn)需要同步到Elasticsearch,仍是繼承抽象類(lèi)ElasticsearchEntity實(shí)體定義,同時(shí)這里有三個(gè)細(xì)節(jié)點(diǎn):
1. public string KeyName,我定義的是Text類(lèi)型,在Elasticsearch使用Text類(lèi)型才會(huì)分詞。
2.在實(shí)體定義我沒(méi)有給KeyName指定分詞器,因?yàn)槲視?huì)使用兩個(gè)分詞器:拼音和默認(rèn)分詞,而我會(huì)在批量寫(xiě)入數(shù)據(jù)創(chuàng)建Mapping時(shí)定義。
3.實(shí)體里的public List SysTagId 與SearchKey在SQL Server是兩張不同的物理表,是一對(duì)多的關(guān)系,在代碼表示如下, 但是在關(guān)系型數(shù)據(jù)庫(kù)是無(wú)法與之對(duì)應(yīng)和體現(xiàn)的,這就是咱們所說(shuō)的“阻抗失配”,但是能在以文檔型存儲(chǔ)系統(tǒng)(MongoDB、Elasticsearch)里很好的解決這個(gè)問(wèn)題,可以以一個(gè)聚合的方式寫(xiě)入,避免多次查詢關(guān)聯(lián)。
[ElasticsearchType(RelationName = "search_key")] public class SearchKey : ElasticsearchEntity { [Number(NumberType.Integer, Name = "key_id")] public int KeyId { get; set; } [Number(NumberType.Integer, Name = "entity_id")] public int EntityId { get; set; } [Number(NumberType.Integer, Name = "entity_type")] public int EntityType { get; set; } [Text(Name = "key_name")] public string KeyName { get; set; } [Number(NumberType.Integer, Name = "weight")] public int Weight { get; set; } [Boolean(Name = "is_subsidiary")] public bool IsSubsidiary { get; set; } [Date(Name = "active_date")] public DateTimeOffset? ActiveDate { get; set; } [Number(NumberType.Integer, Name = "sys_tag_id")] public List SysTagId { get; set; } }
br
數(shù)據(jù)同步
數(shù)據(jù)同步我采用了Quartz.Net定時(shí)調(diào)度任務(wù)框架,因此時(shí)效不高,所以每4小時(shí)同步一次即可,有42W多的數(shù)據(jù),分批進(jìn)行同步,每次查詢1000條數(shù)據(jù)同時(shí)進(jìn)行一次批量寫(xiě)入。全量同步一次的時(shí)間大概2分鐘。因此使用RPC調(diào)用[ES業(yè)務(wù)API服務(wù)]。
因?yàn)榫唧w業(yè)務(wù)邏輯已經(jīng)封裝在[ES業(yè)務(wù)API服務(wù)],因此同步邏輯也相對(duì)簡(jiǎn)單,查詢出SQL Server數(shù)據(jù)源、聚合整理、調(diào)用[ES業(yè)務(wù)API服務(wù)]的批量寫(xiě)入接口、重新綁定別名到新的Index。
[DisallowConcurrentExecution] public class SearchKeySynchronousJob : BaseJob { public override void Execute() { var rm = SFNovelReadManager.Instance(); var maxId = 0; var size = 1000; string indexName = ""; while (true) { //避免一次性全部查詢出來(lái),每1000條一次寫(xiě)入。var searchKey = sm.searchKey.GetList(size, maxId); if (!searchKey.Any()) break; var entityIds = searchKey.Select(a => a.EntityID).Distinct().ToList(); var sysTagRecord = rm.Novel.GetSysTagRecord(entityIds); var items = searchKey.Select(a => new SearchKeyPostItem { Weight = a.Weight, EntityType = a.EntityType, EntityId = a.EntityID, IsSubsidiary = a.IsSubsidiary ?? false, KeyName = a.KeyName, ActiveDate = a.ActiveDate, SysTagId = sysTagRecord.Where(c => c.EntityID == a.EntityID).Select(c => c.SysTagID).ToList(), KeyID = a.KeyID }).ToList(); //以一個(gè)聚合寫(xiě)入到ES var postResult = new SearchKeyPostRequest { IndexName = indexName, Items = items }.Excute(); if (postResult.Success) { indexName = (string)postResult.Data; maxId = searchKey.Max(a => a.KeyID); } } //別名從舊Index指向新的Index,最后刪除舊Index var renameResult = new SearchKeyRenameRequest { IndexName = indexName }.Excute(); } }}
br
業(yè)務(wù)API接口
批量新增接口這里有2個(gè)細(xì)節(jié)點(diǎn):
1.在第一次有數(shù)據(jù)進(jìn)來(lái)的時(shí)候需要?jiǎng)?chuàng)建Mapping,因?yàn)榈脤?duì)KeyName字段定義分詞器,其余字段都可以使用AutoMap即可。
2.新創(chuàng)建的Index名稱(chēng)是精確到秒的 SearchKey-202112261121
/// 批量新增作品搜索列表(返回創(chuàng)建的indexName) ///////// [HttpPost] public ApiResult Post(SearchKeyPostRequest request) { if (!request.Items.Any()) return ApiResult.IsFailed("無(wú)傳入數(shù)據(jù)"); var date = DateTime.Now; var relationName = typeof(SearchKey).GetRelationName(); var indexName = request.IndexName.IsNullOrWhiteSpace() ? (relationName + "-" + date.ToString("yyyyMMddHHmmss")) : request.IndexName; if (request.IndexName.IsNullOrWhiteSpace()) { var createResult = _elasticClient.Indices.Create(indexName, a => a.Map (m => m.AutoMap().Properties(p => p.Custom(new TextProperty { Name = "key_name", Analyzer = "standard", Fields = new Properties(new Dictionary { { new PropertyName("pinyin"),new TextProperty{ Analyzer = "pinyin"} }, { new PropertyName("standard"),new TextProperty{ Analyzer = "standard"} } }) })))); if (!createResult.IsValid && request.IndexName.IsNullOrWhiteSpace()) return ApiResult.IsFailed("創(chuàng)建索引失敗"); } var document = request.Items.MapTo >(); var result = _elasticClient.BulkAll(indexName, document); return result ? ApiResult.IsSuccess(data: indexName) : ApiResult.IsFailed(); }br
重新綁定別名接口這里有4個(gè)細(xì)節(jié)點(diǎn):
1.別名使用searchkey,只會(huì)有一個(gè)Index[searchkey-yyyyMMddHHmmss]會(huì)跟searchkey綁定。
2.優(yōu)先把已綁定的Index查詢出來(lái),方便解綁與刪除。
3.別名綁定在Elasticsearch雖然是原子性的,但是不是數(shù)據(jù)一致性的,因此得先Add后Remove。
4.刪除舊得Index免得占用過(guò)多資源。
/// 重新綁定別名 ////// [HttpPut] public ApiResult Rename(SearchKeyRanameRequest request) { var aliasName = typeof(SearchKey).GetRelationName(); var getAliasResult = _elasticClient.Indices.GetAlias(aliasName); //給新index指定別名 var bulkAliasRequest = new BulkAliasRequest { Actions = new List { new AliasAddDescriptor().Index(request.IndexName).Alias(aliasName) } }; //移除別名里舊的索引 if (getAliasResult.IsValid) { var indeNameList = getAliasResult.Indices.Keys; foreach (var indexName in indeNameList) { bulkAliasRequest.Actions.Add(new AliasRemoveDescriptor().Index(indexName.Name).Alias(aliasName)); } } var result = _elasticClient.Indices.BulkAlias(bulkAliasRequest); //刪除舊的index if (getAliasResult.IsValid) { var indeNameList = getAliasResult.Indices.Keys; foreach (var indexName in indeNameList) { _elasticClient.Indices.Delete(indexName); } } return result != null && result.ApiCall.Success ? ApiResult.IsSuccess() : ApiResult.IsFailed(); }br
查詢接口這里跟前面細(xì)節(jié)得差不多:
但是這里有一個(gè)得特別注意的點(diǎn),可以看到這個(gè)查詢接口同時(shí)使用了should和must,這里得設(shè)置minimumShouldMatch才能正常像SQL過(guò)濾。
should可以理解成SQL的Or,Must可以理解成SQL的And。
默認(rèn)情況下minimumShouldMatch是等于0的,等于0的意思是,should不命中任何的數(shù)據(jù)仍然會(huì)返回must命中的數(shù)據(jù), 也就是你們可能想搜索(keyname.pinyin=’chengong‘ or keyname.standard=’chengong‘) and id > 0,但是es里沒(méi)有存keyname='chengong'的數(shù)據(jù),會(huì)把id> 0 而且 keyname != 'chengong' 數(shù)據(jù)給查詢出來(lái)。
因此我們得對(duì)minimumShouldMatch=1,就是should條件必須得任意命中一個(gè)才能返回結(jié)果。
在should和must混用的情況下必須得注意minimumShouldMatch的設(shè)置!
///
/// 作品搜索列表 ///
////// [HttpPost] [Route("search")] public ApiResult > Get(SearchKeyGetRequest request) { var shouldQuerys = new List , QueryContainer>>(); int minimumShouldMatch = 0; if (!request.KeyName.IsNullOrWhiteSpace()) { shouldQuerys.Add(a => a.MatchPhrase(m => m.Field("key_name.pinyin").Query(request.KeyName))); shouldQuerys.Add(a => a.MatchPhrase(m => m.Field("key_name.standard").Query(request.KeyName))); minimumShouldMatch = 1; } var mustQuerys = new List , QueryContainer>> { a => a.Range(t => t.Field(f => f.Weight).GreaterThanOrEquals(0)) }; if (request.IsSubsidiary.HasValue) mustQuerys.Add(a => a.Term(t => t.Field(f => f.IsSubsidiary).Value(request.IsSubsidiary.Value))); if (request.SysTagIds != null && request.SysTagIds.Any()) mustQuerys.Add(a => a.Terms(t => t.Field(f => f.SysTagId).Terms(request.SysTagIds))); if (request.EntityType.HasValue) { if (request.EntityType.Value == ESearchKey.EntityType.AllNovel) { mustQuerys.Add(a => a.Terms(t => t.Field(f => f.EntityType).Terms(ESearchKey.EntityType.Novel, ESearchKey.EntityType.ChatNovel, ESearchKey.EntityType.FanNovel))); } else mustQuerys.Add(a => a.Term(t => t.Field(f => f.EntityType).Value((int)request.EntityType.Value))); } var sortDescriptor = new SortDescriptor (); sortDescriptor = request.Sort == ESearchKey.Sort.Weight ? sortDescriptor.Field(f => f.Weight, SortOrder.Descending) : sortDescriptor.Field(f => f.ActiveDate, SortOrder.Descending); var searchResult = _elasticClient.Search (a => a.Index(typeof(SearchKey).GetRelationName()) .From(request.Size * request.Page) .Size(request.Size) .Query(q => q.Bool(b => b.Should(shouldQuerys).Must(mustQuerys).MinimumShouldMatch(minimumShouldMatch))) .Sort(s => sortDescriptor)); var apiResult = searchResult.GetApiResult >(); if (apiResult.Success) return apiResult; return ApiResult >.IsSuccess("空集合數(shù)據(jù)");
br
—16—
APM監(jiān)控
雖然在上面我做了足夠的實(shí)現(xiàn)準(zhǔn)備,但是對(duì)于上生產(chǎn)后的實(shí)際使用效果我還是希望有一個(gè)直觀的體現(xiàn)。我之前寫(xiě)了一篇文章《.Net微服務(wù)實(shí)戰(zhàn)之可觀測(cè)性》很好敘述了該種情況,有興趣的可以移步去看看。
在之前公司做微服務(wù)的時(shí)候的APM選型我們使用了Skywalking,但是現(xiàn)在這家公司的運(yùn)維沒(méi)有接觸過(guò),但是對(duì)于Elastic Stack他相對(duì)比較熟悉,如同上文所說(shuō)架構(gòu)設(shè)計(jì)的輸入核心為兩點(diǎn):滿足需求與組織架構(gòu),秉著我的技術(shù)選型原則是基于團(tuán)隊(duì)架構(gòu),我們采用了Elastic APM + Kibana(7.4版本),如下圖所示:
—17—
結(jié)尾
最后上生產(chǎn)的時(shí)候也是平滑無(wú)損的切換到Elasticsearch,總體情況都十分滿意。