微服務改造那些痛!蘇寧數據中台基於Spring Cloud架構實踐

從單體程序到微服務,再到當下流行的服務網格概念,Spring 連接起了這兩個時代。它曾是單體程序的代名詞,但是卻在微服務時代浴火重生,給我們帶來了 Spring Cloud。

藉助於 Spring Cloud,蘇寧大數據中心完成了微服務架構轉型,在實踐中並不是一帆風順,有思索、有迷茫,更有解決問題的樂趣。

為什麼要微服務化?

為什麼是 Spring Cloud?

蘇寧數據中臺後端是傳統的開發架構, VIP 負載均衡 + Nginx + SpringMVC,代碼以單體程序為主。

正常情況下一個項目使用統一域名,在蘇寧現有開發架構下,統一域名導致後端只能有一個 war 包,程序變成單體程序變成必然。

如下圖所示是典型的舊式項目代碼目錄:

項目名稱-web:對外 war 包模塊項目名稱-interface:統一定義接口項目名稱-service:統一定義接口實現

整個項目管理、開發思路,圍繞單體程序開發模型設計,帶來的弊端很明顯:

代碼職責不清晰,每個人都在同一模塊下提交代碼違反高內聚低耦合服務擴展不方便

首先微服務化思路,並不高大上,我們為什麼選擇微服務化,首要原因是管理問題。

結合蘇寧現有開發架構,整個微服務架構如圖:域名解析 + VIP 負載均衡 + Nginx + 服務網關 + 各個服務。

下圖是某個項目採用微服務化後的工程代碼目錄:

項目名稱-模塊1項目名稱-模塊2

整個代碼目錄更清晰,利於模塊拆分、人員職責安排。

數據中臺項目背景介紹

蘇寧數據中臺是一個大項目群:

OLAP 是底層的加速、查詢引擎,底層支持 Druid、ES、PGCitus 集群,類似 Presto,跟 Presto 不同的是 OLAP 會主動對數據進行 Cube 預加速。
百川是指標平臺層,讓用戶建模、定義指標,對外提供指標查詢服務。百川主要支持的建模方式是:星型模型。數據建模自然離不開維表維度,UDMS 系統就是來定義、管理所有維度、維表,目前收錄了整個集團近 200 多個維度,對外提供維度、維表信息服務。天工是類似 Tableau、Superset 的可視化報表設計平臺,與這些 BI 軟件最大的不同點是,天工基於百川的指標、UDMS 的維度來製作報表,數據來源已經高度標準化、歸一化。目前商業報告分析工具:Cognos、阿里 QuickBI 等,是將數據建模、可視化設計能力放到一起,這是天工與它們的最大區別。慧眼,是統一報表門戶,所有的報表統一發布到慧眼面向業務。慧眼最大的挑戰在於報表權限管控與自動匹配,總共 4000 多張報表,用戶 2w 多,一張報表開放給8000+人員是很常見的。所有這一切靠人工維護,既容易出錯又不利於數據安全,也不能及時響應用戶需求,這些都是慧眼系統要解決的問題。

微服務框架選型

Dubbo 架構介紹

Dubbo 主要有四個模塊:

Monitor(監控)Regsitry(註冊中心)Provider(服務方)Consumer(消費方)

Provider 註冊服務到 Regsitry,Consumer 向 Regsitry 訂閱服務信息,Monitor服務監控服務調用情況。

整個服務調用流程如下:

消費方在本地發起服務調用動態代理將調用交給 Loadbalance 模塊Loadbalance 從 Registry 拿到服務實例信息將請求發送到一臺服務實例記錄監控日誌等信息

Spring Cloud 架構介紹

Spring Cloud 整個架構與 Dubbo 非常類似:

Eureka(註冊中心)Gateway(服務網關)Provider(服務方)Consumer(消費方)Zipkin(監控)

不同的有如下幾點:

Spring Cloud 是 Http Rest 接口,Dubbo 不是。Spring Cloud 註冊中心不使用 Zookeeper,使用自研的 Eureka。關於 Zookeeper 是否適合做註冊中心,請參考文章:《Eureka! Why You Shouldn’t Use ZooKeeper for Service Discovery》、《阿里巴巴為什麼不用 ZooKeeper 做服務發現》Spring Cloud 提供了 Gateway 網關組件。與 Spring 生態兼容,生態鏈豐富,自定義 Filter、攔截器,來加強功能, 如:權限校驗、日誌打印等;Spring Cloud Netflix 提供了熔斷、限流等組件。

