1. 程式人生 > >從 Spring Cloud 開始,聊聊微服務架構的實踐之路

從 Spring Cloud 開始,聊聊微服務架構的實踐之路



使用微服務架構開發應用程式,我們實際上是針對一個個微服務進行設計、開發、測試、部署,因為每個服務之間是沒有彼此依賴的,大概的交付流程就像上圖這樣。
設計階段:
架構組將產品功能拆分為若干微服務,為每個微服務設計 API 介面(例如 REST API),需要給出 API 文件,包括 API 的名稱、版本、請求引數、響應結果、錯誤程式碼等資訊。
在開發階段,開發工程師去實現 API 介面,也包括完成 API 的單元測試工作,在此期間,前端工程師會並行開發 Web UI 部分,可根據 API 文件造出一些假資料(我們稱為“mock 資料”),這樣一來,前端工程師就不必等待後端 API 全部開發完畢,才能開始自己的工作了,實現了前後端並行開發。
測試階段:

這一階段過程全自動化過程,開發人員提交程式碼到程式碼伺服器,程式碼伺服器觸發持續整合構建、測試,如果測試通過則會自動通過Ansible指令碼推送到模擬環境;在實踐中對於線上環境則是先要走稽核流程,通過之後才能推送到生產環境。提高工作效率,並且控制了部分可能因為測試不充分而導致的線上不穩定。
開發模式
在以上交付流程中,開發、測試、部署這三個階段可能都會涉及到對程式碼行為的控制,我們還需要制定相關開發模式,以確保多人能夠良好地協作。
實踐"絞殺者模式":

由於第三代架構跨度較大,並且面臨了無法修改的.net遺留系統,我們採用絞殺者模式,在遺留系統外面增加新的Proxy代理微服務,並且在LB控制upstream的方式,而不是直接修改原有系統,逐步的實現對老系統的替換。
開發規範



經驗表明,我們需要善用程式碼版本控制系統,我曾經遇到一個開發團隊,由於分支沒有規範,最後一個小版本上線合程式碼居然化了幾個小時,最後開發人員自己都不知道合到哪個分支。拿 Gitlab 來說,它很好地支援了多分支程式碼版本,我們需要利用這個特性來提高開發效率,上圖就是我們目前的分支管理規範。
最穩定的程式碼放在 master 分支上,我們不要直接在 master 分支上提交程式碼,只能在該分支上進行程式碼合併操作,例如將其它分支的程式碼合併到 master 分支上。

我們日常開發中的程式碼需要從 master 分支拉一條 develop 分支出來,該分支所有人都能訪問,但一般情況下,我們也不會直接在該分支上提交程式碼,程式碼同樣是從其它分支合併到 develop 分支上去。
當我們需要開發某個特性時,需要從 develop 分支拉出一條 feature 分支,例如 feature-1 與 feature-2,在這些分支上並行地開發具體特性。

當特性開發完畢後,我們決定需要釋出某個版本了,此時需要從 develop 分支上拉出一條 release 分支,例如 release-1.0.0,並將需要釋出的特性從相關 feature 分支一同合併到 release 分支上,隨後將針對 release 分支推送到測試環境,測試工程師在該分支上做功能測試,開發工程師在該分支上修改 bug。待測試工程師無法找到任何 bug 時,我們可將該 release 分支部署到預發環境,再次驗證以後,均無任何 bug,此時可將 release 分支部署到生產環境。待上線完成後,將 release 分支上的程式碼同時合併到 develop 分支與 master 分支,並在 master 分支上打一個 tag,例如 v1.0.0。

當生產環境發現 bug 時,我們需要從對應的 tag 上(例如 v1.0.0)拉出一條 hotfix 分支(例如 hotfix-1.0.1),並在該分支上做 bug 修復。待 bug 完全修復後,需將 hotfix 分支上的程式碼同時合併到 develop 分支與 master 分支。

對於版本號我們也有要求,格式為:x.y.z,其中,x 用於有重大重構時才會升級,y 用於有新的特性發布時才會升級,z 用於修改了某個 bug 後才會升級。針對每個微服務,我們都需要嚴格按照以上開發模式來執行。
微服務開發體系

我們已經對微服務團隊的架構、交付流程、開發模式進行了描述,下面我們聊聊歸納一下微服務開發體系。
什麼是微服務架構
Martin Flower的定義:

Martin Flower的定義:

In short, the microservice architectural style [1] is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies

簡單的說,微服務是軟體系統架構上的一個設計風格,它倡導將一個原本獨立的系統分成多個小型服務,這些小型服務都在各自獨立的程序中執行,服務之間通過基於HTTP的RESTful 輕量級API進行通訊協作。被拆分的每個微服務圍繞系統中的某項或一些耦合度較高的業務進行構建,並且每個服務都維護著自身的資料儲存、業務開發、自動化測試案例以及獨立部署機制。由於有了輕量級通訊機制,這些微服務間可以使用不通的語言來編寫。
微服務的拆分粒度
微服務到底拆分到一個多大的粒度,更多時候是需要在粒度與團隊之間找到一個平衡點,微服務越小,微服務獨立性帶來的好處就越多。但是管理大量微服務也會越複雜。基本上拆分需要遵循以下幾個原則:
單一職責原則:即"把因相同原因而變化的東西聚合到一起,把因不同原因而變化的東西分離開來"。通過這個原則確定微服務邊界。

