自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

老板要搞微服務(wù),只能硬著頭皮上了...

開發(fā) 架構(gòu) 開發(fā)工具
微服務(wù)越來(lái)越火。很多互聯(lián)網(wǎng)公司,甚至一些傳統(tǒng)行業(yè)的系統(tǒng)都采用了微服務(wù)架構(gòu)。

 微服務(wù)越來(lái)越火。很多互聯(lián)網(wǎng)公司,甚至一些傳統(tǒng)行業(yè)的系統(tǒng)都采用了微服務(wù)架構(gòu)。

[[330807]]

 

圖片來(lái)自 Pexels

體會(huì)到微服務(wù)帶來(lái)好處的同時(shí),很多公司也明顯感受到微服務(wù)化帶來(lái)的一系列讓人頭疼的問(wèn)題。

本文是筆者對(duì)自己多年微服務(wù)化經(jīng)歷的總結(jié)。如果你正準(zhǔn)備做微服務(wù)轉(zhuǎn)型,或者在微服務(wù)化過(guò)程中遇到了困難。此文很可能會(huì)幫到你!

正文開始前,為了讓各位讀友更好的理解本文內(nèi)容,先花兩分鐘了解一下微服務(wù)的優(yōu)缺點(diǎn)。

聊起微服務(wù),很多朋友都了解微服務(wù)帶來(lái)的好處,羅列幾點(diǎn):

  • 模塊化,降低耦合。將單體應(yīng)用按業(yè)務(wù)模塊拆分成多個(gè)服務(wù),如果某個(gè)功能需要改動(dòng),大多數(shù)情況,我們只需要弄清楚并改動(dòng)對(duì)應(yīng)的服務(wù)即可。

只改動(dòng)一小部分就能滿足要求,降低了其他業(yè)務(wù)模塊受影響的可能性。從而降低了業(yè)務(wù)模塊間的耦合性。

  • 屏蔽與自身業(yè)務(wù)無(wú)關(guān)技術(shù)細(xì)節(jié)。例如,很多業(yè)務(wù)需要查詢用戶信息,在單體應(yīng)用的情況下,所有業(yè)務(wù)場(chǎng)景都通過(guò) DAO 去查詢用戶信息,隨著業(yè)務(wù)發(fā)展,并發(fā)量增加,用戶信息需要加緩存。

這樣所有業(yè)務(wù)場(chǎng)景都需要關(guān)注緩存,微服務(wù)化之后,緩存由各自服務(wù)維護(hù),其他服務(wù)調(diào)用相關(guān)服務(wù)即可,不需要關(guān)注類似的緩存問(wèn)題。

  • 數(shù)據(jù)隔離,避免不同業(yè)務(wù)模塊間的數(shù)據(jù)耦合。不同的服務(wù)對(duì)應(yīng)不同數(shù)據(jù)庫(kù)表,服務(wù)之間通過(guò)服務(wù)調(diào)用的方式來(lái)獲取數(shù)據(jù)。
  • 業(yè)務(wù)邊界清晰,代碼邊界清晰。單體架構(gòu)中不同的業(yè)務(wù),代碼耦合嚴(yán)重,隨著業(yè)務(wù)量增長(zhǎng),業(yè)務(wù)復(fù)雜后,一個(gè)小功能點(diǎn)的修改就可能影響到其他業(yè)務(wù)點(diǎn),開發(fā)質(zhì)量不可控,測(cè)試需要回歸,成本持續(xù)提高。
  • 顯著減少代碼沖突。在單體應(yīng)用中,很多人在同一個(gè)工程上開發(fā),會(huì)有大量的代碼沖突問(wèn)題。微服務(wù)化后,按業(yè)務(wù)模塊拆分成多個(gè)服務(wù),每個(gè)服務(wù)由專人負(fù)責(zé),有效減少代碼沖突問(wèn)題。
  • 可復(fù)用,顯著減少代碼拷貝現(xiàn)象。

微服務(wù)確實(shí)帶來(lái)不少好處,那么微服務(wù)有沒(méi)有什么問(wèn)題呢?答案是肯定的!

例如:

  • 微服務(wù)系統(tǒng)穩(wěn)定性問(wèn)題。微服務(wù)化后服務(wù)數(shù)量大幅增加,一個(gè)服務(wù)故障就可能引發(fā)大面積系統(tǒng)故障。比如服務(wù)雪崩,連鎖故障。當(dāng)一個(gè)服務(wù)故障后,依賴他的服務(wù)受到牽連也發(fā)生故障。
  • 服務(wù)調(diào)用關(guān)系錯(cuò)綜復(fù)雜,鏈路過(guò)長(zhǎng),問(wèn)題難定位。微服務(wù)化后,服務(wù)數(shù)量劇增,大量的服務(wù)管理起來(lái)會(huì)變的更加復(fù)雜。由于調(diào)用鏈路變長(zhǎng),定位問(wèn)題也會(huì)更加困難。
  • 數(shù)據(jù)一致性問(wèn)題。微服務(wù)化后單體系統(tǒng)被拆分成多個(gè)服務(wù),各服務(wù)訪問(wèn)自己的數(shù)據(jù)庫(kù)。而我們的一次請(qǐng)求操作很可能要跨多個(gè)服務(wù),同時(shí)要操作多個(gè)數(shù)據(jù)庫(kù)的數(shù)據(jù),我們發(fā)現(xiàn)以前用的數(shù)據(jù)庫(kù)事務(wù)不好用了??绶?wù)的數(shù)據(jù)一致性和數(shù)據(jù)完整性問(wèn)題也就隨之而來(lái)了。
  • 微服務(wù)化過(guò)程中,用戶無(wú)感知數(shù)據(jù)庫(kù)拆分、數(shù)據(jù)遷移的挑戰(zhàn)。

如何保障微服務(wù)系統(tǒng)穩(wěn)定性?

互聯(lián)網(wǎng)系統(tǒng)為大量的 C 端用戶提供服務(wù),如果隔三差五的出問(wèn)題宕機(jī),會(huì)嚴(yán)重影響用戶體驗(yàn),甚至導(dǎo)致用戶流失。所以穩(wěn)定性對(duì)互聯(lián)網(wǎng)系統(tǒng)非常重要!

接下來(lái)筆者根據(jù)自己的實(shí)際經(jīng)驗(yàn)來(lái)聊聊基于微服務(wù)的互聯(lián)網(wǎng)系統(tǒng)的穩(wěn)定性。

①雪崩效應(yīng)產(chǎn)生原因,如何避免?

微服務(wù)化后,服務(wù)變多,調(diào)用鏈路變長(zhǎng),如果一個(gè)調(diào)用鏈上某個(gè)服務(wù)節(jié)點(diǎn)出問(wèn)題,很可能引發(fā)整個(gè)調(diào)用鏈路崩潰,也就是所謂的雪崩效應(yīng)。

舉個(gè)例子,詳細(xì)理解一下雪崩。如上圖,現(xiàn)在有 A,B,C 三個(gè)服務(wù),A 調(diào) B,B 調(diào) C。

 

假如 C 發(fā)生故障,B 方法 1 調(diào)用 C 方法 1 的請(qǐng)求不能及時(shí)返回,B 的線程會(huì)發(fā)生阻塞等待。

B 會(huì)在一定時(shí)間后因?yàn)榫€程阻塞耗盡線程池所有線程,這時(shí) B 就會(huì)無(wú)法響應(yīng) A 的請(qǐng)求。

A 調(diào)用 B 的請(qǐng)求不能及時(shí)返回,A 的線程池線程資源也會(huì)逐漸被耗盡,最終 A 也無(wú)法對(duì)外提供服務(wù)。這樣就引發(fā)了連鎖故障,發(fā)生了雪崩。

縱向:C 故障引發(fā) B 故障,B 故障引發(fā) A 故障,最終發(fā)生連鎖故障。橫向:方法 1 出問(wèn)題,導(dǎo)致線程阻塞,進(jìn)而線程池線程資源耗盡,最終服務(wù)內(nèi)所有方法都無(wú)法訪問(wèn),這就是“線程池污染”。

為了避免雪崩效應(yīng),我們可以從兩個(gè)方面考慮:

在服務(wù)間加熔斷:解決服務(wù)間縱向連鎖故障問(wèn)題。比如在 A 服務(wù)加熔斷,當(dāng) B 故障時(shí),開啟熔斷,A 調(diào)用 B 的請(qǐng)求不再發(fā)送到 B,直接快速返回。這樣就避免了線程等待的問(wèn)題。

當(dāng)然快速返回什么,F(xiàn)allback 方案是什么,也需要根據(jù)具體場(chǎng)景,比如返回默認(rèn)值或者調(diào)用其他備用服務(wù)接口。