綜合以上幾點,考慮到架構統一,未來發展趨勢,我們選擇了 Spring Cloud。

Spring Cloud 主要幫助我們做系統內部服務化,REST 接口形式,不會破壞現有服務,前端服務調用無需做任何調整。

選擇 Spring Cloud 還有一個重要原因是 Dubbo 與蘇寧 RSF 服務框架高度重合,在對外服務接口上,我們還是以 RSF 接口為主。

基於 Spring Cloud 的服務化實踐

整體架構介紹

整體有幾個組件:註冊中心、服務網關、服務監控、負載均衡器。註冊中心使用 Spring Cloud 提供的 Eureka,服務網關使用 Spring Cloud 提供的 Zuul 組件,負載均衡器使用 Ribbon 組件。

服務網關的負載均衡策略選擇的是:WeightedResponseTimeRule,根據服務器響應時間來決定路由到哪個節點。

服務監控組件,用來監控服務性能、調用情況,最重要的一點,是將整個服務鏈路能串聯起來。

服務監控設計

監控是一個系統的眼睛,是斷然不可缺少的一部分,Zipkin 提供了很好的服務鏈路監控,結合我們自身的使用場景,最終我們沒有選擇 Zipkin,為什麼?

首先了解下 Zipkin 整體架構:

數據採集(Brave、Sleuth)Tranport 數據傳輸(支持 Kafka、直接發送 Collector)Collector(數據收集)Storage(存儲:ES)Search + Webui(監控展示)

整體架構如下圖所示,Zipkin 監控數據格式如下:

Zipkin 有如下缺點:

我們不止是監控 Spring Cloud 服務調用,如:蘇寧 RSF 服務調用、SQL 的執行時間、本地方法執行時間等。監控數據格式不滿足業務需要。Collector 節點容易出現性能瓶頸,ES 聚合查詢性能較差。跨線程,鏈路無法串聯。

基於以上幾點,我們決定自研服務鏈路監控系統,整個系統架構如下,我們利用 Kafka、Druid,原則上提供了無限擴展性。

Druid 對應 Zipkin 中的角色:Collector(數據收集) + Storage(存儲:ES)。

我們結合業務的需要,設計了監控日誌格式,如下圖所示:

一條調用鏈路,有相同的根 ID,服務名由三部分組成:

系統名一級名稱二級名稱

一級名稱有 7 種值:

url:http 接口url-call:調用 http 接口rs:rsf 接口rsf-call:調用 rsf 接口sql:執行 sqlcache:操作緩存method:本地方法

你可能會問,沒有存儲父 ID,如何判斷一條鏈路中的父子關係?這裡我們設計一個特殊的事務 ID 生成規則,通過事務 ID 本身即能判斷父子關係,如下圖所示:

下圖監控系統的鏈路展示頁面:

基於 Hystrix 的熔斷設計

Hystrix 對應的中文名字是“豪豬”,豪豬周身長滿了刺,能保護自己不受天敵的傷害,代表了一種防禦機制,這與 Hystrix 本身的功能不謀而合。

因此 Netflix 團隊將該框架命名為 Hystrix,並使用了對應的卡通形象作為 Logo。

在一個分佈式系統裡,許多依賴會不可避免的調用失敗,比如超時、異常等。

如何能夠保證在一個依賴出問題的情況下,不會導致整體服務失敗,這個就是 Hystrix 需要做的事情。

Hystrix 提供了熔斷、隔離、Fallback、Cache、監控等功能,它能夠在一個、或多個依賴同時出現問題時保證系統依然可用。

使用 Hystrix 很簡單,只需要添加相應依賴即可,最方便的方式是使用註解 HystrixCommand:

fallbackMethod:指定 Fallback 方法threadPoolKey:線程池名稱threadPoolProperties:指定線程池參數(線程池大小、最大隊列排隊數量)
commandProperties:CIRCUIT_BREAKER 開頭的參數配置熔斷相關參數,METRICS_ROLLING 開頭的參數設置指標計算相關參數相關參數定義,參考類:HystrixPropertiesManager

@RestControllerpublic class HystrixTest { @RequestMapping(value = "/query/user/name", method = RequestMethod.GET ) @HystrixCommand(fallbackMethod = "getDefaultUserName", threadPoolKey = "query_user", threadPoolProperties = { @HystrixProperty(name = CORE_SIZE, value = "10"), @HystrixProperty(name = MAX_QUEUE_SIZE, value = "10") }, commandProperties = { @HystrixProperty(name = CIRCUIT_BREAKER_ENABLED, value = "true"), @HystrixProperty(name = CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD, value = "1000"), @HystrixProperty(name = CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE, value = "25") } ) static String getUserName(String userID) throws InterruptedException { Thread.sleep(-1); return userID; } public String getDefaultUserName(String userID) { return ""; }}

基於服務網關 Zuul 實現的廣播功能

有些時候我們希望 url 請求被所有服務實例執行,這裡我們對 Zuul 做了一個改造,增加了一個 BroadCastFilter,在 url 請求 header 設置 gate_broadcast 為 true,那麼這個請求,將被轉發給所有服務實例。

邏輯流程如下:

判斷 gate_broadcast 參數為 true從 url 獲取 ServiceId從 Ribbon 獲取服務所有實例將請求發送給所有實例將所有實例返回結果封裝,返回

微服務帶來的問題

服務拆分粒度不好把握

Spring Cloud 的微服務有一個 ServiceId 的概念,通常一個 war 包對應一個 ServiceId,這個 ServiceId 下可以有多個服務。粒度拆分方式主要有:橫向、縱向。

縱向切分主要有如下幾個方式:

按功能切,如用戶管理、指標管理、模型管理等。按角色切,如管理員、商家、用戶。

橫向切分,一般用來提取公共的基礎服務,比如:用戶名密碼校驗服務、用戶基本信息查詢。

運維、開發複雜度增加

單體程序時代只有一個 war 包,微服務鼓勵服務拆分,war 數量、部署節點大大增加。

此外,一個流程處理往往會由多個分佈式服務協同完成,帶來了不少棘手的問題:

需要通過分佈式事務保障數據最終一致性防止單個服務問題造成雪崩

這些都給開發者提出了更高的要求。

調試難度增加

微服務方式鼓勵服務拆分,通過服務間依賴完成功能,給開發、測試帶來了挑戰,合理選擇微服務、代碼複用兩種方案。

後續架構演進

服務版本控制

沒有版本控制,意味著我們無法做灰度發佈,毀滅性版本發佈後,無法做到對老版本兼容,下圖為服務 A、B、C、D 間的版本依賴關係:

我們實現思路是對 Zuul 進行改造:

打版本標籤,在 Zuul 對訪問來源判斷(比如 App 版本 5.1 對應的查詢接口版本為 2.1),打上版本標籤根據版本信息,路由到對應版本服務實例

基於 Gateway 的服務熔斷、限流機制

目前有一些開源的框架如 ratelimit,通過在 Ruul 增加 filter 來實現限流熔斷。

但是有幾個問題:

不支持動態配置不能滿足業務變化,如配合版本控制

綜上所述,我們已經著手一些自研工作,能與我們業務場景貼合得更緊密。

總結

從 2016 年到現在,兩年的時間裡,蘇寧大數據中心從傳統的單例開發模式,切換到基於 Spring Cloud 的微服務開發模式,並摸索出了一條適合自己的路,這不只是技術框架的切換,更是開發思維的升級。

令人欣喜的是,這兩年間 Spring Cloud 飛速發展,2018 年發佈了革命性的 2.0 版本,這離不開社區的支持,許多像 Netflix 一樣的公司在筆耕不輟地為 Spring Cloud 生態添磚加瓦。

我們基於 Spring Cloud 開發出了一些服務於自己業務的組件,讓我們認識到自己也是有能力有責任去回饋社區。

路漫漫其修遠兮,好的架構一定是適應業務發展的架構,對於 Spring 這不是終點,對於我們更不是。