墨墨導讀:本文節選自《高可用可伸縮微服務架構:基於Dubbo、Spring Cloud和Service Mesh》一書,程超 等著,由電子工業出版社博文視點出版,已得到受權。近年來微服務架構已經成爲大規模分佈式架構的主流技術,愈來愈多的公司已經或開始轉型爲微服務架構。本書不以某一種微服務框架的使用爲主題,而是對整個微服務生態進行系統性的講解,並結合工做中的大量實戰案例爲讀者呈現一本讀完便可實際上手應用的工具書。前端
常見的互聯網支付的使用場景主要有如下幾種。java
刷卡支付:用戶展現微信錢包內的「刷卡條碼/二維碼」給商戶系統,掃描後直接完成支付,適用於線下面對面收銀的場景,如超市、便利店等(被掃,線下)。數據庫
掃碼支付:商戶系統按微信支付協議生成支付二維碼,用戶再用微信「掃一掃」來完成支付,適用於PC網站支付、實體店單品等場景(主掃,線上)。apache
公衆號支付:用戶在微信中打開商戶的H5頁面,商戶在H5頁面經過調用微信支付提供的JSAPI接口調用微信支付模塊來完成支付,適用於在公衆號、朋友圈、聊天窗口等微信內完成支付的場景。後端
WAP支付:基於公衆號基礎開發的一種非微信內瀏覽器支付方式(須要單獨申請支付權限),能夠知足在微信外的手機H5頁面進行微信支付的需求!簡單來講,就是經過PC、手機網頁來實現下單支付(俗稱H5支付)。瀏覽器
App支付:商戶經過在移動端應用App中集成開放SDK調用微信支付模塊來完成支付。緩存
網關支付:用戶須要開通網上銀行後在線完成支付,主要對象是國內銀行借記卡和信用卡,是銀行系統爲企業或我的提供的安全、快捷、穩定的支付服務。安全
快捷支付:快捷支付指用戶購買商品時,不需開通網銀,只需提供銀行卡卡號、戶名、手機號碼等信息,銀行驗證手機號碼正確性後,第三方支付發送手機動態口令到用戶手機號上,用戶輸入正確的手機動態口令便可完成支付。性能優化
在支付場景下實現微服務的最終目標:可以將單體支付系統按業務進行解耦,利用微服務生態來實施支付系統,而且可以保證系統的可靠性和併發能力,建設完整的運維體系以支撐日益龐大的微服務系統。服務器
咱們在第2章介紹了領域建模的相關知識,由此能夠知道幾個關鍵詞:領域、子域、限界上下文。有些讀者對領域、子域的概念比較容易理解,可是限界上下文就理解得比較模糊,這裏再對這個關鍵詞簡單作一下介紹。
能夠把限界上下文理解爲:一個系統、一個應用、一個服務或一個組件,而它又存在於領域之中。舉個生活中的例子:我天天上班都會坐地鐵,從家裏出發到單位須要換乘三次地鐵,分別是5號線、8號線和2號線。那麼地鐵就能夠理解爲限界上下文,從5號線走到8號線這個過程就是領域事件,而爲了到達目的地中間換乘地鐵,這個過程叫做上下文切換。
再回到支付業務中,該如何根據業務和領域相關知識來劃分服務呢?咱們以一個業務架構示例來說解,如圖11-1所示。
當咱們在工做中遇到一個完整的業務場景時,首先須要識別出一共有哪些領域,根據大的領域再來劃分子域,最後將具備相同領域或子域的限界上下文進行歸類。正確識別出領域實際上是比較難的,須要設計人員前期對業務有大量的調研,有比較深刻的瞭解後才能識別領域。
從圖11-1中能夠看到整個業務架構圖分兩大部分,中間的是業務核心領域,兩邊的是支撐子域。
咱們重點介紹中間的部分,每一層就是一個領域,領域中又包括特定子域。
(1)對接業務層:主要是一些業務系統對接支付系統,包括電商業務、互金業務和一鍵支付三個限界上下文。
(2)統一接入網關層:主要功能是對請求入口進行加解密、分流、限流和准入控制等。
圖11-1
(3)產品服務層。
收銀臺:包括兩個限界上下文,分別是PC收銀臺和手機手銀臺。
商戶:包括四個限界上下文,分別是分帳、鑑權、擔保和代扣。
我的:包括兩個限界上下文,分別是充值和提現。
(4)業務服務層:包括五個限界上下文,分別是交易服務、支付服務、退款服務、計費服務和風控服務。
(5)基礎服務層。
網關:包括三個限界上下文,分別是支付網關、鑑權和支付路由。
資金處理平臺:包括四個限界上下文—對帳、清結算、備付金和會計。
3、支付場景下微服務架構的詳解與分析
使用微服務的核心是業務,沒有業務進行支撐的微服務是「虛的」,但只有業務與微服務相結合的思想而沒有微服務的架構體系也是沒法將微服務落地的,因此本章重點介紹要作好微服務還須要完善哪些技術架構。
下面咱們將以一個實際工做中的案例爲出發點,分析在中小公司中如何落地微服務。如圖11-2所示,左半部分是微服務的業務架構,右半部分是微服務的基礎技術架構。
圖11-2
3.1 業務架構分析
根據前面介紹的如何根據業務來劃分領域能夠看到,整個業務架構部分已經完成了領域的劃分,咱們重點來看服務層。服務層是一個核心域裏面包含了多個子域,每一個子域都是按功能進行劃分的,好比支付中心子域裏面包括支付服務、路由系統和銀行渠道等限界上下文,這些限界上下文是一個服務,仍是一個系統呢?這就要結合康威定律來綜合考量團隊的規模,小公司創業初期研發人員少,能夠將支付中心子域定義爲限界上下文,裏面包括三個獨立模塊,分別是支付服務模塊、路由模塊和銀行渠道模塊,待人員逐步增長到必定規模後,多個項目組同時修改一個支付中心限界上下文會致使互相影響的時候,就須要將支付中心上升爲一個業務領域,而將以前的三個獨立模塊拆分爲獨立系統,由不一樣的項目組分別接管,各自維護各自部署,如圖11-3所示。
圖11-3
能夠看出左邊是未拆分前的結構,交易服務想要調用支付模塊就必須統一調用支付系統,而後才能調用支付模塊,而右邊是通過拆分後的結構,這時交易服務能夠直接調用支付服務系統、路由系統和銀行渠道系統中的任意一個,固然從業務流來說確定要先調用支付服務系統。
而數據層是根據業務進行數據庫的拆分,拆分原則與應用拆分相同,如圖11-4所示。
圖11-4
能夠看到業務、應用和數據庫三者一體,物理上與其餘業務隔離,不一樣應用服務的數據庫是不能直接訪問的,只能經過服務調用進行訪問。
3.2 技術平臺詳解
當咱們將整個支付業務根據微服務理念作了合理劃分以後,業務架構的各層次就逐步清晰起來,而微服務架構的成功建設除了業務上面的劃分,技術平臺和運維體系的支撐也是很是重要的,圖11-2的右半部分共分爲三個層次,分別是統一平臺業務層、微服務基礎中間件層和自動化運維層。
1) 統一業務平臺層
這一層主要是通用的平臺業務系統,包括數據分析服務、商戶運營服務、運維管控服務和進件報備服務,它們沒法根據業務被歸類到某一業務系統中,只能做爲支撐域存在,因此放到統一業務平臺層供全部業務線共同使用。
2)微服務基礎中間件層
微服務自己是一個生態,爲了支撐微服務這個龐大的體系,必須有不少基礎中間件進行輔助才能使微服務平穩地運行。下面將根據筆者積累的實踐經驗對圖中一些重要的組件進行技術選型方面的介紹,另外圖中有不少組件在本書其餘章節進行了詳細介紹,這裏就再也不作說明。
微服務框架
目前市面上很是流行Spring Boot+Spring Cloud的微服務框架,這套框架確實是微服務的集大成者,涵蓋的範圍廣,能夠支持動態擴展和多種插件。可是做爲公司的管理者來講,並不能由於出了新的技術就馬上將公司核心業務用新的技術進行更替,這樣在生產上所帶來的風險將會很是大。比較合理的作法是,若是公司或部門是新成立的,尚未作技術框架的選型,又想在公司內部推廣微服務的時候,嘗試使用Spring Boot和Spring Cloud框架,能夠節省出公司或部門的不少時間來攻關前端業務,而不須要將更多精力放在如何進行微服務的建設上來。
目前不少互聯網公司在生產過程當中使用的微服務框架並非Spring Boot和Spring Cloud,會使用如Dubbo、gRPC、Thrift等RPC框架進行服務治理,而公司內部本身研發出不少微服務的外圍組件,好比APM監控系統、分庫分表組件、統一配置中心、統必定時任務等。在這種狀況下公司內部已經自建了比較完善的基礎架構平臺就不必總體更換爲Spring Boot和Spring Cloud,不然代價極大,甚至會對公司的業務形成嚴重的後果。公司發展的策略通常都是以客戶(用戶)穩定優先,但公司技術也須要更新,能夠先嚐試在公司邊緣業務中使用,達到承認後逐步推廣,循環漸進。
筆者在進行微服務改造的過程當中其實是基於原有的Dubbo作的改進,將Duboo和Spring Boot相結合造成服務治理框架。
消息服務
咱們在談技術選型的時候,不能脫離業務空談選型,每種消息中間件一定有其優勢和不足,咱們能夠根據自身的場景擇優選擇,下面筆者結合本身使用的兩種類型的MQ簡單說一下選型與使用場景。
RabbitMQ是使用Erlang編寫的一個開源的消息隊列,自己支持不少協議:AMQP、XMPP、SMTP、STOMP,也正是如此,使它變得很是重量級,更適合企業級的開發。RabbitMQ是AMQP協議領先的一個實現,它實現了代理(Broker)架構,意味着消息在發送到客戶端以前能夠在中央節點上排隊。對路由(Routing)、負載均衡(Load balance)或數據持久化都有很好的支持。可是在集羣中使用的時候,分區配置不當偶爾會有腦裂現象出現,總的來講,在支付行業用RabbitMQ仍是很是多的。
Kafka是LinkedIn於2010年12月開發並開源的一個分佈式MQ系統,如今是Apache的一個孵化項目,是一個高性能跨語言分佈式Publish/Subscribe消息隊列系統,其性能和效率在行業中是領先的,可是原先的版本通過大量測試,由於其主備Partition同步信息的機制問題,偶爾會形成數據丟失等問題,因此更多的應用場景仍是在大數據、監控等領域。
目前市面上有不少支付公司都在使用RabbitMQ做爲消息中間件,雖然很「重」可是卻具備支付行業的不丟消息、MQ相對穩定等特色。缺點則是不像ActiveMQ那樣可使用Java實現定製化,好比想知道消息隊列中有多少剩餘消息沒有消費,哪些通道獲取過消息,共有多少條,是否能夠手動或自動觸發重試等,還有監控和統計信息,目前作得還不是太完善,只能知足基本功能的要求。
接下來咱們再來講說消息隊列在技術領域的使用場景。
(1)能夠作延遲設計。
好比一些數據須要過五分鐘後再使用,這時就須要使用延遲隊列設計,好比在RabbitMQ中利用死信隊列實現。
(2)異步處理。
主要應用在多任務執行的場景。
(3)應用解耦。
在大型微服務架構中,有一些無狀態的服務常常考慮使用MQ作消息通知和轉換。
(4)分佈式事務最終一致性。
可使用基於消息中間件的隊列作分佈式事務的消息補償,實現最終一致性。
(5)流量削峯。
通常在秒殺或團搶活動中使用普遍,能夠經過隊列控制秒殺的人數和商品,還能夠緩解短期壓垮應用系統的問題。
(6)日誌處理。
咱們在作監控或日誌採集的時候常常用隊列來作消息的傳輸和暫存。
統一配置中心
目前市面有不少種開源的統一配置中心組件可供使用,如攜程開源的Apollo、阿里的Diamond、百度的Disconf,每種組件都各有特色,咱們在使用的過程當中還須要根據實際狀況來綜合考量。筆者公司目前採用的微服務架構是Spring Boot+Dubbo的方式,Apollo的架構使用了Spring Boot+SpringCloud的方式,在架構方式上正好能夠無縫對接,同時Apollo能夠解決同城雙活方面的問題,因此從這些角度來看比較適合目前的場景。
銀行通道監控與切換
因爲每家銀行提供的業務及產品不一樣,例如B2C、B2B、大額支付、銀企直連、代收代付、快捷支付等,這些產品及服務並沒有統一的接口,要使用這些產品服務,支付機構只能一家家銀行進行接入,當對接的銀行通道過多時,每條通道的穩定性就是支付工做中的重中之重,這是涉及用戶支付是否成功的關鍵,也是支付機構支付成功率的重要指標,基於此,要有針對性地進行銀行通道穩定性的監控與故障切換系統的建設,如圖11-5所示。
圖11-5
圖11-5是通道監控與切換系統的總體架構,經過在相應組件或應用上面增長Agent監控代理攔截通道的請求狀況,通過Collector進行數據彙總,而後將通道評分數據發送給Redis集羣,而支付路由系統在進行通道選取的時候會從Redis集羣中獲取通道的評分及通道相應的配置項進行綜合評定從而選取合適的通道,另外採集全部的監控數據都會存放到InfluxDB中,經過Grafana進行預警展現,若是通道不可用則自動將通道關閉,同時通知研發部門進行問題排查。
4、從代碼層面提高微服務架構的性能
不少架構變遷或演進方面的文章大可能是針對架構方面的介紹,不多有針對代碼級別的性能優化介紹,這就比如蓋樓同樣,樓房的基礎架子搭得很好,可是蓋房的工人不夠專業,有不少須要注意的地方忽略了,在往裏面添磚加瓦的時候出了問題,後果就是房子常常漏雨、牆上有裂縫等各類問題出現,雖然不至於樓房塌陷,但樓房已經變成了危樓。
判斷一個項目是否具備良好的設計須要從優秀的代碼和高可用架構兩個方面來衡量,如圖11-6所示。
圖11-6
優秀的代碼是要看程序的結構是否合理,程序中是否存在性能問題,依賴的第三方組件是否被正確使用等。而高可用架構是要看項目的可用性、擴展型,以及可以支持的併發能力。能夠說一個良好的項目設計是由兩部分組成的,缺一不可。
在實戰的過程當中,不一樣的公司所研發的項目和場景也不同,下面主要以支付場景爲出發點,從代碼和設計的角度總結一些常見的問題。
1)數據庫常常發生死鎖現象
以MySQL數據庫爲例,select......for update語句是手工加鎖(悲觀鎖)語句,是一種行級鎖。一般狀況下單獨使用select語句不會對數據庫數據加鎖,而使用for update語句則能夠在程序層面實現對數據的加鎖保護,若是for update語句使用不當,則很是容易形成數據庫死鎖現象的發生,如表11-1所示。
在上述事例中,會話B會拋出死鎖異常,死鎖的緣由就是A和B兩個會話互相等待,出現這種問題其實就是咱們在項目中混雜了大量的事務+for update語句而且使用不當所形成的。
MySQL數據庫鎖主要有三種基本鎖。
Record Lock:單個行記錄的鎖。
Gap Lock:間隙鎖,鎖定一個範圍,但不包括記錄自己。
Gap Lock+Record Lock(next-key lock):鎖定一個範圍,而且也鎖定記錄自己。
當for update語句和gap lock、next-key lock鎖相混合使用,又沒有注意用法的時候,就很是容易出現死鎖的狀況。
2)數據庫事務佔用時間過長
先看一段僞代碼:
public void test() { Transaction.begin //事務開啓 try { dao.insert //插入一行記錄 httpClient.queryRemoteResult() //請求訪問 dao.update //更新一行記錄 Transaction.commit()//事務提交 } catch(Exception e) { Transaction.rollFor//事務回滾 } }
項目中相似這樣的程序有不少,常常把相似httpClient,或者有可能形成長時間超時的操做混在事務代碼中,不只會形成事務執行時間超長,並且會嚴重下降併發能力。
咱們在使用事務的時候,遵循的原則是快進快出,事務代碼要儘可能小。針對以上僞代碼,咱們要把httpClient這一行拆分出來,避免同事務性的代碼混在一塊兒。
3)濫用線程池,形成堆和棧溢出
Java經過Executors提供了四種線程池可供咱們直接使用。
newCachedThreadPool:建立一個可緩存線程池,這個線程池會根據實際須要建立新的線程,若是有空閒的線程,則空閒的線程也會被重複利用。
newFixedThreadPool:建立一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
newScheduledThreadPool:建立一個定長線程池,支持定時及週期性任務執行。
newSingleThreadExecutor:建立一個單線程化的線程池,它只會用惟一的工做線程來執行任務,保證全部任務按照指定順序(FIFO,LIFO,優先級)執行。
JDK提供的線程池從功能上替咱們作了一些封裝,也節省了不少參數設置的過程。若是使用不當則很容易形成堆和棧溢出的狀況,示例代碼以下所示。
private staticfinal ExecutorService executorService = Executors.newCachedThreadPool(); /** * 異步執行短頻快的任務 * @param task */ public static voidasynShortTask(Runnable task){ executorService.submit(task); //task.run(); } CommonUtils.asynShortTask(newRunnable() { @Override public void run() { String sms =sr.getSmsContent(); sms = sms.replaceAll(finalCode, AES.encryptToBase64(finalCode,ConstantUtils.getDB_AES_KEY())); sr.setSmsContent(sms); smsManageService.addSmsRecord(sr); } });
以上代碼的場景是每次請求過來都會建立一個線程,將DUMP日誌導出進行分析,發現項目中啓動了一萬多個線程,並且每一個線程都顯示爲忙碌狀態,已經將資源耗盡。咱們仔細查看代碼會發現,代碼中使用的線程池是使用如下代碼來申請的。
private static final ExecutorServiceexecutorService = Executors.newCachedThreadPool();
在高併發的狀況下,無限制地申請線程資源會形成性能嚴重降低,採用這種方式最大能夠產生多少個線程呢?答案是Integer的最大值!查看以下源碼:
public static ExecutorServicenewCachedThreadPool() { return newThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, newSynchronousQueue<Runnable>()); }
既然使用newCachedThreadPool可能帶來棧溢出和性能降低,若是使用newFixedThreadPool設置固定長度是否是能夠解決問題呢?使用方式如如下代碼所示,設置固定線程數爲50:
private static final ExecutorServiceexecutorService = Executors.newFixedThreadPool(50);
修改完成之後,併發量從新上升到100TPS以上,可是當併發量很是大的時候,項目GC(垃圾回收能力降低),分析緣由仍是由於Executors.newFixedThreadPool(50)這一行,雖然解決了產生無限線程的問題,但採用newFixedThreadPool這種方式會形成大量對象堆積到隊列中沒法及時消費,源碼以下:
public static ExecutorService newFixedThreadPool(int nThreads,ThreadFactory threadFactory) { return newThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, newLinkedBlockingQueue<Runnable>(), threadFactory); }
能夠看到採用的是無界隊列,也就是說隊列能夠無限地存放可執行的線程,形成大量對象沒法釋放和回收。
其實JDK還提供了原生的線程池ThreadPoolExecutor,這個線程池基本上把控制的權力交給了使用者,使用者設置線程池的大小、任務隊列、拒絕策略、線程空閒時間等,無論使用哪一種線程池,都是創建在咱們對其精準把握的前提下才能真正使用好。
4)經常使用配置信息依然從數據庫中讀取
無論是什麼業務場景的項目,只要是老項目,咱們常常會遇到一個很是頭疼的問題就是項目的配置信息是在本地項目的properties文件中存放的,或者是將經常使用的配置信息存放到數據庫中,這樣形成的問題是:
若是使用本地properties文件,每次修改文件都須要一臺一臺地在線上環境中修改,在服務器數量很是多的狀況下很是容易出錯,若是修改錯了則會形成生產事故。
若是是用採集數據庫來統一存放配置信息,在併發量很是大的狀況下,每一次請求都要讀取數據庫配置則會形成大量的I/O操做,會對數據庫形成較大的壓力,嚴重的話對項目也會產生性能影響。
比較合理的解決方案之一:使用統一配置中心利用緩存對配置信息進行統一管理,具體的實現方案能夠參考《深刻分佈式緩存》這本書。
5)從庫中查詢數據,每次所有取出
咱們在代碼中常常會看到以下SQL語句:
select * from order where status = 'init'
這句SQL從語法上確實看不出什麼問題,可是放在不一樣的環境上卻會產生不一樣的效果,若是此時咱們的數據庫中狀態爲init的數據只有100條,那麼這條SQL會很是快地查詢出來並返回給調用端,在這種狀況下對項目沒有任何影響。若是此時咱們的數據庫中狀態爲init的數據有10萬條,那麼這條SQL語句的執行結果將是一次性把10萬記錄所有返回給調用端,這樣作不只會給數據庫查詢形成沉重的壓力,還會給調用端的內存形成極大的影響,帶來很是很差的用戶體驗。
比較合理的解決方案之一:使用limit關鍵字控制返回記錄的數量。
6)業務代碼研發不考慮冪等操做
冪等就是用戶對於同一操做發起的一次請求或屢次請求所產生的結果是一致的,不會由於屢次點擊而產生多種結果。
以支付場景爲例,用戶在網上購物選擇完商品後進行支付,由於網絡的緣由銀行卡上面的錢已經扣了,可是網站的支付系統返回的結果倒是支付失敗,這時用戶再次對這筆訂單發起支付請求,此時會進行第二次扣款,返回結果成功,用戶查詢餘額返發現多扣錢了,流水記錄也變成了兩條,這種場景就不是冪等。
實際工做中的冪等其實就是對訂單進行防重,防重措施是經過在某條記錄上加鎖的方式進行的。
針對以上問題,徹底沒有必要使用悲觀鎖的方式來進行防重,不然不只對數據庫自己形成極大的壓力,對於項目擴展性來講也是很大的擴展瓶頸,咱們採用了三種方法來解決以上問題:
使用第三方組件來作控制,好比ZooKeeper、Redis均可以實現分佈式鎖。
使用主鍵防重法,在方法的入口處使用防重表,可以攔截全部重複的訂單,當重複插入時數據庫會報一個重複錯,程序直接返回。
使用版本號(version)的機制來防重。
注意:以上三種方式都必須設置過時時間,當鎖定某一資源超時的時候,可以釋放資源讓競爭從新開始。
7)使用緩存不合理,存在驚羣效應、緩存穿透等狀況
緩存穿透
咱們在項目中使用緩存一般先檢查緩存中數據是否存在,若是存在則直接返回緩存內容,若是不存在就直接查詢數據庫,而後進行緩存並將查詢結果返回。若是咱們查詢的某一數據在緩存中一直不存在,就會形成每一次請求都查詢DB,這樣緩存就失去了意義,在流量大時,可能DB就「掛掉」了,這就是緩存穿透,如圖11-7所示。
圖11-7
要是有黑客利用不存在的緩存key頻繁攻擊應用,就會對數據庫形成很是大的壓力,嚴重的話會影響線上業務的正常進行。一個比較巧妙的作法是,能夠將這個不存在的key預先設定一個值,好比「key」「NULL」。在返回這個NULL值的時候,應用就能夠認爲這是不存在的key,應用就能夠決定是繼續等待訪問,仍是放棄掉此次操做。若是繼續等待訪問,則過一個時間輪詢點後,再次請求這個key,若是取到的值再也不是NULL,則能夠認爲這時候key有值了,從而避免透傳到數據庫,把大量的相似請求擋在了緩存之中。
緩存併發
看完上面的緩存穿透方案後,可能會有讀者提出疑問,若是第一次使用緩存或緩存中暫時沒有須要的數據,那麼又該如何處理呢?
在這種場景下,客戶端從緩存中根據key讀取數據,若是讀到了數據則流程結束,若是沒有讀到數據(可能會有多個併發都沒有讀到數據),則使用緩存系統中的setNX方法設置一個值(這種方法相似加鎖),沒有設置成功的請求則「sleep」一段時間,設置成功的請求則讀取數據庫獲取值,若是獲取到則更新緩存,流程結束,以前sleep的請求喚醒後直接從緩存中讀取數據,此時流程結束,如圖11-8所示。
圖11-8
這個流程裏面有一個漏洞,若是數據庫中沒有咱們須要的數據該怎麼處理?若是不處理請求則會形成死循環,不斷地在緩存和數據庫中查詢,這時就能夠結合緩存穿透的思路,這樣其餘請求就能夠根據「NULL」直接進行處理,直到後臺系統在數據庫成功插入數據後同步更新清理NULL數據和更新緩存。
緩存過時致使驚羣效應
咱們在使用緩存組件的時候,常常會使用緩存過時這一功能,這樣能夠不按期地釋放使用頻率很低的緩存,節省出緩存空間。若是不少緩存設置的過時時間是同樣的,就會致使在一段時間內同時生成大量的緩存,而後在另一段時間內又有大量的緩存失效,大量請求就直接穿透到數據庫中,致使後端數據庫的壓力陡增,這就是「緩存過時致使的驚羣效應」!
比較合理的解決方案之一:爲每一個緩存的key設置的過時時間再加一個隨機值,能夠避免緩存同時失效。
最終一致性
緩存的最終一致性是指當後端的程序在更新數據庫數據完成以後,同步更新緩存失敗,後續利用補償機制對緩存進行更新,以達到最終緩存的數據與數據庫的數據是一致的狀態。
經常使用的方法有兩種,分別是基於MQ和基於binlog的方式。
(1)基於MQ的緩存補償方案。
這種方案是當緩存組件出現故障或網絡出現抖動的時候,程序將MQ做爲補償的緩衝隊列,經過重試的方式機制更新緩存,如圖11-9所示。
圖11-9
說明:
應用同時更新數據庫和緩存。
若是數據庫更新成功,則開始更新緩存;若是數據庫更新失敗,則整個更新過程失敗。
判斷更新緩存是否成功,若是成功則返回。
若是緩存沒有更新成功,則將數據發到MQ中。
應用監控MQ通道,收到消息後繼續更新Redis。
問題點:
若是更新Redis失敗,同時在將數據發到MQ以前應用重啓了,那麼MQ就沒有須要更新的數據,若是Redis對全部數據沒有設置過時時間,同時在讀多寫少的場景下,那麼只能經過人工介入來更新緩存。
(2)基於binlog的方式來實現統一緩存更新方案。
第一種方案對於應用的研發人員來說比較「重」,須要研發人員同時判斷據庫和Redis是否成功來作不一樣的考慮,而使用binlog更新緩存的方案可以減輕業務研發人員的工做量,而且也有利於造成統一的技術方案,如圖11-10所示。
圖11-10
說明:
應用直接寫數據到數據庫中。
數據庫更新binlog日誌。
利用Canal中間件讀取binlog日誌。
Canal藉助於限流組件按頻率將數據發到MQ中。
應用監控MQ通道,將MQ的數據更新到Redis緩存中。
能夠看到這種方案對研發人員來講比較輕量,不用關心緩存層面,雖然這個方案實現起來比較複雜,但卻容易造成統一的解決方案。
問題點:
這種方案的弊端是須要提早約定緩存的數據結構,若是使用者採用多種數據結構來存放數據,則方案沒法作成通用的方式,同時極大地增長了方案的複雜度。
8)程序中打印了大量的無用日誌,而且引發性能問題
先來看一段僞代碼:
QuataDTO quataDTO = null; try { quataDTO = getRiskLimit(payRequest.getQueryRiskInfo(),payRequest.getMerchantNo(), payRequest.getIndustryCatalog(),cardBinResDTO.getCardType(), cardBinResDTO.getBankCode(), bizName); } catch (Exception e) { logger.info("獲取風控限額異常", e); }
經過上面的代碼,發現瞭如下須要注意的點:
日誌的打印必須以logger.error或logger.warn的方式打印出來。
日誌打印格式:[系統來源] 錯誤描述 [關鍵信息],日誌信息要打印出能看懂的信息,有前因和後果。甚至有些方法的入參和出參也要考慮打印出來。
在輸入錯誤信息的時候,Exception不要以e.getMessage的方式打印出來。
合理地日誌打印,能夠參考以下格式:
logger.warn("[innersys] - ["+ exceptionType.description + "] - [" + methodName + "] - " +"errorCode:[" + errorCode + "], " +"errorMsg:[" + errorMsg + "]", e); logger.info("[innersys] - [入參] - [" +methodName + "] - " + LogInfoEncryptUtil.getLogString(arguments)+ "]"); logger.info("[innersys] - [返回結果] - [" +methodName + "] - " + LogInfoEncryptUtil.getLogString(result));
在程序中大量地打印日誌,雖然可以打印不少有用信息幫助咱們排查問題,但日誌量太多不只影響磁盤I/O,還會形成線程阻塞,對程序的性能形成較大影響。在使用Log4j1.2.14設置ConversionPattern的時候,使用以下格式:
%d %-5p %c:%L [%t] - %m%n
在對項目進行壓測的時候卻發現了大量的鎖等待,如圖11-11所示。
圖11-11
對Log4j進行源碼分析,發如今org.apache.log4j.spi.LocationInfo類中有以下代碼:
String s; // Protect against multiple access to sw. synchronized(sw) { t.printStackTrace(pw); s = sw.toString(); sw.getBuffer().setLength(0); } //System.out.println("s is ["+s+"]."); int ibegin, iend;
能夠看出在該方法中用了synchronized鎖,而後又經過打印堆棧來獲取行號,因而將ConversionPattern的格式修改成%d %-5p %c [%t] - %m%n後,線程大量阻塞的問題解決了,極大地提升了程序的併發能力。
9)關於索引的優化
組合索引的原則是偏左原則,因此在使用的時候須要多加註意。
不須要過多地添加索引的數量,在添加的時候要考慮彙集索引和輔助索引,二者的性能是有區別的。
索引不會包含NULL值的列。
只要列中包含NULL值都不會被包含在索引中,複合索引中只要有一列含有NULL值,那麼這一列對於此複合索引就是無效的。因此咱們在設計數據庫時不要讓字段的默認值爲NULL。
MySQL索引排序。
MySQL查詢只使用一個索引,若是where子句中已經使用了索引,那麼order by中的列是不會使用索引的。所以數據庫默認排序能夠在符合要求的狀況下不使用排序操做;儘可能不要包含多個列的排序,若是須要,最好給這些列建立複合索引。
使用索引的注意事項。
如下操做符能夠應用索引:
m 大於等於;
m Between;
m IN;
m LIKE 不以%開頭。
如下操做符不能應用索引:
m NOT IN;
m LIKE %_開頭。
4.2 從總體架構的角度看
1)採用單體集羣的部署模式
當團隊和項目發展到必定規模後,就須要根據業務和團隊人數進行適當拆分。若是依然使用單體項目作總體部署,則項目之間互相影響極大,再加上團隊人員達到必定規模後,沒有辦法進行項目的維護和升級。
2)採用單機房的部署方式
如今互聯網項目對穩定性的要求愈來愈高,採用單機房部署的風險性也愈來愈高,像黑客惡意攻擊、機房斷電、網線損壞等不可預知的故障發生時,單機房是沒法提供穩定性保障的,這就須要互聯網企業開始建設同城雙活、異步多活等確保機房的穩定性。
3)採用Nginx+Hessian的方式實現服務化
Hessian是一個輕量級的Remoting on HTTP框架,採用的是Binary RPC協議。由於其易用性等特色,直到如今依然有不少企業還在使用Hessian做爲遠程通訊工具,但Hessian並不具有微服務的特色,只做爲遠程通訊工具使用,並且Hessian多偏重於數據如何打包、傳輸與解包,因此不少時候須要藉助Nginx來作服務路由、負載和重試等,並且還須要在Nginx中進行配置,也不能動態對服務進行加載和卸載,因此在業務愈來愈複雜,請求量愈來愈多的狀況下,Hessian不太適合做爲微服務的服務治理框架,這時就須要Spring Cloud或Dubbo了。
4)項目拆分不完全,一個Tomcat共用多個應用(見圖11-12)
圖11-12
注:一個Tomcat中部署多個應用war包,彼此之間互相牽制,在併發量很是大的狀況下性能下降很是明顯,如圖11-13所示。
圖11-13
注:拆分前的這種狀況其實仍是挺廣泛的,以前一直認爲項目中不會存在這種狀況,但事實上仍是存在了。解決的方法很簡單,每個應用war只部署在一個Tomcat中,這樣應用程序之間就不會存在資源和鏈接數的競爭狀況,性能和併發能力提高較爲明顯。
5)無服務降級策略
舉個例子來講明什麼是服務降級,咱們要出門旅遊但只有一隻箱子,咱們想帶的東西太多了把箱子都塞滿了,結果發現還有不少東西沒有放,因而只能把全部東西所有再拿出來作對比和分類,找到哪些是必需要帶的,哪些是非必需的,最終箱子裏面放滿了必需品,爲了防止這種狀況再次發生,下次再旅遊的時候就能夠提早多準備幾隻箱子。其實服務降級也是相似的思路,在資源有限的狀況下捨棄一些東西以保證更重要的事情可以進行下去。
服務降級的主要應用場景就是當微服務架構總體的負載超出了預設的上限閾值或即將到來的流量預計超過預設的閾值時,爲了保證重要的服務能正常運行,將一些不重要、不緊急的服務延遲或暫停使用。
6)支付運營報表,大數據量查詢
咱們先來回顧一下微服務的數據去中心化核心要點:
每一個微服務有本身私有的數據庫。
每一個微服務只能訪問本身的數據庫,而不能訪問其餘服務的數據庫。
某些業務場景下,請求除了要操做本身的數據庫,還要對其餘服務的數據庫進行添加、刪除和修改等操做。在這種狀況下不建議直接訪問其餘服務的數據庫,而是經過調用每一個服務提供的接口完成操做。
數據的去中心化進一步下降了微服務之間的耦合度。
經過上述核心要點能夠看到,微服務中關於數據的描述是去中心化,也就是說要根據業務屬性獨立拆分數據庫,使其業務領域與數據庫的關係是一一對應的。咱們仍是以支付業務場景爲例,單體支付項目進行微服務改造後,業務架構如圖11-14所示。
圖11-14
能夠看到將單體支付項目進行微服務改造後增長了多個服務項目,咱們能夠把每一個服務項目都理解爲一個限界上下文,每一個服務項目又對應一個數據庫,這樣數據庫由原來適應單體支付系統的大庫拆分紅了多個獨立的數據庫。問題來了,對於後臺運營統計來講這就是噩夢的開始,由於運營報表常常會跨業務進行統計和彙總,在原有運營系統上面作報表會給運營人員額外增長巨大的工做量,須要逐庫進行統計,而後進行彙總。
凡事都有兩面性,微服務給咱們帶來去中心化高度解耦的同時,也會帶來報表數據及歷史數據沒法統一彙總和查詢的問題,這時咱們就須要從各個服務數據庫中抽取數據到大數據平臺作數據集中化,如圖11-15所示。
圖11-15
一般大數據平臺也會和每一個服務的讀庫配合使用,大數據平臺存放的每每是大而全的數據。能夠把大數據平臺理解爲一個數據倉庫裏面存放若干年的數據,研發人員能夠根據數據量的大小及業務狀況合理利用服務的讀庫,這樣也能夠減輕查詢大數據平臺的壓力。好比用戶要查詢某個服務一週內的訂單狀況,則能夠直接從讀庫中進行查詢,這樣既能夠查詢到最新的訂單詳細信息,也能夠充分發揮讀庫的做用。若是用戶要查詢半年以上的數據,由於數據量大的緣由歷史數據早已經被遷移走,這時能夠在大數據平臺進行查詢。
7)運維手動打包和上線
微服務架構的順利實施還須要強有力的運維作支撐,這就至關於一輛寶馬車表面看上去特別豪華,但裏面裝的倒是老舊的發動機。這時就須要將DevOps在全公司推廣,讓自動化運維和部署成爲微服務的「發動機」。
5、微服務架構中常見的一些故障分析技巧
1)開發者的自測利器——hprof命令
示例程序以下所示。
注:這是一段測試代碼,經過sleep方法進行延時。
如何分析程序中哪塊代碼出現延時故障呢?
在程序中加上以下運行參數:
再次運行程序,發如今工程目錄裏面多了一個文本文件java.hprof.txt,打開文件,內容以下所示。
注:經過上面內容能夠看到是哪一個類的方法執行時間長,耗費了CPU時間,一目瞭然,方便咱們快速定位問題。
hprof不是獨立的監控工具,它只是一個Java Agent工具,它監控Java應用程序在運行時的CPU信息和堆內容,使用Java -agentlib:hprof=help命令能夠查看hprof的使用文檔。
上面的例子統計的是CPU時間,一樣咱們還能夠統計內存佔用的dump信息。例如:-agentlib:hprof=heap,format=b,file=/test.hprof。
咱們在用JUnit自測代碼的時候結合hprof,既能夠解決業務上的bug,又可以在必定程度上解決可發現的性能問題,很是實用。
2)性能排查工具——pidstat
示例代碼以下所示。
將示例代碼運行起來後,在命令行中輸入:
pidstat -p 843 1 3 -u -t /* -u:表明對CPU使用率的監控 參數1 3表明每秒採樣一次,一共三次 -t:將監控級別細化到線程 */
結果如圖11-16所示。
圖11-16
注:其中TID就是線程ID,%usr表示用戶線程使用率,從圖中能夠看到855這個線程的CPU佔用率很是高。
再次在命令行中輸入命令:
jstack -l 843 > /tmp/testlog.txt
查看testlog.txt,顯示以下所示的內容。
注:咱們關注的是日誌文件的NID字段,它對應的就是上面說的TID,NID是TID的16進製表示,將上面的十進制855轉換成十六進制爲357,在日誌中進行搜索看到以下內容。
以此能夠推斷出有性能瓶頸的問題點。
出處:架構文摘(ID:ArchDigest)
資源下載
關注公衆號:數據和雲(OraNews)回覆關鍵字獲取
2018DTCC , 數據庫大會PPT
2018DTC,2018 DTC 大會 PPT
ENMOBK,《Oracle性能優化與診斷案例》
DBALIFE ,「DBA 的一天」海報
DBA04 ,DBA 手記4 電子書
122ARCH ,Oracle 12.2體系結構圖
2018OOW ,Oracle OpenWorld 資料
產品推薦雲和恩墨Bethune Pro企業版,集監控、巡檢、安全於一身,你的專屬數據庫實時監控和智能巡檢平臺,漂亮的不像實力派,你值得擁有!
雲和恩墨zData一體機現已發佈超融合版本和精簡版,支持各類簡化場景部署,零數據丟失備份一體機ZDBM也已發佈,歡迎關注。
雲和恩墨大講堂 | 一個分享交流的地方
長按,識別二維碼,加入萬人交流社羣
請備註:雲和恩墨大講