如果你的場(chǎng)景適合異步通信,可以采用消息隊(duì)列,這樣也可以有效避免同步調(diào)用的線程等待問(wèn)題。

 

服務(wù)內(nèi)(JVM 內(nèi))線程隔離:解決橫向線程池污染的問(wèn)題。為了避免因?yàn)橐粋€(gè)方法出問(wèn)題導(dǎo)致線程等待最終引發(fā)線程資源耗盡的問(wèn)題,我們可以對(duì) Tomcat,Dubbo 等的線程池分成多個(gè)小線程組,每個(gè)線程組服務(wù)于不同的類或方法。

一個(gè)方法出問(wèn)題,只影響自己不影響其他方法和類。常用開源熔斷隔離組件:Hystrix,Resilience4j。

②如何應(yīng)對(duì)突發(fā)流量對(duì)服務(wù)的巨大壓力?

促銷活動(dòng)或秒殺時(shí),訪問(wèn)量往往會(huì)猛增數(shù)倍。技術(shù)團(tuán)隊(duì)在活動(dòng)開始前一般都會(huì)根據(jù)預(yù)估訪問(wèn)量適當(dāng)增加節(jié)點(diǎn),但是假如流量預(yù)估少了(實(shí)際訪問(wèn)量遠(yuǎn)大于預(yù)估的訪問(wèn)量),系統(tǒng)就可能會(huì)被壓垮。

所以我們可以在網(wǎng)關(guān)層(Zuul,Gateway,Nginx 等)做限流,如果訪問(wèn)量超出系統(tǒng)承載能力,就按照一定策略拋棄超出閾值的訪問(wèn)請(qǐng)求(也要注意用戶體驗(yàn),可以給用戶返回一個(gè)友好的頁(yè)面提示)。

可以從全局,IP,userID 等多維度做限流。限流的兩個(gè)主要目的:

  • 應(yīng)對(duì)突發(fā)流量,避免系統(tǒng)被壓垮(全局限流和 IP 限流)
  • 防刷,防止機(jī)器人腳本等頻繁調(diào)用服務(wù)(userID 限流和 IP 限流)

③數(shù)據(jù)冗余

在核心鏈路上,服務(wù)可以冗余它依賴的服務(wù)的數(shù)據(jù),依賴的服務(wù)故障時(shí),服務(wù)盡量做到自保。

比如訂單服務(wù)依賴庫(kù)存服務(wù)。我們可以在訂單服務(wù)冗余庫(kù)存數(shù)據(jù)(注意控制合理的安全庫(kù)存,防超賣)。

下單減庫(kù)存時(shí),如果庫(kù)存服務(wù)掛了,我們可以直接從訂單服務(wù)取庫(kù)存??梢越Y(jié)合熔斷一起使用,作為熔斷的 Fallback(后備)方案。

④服務(wù)降級(jí)

可能很多人都聽過(guò)服務(wù)降級(jí),但是又不知道降級(jí)是怎么回事。實(shí)際上,上面說(shuō)的熔斷,限流,數(shù)據(jù)冗余,都屬于服務(wù)降級(jí)的范疇。

還有手動(dòng)降級(jí)的例子,比如大促期間我們會(huì)關(guān)掉第三方物流接口,頁(yè)面上也關(guān)掉物流查詢功能,避免拖垮自己的服務(wù)。

這種降級(jí)的例子很多。不管什么降級(jí)方式,目的都是讓系統(tǒng)可用性更高,容錯(cuò)能力更強(qiáng),更穩(wěn)定。關(guān)于服務(wù)降級(jí)詳見本文后面的內(nèi)容。

⑤緩存要注意什么?

主要有如下三點(diǎn):

緩存穿透:對(duì)于數(shù)據(jù)庫(kù)中根本不存在的值,請(qǐng)求緩存時(shí)要在緩存記錄一個(gè)空值,避免每次請(qǐng)求都打到數(shù)據(jù)庫(kù)

緩存雪崩:在某一時(shí)間緩存數(shù)據(jù)集中失效,導(dǎo)致大量請(qǐng)求穿透到數(shù)據(jù)庫(kù),將數(shù)據(jù)庫(kù)壓垮。

可以在初始化數(shù)據(jù)時(shí),差異化各個(gè) key 的緩存失效時(shí)間,失效時(shí)間=一個(gè)較大的固定值+較小的隨機(jī)值。

緩存熱點(diǎn)。有些熱點(diǎn)數(shù)據(jù)訪問(wèn)量會(huì)特別大,單個(gè)緩存節(jié)點(diǎn)(例如 Redis)無(wú)法支撐這么大的訪問(wèn)量。

如果是讀請(qǐng)求訪問(wèn)量大,可以考慮讀寫分離,一主多從的方案,用從節(jié)點(diǎn)分?jǐn)傋x流量;如果是寫請(qǐng)求訪問(wèn)量大,可以采用集群分片方案,用分片分?jǐn)倢懥髁俊?/p>

以秒殺扣減庫(kù)存為例,假如秒殺庫(kù)存是 100,可以分成 5 片,每片存 20 個(gè)庫(kù)存。

⑥關(guān)于隔離的考慮

需要考慮如下幾點(diǎn):

部署隔離:我們經(jīng)常會(huì)遇到秒殺業(yè)務(wù)和日常業(yè)務(wù)依賴同一個(gè)服務(wù),以及 C 端服務(wù)和內(nèi)部運(yùn)營(yíng)系統(tǒng)依賴同一個(gè)服務(wù)的情況,比如說(shuō)都依賴訂單服務(wù)。

而秒殺系統(tǒng)的瞬間訪問(wèn)量很高,可能會(huì)對(duì)服務(wù)帶來(lái)巨大的壓力,甚至壓垮服務(wù)。內(nèi)部運(yùn)營(yíng)系統(tǒng)也經(jīng)常有批量數(shù)據(jù)導(dǎo)出的操作,同樣會(huì)給服務(wù)帶來(lái)一定的壓力。

這些都是不穩(wěn)定因素。所以我們可以將這些共同依賴的服務(wù)分組部署,不同的分組服務(wù)于不同的業(yè)務(wù),避免相互干擾。

數(shù)據(jù)隔離:極端情況下還需要緩存隔離,數(shù)據(jù)庫(kù)隔離。以秒殺為例,庫(kù)存和訂單的緩存(Redis)和數(shù)據(jù)庫(kù)需要單獨(dú)部署!

數(shù)據(jù)隔離后,秒殺訂單和日常訂單不在相同的數(shù)據(jù)庫(kù),之后的訂單查詢?cè)趺凑故?可以采用相應(yīng)的數(shù)據(jù)同步策略。

比如,在創(chuàng)建秒殺訂單后發(fā)消息到消息隊(duì)列,日常訂單服務(wù)收到消息后將訂單寫入日常訂單庫(kù)。注意,要考慮數(shù)據(jù)的一致性,可以使用事務(wù)型消息。

業(yè)務(wù)隔離:還是以秒殺為例。從業(yè)務(wù)上把秒殺和日常的售賣區(qū)分開來(lái),把秒殺做為營(yíng)銷活動(dòng),要參與秒殺的商品需要提前報(bào)名參加活動(dòng),這樣我們就能提前知道哪些商家哪些商品要參與秒殺。

可以根據(jù)提報(bào)的商品提前生成商品詳情靜態(tài)頁(yè)面并上傳到 CDN 預(yù)熱,提報(bào)的商品庫(kù)存也需要提前預(yù)熱,可以將商品庫(kù)存在活動(dòng)開始前預(yù)熱到 Redis,避免秒殺開始后大量訪問(wèn)穿透到數(shù)據(jù)庫(kù)。

 

⑦CI 測(cè)試&性能測(cè)試

CI 測(cè)試,持續(xù)集成測(cè)試,在我們每次提交代碼到發(fā)布分支前自動(dòng)構(gòu)建項(xiàng)目并執(zhí)行所有測(cè)試用例,如果有測(cè)試用例執(zhí)行失敗,拒絕將代碼合并到發(fā)布分支,本次集成失敗。CI 測(cè)試可以保證上線質(zhì)量,適用于用例不會(huì)經(jīng)常變化的穩(wěn)定業(yè)務(wù)。

性能測(cè)試,為了保證上線性能,所有用戶側(cè)功能需要進(jìn)行性能測(cè)試。上線前要保證性能測(cè)試通過(guò)。而且要定期做全鏈路壓測(cè),有性能問(wèn)題可以及時(shí)發(fā)現(xiàn)。

⑧監(jiān)控

我們需要一套完善的監(jiān)控系統(tǒng),系統(tǒng)出問(wèn)題時(shí)能夠快速告警,最好是系統(tǒng)出問(wèn)題前能提前預(yù)警。

