1 引言
照護平臺是一個典型的面向終端用戶的互聯網應用,用戶通過照護平臺可以實現在線預約,在線下單,在線照護等業務功能,將原線下的照護複診等業務延伸到線上,產品考慮到要儘可能多的覆蓋終端用戶,確定了終端需要支持iOS,Android,小程序三種類型的APP.
互聯網應用首先考慮如何解決大規模併發和用戶體驗的問題,如果因爲訪問速度慢導致了用戶體驗下降,最終的結果就是用戶流失,基於這樣的考慮,做架構設計時就選擇最好硬件和最好性能的服務器,這樣能解決問題但實際上不可行,從產品上線到穩定運營的生命期裏,我們的線上業務是需要經歷一個培育期和多個發展期的,經歷幾個發展階段後纔可能會達到日均100萬單,但在最開始的培育階段有可能是日均100單。所以照護平臺確定系統總體架構時,其中的一個標準是:足夠靈活,適應快速迭代,隨着業務發展能實現可伸縮計算能力的架構。
照護平臺確定了基於微服務架構去實現這款互聯網應用,微服務架構不是一個很新的架構理論,已經有多個大型互聯網應用例如京東、淘寶等經過不斷的演進,最終都形成了微服務架構。本文主要講述照護平臺如何基於自己的業務需求,落地且實現了微服務架構,着重講述微服務治理部分。
2 正文
2.1 微服務的選型
確定了基於微服務架構設計應用系統以後,第一件事情就是確定微服務治理框架,從社區活躍度和知名度來看,Dubbo,SpringCloud是國內開發者選型最多的兩種框架,關於他們的選型標準及其他們之間的比較,百度搜出來的文章有很多,主流的結論是Dubbo是SpringCloud的一個子集,Spring Cloud是一系列的分佈式方案的解決包,他們之間的比較如下表所示:
組件 | Dubbo | Spring Cloud |
服務註冊中心 | Zookeeper | Spring Cloud Netflix Eureka |
服務調用方式 | RPC | REST API |
服務網關 | 無 | Spring Cloud Netflix Zuul |
斷路器 | 不完善 | Spring Cloud Netflix Hystrix |
分佈式配置 | 無 | Spring Cloud Config |
服務跟蹤 | 無 | Spring Cloud Sleuth |
消息總線 | 無 | Spring Cloud Bus |
數據流 | 無 | Spring Cloud Stream |
批量任務 | 無 | Spring Cloud Task |
... | ... | ... |
照護平臺在做選型時,根據自己的項目人員配置和實際業務需求,也有很多自己的顧慮,項目首先排除了Dubbo,主要基於以下的顧慮:
Dubbo的RPC協議是通過Netty來實現。Netty是基於Java的NIO模型來處理Socket數據,作爲一個新組建的團隊,沒人知道Netty裏的I/O線程和Worker線程的關係,也不會根據最佳經驗去設置他們的參數,從編程模型上也不會有人能基於Netty的Handler模型去擴展這些Pipeline。新團隊沒有團隊技術積累,讓新員工基於這個協議之上去寫業務代碼,我們認爲代碼質量不可控。
Dubbo的RPC協議是私有協議。 考慮到這個項目寫的服務會不斷的被重構也有可能被其他項目所調用,基於Dubbo的RPC私有協議是做不到跨平臺的,爲了解決這個問題,有人又開源了DubboX, 解決方案是在RPC的前面在加一層代理層,用來將RPC接口轉換成Rest接口,即將HTTP協議的請求進行處理並轉換成RPC調用協議,這又增加了一層複雜度。
後來項目開發進行過程中,驗證了項目的初期判斷是正確的,因爲我們的SSO模塊和短消息發送模塊都是由其他的兄弟部門開發的,他們開發的模塊也不會開發基於Netty的RPC接口而是基於HTTP協議的Rest接口,看來基於HTTP協議纔是最通用的。
排除了Dubbo以後,實際上我們也沒有選擇Spring Cloud, 主要是基於如下的一點考慮。
Spring Cloud是要完全基於Spring Boot去實現。 照護平臺有一個接入業務層,這個接入層要完成SSO,服務埋點,分佈式事務一致性,長連接管理等等業務,未來需要配置的地方和擴展的地方比較多。而Spring Boot是用內嵌的Tomcat 8.5去做,首先是Tomcat的版本已經落後了很久,其次是SpringBoot把Tomcat的配置全部封裝起來,對於更改內嵌的Tomcat的配置參數會對程序有什麼影響,我們都沒有人走過這條路。出於未來業務擴展的考慮,業務接入層我們決定基於Tomcat 9去開發。因此也排除了Spring Cloud.
最終我們確定了基於NetFlix的微服務框架去做,實際上,NetFlix是微服務界最受人尊敬的公司之一; NetFlix的微服務框架是自己項目中實際長期使用的,經過不斷的去業務代碼耦合和不斷的抽象,最終形成了不同的各自獨立的模塊,NetFlix又把這些獨立的模塊進行了開源,形成了整個微服務的生態體系,說NetFlix的開源產品是產品級別的品質,也是恰如其分的。反觀Dubbo是阿里自己不用而專用來開源的產品,他們自己用的HSF不開源(爲什麼不開源呢?)。另一個對標產品SpringCloud,他的微服務的核心組件都是基於NetFlix的微服務組件進行包裝,並進行了SpringBoot的適配,直接的講,SPringCloud是對NetFlix的開源組件包裝了一個外殼。
NetFlix解決了我們架構上的一些顧慮。我們採用他,有如下的理由:
NetFlix是基於HTTP協議的服務調用。所有的Java人員都掌握如何基於HTTP協議去編寫業務代碼的技能,任何新進入團隊的員工都沒有學習成本,新員工在上班第一天就可以根據接口定義去實現業務代碼,容易招聘到研發人員,對於新組建的團隊,這一點是按期交付項目的前提條件。
Netflix是經過規模生產環境驗證過的產品。Netflix的微服務治理框架被用來管理數萬臺服務器,來爲全世界的用戶提供視頻點播服務。照護平臺可以預見到的是在初始階段會部署在20臺服務器左右,一個培育階段以後,可能會擴展到100臺服務器上,計劃的更遠一點,即使將來有數百個計算節點,都可以使用這套框架。
2.2 微服務的應用
2.2.1 微服務治理功能
絕大多數項目的微服務治理功能都是類似的,照護平臺的微服務治理功能確定如下:
服務註冊
服務發現
服務路由
Health Check
熔斷 (V1.2實現)
服務降級(V1.2實現)
Fail Tolerance (V1.2實現)
微服務監控
分佈式事務一致性
2.2.2 微服務治理框架
照護平臺V1.0 的微服務治理框架如下圖所示:
從架構的拓撲圖中可以看到,VHRemoteCall是照護平臺自己實現的組件,他封裝了2.2.1服務治理的所有的功能。對於應用接入層和微服務層來講,所有的微服務治理功能都是透明的,VHRemoteCall封裝了服務發現,服務路由,熔斷,服務降級等功能,也就是說所有的業務代碼編寫人員不再關心Erureka,Ribbon,Hystrix Appache HttpClient, 業務代碼編寫人員只需要知道一個VHRemoteCall就可以了,VHRemoteCall是根據照護平臺的業務需求特性所實現的服務調用框架。
框架圖中各個組件的分工如下:
SpringConfig Server: Spring Config的集羣是實現了中央配置庫。
EureKa: EureKa 集羣實現了服務註冊中心。
EureKa Client: 實現了服務註冊,服務發現。
Ribbon: 實現了服務路由,服務的負載均衡。
Hyrstrix: 實現了熔斷、服務降級、Fail Tolerance。
Apache HttpClient: 服務之間調用的具體的HTTP協議的實現。
2.2.3 VHRemoteCall
實現VHRemoteCall的初衷就是把服務治理包括通訊及傳輸對象的序列化/反序列化等所有細節屏蔽起來,對業務編程人員透明。在照護平臺V1.0裏,VHRemoteCall封裝了多個組件的關聯關係,並對外部應用提供了最小化的接口訪問,可以說是設計模式中的Facade模式的一個應用。
整個微服務調用過程的基本流程大致如下:
服務提供者的程序啓動時,把服務名和自己的URL向服務中心註冊,並對外提供服務。
服務消費者應用系統啓動時,啓動服務發現程序和負載均衡模塊模塊,他們都在同一個應用裏,只是不同的模塊而已。
服務發現程序向註冊中心發起請求,獲得最新的可以對外提供服務的服務列表,以事件通知的方式通知負載均衡模塊,負載均衡模塊就獲得最新的服務列表。
消費者根據服務名去調用某個服務時,負載均衡模塊基於自己維護的服務列表,通過輪訓、加權、Hash隨機三種算法計算出可有的某個服務。消費者向具體的服務發起調用。
服務提供者因爲系統升級、健康檢查等原因需要下線時,服務提供者告訴註冊中心,自己下線了,服務註冊中心將這個服務從列表中消除。
消費者的服務發現程序有心跳檢查功能,每隔60秒(可以設定),向註冊中心發起請求,服務發現程序將註冊中心的服務列表拉到本地,並進行檢查,當且僅當註冊中心給出的列表和服務發現程序的上一次列表有變化時,服務發現模塊向負載均衡模塊發起事件通知,告訴其服務列表有變化了,負載均衡模塊把自己維護的列表從緩存中進行更新,從而實現了動態更新。
VHRemoteCall的幾個技術點:
服務發現和服務負載均衡相獨立:他們是兩個獨立的模塊,當微服務列表有變化時,以事件通知的方式與服務負載均衡模塊聯繫,進行服務列表更新。
採用事件通知的方式:本方案不採用資源鎖,而是採用事件通知,只有變化時才通知負載均衡模塊。當微服務列表發生變化時,通過事件通知的方式,負載均衡模塊能馬上感知到目標微服務列表發生了變化,做到服務列表的實時更新。相比於VHRemoteCall的機制,SPringCloud封裝的負載均衡模塊有自己的策略去更新檢查服務列表(不太確定)
緩存機制: 服務發現是用緩存機制,如果發現服務註冊中心聯繫不上了(Eureka掛掉了),服務發現模塊會一直使用本地的緩存列表,直至和服務註冊中心聯繫上。
健康檢查機制:Eureka服務註冊中心每隔一段時間檢查註冊的服務的健康狀況。Ribbon自身也提供了健康檢查機制,在照護平臺裏,我們沒有使用Ribbon的健康檢查機制。
優雅下線功能:照護平臺封裝了一個接口,微服務下線之前,可以通知Eureka服務註冊中心,我這個服務下線了,Eureka服務註冊中心會立即更新自己的服務列表。而不是當服務停掉了以後,過了幾十秒,由Eureka服務註冊中心去檢查,確認服務掛了以後再更新自己的服務列表,這會導致業務接入層的某些請求在幾十秒內將請求扔給這個掛掉了的服務。
HTTP 連接池:照護平臺的微服務調用通訊實現是採用的Apache Http Client,缺省設置是我們使用HTTP連接池,通過連接池的使用,理論上講他的效率和RPC調用差不多了。實際上業務接入層和微服務之間的業務通訊連接就應該可以複用的。
2.2.3.1 VHRemoteCall的具體使用
具體使用分爲兩個端的使用,分別是應用端和服務提供端。
應用端對VHRemoteCall的使用步驟如下:
1. 在Spring配置文件裏實現對VHRemoteCall的定義
<!-- 爲主診醫師照護平臺客戶化的遠程調用代理程序 -->
<bean id="VHRemoteCall"
class="com.viewhigh.healthcare.base.VHRemoteCall">
<constructor-arg ref="restTemplate" />
</bean>
2 程序裏對微服務進行調用
PatientODao resultData = vhReomoteCall.postCall("http://patientservice/patient/patientdaolist", postData, PatientODao.class);
對於調用者來說,只需要一行代碼就實現了對微服務的調用,就像普通的函數調用一樣,收到了返回對象PatientODao, 屏蔽了所有的細節,代碼很清晰,很整潔。
在微服務端,服務提供者的代碼如下:
@RequestMapping(value = "/patientdaolist", produces={"application/json"})
@ResponseBody
public PatientODao patientDaoDetail(String doctorid) {
。。。
}
對於服務提供者來說,沒有任何編程上的改變,就是基於Spring MVC正常的寫Rest接口就可以,只要有Java基礎的人就可以寫業務代碼,而且是基於運行了近20年之久的tomcat上編寫代碼,相對於在Netty上編寫代碼,讓新手在Tomcat上編寫代碼更讓人心理上踏實一些。
項目運行過程中,Netflix Eureka的實際監控運行界面如下, 和網上的Eureka圖片都不一樣的,網上都是用的Spring Eureka Server.
2.2.4 服務調用鏈監控
微服務監控選型:
照護平臺的部分功能如支付功能,其需要調用系統的10—20個微服務,因此係統需要一套分佈式的日誌記錄系統,來對微服務的整個調用鏈來監控,也需要在上線後對錯誤進行追蹤和定位。照護平臺決定採用分佈式日誌記錄系統,確定採用開源的軟件Jaeger, 其實現了Opentracing的接口規範。
Jaeger的架構圖如下所示:
如上圖所示,Jaeger 主要由以下幾部分組成。
Jaeger Client - 爲不同語言實現了符合 OpenTracing 標準的 SDK。應用程序通過 API 寫入數據,client library 把 trace 信息按照應用程序指定的採樣策略傳遞給 jaeger-agent。
Agent - 它是一個監聽在 UDP 端口上接收 span 數據的網絡守護進程,它會將數據批量發送給 collector。它被設計成一個基礎組件,部署到所有的宿主機上。Agent 將 client library 和 collector 解耦,爲 client library 屏蔽了路由和發現 collector 的細節。
Collector - 接收 jaeger-agent 發送來的數據,然後將數據寫入後端存儲。Collector 被設計成無狀態的組件,因此您可以同時運行任意數量的 jaeger-collector。
Data Store - 後端存儲被設計成一個可插拔的組件,支持將數據寫入 cassandra、elastic search。
Query - 接收查詢請求,然後從後端存儲系統中檢索 trace 並通過 UI 進行展示。Query 是無狀態的,您可以啓動多個實例,把它們部署在 nginx 這樣的負載均衡器後面。
照護平臺的微服務監控的實現主要是如下的過程:
整個部分包含三部分:代碼埋點,數據存儲、查詢展示
代碼埋點
照護平臺所有的對微服務的調用都是通過VHRemoteCall來實現。直接在VHRemoteCall裏進行通用的埋點。
這樣對業務的侵入性是最少,但是需要在VHRemtoeCall裏得到OperationName, TraceID, SpanID.
2. 數據存儲
用Jaeger支持的列式數據庫 Cassandra來存儲數據,用Jaeger提供的缺省的數據庫支持進行安裝和配置就可以了。
3. 查詢展示
爲了實現客戶化的查詢,需要基於Jaeger-UI進行客戶化定製,基於React的前端框架進行定製。
2.2.5分佈式事務一致性:
在照護平臺裏,用戶可以通過平臺實現在線支付、在線下單業務,一個在線下單業務涉及到數個服務的支持和多個獨立數據庫的協作,分佈式事務是一個繞不過去的坎。
舉例來講,在照護平臺裏一個訂單涉及到物理上3個庫的相應數據的變更,在MySQL的數據庫操作裏,MYSQL是不支持分佈式數據庫的事務一致性,照護平臺採用了TCC(兩階段,補償型)方案,即所有涉及到分佈式事務的一致性的服務,需要提供Cancel接口,而且通過追加交易流水日誌的方式來記錄追蹤整個過程;
照護平臺的在線下單業務的部分場景如下:
讓開發人員比較高興的是:照護平臺裏大部分的業務都不需要分佈式數據庫的事務一致性,因爲我們大部分業務都是涉及到查詢,只有涉及到資金和訂單的一小部分業務會用到分佈式事務。用TCC模式的不便之處是我們要增加額外的代碼量,而且業務侵入性較大,但是在1.0裏,我們不傾向於用自動化的分佈式事務一致性,也沒有采用基於消息中間件的最終一致性方案,主要是考慮到效率和技術的掌控度上,而TCC(兩階段,補償型)方案是最直接有效的一種方式,未來版本演進過程中也會嘗試基於消息中間件的最終一致性解決方案。
2.2.6批量服務(Job服務)
照護平臺和其他的互聯網應用一樣,也有很多的Job需求,如在照護開始前給相關的人發送短消息,每天凌晨1:00定時處理數據等等。照護平臺的Job引擎採用的不是NetFlix的JOB引擎平臺,而是採用了技術平臺部開發的Job引擎平臺,技術平臺部的JOB引擎能做到如下的功能:
可視化管理JOB:可視化的增加、刪除、啓動、停止job
Cron時間的可視化定義: 非常靈活的可視化界面,Cron標準的所有定義都可以滿足
自動JOB報警功能:錯誤時可以發短消息,發email等。
如上的三點已經很符合我們的需求了。
JOB的界面如下:
三 、總結
本文總結了微服務架構在照護平臺的整個落地過程,講解了微服務架構選型的一些考慮點,闡述了照護平臺是如何應用和定製化了微服務治理結構。 NetFlix的微服務組件都是彼此獨立的,每個項目可以根據自己的需要只使用其中的一個組件或者幾個組件,有人曾發表文章闡述如何在.Net項目使用Eureka。