團隊自主原則:團隊越大,溝通與協助成本就會越高,我們在實踐中一個團隊不會超過8人,團隊內全棧,是一個全功能團隊。

先分資料庫、後分服務:資料模型能否徹底分開,決定了微服務的邊界功能是否徹底劃清,實踐中我們先討論資料模型邊界,資料模型的邊界映射了業務的邊界,進而從底向上完成服務拆分。

如何搭建微服務架構
為了搭建好微服務架構,技術選型是一個非常重要的階段,只有選擇合適的"演員",才能把這臺戲演好。



我們使用 Spring Cloud 作為微服務開發框架,Spring Boot 擁有嵌入式 Tomcat,可直接執行一個 jar 包來發布微服務,此外它還提供了一系列“開箱即用”的外掛,例如:配置中心,服務註冊與發現,熔斷器,路由,代理,控制匯流排,一次性令牌,全域性鎖,leader選舉,分散式 會話,叢集狀態等,可大量提高我們的開發效率。
功能
Spring Cloud

路由與負載均衡
Ribbon

註冊中心
Eureka

閘道器
Zuul

斷路器
Hystrix

分散式配置
Config

服務呼叫跟蹤
sleuth

日誌輸出
elk

認證整合
oauth2

訊息匯流排
Bus

批量任務
Task

工程結構規範



上圖是我們實踐中每個服務應該具有的專案組成結構。
其中:
微服務名+service:
為對內其它微服務提供服務呼叫。服務名+api模組為服務間定義的介面規範,使用swagger+rest介面定義。服務名+server模組包含了能直接啟動該服務的應用與配置。

微服務名+web:
供上層web應用請求的入口,該服務中一般會呼叫底層微服務完成請求。

API 閘道器實踐



API閘道器作為後端所有微服務和API的訪問入口, 對微服務和API進行審計,流控, 監控,計費等。常用的API閘道器解決方案有:
應用層方案
最有名的當然是Netflix的zuul, 但這不意味著這種方案就最適合你, 比如Netfilx是因為使用AWS,對基礎設施把控有限, 所以才不得不在應用層做了zuul這樣的方案,如果通盤考慮, 這種方案不是最合適或者說最有的方案。
但如果自己的團隊對整體技術設施把控有限,且團隊結構不完善,應用層方案也可能是最適合你的最佳方案。

nginx + lua方案
也是我們採用並認為最合適的方案,OpenResty和Kong是比較成熟的可選方案, 不過Kong使用Postgres或者Cassandra, 國內公司估計選擇這倆貨的不多,但Kong的HTTP API設計還是很不錯的。

我們的方案
使用nginx+lua+consul組合方案,雖然我們團隊大多是java,選擇zookeeper會是更加自然的選擇,但作為新銳派,對壓測結果進行了分析, 我們最終選擇使用consul。
良好的HTTP API支援, 可以動態管理upstreams, 這也意味著我們可以通過釋出平臺或者膠水系統無縫的實現服務註冊和發現, 對服務的訪問方透明。



在以上的方案裡:
consul作為狀態儲存或者說配置中心(主要使用consul的KV儲存功能);nginx作為API閘道器, 根據consul中upstreams的相關配置,動態分發流量到配置的upstreams結點;
nginx根據配置項, 連線到consul叢集;
啟動的API或者微服務例項, 通過手工/命令列/釋出部署平臺, 將例項資訊註冊/寫入consul;
nginx獲取到相應的upstreams資訊更新, 則動態變更nginx內部的upstreams分發配置,從而將流量路由和分發到對應的API和微服務例項結點;
將以上註冊和發現邏輯通過指令碼或者統一的釋出部署平臺固化後,就可以實現透明的服務訪問和擴充套件。
鏈路監控實踐
我們發現,以前在單應用下的日誌監控很簡單,在微服務架構下卻成為了一個大問題,如果無法跟蹤業務流,無法定位問題,我們將耗費大量的時間來查詢和定位問題,在複雜的微服務互動關係中,我們就會非常被動,此時分散式鏈路監控應運而生,其核心就是呼叫鏈。通過一個全域性的ID將分佈在各個服務節點上的同一次請求串聯起來,還原原有的呼叫關係、追蹤系統問題、分析呼叫資料、統計系統指標。
分散式鏈路跟蹤最早見於2010年Google發表的一篇論文《dapper》。

那麼我們先來看一下什麼是呼叫鏈,呼叫鏈其實就是將一次分散式請求還原成呼叫鏈路。顯式的在後端檢視一次分散式請求的呼叫情況,比如各個節點上的耗時、請求具體打到了哪臺機器上、每個服務節點的請求狀態,等等。它能反映出一次請求中經歷了多少個服務以及服務層級等資訊(比如你的系統A呼叫B,B呼叫C,那麼這次請求的層級就是3),如果你發現有些請求層級大於10,那這個服務很有可能需要優化了 常見的解決方案有:
Pinpoint