包括系統(tǒng)監(jiān)控(CPU,內(nèi)存,網(wǎng)絡(luò) IO,帶寬等監(jiān)控),數(shù)據(jù)庫(kù)監(jiān)控(QPS,TPS,慢查詢,大結(jié)果集等監(jiān)控),緩存中間件監(jiān)控(如 Redis),JVM 監(jiān)控(堆內(nèi)存,GC,線程等監(jiān)控),全鏈路監(jiān)控(pinpoint,skywaking,cat等),各種接口監(jiān)控(QPS,TPS 等)

⑨CDN

可以充分利用 CDN。除了提高用戶訪問(wèn)速度之外,頁(yè)面靜態(tài)化之后存放到 CDN,用 CDN 扛流量,可以大幅減少系統(tǒng)(源站)的訪問(wèn)壓力。同時(shí)也減少了網(wǎng)站帶寬壓力。對(duì)系統(tǒng)穩(wěn)定性非常有好處。

⑩避免單點(diǎn)問(wèn)題

除了服務(wù)要多點(diǎn)部署外,網(wǎng)關(guān),數(shù)據(jù)庫(kù),緩存也要避免單點(diǎn)問(wèn)題,至少要有一個(gè) Backup,而且要可以自動(dòng)發(fā)現(xiàn)上線節(jié)點(diǎn)和自動(dòng)摘除下線和故障節(jié)點(diǎn)。

⑪網(wǎng)絡(luò)帶寬

避免帶寬成為瓶頸,促銷和秒殺開始前提前申請(qǐng)帶寬。不光要考慮外網(wǎng)帶寬,還要考慮內(nèi)網(wǎng)帶寬,有些舊服務(wù)器網(wǎng)口是千兆網(wǎng)口,訪問(wèn)量高時(shí)很可能會(huì)打滿。

此外,一套完善的灰度發(fā)布系統(tǒng),可以讓上線更加平滑,避免上線大面積故障。DevOps 工具,CI,CD 對(duì)系統(tǒng)穩(wěn)定性也有很大意義。

關(guān)于服務(wù)降級(jí)

提起服務(wù)降級(jí),估計(jì)很多人都聽說(shuō)過(guò),但是又因?yàn)橛H身經(jīng)歷不多,所以可能不是很理解。下面結(jié)合具體實(shí)例從多方面詳細(xì)闡述服務(wù)降級(jí)。

互聯(lián)網(wǎng)分布式系統(tǒng)中,經(jīng)常會(huì)有一些異常狀況導(dǎo)致服務(wù)器壓力劇增,比如促銷活動(dòng)時(shí)訪問(wèn)量會(huì)暴增,為了保證系統(tǒng)核心功能的穩(wěn)定性和可用性,我們需要一些應(yīng)對(duì)策略。

這些應(yīng)對(duì)策略也就是所謂的服務(wù)降級(jí)。下面根據(jù)筆者的實(shí)際經(jīng)歷,跟大家聊聊服務(wù)降級(jí)那些事兒。希望對(duì)大家有所啟發(fā)!

①關(guān)閉次要功能

在服務(wù)壓力過(guò)大時(shí),關(guān)閉非核心功能,避免核心功能被拖垮。

例如,電商平臺(tái)基本都支持物流查詢功能,而物流查詢往往要依賴第三方物流公司的系統(tǒng)接口。

物流公司的系統(tǒng)性能往往不會(huì)太好。所以我們經(jīng)常會(huì)在雙 11 這種大型促銷活動(dòng)期間把物流接口屏蔽掉,在頁(yè)面上也關(guān)掉物流查詢功能。這樣就避免了我們自己的服務(wù)被拖垮,也保證了重要功能的正常運(yùn)行。

②降低一致性之讀降級(jí)

對(duì)于讀一致性要求不高的場(chǎng)景。在服務(wù)和數(shù)據(jù)庫(kù)壓力過(guò)大時(shí),可以不讀數(shù)據(jù)庫(kù),降級(jí)為只讀緩存數(shù)據(jù)。以這種方式來(lái)減小數(shù)據(jù)庫(kù)壓力,提高服務(wù)的吞吐量。

例如,我們會(huì)把商品評(píng)論評(píng)價(jià)信息緩存在 Redis 中。在服務(wù)和數(shù)據(jù)庫(kù)壓力過(guò)大時(shí),只讀緩存中的評(píng)論評(píng)價(jià)數(shù)據(jù),不在緩存中的數(shù)據(jù)不展示給用戶。

當(dāng)然評(píng)論評(píng)價(jià)這種不是很重要的數(shù)據(jù)可以考慮用 NOSQL 數(shù)據(jù)庫(kù)存儲(chǔ),不過(guò)我們?cè)?jīng)確實(shí)用 MySQL 數(shù)據(jù)庫(kù)存儲(chǔ)過(guò)評(píng)論評(píng)價(jià)數(shù)據(jù)。

③降低一致性之寫入降級(jí)

在服務(wù)壓力過(guò)大時(shí),可以將同步調(diào)用改為異步消息隊(duì)列方式,來(lái)減小服務(wù)壓力并提高吞吐量。

既然把同步改成了異步也就意味著降低了數(shù)據(jù)一致性,保證數(shù)據(jù)最終一致即可。

例如,秒殺場(chǎng)景瞬間生成訂單量很高。我們可以采取異步批量寫數(shù)據(jù)庫(kù)的方式,來(lái)減少數(shù)據(jù)庫(kù)訪問(wèn)頻次,進(jìn)而降低數(shù)據(jù)庫(kù)的寫入壓力。

詳細(xì)步驟:后端服務(wù)接到下單請(qǐng)求,直接放進(jìn)消息隊(duì)列,消費(fèi)端服務(wù)取出訂單消息后,先將訂單信息寫入 Redis,每隔 100ms 或者積攢 100 條訂單,批量寫入數(shù)據(jù)庫(kù)一次。

前端頁(yè)面下單后定時(shí)向后端拉取訂單信息,獲取到訂單信息后跳轉(zhuǎn)到支付頁(yè)面。用這種異步批量寫入數(shù)據(jù)庫(kù)的方式大幅減少了數(shù)據(jù)庫(kù)寫入頻次,從而明顯降低了訂單數(shù)據(jù)庫(kù)寫入壓力。

不過(guò),因?yàn)橛唵问钱惒綄懭霐?shù)據(jù)庫(kù)的,就會(huì)存在數(shù)據(jù)庫(kù)訂單和相應(yīng)庫(kù)存數(shù)據(jù)暫時(shí)不一致的情況,以及用戶下單后不能及時(shí)查到訂單的情況。

因?yàn)槭墙导?jí)方案,可以適當(dāng)降低用戶體驗(yàn),所以我們保證數(shù)據(jù)最終一致即可。流程如下圖:

④屏蔽寫入

 

很多高并發(fā)場(chǎng)景下,查詢請(qǐng)求都會(huì)走緩存,這時(shí)數(shù)據(jù)庫(kù)的壓力主要是寫入壓力。所以對(duì)于某些不重要的服務(wù),在服務(wù)和數(shù)據(jù)庫(kù)壓力過(guò)大時(shí),可以關(guān)閉寫入功能,只保留查詢功能。這樣可以明顯減小數(shù)據(jù)庫(kù)壓力。

例如,商品的評(píng)論評(píng)價(jià)功能。為了減小壓力,大促前可以關(guān)閉評(píng)論評(píng)價(jià)功能,關(guān)閉寫接口,用戶只能查看評(píng)論評(píng)價(jià)。而大部分查詢請(qǐng)求走查詢緩存,從而大幅減小數(shù)據(jù)庫(kù)和服務(wù)的訪問(wèn)壓力。

⑤數(shù)據(jù)冗余

服務(wù)調(diào)用者可以冗余它所依賴服務(wù)的數(shù)據(jù)。當(dāng)依賴的服務(wù)故障時(shí),服務(wù)調(diào)用者可以直接使用冗余數(shù)據(jù)。

例如,我之前在某家自營(yíng)電商公司。當(dāng)時(shí)的商品服務(wù)依賴于價(jià)格服務(wù),獲取商品信息時(shí),商品服務(wù)要調(diào)用價(jià)格服務(wù)獲取商品價(jià)格。

因?yàn)槭亲誀I(yíng)電商,商品和 SKU 數(shù)量都不太多,一兩萬(wàn)的樣子。所以我們?cè)谏唐贩?wù)冗余了價(jià)格數(shù)據(jù)。當(dāng)價(jià)格服務(wù)故障后,商品服務(wù)還可以從自己冗余的數(shù)據(jù)中取到價(jià)格。

