前言
我們 2.0 版本的交易系統整體架構就如上圖所示,劃分為了行情服務、客戶端服務、撮合服務、管理端服務。行情服務主要對外提供推送行情資料的 WebSocket API。撮合服務就是一個記憶體撮合引擎,其輸入是一個定序的委託訂單佇列,而輸出包含成交記錄和其他各種事件,包括撤單成功、撤單失敗、訂單進入了 Orderbook 等。撮合服務如果重啟,則會從 MySQL 資料庫查詢出所有未成交訂單,重新組成 Orderbook。客戶端服務的核心功能就是接收和處理客戶端各種 HTTP 介面請求,管理端則是提供給系統管理人員對整個系統的使用者、訂單、資產、配置等進行統一檢視和管理。
雖然拆解為了 4 個服務,但我覺得,這還不是微服務架構,只能說已經變成分散式架構了,但「分散式」和「微服務」是兩個不同的概念。微服務是分散式的,但分散式並不一定用微服務。其實,在實際專案中,從單體應用到微服務應用也不是一蹴而就的,而是一個逐漸演變的過程。而 2.0 版本,只是整個演變過程中的第一個階段。
現在,好多小團隊小專案,一上來就微服務,很多隻是為了微服務而微服務,這絕對不是合適的做法。從本質上來說,架構的目的是為了「降本增效」——這四字真言是我從玄姐(真名孫玄)那學來的。專案一開始就採用微服務,一般都達不到降本增效的目的,因為微服務架構應用相比單體應用,其實現成本、維護成本都比單體應用高得多,除非一開始就是構建一個大型應用。
當業務規模和開發人員規模都已經不小的時候,比較適合用微服務,這時候用微服務主要解決兩個問題:快速迭代和高併發。當業務和人員規模比較小的時候,用一個或幾個單體應用完成整個系統,一般迭代速度會更快。但到了某個臨界點,就會開始出現一個或多個痛點,這之後,反而會拖慢迭代速度。
而遇到高併發時,其實,單體應用只要是無狀態化的,透過部署多個應用例項,也可以承載一定的併發量。但如果單體應用變得龐大了,承載了比較多業務功能的時候,再對整個單體應用橫向擴容,就會嚴重浪費資源。因為,並非所有業務都是需要擴容的,比如,下單容易產生高併發,需要擴容,但註冊並不需要擴容。全部業務都繫結到同個單體中一起擴容,那消耗的資源就會比較龐大。另外,當某一塊業務出現高併發,伺服器承載不了的時候,影響的是該單體應用的所有業務。因此,拆分微服務就可以解決這些因為高併發而導致的問題。
那麼,接下來,就來聊聊我們的交易系統,微服務化的架構是如何逐步演進的。
迭代業務需求2.0 版本之後,就會進入集中迭代業務需求的階段了,有大量業務需求有待完善和增加。大的業務板塊包括:
場外交易:也叫法幣交易、OTC交易、C2C交易等,在三大所(火幣/幣安/OK)現在都叫「買幣」。交易平臺作為第三方,提供安全、可靠的交易環境,為使用者與使用者之間提供用法幣買賣數字資產的功能。使用者可以是個人使用者,也可以是企業使用者。槓桿交易:主要為使用者提供借幣交易的功能,使用者透過向平臺借幣的方式,以實現以小博大、收益倍翻的目標。平臺一般最高提供 10 倍槓桿。合約交易:還分為了交割合約、永續合約、期權合約等,其中永續合約槓桿倍數最高可達到 125 倍。開放 API:對每種交易都需要開放 API,包括行情介面、交易介面、賬戶介面等,全部都需要提供 HTTP 介面,而行情資料和一些賬戶資訊的更新推送則再加上 WebSocket 介面。相比這些交易,現有業務的交易板塊一般就稱為「幣幣交易」。另外,幣幣交易和槓桿交易都屬於現貨交易的範疇,合約交易屬於金融衍生品的範疇。這些板塊的業務現在基本成為每個交易平臺的標配了。另外,如果業務繼續擴充套件下去,還有持幣生息、借貸、挖礦、 DEX(去中心化交易),以及各種 DeFi(去中心化金融) 。就說現在的三大所,每一個的業務線都是已經很多了。但我們現在先不去考慮這些業務。
然而,以上這麼多大小業務的需求,肯定不是一蹴而就的,需要根據優先順序,透過一個又一個迭代版本逐漸去完成。按需求的優先順序來規劃的話,應是先完成那些中小業務的需求,緊接著可以依次完成:幣幣交易開放 API、場外交易、槓桿交易、槓桿交易開放 API、交割合約、永續合約、期權合約、合約開放 API。
因為每個版本的迭代週期比較短,目標就是快速實現功能並上線,因此就直接在原有的服務裡面新增各業務板塊的功能了。開放的 HTTP API 也沒有獨立出來,就直接和內部 API 共用一套了,只從引數上區分是開放 API 還是內部 API。內部 API 會傳 Token,走 JWT 鑑權;開放 API 會傳 Sign 引數,走 API Key 的簽名校驗機制。
這些業務板塊都上線之後,我們整個交易系統的架構圖就大致如下了:
業務拆分加班加點把這些業務板塊的需求都上線之後,做個覆盤總結,就會發現存在幾個比較嚴重的問題:
客戶端後臺服務變得好臃腫,裡面的好多業務邏輯也變得好複雜,提交程式碼出現衝突的情況越來越頻繁,嚴重影響了迭代速度。開放 API 和內部 API 的強耦合,導致開放 API 訪問量高的時候,就影響到了內部 API 的訪問,從而使得客戶端使用者時不時就反映說應用慢和卡,甚至超時。當某個業務板塊的交易請求併發量很大的時候,伺服器承載不了,導致所有業務都不可用。服務拆分的時機,是由痛點驅動的。以上這些問題,就是已經出現的痛點,那要解決這些痛點,方法就是一個字:「拆」。那接下來的問題就是:如何拆分?
微服務拆分,本質就是對業務複雜度進行分解,將整套系統分解為多個獨立的微服務,就可以讓不同小團隊負責不同的微服務,實現整個微服務從產品設計、開發測試,到部署上線,都能由一個團隊獨立完成。從而,多個小團隊就能並行研發多條業務線,實現整套系統的快速迭代。
因此,進行服務拆分,考慮的第一個拆分維度就是相互獨立的業務域。很明顯,對於我們的交易系統來說,可以拆分的業務域就是:現貨交易、場外交易、合約交易。現貨交易包括了幣幣交易和槓桿交易,這兩者不能拆分,因為兩者是在同一套撮合機制裡的,即是說幣幣交易的訂單和槓桿交易的訂單是在同一個訂單池裡撮合的,行情資料也是同一套。合約交易還有各種子域,雖然每種子域也基本是獨立執行的,但很多業務規則是大同小異的,所以當前沒必要再進一步拆分。
再考慮第二個拆分維度,分析業務流程,如果有非同步操作,那就可以拆分。對於交易系統,就看看最核心的撮合交易流程是如何的,最通用的簡化流程如下:
下單 ——> 定序佇列 ——> 撮合 ——> 輸出佇列 ——> 清算
撮合的前後,分別有定序佇列和輸出佇列,因此,下單與撮合之間是非同步的,撮合與清算之間又是非同步的。那就可以將下單、撮合、清算分離獨立服務。下單(包括撤單等)部分可以獨立為交易服務,撮合就是撮合服務了,清算邏輯則抽離成清算服務。
行情資料模組也是相對獨立的,所以我們之前也抽離成了獨立的行情服務。
另外,槓桿交易和各種合約交易都有保證金制度,需要實時監控使用者的資產並計算風險率,如果達到風險閥值則自動執行對應策略,如強制平倉、自動清算等。實現這些功能的也最好單獨服務,我們可以稱為風控服務。這塊也是需要全記憶體高速計算的,以後再講具體如何設計。
除了場外交易不屬於撮合交易,現貨和合約都可以按上面的拆分維度進一步拆分:
其實,還有一些通用業務,如使用者的註冊、登入,以及公告內容、Banner 圖、線上客服等等,這些可以歸到一個公共服務,做統一管理。
還有,管理端後臺服務只是一些 CRUD,也沒出現痛點問題,可以暫不拆分。
最終,在業務層,我們將系統拆分為了這些業務服務:管理端後臺服務、公共服務、場外交易服務、現貨交易服務、現貨撮合服務、現貨清算服務、現貨行情服務、現貨風控服務、合約交易服務、合約撮合服務、合約清算服務、合約行情服務、合約風控服務。
資料庫拆分業務服務都拆分之後,大一統的資料庫就很容易成為效能瓶頸,且還有單點故障的風險。另外,只有一個數據庫,所有服務都依賴它,資料庫一旦進行調整,就會牽一髮而動全身。所以,我們要將資料庫也進行拆分。
微服務架構下,一套完整的微服務元件,其獨立性不僅僅只是程式碼上對業務層需求上的研發和部署上線獨立,還包括該業務元件對自身的資料層的獨立自治和解耦。因此,理想的設計是每個微服務業務元件都有自己單獨的資料庫,其他服務不能直接呼叫你的資料庫,只能透過服務呼叫的方式訪問其他服務的資料。
所以,資料庫如何拆分,基本也是跟隨業務元件而定。對於我們的交易系統來說,撮合服務和風控服務是全記憶體計算的,沒有自己獨立的資料庫,其他服務都有自己的獨立資料庫或快取。如下圖:
但拆分之後,資料庫變成了分散式的,不可避免地就會引入一些新問題,主要有三大塊:
分散式事務問題資料統計分析問題跨庫查詢問題單個數據庫的時候,資料庫事務的 ACID 是很容易達到的。但到了分散式環境下,ACID 就很難滿足了,就需要在某些特性之間進行平衡取捨。我們應該知道,分散式環境下,有個 CAP 理論,即一致性、可用性、分割槽容忍性,三者在分散式系統中無法同時滿足,最多隻能滿足兩項。P 是必選項,所以一般就需要在 C(一致性)和 A(可用性)之間進行抉擇。如果是選擇了 C,則需要保證強一致性,保證強一致性的事務,也稱為剛性事務。解決剛性事務的方案主要有 2PC、3PC,能夠保證強一致性,但效能很差。大部分場景下的分散式事務其實對強一致性的要求不會太高,所以只要在一定時間內做到最終一致性就可以了。保證最終一致性的事務,稱為柔性事務,其設計思想則是基於 BASE 理論。柔性事務的解決方案主要有 TCC補償型、非同步確保型、最大努力型。而具體選擇哪種方案來解決分散式事務問題,就要根據具體的業務場景來分析和選型了。關於分散式事務更詳細的內容,以後再單獨細說。
資料統計分析問題,更多是管理後臺的需求,管理後臺需要為運營人員提供統計報表、資料分析等功能,這其實可以歸到 OLAP 的範疇了,因此推薦的方法就是將各個庫的資料整合到 NewSQL 資料庫裡進行處理。
跨庫查詢,其實最多的場景就是 A 業務元件需要查詢 B 業務元件甚至更多其他業務元件的資料的時候,那要解決此問題,常用的有幾種解決思路。第一種思路就是增加冗餘欄位,但冗餘欄位不宜太多,且還需要解決冗餘欄位資料同步的問題。第二種方案則是增加聚合服務,將不同服務的資料統一封裝到一個新的服務裡做聚合,對外提供統一的 API 查詢介面。還有一種方案就是分次查詢每個服務,再組裝資料,可以直接在客戶端做,也可以在服務端做。
水平分層微服務化的最後一步拆分則是採用水平方向的分層架構,可用最簡單的三層架構,將所有微服務劃分為閘道器層、業務邏輯層、資料訪問層。
增加閘道器層是毋庸置疑的,這不用考慮,需要考慮的是配置多少個閘道器才合適?一個統一的閘道器無法解決我們前面提到的開放 API 和內部 API 強耦合的問題,所以是肯定需要多閘道器的。開放 API 和內部 API 是應該要分開的,兩者的鑑權方式不同,限流的策略也不同,更重要的是考慮隔離性,互不影響。管理端和客戶端的 API 也最好分開,兩者使用者和許可權是不同的,管理端的管理員使用者有著訪問和操作更多資料的許可權,如果不小心洩露給到了客戶端,那就是嚴重的安全事故了。所以,閘道器層至少可以分為三個閘道器:Open API Gateway(開放API閘道器)、Client API Gateway(客戶端API閘道器)、Admin API Gateway(管理端API閘道器)。與各端的訪問關係如下圖:
如果再細分,還可以把 WebSocket API 和 HTTP API 再拆分,不過目前階段可以暫時先不做分離。
閘道器層就先聊這麼多,接著聊聊業務邏輯層和資料訪問層。據我瞭解,很多專案是沒有再獨立出資料訪問層的微服務的,我以前做過的專案也是。我以前看過的文章和書籍,也從沒有提到過這樣拆分的思路。這思路還是我從玄姐那學到的,從他那裡才瞭解到,那些大廠的專案,很多都是這麼拆分的。
在不拆分的情況下,其實每個服務內部也有劃分為業務邏輯層和資料訪問層的。在這種情況下,A 業務的服務需要查詢 B 業務的資料時,就直接訪問 B 服務提供的介面。這種方式,最大缺點就是容易造成資料流紊亂,因為這是橫向的服務呼叫,隨著服務越來越多,服務間的呼叫關係越來越複雜,會變成混亂的網狀關係,容易出現非預期的結果,還有可能會發生迴圈呼叫,且定位問題也會變得困難。
拆分之後,則 A 需要查詢 B 的資料時,就是 A 的業務邏輯層服務呼叫 B 的資料訪問層服務,變成了縱向的服務呼叫,資料流很清晰。
至此,我們整個系統的微服務化拆分就算基本完成了,最終的整體架構圖大致如下:
最後一塊其實,微服務化還剩下最後一塊拼圖,那就是註冊中心,這也是微服務架構中的一個基礎元件。
註冊中心主要解決以下幾個問題:
服務註冊後,如何被及時發現服務宕機後,如何及時下線服務發現時,如何進行路由服務異常時,如何進行降級服務如何有效的水平擴充套件簡單地說,註冊中心主要實現服務的註冊與發現。現在,註冊中心的選型有好多種,包括 Zookeeper、Eureka、Consul、Etcd、CoreDNS、Nacos 等,還可以自研。這麼多選擇,應該選哪個比較好呢?其實,可以先從 CAP 理論去考慮,本質上,註冊中心應該是 CP 模型還是 AP 模型的?
對於服務發現場景來說,針對同一個服務,即使註冊中心的不同節點儲存的服務提供者資訊不盡相同,也並不會造成災難性的後果。但是對於服務消費者來說,如果因為註冊中心的異常導致消費不能正常進行,對於系統來說則是災難性的。因此,對於服務發現來講,註冊中心應該是 AP 模型。
再從服務註冊的角度分析,如果註冊中心發生了網路分割槽,CP 場景下新的節點無法註冊,新部署的服務節點就不能提供服務,站在業務角度這是我們不想看到的,因為我們希望將新的服務節點通知到儘量多的服務消費方,不能因為註冊中心要保證資料的一致性而讓所有新節點都不生效。所以,服務註冊場景,也是 AP 的效果好於 CP 的。
綜上所述,註冊中心應該優先選擇 AP 模型,保證高可用,而非強一致性。那以上所羅列出來的註冊中心,可選的就只剩下 Eureka、Nacos,或者自研了。在實際專案中,自研的相對比較少,現在越來越多專案選擇了使用 Nacos,因為其功能特性最強大,而且 Nacos 不只是註冊中心,還是配置中心。所以,如果不是自研的話,其實可以直接選擇 Nacos。
總結微服務化的落地,遠沒有很多網上教程說的那麼簡單,只有自己去經歷過,才知道最佳實踐的落地方法。另外,微服務化之後,後續還有很多更復雜的問題需要一一去解決,包括服務治理,比如服務降級、熔斷、負載均衡等,以及服務網格化,甚至無服務化,都是需要一步步去實施的。