對APM有興趣的朋友都應該看看這個開源專案,這個是一個韓國團隊開源出來的,通過JavaAgent的機制來做位元組碼程式碼植入(探針),實現加入traceid和抓取效能資料的目的。 NewRelic、Oneapm之類的工具在java平臺上的效能分析也是類似的機制。
Zipkin

官網:OpenZipkin · A distributed tracing system
github地址:GitHub - openzipkin/zipkin: Zipkin is a distributed tracing system

這個是twitter開源出來的,也是參考Dapper的體系來做的。
Zipkin的java應用端是通過一個叫Brave的元件來實現對應用內部的效能分析資料採集。
Brave的github地址:https://github.com/openzipkin/brave

這個元件通過實現一系列的java攔截器,來做到對http/servlet請求、資料庫訪問的呼叫過程跟蹤。然後通過在spring之類的配置檔案里加入這些攔截器,完成對java應用的效能資料採集。
CAT

這個是大眾點評開源出來的,實現的功能也還是蠻豐富的,國內也有一些公司在用了。不過CAT實現跟蹤的手段,是要在程式碼裡硬編碼寫一些“埋點”,也就是侵入式的。
這樣做有利有弊,好處是可以在自己需要的地方加埋點,比較有針對性;壞處是必須改動現有系統,很多開發團隊不願意。
前面三個工具裡面,如果不想重複造輪子,我推薦的順序依次是Pinpoint—>Zipkin—>CAT。原因很簡單,就是這三個工具對於程式原始碼和配置檔案的侵入性,是依次遞增的。
我們的解決方案
針對於微服務,我們在spring cloud基礎上,對微服務架構進行了擴充套件,基於Google Dapper的概念,設計了一套基於微服務架構的分散式跟蹤系統(WeAPM)。



如上圖所示,我們可以通過服務名、時間、日誌型別、方法名、異常級別、介面耗時等引數查詢響應的日誌。在得到的TrackID可以查詢到該請求的整個鏈路日誌,為重現問題、分析日誌提供了極大方便。


斷路器實踐
在微服務架構中,我們將系統拆分成了一個個的微服務,這樣就有可能因為網路原因或是依賴服務自身問題出現呼叫故障或延遲,而這些問題會直接導致呼叫方的對外服務也出現延遲,若此時呼叫方的請求不斷增加,最後就會出現因等待出現故障的依賴方響應而形成任務積壓,最終導致自身服務的癱瘓。為了解決這樣的問題,因此產生了斷路器模式


我們在實踐中使用了Hystrix 來實現斷路器的功能。Hystrix是Netflix開源的微服務框架套件之一,該框架目標在於通過控制那些訪問遠端系統、服務和第三方庫的節點,從而對延遲和故障提供更強大的容錯能力。Hystrix具備擁有回退機制和斷路器功能的執行緒和訊號隔離,請求快取和請求打包,以及監控和配置等功能。
斷路器的使用流程如下:
啟用斷路器
    @SpringBootApplication
    @EnableCircuitBreaker
    public class Application {
    public static void main(String[] args) {
        SpringApplication.run(DVoiceWebApplication.class, args);
        }
    }

代用使用方式

    @Component
    public class StoreIntegration {
    @HystrixCommand(fallbackMethod = "defaultStores")
    public Object getStores(Map<String, Object> parameters) {
        //do stuff that might fail
    }
    public Object defaultStores(Map<String, Object> parameters) {
        return /* something useful */;
        }
    }

配置檔案



資源控制實踐
聊到資源控制,估計很多小夥伴會聯絡到docker,docker確實是一個實現資源控制很不錯的解決方案,我們前期做調研時也對是否使用docker進行了評審,但是最終選擇放棄,而使用linux 的libcgroup指令碼控制,原因如下:
docker 更適合大記憶體做資源控制、容器化,但是我們線上伺服器一般都是32G左右,使用docker會有資源浪費。

使用docker會使運維複雜、來自業務的壓力會很大。

為什麼要有cgroup?
Linux系統中經常有個需求就是希望能限制某個或者某些程序的分配資源。也就是能完成一組容器的概念,在這個容器中,有分配好的特定比例的cpu時間,IO時間,可用記憶體大小等。於是就出現了cgroup的概念,cgroup就是controller group,最初由google的工程師提出,後來被整合進Linux核心中,docker也是基於此來實現。
libcgroup使用流程:
安裝

yum install libcgroup

啟動服務

service cgconfig start

配置檔案模板(以memory為例):

cat /etc/cgconfig.conf

看到memory子系統是掛載在目錄/sys/fs/cgroup/memory下,進入這個目錄建立一個資料夾,就建立了一個

control group了。
mkdir test echo "服務程序號">> tasks(tasks是test目錄下的一個檔案)

這樣就將當前這個終端程序加入到了記憶體限制的cgroup中了。