當(dāng)然這樣做價(jià)格有可能不是最新的,但畢竟這是降級(jí)方案,犧牲一些數(shù)據(jù)準(zhǔn)確性,換來(lái)系統(tǒng)的可用性還是很有意義的!

注:由于一個(gè)商品會(huì)有多個(gè)價(jià)格,比如普通價(jià),會(huì)員價(jià),促銷直降價(jià),促銷滿減價(jià),所以我們把價(jià)格做成了單獨(dú)的服務(wù)。

數(shù)據(jù)冗余可以結(jié)合熔斷一起使用,實(shí)現(xiàn)自動(dòng)降級(jí)。下面的熔斷部分會(huì)詳細(xì)說(shuō)明。

⑥熔斷和 Fallback

熔斷是一種自動(dòng)降級(jí)手段。當(dāng)服務(wù)不可用時(shí),用來(lái)避免連鎖故障,雪崩效應(yīng)。發(fā)生在服務(wù)調(diào)用的時(shí)候,在調(diào)用方做熔斷處理。

熔斷的意義在于,調(diào)用方快速失敗(Fail Fast),避免請(qǐng)求大量阻塞。并且保護(hù)被調(diào)用方。

詳細(xì)解釋一下,假設(shè) A 服務(wù)調(diào)用 B 服務(wù),B 發(fā)生故障后,A 開啟熔斷:

  • 對(duì)于調(diào)用方 A:請(qǐng)求在 A 直接快速返回,快速失敗,不再發(fā)送到 B。 避免因?yàn)?B 故障,導(dǎo)致 A 的請(qǐng)求線程持續(xù)等待,進(jìn)而導(dǎo)致線程池線程和 CPU 資源耗盡,最終導(dǎo)致 A 無(wú)法響應(yīng),甚至整條調(diào)用鏈故障。
  • 對(duì)于被調(diào)用方 B:熔斷后,請(qǐng)求被 A 攔截,不再發(fā)送到 B,B 壓力得到緩解,避免了仍舊存活的 B 被壓垮,B 得到了保護(hù)。

還是以電商的商品和價(jià)格服務(wù)為例。獲取商品信息時(shí),商品服務(wù)要調(diào)用價(jià)格服務(wù)獲取商品價(jià)格。為了提高系統(tǒng)穩(wěn)定性,我們要求各個(gè)服務(wù)要盡量自保。

所以我們?cè)谏唐贩?wù)加了熔斷,當(dāng)價(jià)格服務(wù)故障時(shí),商品服務(wù)請(qǐng)求能夠快速失敗返回,保證商品服務(wù)不被拖垮,進(jìn)而避免連鎖故障。

看到這,可能有讀者會(huì)問(wèn),快速失敗后價(jià)格怎么返回呢?因?yàn)槭亲誀I(yíng)電商,商品和 SKU 數(shù)量都不太多,一兩萬(wàn)的樣子。所以我們做了數(shù)據(jù)冗余,在商品服務(wù)冗余了價(jià)格數(shù)據(jù)。

這樣我們?cè)谌蹟嗪螳@取價(jià)格的 Fallback 方案就變成了從商品服務(wù)冗余的數(shù)據(jù)去取價(jià)格。

下圖為商品服務(wù)熔斷關(guān)閉和開啟的對(duì)比圖:

 

開源熔斷組件:Hystrix,Resilience4j 等。

⑦限流

說(shuō)起服務(wù)降級(jí),就不可避免的要聊到限流。我們先考慮一個(gè)場(chǎng)景,例如電商平臺(tái)要搞促銷活動(dòng),我們按照預(yù)估的峰值訪問(wèn)量,準(zhǔn)備了 30 臺(tái)機(jī)器。

但是活動(dòng)開始后,實(shí)際參加的人數(shù)比預(yù)估的人數(shù)翻了 5 倍,這就遠(yuǎn)遠(yuǎn)超出了我們的服務(wù)處理能力,給后端服務(wù)、緩存、數(shù)據(jù)庫(kù)等帶來(lái)巨大的壓力。

隨著訪問(wèn)請(qǐng)求的不斷涌入,最終很可能造成平臺(tái)系統(tǒng)崩潰。對(duì)于這種突發(fā)流量,我們可以通過(guò)限流來(lái)保護(hù)后端服務(wù)。

因?yàn)榇黉N活動(dòng)流量來(lái)自于用戶,用戶的請(qǐng)求會(huì)先經(jīng)過(guò)網(wǎng)關(guān)層再到后端服務(wù),所以網(wǎng)關(guān)層是最合適的限流位置,如下圖:

 

另外,考慮到用戶體驗(yàn)問(wèn)題,我們還需要相應(yīng)的限流頁(yè)面。當(dāng)某些用戶的請(qǐng)求被限流攔截后,把限流頁(yè)面返回給用戶。頁(yè)面如下圖:

 

另外一個(gè)場(chǎng)景,假如有一個(gè)核心服務(wù),有幾十個(gè)服務(wù)都調(diào)用他。如果其中一個(gè)服務(wù)調(diào)用者出了 Bug,頻繁調(diào)用這個(gè)核心服務(wù),可能給這個(gè)核心服務(wù)造成非常大的壓力,甚至導(dǎo)致這個(gè)核心服務(wù)無(wú)法響應(yīng)。

同時(shí)也會(huì)影響到調(diào)用他的幾十個(gè)服務(wù)。所以每個(gè)服務(wù)也要根據(jù)自己的處理能力對(duì)調(diào)用者進(jìn)行限制。

對(duì)于服務(wù)層的限流,我們一般可以利用 Spring AOP,以攔截器的方式做限流處理。這種做法雖然可以解決問(wèn)題,但是問(wèn)題也比較多。

比如一個(gè)服務(wù)中有 100 個(gè)接口需要限流,我們就要寫 100 個(gè)攔截器。而且限流閾值經(jīng)常需要調(diào)整,又涉及到動(dòng)態(tài)修改的問(wèn)題。

為了應(yīng)對(duì)這些問(wèn)題,很多公司會(huì)有專門的限流平臺(tái),新增限流接口和閾值變動(dòng)可以直接在限流平臺(tái)上配置。

關(guān)于限流,還有很多細(xì)節(jié)需要考慮,比如限流算法、毛刺現(xiàn)象等。篇幅原因,這些問(wèn)題就不在本文討論了。

開源網(wǎng)關(guān)組件:Nginx,Zuul,Gateway,阿里 Sentinel 等。

⑧服務(wù)降級(jí)總結(jié)和思考

上面我們結(jié)合具體案例解釋了多種降級(jí)方式。實(shí)際上,關(guān)于服務(wù)降級(jí)的方式和策略,并沒(méi)有什么定式,也沒(méi)有標(biāo)準(zhǔn)可言。

上面的降級(jí)方式也沒(méi)有涵蓋所有的情況。不同公司不同平臺(tái)的做法也不完全一樣。

不過(guò),所有的降級(jí)方案都要以滿足業(yè)務(wù)需求為前提,都是為了提高系統(tǒng)的可用性,保證核心功能正常運(yùn)行。

⑨降級(jí)分類

一般我們可以把服務(wù)降級(jí)分為手動(dòng)和自動(dòng)兩類。手動(dòng)降級(jí)應(yīng)用較多,可以通過(guò)開關(guān)的方式開啟或關(guān)閉降級(jí)。

自動(dòng)降級(jí),比如熔斷和限流等屬于自動(dòng)降級(jí)的范疇。大多手動(dòng)降級(jí)也可以做成自動(dòng)的方式,可以根據(jù)各種系統(tǒng)指標(biāo)設(shè)定合理閾值,在相應(yīng)指標(biāo)達(dá)到閾值上限自動(dòng)開啟降級(jí)。

在很多場(chǎng)景下,由于業(yè)務(wù)過(guò)于復(fù)雜,需要參考的指標(biāo)太多,自動(dòng)降級(jí)實(shí)現(xiàn)起來(lái)難度會(huì)比較大,而且也很容易出錯(cuò)。

所以在考慮做自動(dòng)降級(jí)之前一定要充分做好評(píng)估,相應(yīng)的自動(dòng)降級(jí)方案也要考慮周全。

⑩大規(guī)模分布式系統(tǒng)如何降級(jí)?

在大規(guī)模分布式系統(tǒng)中,經(jīng)常會(huì)有成百上千的服務(wù)。在大促前往往會(huì)根據(jù)業(yè)務(wù)的重要程度和業(yè)務(wù)間的關(guān)系批量降級(jí)。

這就需要技術(shù)和產(chǎn)品提前對(duì)業(yè)務(wù)和系統(tǒng)進(jìn)行梳理,根據(jù)梳理結(jié)果確定哪些服務(wù)可以降級(jí),哪些服務(wù)不可以降級(jí),降級(jí)策略是什么,降級(jí)順序怎么樣。

