【下載本文PDF進行閱讀】數據庫
這裏所說的三架馬車是指微服務、消息隊列和定時任務。以下圖所示,這裏是一個三駕馬車共同驅動的一個立體的互聯網項目的架構。無論項目是大是小,這個架構模板的形態一旦定型了以後就不太會變,區別只是咱們有更多的服務有更復雜的調用,更復雜的消息流轉,更多的Job,整個架構總體是可擴展的,並且不會變形,這個架構能夠在很長的一段時間內無需有大的調整。api
圖上畫了虛線框的都表明這個模塊或項目是不包含太多業務邏輯的,純粹是一層皮(會調用服務可是不會觸碰數據庫)。黑色線的箭頭表明依賴關係,綠色和紅色箭頭分別是MQ的發送和訂閱消息流的方向。具體在後文都會進一步詳細說明。緩存
微服務並非一個很新的概念,在10年前的時候我就開始實踐這個架構風格,在四個公司的項目中全面實現了微服務,愈來愈堅信這是很是適合互聯網項目的一個架構風格。不是說咱們的服務必定要跨物理機器進行遠程調用,而是咱們經過進行有意的設計讓咱們的業務在一開始的時候就按照領域進行分割,這能讓咱們對業務有更充分的理解,能讓咱們在以後的迭代中輕易在不一樣的業務模塊上進行耕耘,能讓咱們的項目開發愈來愈輕鬆,輕鬆來源於幾個方面:網絡
1. 若是咱們能進行微服務化,那麼咱們必定事先通過比較完善的產品需求討論和領域劃分,每個服務精心設計本身領域內的表結構,這是一個很重要的設計過程,也決定了整個技術架構和產品架構是匹配的,對於All-In-One的架構每每會省略這一過程,需求到哪裏代碼寫到哪裏。數據結構
2. 咱們對服務的劃分和職責的定位若是是清晰的,對於新的需求,咱們就能知道須要在哪裏改怎麼樣的代碼,沒有複製粘貼的存在少了不少坑。架構
3. 咱們大多數的業務邏輯已經開發完畢,直接重用便可,咱們的新業務只是現有邏輯的聚合。在PRD評審後,開發獲得的結論是隻須要組合分別調用ABC三個服務的XYZ方法,而後在C服務中修改一下Z方法增長一個分支邏輯,就能夠構建起新的邏輯,這種爽快的感受不可思議。併發
4. 在性能存在明顯瓶頸的時候,咱們能夠針對性地對某些服務增長更多機器進行擴容,並且由於服務的劃分,咱們更清楚系統的瓶頸所在,從10000行代碼定位到一行性能存在問題的代碼是比較困難的,可是若是這10000行代碼已是由10個服務構成的,那麼先定位到某個服務存在性能問題而後再針對這個服務進行分析一會兒下降了定位問題的複雜度。app
5. 若是業務有比較大的變更須要下線,那麼咱們能夠確定的是底層的公共服務是不會淘汰的,下線對應業務的聚合業務服務停掉流量入口,而後下線相關涉及到的基礎服務進行部分接口便可。若是擁有完善的服務治理平臺,整個操做甚至無需改動代碼。負載均衡
這裏也要求咱們作到幾個方面的原則:框架
1. 服務的粒度劃分須要把控好。個人習慣是先按照領域來分不會錯,隨着項目的進展慢慢進行更細粒度的拆分。好比對於互聯網金融P2P業務,一開始能夠分爲:
2. 服務必定是立體的,不是在一個層次上的,如上圖,咱們的服務有三個層次:
但願在這裏把這個事情說清楚了,怎麼來劃分服務怎麼劃分三個層次的服務是一個頗有意思頗有必要的事情,在服務劃分以後最好有一個明確的文檔來描述每個服務的職責,這樣咱們在無需閱讀API的狀況下能夠大概定位到業務所在的服務,整個複雜的系統就變得很直白了。
3.每個服務對接的底層數據表是獨立的沒有交叉關聯的,也就是數據結構是不直接對外的,須要使用其餘服務的數據必定經過訪問接口進行。好處也就是面向對象設計中封裝的好處:
說白了就是個人數據我作主,我想怎麼搞外面管不着,在重構或是作一些高層次技術架構(好比異地多活)的時候,沒有底層數據被依賴,這過重要了。固然,壞處或是麻煩的地方就是跨服務的調用使得數據操做沒法在一個數據庫事務中完成,這並非什麼大問題,一是由於咱們這種拆分方式並不會讓粒度太細,大部分的業務邏輯是在一個業務服務裏完成的,二是後面會提到跨服務的調用無論是經過MQ進行的仍是直接調用進行的,都會有補償來實現最終一致性。
4.考慮到跨機器跨進程調用服務穩定性方面的顯著差別。在方法內部進行方法調用,咱們須要考慮調用出現異常的狀況,可是幾乎不須要考慮超時的狀況,幾乎不須要考慮請求丟失的狀況,幾乎不須要考慮重複調用的狀況,對於遠程服務調用,這些點都須要去重點考慮,不然系統總體就是基本可用,測試環境不出問題,可是到了線上問題百出的狀態。這就要求對於每個服務的提供和調用多問幾個上面的問題,細細考慮到由於網絡問題方法沒有執行屢次執行或部分執行的狀況:
若是你說,這麼多服務,我在實現的時候很難考慮到這些點,我徹底不去考慮分佈式事務、冪等性、補償(絕不誇張地說,有的時候咱們花了20%的時間實現了業務邏輯,而後花80%的時間在實現這些可靠性方面的外圍邏輯),行不行?也不是不能夠,那麼業務在線上跑的時候必定會是千瘡百孔的,若是整個業務的處理對可靠性方面的要求不高或是業務不面向用戶不會受到投訴的話,這部分業務的是能夠暫時不考慮這些點,可是諸如訂單業務這種核心的不容許有不一致性的業務仍是須要全面考慮這些點的。
5. 考慮到跨機器跨進程調用服務數據傳輸方面的顯著差別。對於本地的方法調用,若是參數和返回值傳的是對象,那麼對於大部分的語言來講,傳的是指針(或指針的拷貝),指針指向的是堆中分配的對象,對象在數據傳輸上的成本幾乎忽略不計,也沒有序列化和反序列化的開銷。對於跨進程的服務調用,這個成本每每不能忽略不計。若是咱們須要返回不少數據,每每接口的定義須要進行特殊的改造:
6. 這裏還引伸出方法粒度的問題,好比咱們能夠定義GetUserInfo經過傳入不用的參數來返回不一樣的數據組合,也能夠分別定義GetUserBasicInfo、GetUserVIPInfo、GetUserInvestData等等細粒度的接口,接口的粒度定義取決於使用者會怎麼來使用數據,更趨向於一次使用單種類型數據仍是複合類型的數據等等。
7. 而後咱們須要考慮接口升級的問題,接口的改動最好是兼容以前的接口,若是接口須要淘汰下線,須要先確保調用方改造到了新接口,確保調用方流量爲0觀察一段時間後方能從代碼下線老接口。一旦服務公開出去,要進行接口定義調整甚至下線每每就沒有這麼容易了,不是本身說了算了。因此對外API的設計須要慎重點。
8. 最後不得不說,在整個公司都搞起了微服務後,跨部門的一些服務調用在商定API的時候不免會有一些扯皮的現象發生,究竟是我傳給你呢仍是你本身來拉,這個數據對我沒用爲何要在我這裏留一下呢?拋開非技術層面的事情不說,這些扯皮也是有一些技術手段來化解的:
你可能看到這裏以爲很頭暈,爲何微服務須要額外考慮這麼多東西,實現的複雜度一會兒上升了。我想說的是咱們須要換一個角度來考慮這個事情:
1. 咱們不須要在一開始的時候對全部邏輯都進行嚴密的考慮,先覆蓋核心流程核心邏輯。由於跨服務成爲了服務的提供方和使用方,至關於除了我本身,還有不少其它人會來關係個人服務能力,你們會提出各類問題,這對設計一個可靠的方法是有好處的。
2. 即便在不跨服務調用的時候咱們把全部邏輯堆積在一塊兒,也不意味着這些邏輯必定是事務性的,實現嚴密的,跨服務調用每每是必定程度放大了問題產生的可能性。
3. 咱們還有服務框架呢,服務框架每每會在監控跟蹤層次和運維繫統結合在一塊兒提供不少一體化的功能,這將封閉在內部的方法邏輯打散暴露出來,對於有一個完善的監控平臺的微服務系統,在排查問題的時候你每每會感嘆這是一個遠程服務調用就行了。
4. 最大的紅利仍是以前說的,當咱們以清晰的業務邏輯造成了一個立體化的服務體系以後,任何需求能夠解剖爲不多量的代碼修改和一些組合的服務調用,並且你知道我這麼作是不會有任何問題的,由於底層的服務ABCDEFG都是通過歷史考驗的,這種爽快感體驗過一次就會大呼過癮。
可是,若是服務粒度劃分的不合理,層次劃分的不合理,底層數據源有交叉,沒考慮到網絡調用失敗,沒考慮到數據量,接口定義不合理,版本升級過於魯莽,整個系統會出各類各樣的擴展問題性能問題和Bug,這是很頭痛的,這也就須要咱們有一個完善的服務框架來幫助咱們定位各類不合理,在以後說到中間件的文章中會再具體着重介紹服務治理這塊。
消息隊列MQ的使用有下面幾個好處,或者說咱們每每處於這些目的來考慮引入MQ:
1. 異步處理:相似於訂單這樣的流程通常能夠定義出一個核心流程,這個流程用於處理核心訂單的狀態機,須要儘快同步落庫完成,而後圍繞訂單會衍生出一系列和用戶相關的庫存相關的後續的業務處理,這些處理徹底不須要卡在用戶點擊提交訂單的那剎那進行處理。下單只是一個確認合法受理訂單的過程,後續的不少事情均可以慢慢在幾十個模塊中進行流轉,這個流轉過程哪怕是消耗5分鐘,用戶也無需感覺到。
2. 流量洪峯:互聯網項目的一個特色是有的時候會作一些toC的促銷,免不了有一些流量洪峯,若是咱們引入了消息隊列在模塊之間做爲緩衝,那麼backend的服務能夠以本身既有的舒服的頻次來被動消耗數據,不會被強壓的流量擊垮。固然,作好監控是必不可少的,下面再細說一下監控。
3. 模塊解耦:隨着項目複雜度的上升,咱們會有各類來源於項目內部和外部的事件(用戶註冊登錄、投資、提現事件等),這些重要事件可能不斷有各類各樣的模塊(營銷模塊、活動模塊)須要關心,核心業務系統去調用這些外部體系的模塊,讓整個系統在內部糾纏在一塊兒顯然是不合適的,這個時候經過MQ進行解耦,讓各類各樣的事件在系統中進行鬆耦合流轉,模塊之間各司其職也相互沒有感知,這是比較適合的作法。
4. 消息羣發:有一些消息是會有多個接收者的,接收者的數量仍是動態的(相似指責鏈的性質也是可能的),在這個時候若是上下游進行一對多的耦合就會更麻煩,對於這種狀況就更適用使用MQ進行解耦了。上游只管發消息說如今發生了什麼事情,下游無論有多少人關心這個消息,上游都是沒有感知的。
這些需求互聯網項目中基本都存在,因此消息隊列的使用是很是重要的一個架構手段。在使用上有幾個注意點:
1. 我更傾向於獨立一個專門的listener項目(而不是合併在server中)來專門作消息的監聽,而後這個模塊其實沒有過多的邏輯,只是在收到了具體的消息以後調用對應的service中的API進行消息處理。listener是能夠啓動多份作一個負載均衡的(取決於具體使用的MQ產品),可是由於這裏幾乎沒有什麼壓力,不是100%必須。注意,不是全部的service都是須要有一個配到的listener項目的,大多數公共基礎服務由於自己很獨立不須要感知到外部的其它業務事件,因此每每是沒有listener的,基礎業務服務也有一些是相似的緣由不須要有listener。
2. 對於重要的MQ消息,應當配以相應的補償線做爲備份,在MQ集羣一切正常做爲補漏,在MQ集羣癱瘓的時候做爲後背。我在日千萬訂單的項目中使用過RabbitMQ,雖然QPS在幾百上千,遠遠低於RabbitMQ壓測下來能抗住的數萬QPS,可是總體上有那麼十萬分之一的丟消息機率(我也用過阿里的RocketMQ,可是由於單量較小目前沒有觀察到有相似的問題),這些丟掉的消息立刻會由補償線進行處理了。在極端的狀況下,RabbitMQ發生了整個集羣宕機,A服務發出的消息沒法抵達B服務了,這個時候補償Job開始工做,按期從A服務批量拉取消息提供給B服務,雖然消息處理是一批一批的,可是至少確保了消息能夠正常處理。作好這套後備是很是重要的,由於咱們沒法確保中間件的可用性在100%。
3. 補償的實現是不帶任何業務邏輯的,咱們再梳理一下補償這個事情。若是A服務是消息的提供者,B-listener是消息監聽器,聽到消息後會調用B-server中具體的方法handleXXMessage(XXMessage message)來執行業務邏輯,在MQ中止工做的時候,有一個Job(可配置補償時間以及每次拉取的量)來按期調用A服務提供的專有方法getXXMessages(LocalDateTime from, LocalDateTime to, int batchSize)來拉取消息,而後仍是(能夠併發)調用B-server的那個handleXXMessage來處理消息。這個補償的Job能夠重用的可配置的,無需每次爲每個消息都手寫一套,惟一須要多作的事情是A服務須要提供一個拉取消息的接口。那你可能會說,我A服務這裏還須要維護一套基於數據庫的消息隊列嗎,這個不是本身搞一套基於被動拉的消息隊列了嗎?其實這裏的消息每每只是一個轉化工做,A必定在數據庫中有落地過去一段時間發生過變更的數據,只要把這些數據轉化爲Message對象提供出去便可。B-server的handleXXMessage因爲是冪等的,因此無所謂消息是否重複處理,這裏只是在應急狀況下進行無腦的過去一段時間的數據的依次處理。
4. 全部消息的處理端最好對相同的消息處理實現冪等,即便有一些MQ產品支持消息處理且只處理一次,靠本身作好冪等能讓事情變得更簡單。
5. 有一些場景下有延遲消息或延遲消息隊列的需求,諸如RabbitMQ、RocketMQ都有不一樣的實現方式。
6. MQ消息通常而言有兩種,一種是(最好)只能被一個消費者進行消費而且只消費一次的,另外一種是全部訂閱者均可以來處理,不限制人數。不用的MQ中間件對於這兩種形式都有不一樣的實現,有的時候使用消息類型來作,有的使用不一樣的交換機來作,有的是使用group的劃分來作(不一樣的group能夠重複消息相同的消息)。通常來講都是支持這兩種實現的。在使用具體產品的時候務必研究相關的文檔,作好實驗確保這兩種消息是以正確的方式在處理,以避免發生妖怪問題。
7. 須要作好消息監控,最最重要的是監控消息是否有堆積,有的話須要及時加強下游處理能力(加機器,加線程),固然作的更好點能夠以熱點拓撲圖繪製全部消息的流向流速一眼就能夠看到目前哪些消息有壓力。你可能會想既然消息都在MQ體系中不會丟失,消息有堆積處理慢一點其實也沒什麼問題。是的,消息能夠有適當的堆積,可是不能大量堆積,若是MQ系統出現存儲問題,大量堆積的消息有丟失也是比較麻煩的,並且有一些業務系統對於消息的處理是看時間的,過晚到達的消息是會認爲業務違例進行忽略的。
8. 圖上畫了兩個MQ集羣,一套對內一套對外。緣由是對內的MQ集羣咱們在權限上控制能夠相對弱點,對外的集羣必須明確每個Topic,並且Topic須要由固定的人來維護不能在集羣上隨意增刪Topic形成混亂。對內對外的消息實現硬隔離對於性能也有好處,建議在生產環境把對內對外的MQ集羣進行隔離劃分。
定時任務的需求有那麼幾類:
1. 如以前所說,跨服務調用,MQ通知不免會有不可達的問題,咱們須要有必定的機制進行補償。
2. 有一些業務是基於任務表進行驅動的,有關任務表的設計下面會詳細說明。
3. 有一些業務是定時按期來進行處理的,根本不須要實時進行處理(好比通知用戶紅包即將過時,和銀行進行日終對帳,給用戶出帳單等)。和2的區別在於,這裏的任務的執行時間和頻次是五花八門的,2的話通常而言是固定頻次的。
詳細說明一下任務驅動是怎麼一回事。其實在數據庫中作一些任務表,以這些表驅動做爲整個數據處理的核心體系,這套被動的運做方式是最最可靠的,比MQ驅動或服務驅動兩種形態可靠多,天生必然是可負載均衡的+冪等處理+補償到底的,任務表能夠設計下面的字段:
除了這些字段以外,還可能會加一些業務本身的字段,好比訂單狀態,用戶ID等等信息做爲冗餘。任務表能夠進行歸檔減小數據量,任務表扮演了消息隊列的性質,咱們須要有監控能夠對數據積壓,出入隊不平衡處理不過來,死信數據發生等等狀況進行報警。若是咱們的流程處理是任務ABCD順序來處理的話,每個任務由於有本身的檢查間隔,這套體系可能會浪費一點時間,沒有經過MQ實時串聯這麼高效,可是咱們要考慮到的是,任務的處理每每是批量數據獲取+並行執行的,和MQ基於單條數據的處理是不同的,整體上來講吞吐上不會有太多的差別,差的只是單條數據的執行時間,考慮到任務表驅動執行的被動穩定性,對於有的業務來講,這不失爲一種選擇。
這裏再說明一下Job的幾個設計原則:
1. Job能夠由各類調度框架來驅動,好比ElasticJob、Quartz等等,須要獨立項目處理,不能和服務混在一塊兒,部署啓動多份每每會有問題。固然,本身實現一個任務調度框架也不是很麻煩的事情,在執行的時候來決定Job在哪臺機器來跑,讓整個集羣的資源使用更合理。說白了就是兩種形態,一種是Job部署在那裏由框架來觸發,還有就是隻是代碼在那裏,由框架來起進程。
2. Job項目只是一層皮,最多有一些配置的整合,不該該有實際的業務邏輯,不會觸碰數據庫,大部分狀況就是在調用具體服務的API接口。Job項目就負責配置和頻次的控制。
3. 補償類的Job注意補償次數,避免整個任務被死信數據卡住的問題。
三馬車都說完了,那麼,最後咱們來梳理一下這麼一套架構下整個項目的模塊劃分:
這每個模塊均可以打包成獨立的包,全部的項目不必定都要在一個項目空間內,能夠拆分爲20個項目,服務的api+server+listener放在一個項目內,這樣其實有利於CICD缺點就是修改代碼的時候須要打開N個項目。
以前開篇的時候說過,使用這套簡單的架構既可以有很強的擴展餘地,複雜程度上或者說工做量上不會比All-In-One的架構多多少,看到這裏你可能以爲並不一樣意這個觀點。其實這個仍是要看團隊的積累的,若是團隊你們熟悉這套架構體系,玩轉微服務多年的話,那麼其實不少問題會在編碼的過程當中直接考慮進去,不少時候設計也能夠認爲是一個熟能生巧的活,作了多了天然知道什麼東西應該放在哪裏,怎麼去分怎麼去合,因此並不會有太多的額外時間成本。這三駕馬車構成的這麼一套簡單實用的架構方案我認爲能夠適用於大多數的互聯網項目,只是有些互聯網項目會更偏重其中的某一方面弱化另外一方面,但願本文對你有用。