大型互聯(lián)網(wǎng)公司基本都會(huì)有自己的降級(jí)平臺(tái),大部分降級(jí)都在平臺(tái)上操作,比如手動(dòng)降級(jí)開關(guān),批量降級(jí)順序管理,熔斷閾值動(dòng)態(tài)設(shè)置,限流閾值動(dòng)態(tài)設(shè)置等等。

本節(jié)的主要目的是通過(guò)具體實(shí)例,讓大家了解服務(wù)降級(jí),并提供一些降級(jí)的思路。具體的降級(jí)方式和方案還是要取決于實(shí)際的業(yè)務(wù)場(chǎng)景和系統(tǒng)狀況。

微服務(wù)架構(gòu)下數(shù)據(jù)一致性問(wèn)題

服務(wù)化后單體系統(tǒng)被拆分成多個(gè)服務(wù),各服務(wù)訪問(wèn)自己的數(shù)據(jù)庫(kù)。而我們的一次請(qǐng)求操作很可能要跨多個(gè)服務(wù),同時(shí)要操作多個(gè)數(shù)據(jù)庫(kù)的數(shù)據(jù),我們發(fā)現(xiàn)以前用的數(shù)據(jù)庫(kù)事務(wù)不好用了。那么基于微服務(wù)的架構(gòu)如何保證數(shù)據(jù)一致性呢?

好,咱們這次就盤一盤分布式事務(wù),最終一致,補(bǔ)償機(jī)制,事務(wù)型消息!

提起這些,大家可能會(huì)想到兩階段提交,XA,TCC,Saga,還有最近阿里開源的 Seata(Fescar),這些概念網(wǎng)上一大堆文章,不過(guò)都太泛泛,不接地氣,讓人看了云里霧里。

我們以 TCC 分布式事務(wù)和 RocketMQ 事務(wù)型消息為例,做詳細(xì)分享!這個(gè)弄明白了,也就清楚分布式事務(wù),最終一致,補(bǔ)償機(jī)制這些概念啦!

①TCC 分布式事務(wù)

TCC(Try-Confirm-Cancel)是分布式事務(wù)的一種模式,可以保證不同服務(wù)的數(shù)據(jù)最終一致。

目前有不少 TCC 開源框架,比如 Hmily,ByteTCC,TCC-Transaction (我們之前用過(guò) Hmily 和公司架構(gòu)組自研組件)。下面以電商下單流程為例對(duì) TCC 做詳細(xì)闡述。

流程圖如下:

基本步驟如下:

 

  • 修改訂單狀態(tài)為“已支付”
  • 扣減庫(kù)存
  • 扣減優(yōu)惠券
  • 通知 WMS(倉(cāng)儲(chǔ)管理系統(tǒng))撿貨出庫(kù)(異步消息)

我們先看扣減庫(kù)存,更新訂單狀態(tài)和扣減優(yōu)惠券這三步同步調(diào)用,通知 WMS 的異步消息會(huì)在后面的“基于消息的最終一致”部分詳細(xì)闡述!

下面是偽代碼,不同公司的產(chǎn)品邏輯會(huì)有差異,相關(guān)代碼邏輯也可能會(huì)有不同,大家不用糾結(jié)代碼邏輯正確性。

  1. public void makePayment() { 
  2.    orderService.updateStatus(OrderStatus.Payed); //訂單服務(wù)更新訂單為已支付狀態(tài) 
  3.    inventoryService.decrStock(); //庫(kù)存服務(wù)扣減庫(kù)存 
  4.    couponService.updateStatus(couponStatus.Used); //卡券服務(wù)更新優(yōu)惠券為已使用狀態(tài)       

看完這段代碼,大家可能覺得很簡(jiǎn)單!那么有什么問(wèn)題嗎?答案是肯定的。沒(méi)法保證數(shù)據(jù)一致性,也就是說(shuō)不能保證這幾步操作全部成功或者全部失敗!

因?yàn)檫@幾步操作是在分布式環(huán)境下進(jìn)行的,每個(gè)操作分布在不同的服務(wù)中,不同的服務(wù)又對(duì)應(yīng)不同的數(shù)據(jù)庫(kù),本地事務(wù)已經(jīng)用不上了!

假如第一步更新訂單為“已支付”成功了,第二步扣減庫(kù)存時(shí),庫(kù)存服務(wù)掛了或者網(wǎng)絡(luò)出問(wèn)題了,導(dǎo)致扣減庫(kù)存失敗。你告訴用戶支付成功了,但是庫(kù)存沒(méi)扣減。這怎么能行!

接下來(lái),我們來(lái)看看TCC是如何幫我們解決這個(gè)問(wèn)題并保證數(shù)據(jù)最終一致的。

TCC 分為兩個(gè)階段:

  • Try(預(yù)留凍結(jié)相關(guān)業(yè)務(wù)資源,設(shè)置臨時(shí)狀態(tài),為下個(gè)階段做準(zhǔn)備)
  • Confirm 或者 Cancel(Confirm:對(duì)資源進(jìn)行最終操作,Cancel:取消資源)

第一階段 Try:

  • 更新訂單狀態(tài):此時(shí)因?yàn)檫€沒(méi)真正完成整個(gè)流程,訂單狀態(tài)不能直接改成已支付狀態(tài)??梢约右粋€(gè)臨時(shí)狀態(tài) Paying,表明訂單正在支付中,支付結(jié)果暫時(shí)還不清楚!
  • 凍結(jié)庫(kù)存:假設(shè)現(xiàn)在可銷售庫(kù)存 stock 是 10,這單扣減 1 個(gè)庫(kù)存,別直接把庫(kù)存減掉,而是在表中加一個(gè)凍結(jié)字段 locked_stock,locked_stock 加 1,再給 stock 減 1,這樣就相當(dāng)于凍結(jié)了 1 個(gè)庫(kù)存。兩個(gè)操作放在一個(gè)本地事務(wù)里。
  • 更新優(yōu)惠券狀態(tài):優(yōu)惠券加一個(gè)臨時(shí)狀態(tài) Inuse,表明優(yōu)惠券正在使用中,具體有沒(méi)有正常被使用暫時(shí)還不清楚!

第二階段 Confirm:假如第一階段幾個(gè) Try 操作都成功了!既然第一階段已經(jīng)預(yù)留了庫(kù)存,而且訂單狀態(tài)和優(yōu)惠券狀態(tài)也設(shè)置了臨時(shí)狀態(tài),第二階段的確認(rèn)提交從業(yè)務(wù)上來(lái)說(shuō)應(yīng)該沒(méi)什么問(wèn)題了。

Confirm 階段我們需要做下面三件事:

  • 先將訂單狀態(tài)從 Paying 改為已支付 Payed,訂單狀態(tài)也完成了。
  • 再將凍結(jié)的庫(kù)存恢復(fù) locked_stock 減 1,stock 第一階段已經(jīng)減掉 1 是 9 了,到此扣減庫(kù)存就真正完成了。
  • 再將優(yōu)惠券狀態(tài)從 Inuse 改為 Used,表明優(yōu)惠券已經(jīng)被正常使用。

第二階段 Cancel,假如第一階段失敗了:

  • 先將訂單狀態(tài)從 Paying 恢復(fù)為待支付 UnPayed。
  • 再將凍結(jié)的庫(kù)存還回到可銷售庫(kù)存中,stock 加 1 恢復(fù)成 10,locked_stock 減 1,可以放在一個(gè)本地事務(wù)完成。
  • 再將優(yōu)惠券狀態(tài)從 Inuse 恢復(fù)為未使用 Unused。

基于 Hmily 框架的代碼:

  1. //訂單服務(wù) 
  2. public class OrderService{ 
  3.  
  4.   //tcc接口 
  5.   @Hmily(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus"
  6.   public void makePayment() { 
  7.      更新訂單狀態(tài)為支付中 
  8.      凍結(jié)庫(kù)存,rpc調(diào)用 
  9.      優(yōu)惠券狀態(tài)改為使用中,rpc調(diào)用 
  10.   } 
  11.  
  12.   public void confirmOrderStatus() { 
  13.      更新訂單狀態(tài)為已支付 
  14.   } 
  15.  
  16.   public void cancelOrderStatus() { 
  17.      恢復(fù)訂單狀態(tài)為待支付  
  18.   }   
  19.  
  1. //庫(kù)存服務(wù) 
  2. public class InventoryService { 
  3.  
  4.   //tcc接口 
  5.   @Hmily(confirmMethod = "confirmDecr", cancelMethod = "cancelDecr"
  6.   public void lockStock() { 
  7.      //防懸掛處理(下面有說(shuō)明) 
  8.      if (分支事務(wù)記錄表沒(méi)有二階段執(zhí)行記錄) 
  9.        凍結(jié)庫(kù)存 
  10.      else 
  11.        return
  12.   } 
  13.  
  14.   public void confirmDecr() { 
  15.      確認(rèn)扣減庫(kù)存 
  16.   } 
  17.   public void cancelDecr() { 
  18.      釋放凍結(jié)的庫(kù)存 
  19.   }   
  20.  
  1. //卡券服務(wù) 
  2. public class CouponService { 
  3.  
  4.   //tcc接口 
  5.   @Hmily(confirmMethod = "confirm", cancelMethod = "cancel"
  6.   public void handleCoupon() { 
  7.      //防懸掛處理(下面有說(shuō)明) 
  8.      if (分支事務(wù)記錄表沒(méi)有二階段執(zhí)行記錄) 
  9.        優(yōu)惠券狀態(tài)更新為臨時(shí)狀態(tài)Inuse 
  10.      else 
  11.        return
  12.   } 
  13.  
  14.   public void confirm() { 
  15.      優(yōu)惠券狀態(tài)改為Used 
  16.   } 
  17.   public void cancel() { 
  18.      優(yōu)惠券狀態(tài)恢復(fù)為Unused 
  19.   }   
  20.  

問(wèn)題 1:有些朋友可能會(huì)問(wèn)了,這些關(guān)于流程的邏輯也要手動(dòng)編碼嗎?這也太麻煩了吧!

實(shí)際上 TCC 分布式事務(wù)框架幫我們把這些事都干了。比如我們前面提到的 Hmily,ByteTCC,TCC-transaction 這些框架。

因?yàn)?Try,Confirm,Cancel 這些操作都在 TCC 分布式事務(wù)框架控制范圍之內(nèi),所以 Try 的各個(gè)步驟成功了或者失敗了。

框架本身都知道,Try 成功了框架就會(huì)自動(dòng)執(zhí)行各個(gè)服務(wù)的 Confirm,Try 失敗了框架就會(huì)執(zhí)行各個(gè)服務(wù)的 Cancel(各個(gè)服務(wù)內(nèi)部的 TCC 分布式事務(wù)框架會(huì)互相通信)。所以我們不用關(guān)心流程,只需要關(guān)注業(yè)務(wù)代碼就可以啦!

問(wèn)題 2:仔細(xì)想想,好像還有問(wèn)題!假如 Confirm 階段更新訂單狀態(tài)成功了,但是扣減庫(kù)存失敗了怎么辦呢?

比如網(wǎng)絡(luò)出問(wèn)題了或者庫(kù)存服務(wù)(宕機(jī),重啟)出問(wèn)題了。當(dāng)然,分布式事務(wù)框架也會(huì)考慮這些場(chǎng)景,框架會(huì)記錄操作日志。

假如 Confirm 階段扣減庫(kù)存失敗了,框架會(huì)不斷重試調(diào)用庫(kù)存服務(wù)直到成功(考慮性能問(wèn)題,重試次數(shù)應(yīng)該有限制)。Cancel 過(guò)程也是一樣的道理。

注意,既然需要重試,我們就要保證接口的冪等性。什么?不太懂冪等性。簡(jiǎn)單說(shuō):一個(gè)操作不管請(qǐng)求多少次,結(jié)果都要保證一樣。這里就不詳細(xì)介紹啦!

再考慮一個(gè)場(chǎng)景,Try 階段凍結(jié)庫(kù)存的時(shí)候,因?yàn)槭?RPC 遠(yuǎn)程調(diào)用,在網(wǎng)絡(luò)擁塞等情況下,是有可能超時(shí)的。

假如凍結(jié)庫(kù)存時(shí)發(fā)生超時(shí),TCC 框架會(huì)回滾(Cancel)整個(gè)分布式事務(wù),回滾完成后凍結(jié)庫(kù)存請(qǐng)求才到達(dá)參與者(庫(kù)存服務(wù))并執(zhí)行,這時(shí)被凍結(jié)的庫(kù)存就沒(méi)辦法處理(恢復(fù))了。

這種情況稱之為“懸掛”,也就是說(shuō)預(yù)留的資源后續(xù)無(wú)法處理。解決方案:第二階段已經(jīng)執(zhí)行,第一階段就不再執(zhí)行了,可以加一個(gè)“分支事務(wù)記錄表”,如果表里有相關(guān)第二階段的執(zhí)行記錄,就不再執(zhí)行 Try(上面代碼有防懸掛處理)。

有人可能注意到還有些小紕漏,對(duì),加鎖,分布式環(huán)境下,我們可以考慮對(duì)第二階段執(zhí)行記錄的查詢和插入加上分布式鎖,確保萬(wàn)無(wú)一失。

②基于消息的最終一致

還是以上面的電商下單流程為例:

 

上圖,下單流程最后一步,通知 WMS 撿貨出庫(kù),是異步消息走消息隊(duì)列。

  1. public void makePayment() { 
  2.    orderService.updateStatus(OrderStatus.Payed); //訂單服務(wù)更新訂單為已支付狀態(tài) 
  3.    inventoryService.decrStock(); //庫(kù)存服務(wù)扣減庫(kù)存 
  4.    couponService.updateStatus(couponStatus.Used); //卡券服務(wù)更新優(yōu)惠券為已使用狀態(tài)       
  5.    發(fā)送MQ消息撿貨出庫(kù); //發(fā)送消息通知WMS撿貨出庫(kù) 

按上面代碼,大家不難發(fā)現(xiàn)問(wèn)題!如果發(fā)送撿貨出庫(kù)消息失敗,數(shù)據(jù)就會(huì)不一致!

有人說(shuō)我可以在代碼上加上重試邏輯和回退邏輯,發(fā)消息失敗就重發(fā),多次重試失敗所有操作都回退。

這樣一來(lái)邏輯就會(huì)特別復(fù)雜,回退失敗要考慮,而且還有可能消息已經(jīng)發(fā)送成功了,但是由于網(wǎng)絡(luò)等問(wèn)題發(fā)送方?jīng)]得到 MQ 的響應(yīng),這些問(wèn)題都要考慮進(jìn)來(lái)!

幸好,有些消息隊(duì)列幫我們解決了這些問(wèn)題。比如阿里開源的 RocketMQ(目前已經(jīng)是 Apache 開源項(xiàng)目),4.3.0 版本開始支持事務(wù)型消息(實(shí)際上早在貢獻(xiàn)給 Apache 之前曾經(jīng)支持過(guò)事務(wù)消息,后來(lái)被閹割了,4.3.0 版本重新開始支持事務(wù)型消息)。

 

先看看 RocketMQ 發(fā)送事務(wù)型消息的流程:

  • 發(fā)送半消息(所有事務(wù)型消息都要經(jīng)歷確認(rèn)過(guò)程,從而確定最終提交或回滾(拋棄消息),未被確認(rèn)的消息稱為“半消息”或者“預(yù)備消息”,“待確認(rèn)消息”)。
  • 半消息發(fā)送成功并響應(yīng)給發(fā)送方。
  • 執(zhí)行本地事務(wù),根據(jù)本地事務(wù)執(zhí)行結(jié)果,發(fā)送提交或回滾的確認(rèn)消息。
  • 如果確認(rèn)消息丟失(網(wǎng)絡(luò)問(wèn)題或者生產(chǎn)者故障等問(wèn)題),MQ 向發(fā)送方回查執(zhí)行結(jié)果。
  • 根據(jù)上一步驟回查結(jié)果,確定提交或者回滾(拋棄消息)。

看完事務(wù)型消息發(fā)送流程,有些讀者可能沒(méi)有完全理解,不要緊,我們來(lái)分析一下!

問(wèn)題 1:假如發(fā)送方發(fā)送半消息失敗怎么辦?

半消息(待確認(rèn)消息)是消息發(fā)送方發(fā)送的,如果失敗,發(fā)送方自己是知道的并可以做相應(yīng)處理。

問(wèn)題 2:假如發(fā)送方執(zhí)行完本地事務(wù)后,發(fā)送確認(rèn)消息通知 MQ 提交或回滾消息時(shí)失敗了(網(wǎng)絡(luò)問(wèn)題,發(fā)送方重啟等情況),怎么辦?

沒(méi)關(guān)系,當(dāng) MQ 發(fā)現(xiàn)一個(gè)消息長(zhǎng)時(shí)間處于半消息(待確認(rèn)消息)的狀態(tài),MQ 會(huì)以定時(shí)任務(wù)的方式主動(dòng)回查發(fā)送方并獲取發(fā)送方執(zhí)行結(jié)果。

這樣即便出現(xiàn)網(wǎng)絡(luò)問(wèn)題或者發(fā)送方本身的問(wèn)題(重啟,宕機(jī)等),MQ 通過(guò)定時(shí)任務(wù)主動(dòng)回查發(fā)送方基本都能確認(rèn)消息最終要提交還是回滾(拋棄)。

當(dāng)然出于性能和半消息堆積方面的考慮,MQ 本身也會(huì)有回查次數(shù)的限制。

問(wèn)題 3:如何保證消費(fèi)一定成功呢?

RocketMQ 本身有 Ack 機(jī)制,來(lái)保證消息能夠被正常消費(fèi)。如果消費(fèi)失敗(消息訂閱方出錯(cuò),宕機(jī)等原因),RocketMQ 會(huì)把消息重發(fā)回 Broker,在某個(gè)延遲時(shí)間點(diǎn)后(默認(rèn) 10 秒后)重新投遞消息。

結(jié)合上面幾個(gè)同步調(diào)用 Hmily 完整代碼如下:

  1. //TransactionListener是rocketmq接口用于回調(diào)執(zhí)行本地事務(wù)和狀態(tài)回查 
  2. public class TransactionListenerImpl implements TransactionListener { 
  3.      //執(zhí)行本地事務(wù) 
  4.      @Override 
  5.      public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { 
  6.          記錄orderID,消息狀態(tài)鍵值對(duì)到共享map中,以備MQ回查消息狀態(tài)使用; 
  7.          return LocalTransactionState.COMMIT_MESSAGE; 
  8.      } 
  9.  
  10.      //回查發(fā)送者狀態(tài) 
  11.      @Override 
  12.      public LocalTransactionState checkLocalTransaction(MessageExt msg) { 
  13.          String status = 從共享map中取出orderID對(duì)應(yīng)的消息狀態(tài);  
  14.          if("commit".equals(status)) 
  15.            return LocalTransactionState.COMMIT_MESSAGE; 
  16.          else if("rollback".equals(status)) 
  17.            return LocalTransactionState.ROLLBACK_MESSAGE; 
  18.          else 
  19.            return LocalTransactionState.UNKNOW; 
  20.      } 
  1. //訂單服務(wù) 
  2. public class OrderService{ 
  3.  
  4.   //tcc接口 
  5.   @Hmily(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus"
  6.   public void makePayment() { 
  7.      1,更新訂單狀態(tài)為支付中 
  8.      2,凍結(jié)庫(kù)存,rpc調(diào)用 
  9.      3,優(yōu)惠券狀態(tài)改為使用中,rpc調(diào)用 
  10.      4,發(fā)送半消息(待確認(rèn)消息)通知WMS撿貨出庫(kù) //創(chuàng)建producer時(shí)這冊(cè)TransactionListenerImpl 
  11.   } 
  12.  
  13.   public void confirmOrderStatus() { 
  14.      更新訂單狀態(tài)為已支付 
  15.   } 
  16.  
  17.   public void cancelOrderStatus() { 
  18.      恢復(fù)訂單狀態(tài)為待支付  
  19.   }   
  20.  
  1. //庫(kù)存服務(wù) 
  2. public class InventoryService { 
  3.  
  4.   //tcc接口 
  5.   @Hmily(confirmMethod = "confirmDecr", cancelMethod = "cancelDecr"
  6.   public void lockStock() { 
  7.      //防懸掛處理 
  8.      if (分支事務(wù)記錄表沒(méi)有二階段執(zhí)行記錄) 
  9.        凍結(jié)庫(kù)存 
  10.      else 
  11.        return
  12.   } 
  13.  
  14.   public void confirmDecr() { 
  15.      確認(rèn)扣減庫(kù)存 
  16.   } 
  17.   public void cancelDecr() { 
  18.      釋放凍結(jié)的庫(kù)存 
  19.   }   
  20.  
  1. //卡券服務(wù) 
  2. public class CouponService { 
  3.  
  4.   //tcc接口 
  5.   @Hmily(confirmMethod = "confirm", cancelMethod = "cancel"
  6.   public void handleCoupon() { 
  7.      //防懸掛處理 
  8.      if (分支事務(wù)記錄表沒(méi)有二階段執(zhí)行記錄) 
  9.        優(yōu)惠券狀態(tài)更新為臨時(shí)狀態(tài)Inuse 
  10.      else 
  11.        return
  12.   } 
  13.  
  14.   public void confirm() { 
  15.      優(yōu)惠券狀態(tài)改為Used 
  16.   } 
  17.   public void cancel() { 
  18.      優(yōu)惠券狀態(tài)恢復(fù)為Unused 
  19.   }   
  20.  

如果執(zhí)行到 TransactionListenerImpl.executeLocalTransaction 方法,說(shuō)明半消息已經(jīng)發(fā)送成功了。

也說(shuō)明 OrderService.makePayment 方法的四個(gè)步驟都執(zhí)行成功了,此時(shí) TCC 也到了 Confirm 階段。

所以在 TransactionListenerImpl.executeLocalTransaction 方法里可以直接返回 LocalTransactionState.COMMIT_MESSAGE 讓 MQ 提交這條消息。

同時(shí)將該訂單信息和對(duì)應(yīng)的消息狀態(tài)保存在共享 map 里,以備確認(rèn)消息發(fā)送失敗時(shí) MQ 回查消息狀態(tài)使用。

微服務(wù)化過(guò)程,無(wú)感知數(shù)據(jù)遷移

微服務(wù)化,其中一個(gè)重要意義在于數(shù)據(jù)隔離,即不同的服務(wù)對(duì)應(yīng)各自的數(shù)據(jù)庫(kù)表,避免不同業(yè)務(wù)模塊間數(shù)據(jù)的耦合。

這也就意味著微服務(wù)化過(guò)程要拆分現(xiàn)有數(shù)據(jù)庫(kù),把單體數(shù)據(jù)庫(kù)根據(jù)業(yè)務(wù)模塊拆分成多個(gè),進(jìn)而涉及到數(shù)據(jù)遷移。

數(shù)據(jù)遷移過(guò)程我們要注意哪些關(guān)鍵點(diǎn)呢?

  • 第一,保證遷移后數(shù)據(jù)準(zhǔn)確不丟失,即每條記錄準(zhǔn)確而且不丟失記錄。
  • 第二,不影響用戶體驗(yàn)(尤其是訪問(wèn)量高的C端業(yè)務(wù)需要不停機(jī)平滑遷移)。
  • 第三,保證遷移后的性能和穩(wěn)定性。

數(shù)據(jù)遷移我們經(jīng)常遇到的兩個(gè)場(chǎng)景:

  • 業(yè)務(wù)重要程度一般或者是內(nèi)部系統(tǒng),數(shù)據(jù)結(jié)構(gòu)不變,這種場(chǎng)景下可以采用掛從庫(kù),數(shù)據(jù)同步完找個(gè)訪問(wèn)低谷時(shí)間段,停止服務(wù),然后將從庫(kù)切成主庫(kù),再啟動(dòng)服務(wù)。簡(jiǎn)單省時(shí),不過(guò)需要停服避免切主庫(kù)過(guò)程數(shù)據(jù)丟失。
  • 重要業(yè)務(wù),并發(fā)高,數(shù)據(jù)結(jié)構(gòu)改變。這種場(chǎng)景一般需要不停機(jī)平滑遷移。下面就重點(diǎn)介紹這部分經(jīng)歷。

互聯(lián)網(wǎng)行業(yè),很多業(yè)務(wù)訪問(wèn)量很大,即便凌晨低谷時(shí)間,仍然有相當(dāng)?shù)脑L問(wèn)量,為了不影響用戶體驗(yàn),很多公司對(duì)這些業(yè)務(wù)會(huì)采用不停機(jī)平滑遷移的方式。

因?yàn)閷?duì)老數(shù)據(jù)遷移的同時(shí),線上還不斷有用戶訪問(wèn),不斷有數(shù)據(jù)產(chǎn)生,不斷有數(shù)據(jù)更新,所以我們不但要考慮老數(shù)據(jù)遷移的問(wèn)題,還要考慮數(shù)據(jù)更新和產(chǎn)生新數(shù)據(jù)的問(wèn)題。下面介紹一下我們之前的做法。

關(guān)鍵步驟如下:

①開啟雙寫,新老庫(kù)同時(shí)寫入(涉及到代碼改動(dòng))。注意:任何對(duì)數(shù)據(jù)庫(kù)的增刪改都要雙寫。

對(duì)于更新操作,如果新庫(kù)沒(méi)有相關(guān)記錄,先從老庫(kù)查出記錄更新后寫入數(shù)據(jù)庫(kù);為了保證寫入性能,老庫(kù)寫完后,可以采用消息隊(duì)列異步寫入新庫(kù)。

同時(shí)寫兩個(gè)庫(kù),不在一個(gè)本地事務(wù),有可能出現(xiàn)數(shù)據(jù)不一致的情況,這樣就需要一定的補(bǔ)償機(jī)制來(lái)保證兩個(gè)庫(kù)數(shù)據(jù)最終一致。下一篇文章會(huì)分享最終一致性解決方案

②將某時(shí)間戳之前的老數(shù)據(jù)遷移到新庫(kù)(需要腳本程序做老數(shù)據(jù)遷移,因?yàn)閿?shù)據(jù)結(jié)構(gòu)變化比較大的話,從數(shù)據(jù)庫(kù)層面做數(shù)據(jù)遷移就很困難了)。

注意兩點(diǎn):

  • 時(shí)間戳一定要選擇開啟雙寫后的時(shí)間點(diǎn),避免部分老數(shù)據(jù)被漏掉。
  • 遷移過(guò)程遇到記錄沖突直接忽略(因?yàn)榈谝徊接懈虏僮?,直接把記錄拉到了新?kù));遷移過(guò)程一定要記錄日志,尤其是錯(cuò)誤日志。

③第二步完成后,我們還需要通過(guò)腳本程序檢驗(yàn)數(shù)據(jù),看新庫(kù)數(shù)據(jù)是否準(zhǔn)確以及有沒(méi)有漏掉的數(shù)據(jù)。

④數(shù)據(jù)校驗(yàn)沒(méi)問(wèn)題后,開啟雙讀,起初新庫(kù)給少部分流量,新老兩庫(kù)同時(shí)讀取,由于時(shí)間延時(shí)問(wèn)題,新老庫(kù)數(shù)據(jù)可能有些不一致,所以新庫(kù)讀不到需要再讀一遍老庫(kù)。

逐步將讀流量切到新庫(kù),相當(dāng)于灰度上線的過(guò)程。遇到問(wèn)題可以及時(shí)把流量切回老庫(kù)。

⑤讀流量全部切到新庫(kù)后,關(guān)閉老庫(kù)寫入(可以在代碼里加上可熱配開關(guān)),只寫新庫(kù)。

⑥遷移完成,后續(xù)可以去掉雙寫雙讀相關(guān)無(wú)用代碼。

第二步的老數(shù)據(jù)遷移腳本程序和第三步的檢驗(yàn)程序可以工具化,以后再做類似的數(shù)據(jù)遷移可以復(fù)用。

目前各云服務(wù)平臺(tái)也提供數(shù)據(jù)遷移解決方案,大家有興趣也可以了解一下!

全鏈路 APM 監(jiān)控

在體會(huì)到微服務(wù)帶來(lái)好處的同時(shí),很多公司也會(huì)明顯感受到微服務(wù)化后那些讓人頭疼的問(wèn)題。

比如,服務(wù)化之后調(diào)用鏈路變長(zhǎng),排查性能問(wèn)題可能要跨多個(gè)服務(wù),定位問(wèn)題更加困難。

服務(wù)變多,服務(wù)間調(diào)用關(guān)系錯(cuò)綜復(fù)雜,以至于很多工程師不清楚服務(wù)間的依賴和調(diào)用關(guān)系,之后的系統(tǒng)維護(hù)過(guò)程也會(huì)更加艱巨。諸如此類的問(wèn)題還很多!

這時(shí)就迫切需要一個(gè)工具幫我們解決這些問(wèn)題,于是 APM 全鏈路監(jiān)控工具就應(yīng)運(yùn)而生了。

有開源的 Pinpoint、Skywalking 等,也有收費(fèi)的 Saas 服務(wù)聽云、OneAPM 等。有些實(shí)力雄厚的公司也會(huì)自研 APM。

下面我們介紹一下如何利用開源 APM 工具 Pinpoint 應(yīng)對(duì)上述問(wèn)題。

拓?fù)鋱D:

微服務(wù)化后,服務(wù)數(shù)量變多,服務(wù)間調(diào)用關(guān)系也變得更復(fù)雜,以至于很多工程師不清楚服務(wù)間的依賴和調(diào)用關(guān)系,給系統(tǒng)維護(hù)帶來(lái)很多困難。

 

通過(guò)拓?fù)鋱D我們可以清晰地看到服務(wù)與服務(wù),服務(wù)與數(shù)據(jù)庫(kù),服務(wù)與緩存中間件的調(diào)用和依賴關(guān)系。對(duì)服務(wù)關(guān)系了如指掌之后,也可以避免服務(wù)間循依賴、循環(huán)調(diào)用的問(wèn)題。

請(qǐng)求調(diào)用棧(Call Stack)監(jiān)控:

 

微服務(wù)化后,服務(wù)變多,調(diào)用鏈路變長(zhǎng),跨多個(gè)服務(wù)排查問(wèn)題會(huì)更加困難。上圖是一個(gè)請(qǐng)求的調(diào)用棧,我們可以清晰看到一次請(qǐng)求調(diào)用了哪些服務(wù)和方法、各個(gè)環(huán)節(jié)的耗時(shí)以及發(fā)生在哪個(gè)服節(jié)點(diǎn)。

上圖的請(qǐng)求耗時(shí)過(guò)長(zhǎng),根據(jù)監(jiān)控(紅框部分)我們可以看到時(shí)間主要消耗在數(shù)據(jù)庫(kù) SQL 語(yǔ)句上。

點(diǎn)擊數(shù)據(jù)庫(kù)圖表還可以看詳細(xì) SQL 語(yǔ)句,如下圖:

 

如果發(fā)生錯(cuò)誤,會(huì)顯示為紅色,錯(cuò)誤原因也會(huì)直接顯示出來(lái)。如下圖:

 

類似性能問(wèn)題和錯(cuò)誤的線上排查。我們?nèi)绻ㄟ^(guò)查日志的傳統(tǒng)辦法,可能會(huì)耗費(fèi)大量的時(shí)間。但是通過(guò) APM 工具分分鐘就可以搞定了!

請(qǐng)求 Server Map:

 

Server Map 是 Pinpoint 另一個(gè)比較重要的功能。如上圖,我們不但能清晰地看到一個(gè)請(qǐng)求的訪問(wèn)鏈路,而且還能看到每個(gè)節(jié)點(diǎn)的訪問(wèn)次數(shù),為系統(tǒng)優(yōu)化提供了有力的依據(jù)。

如果一次請(qǐng)求訪問(wèn)了多次數(shù)據(jù)庫(kù),說(shuō)明代碼邏輯可能有必要優(yōu)化了!

JVM 監(jiān)控:

 

此外,Pinpoint 還支持堆內(nèi)存,活躍線程,CPU,文件描述符等監(jiān)控。

關(guān)于微服務(wù)化,我們就分享這么多。希望對(duì)大家有幫助。

作者:二馬讀書

簡(jiǎn)介:曾任職于阿里巴巴,每日優(yōu)鮮等互聯(lián)網(wǎng)公司,任技術(shù)總監(jiān),15 年電商互聯(lián)網(wǎng)經(jīng)歷。

編輯:陶家龍

出處:轉(zhuǎn)載自微信公眾號(hào)架構(gòu)師進(jìn)階之路(ID:ermadushu)

責(zé)任編輯:武曉燕 來(lái)源: 架構(gòu)師進(jìn)階之路
相關(guān)推薦

2020-11-06 09:36:11

命令微服務(wù)軟件

2020-08-05 08:23:19

架構(gòu)Java微服務(wù)

2020-04-23 11:18:14

Redis分布式

2011-05-17 09:28:18

2021-12-09 20:30:17

變量面試方法

2022-04-20 07:48:09

微服務(wù)鏈路服務(wù)器

2021-04-23 23:22:07

iOS蘋果系統(tǒng)

2024-09-04 17:49:27

2022-07-17 11:45:39

微服務(wù)架構(gòu)

2017-07-25 09:55:13

微服務(wù)架構(gòu)種類

2009-08-13 17:52:27

C#數(shù)據(jù)采集

2020-09-16 09:08:49

訂單微服務(wù)架構(gòu)

2017-03-06 17:30:11

微服務(wù)架構(gòu)系統(tǒng)

2015-12-11 17:24:50

Androidgradle開發(fā)

2023-01-09 17:46:07

項(xiàng)目版本號(hào)字段

2020-01-18 09:35:03

微服務(wù)團(tuán)隊(duì)架構(gòu)

2024-07-08 13:56:12

微服務(wù)API代碼

2012-11-22 10:39:37

漏洞PDF文件

2015-07-30 09:27:04

2021-04-19 09:15:14

老板公司企業(yè)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)