服務端指南 | 微服務初級設計指南

樑桂釗發表於2019-03-03

微服務架構越來越被重視與應用,然而在擁抱微服務的過程中,我們或多或少會遇到一些常見的問題。那麼,本節將介紹幾種常見的問題以及應對思路。

原文地址:服務端指南 | 微服務初級設計指南
部落格地址:blog.720ui.com/

如何拆分服務

微服務要如何拆分,是否拆分粒度越小越好?一般情況下,對於服務的拆分並非越小越好,甚至極端的案例是把一塊功能拆分成一個服務,這種做法是不對的。因此,拆分粒度應該保證微服務具有業務的獨立性與完整性,服務的拆分圍繞業務模組進行拆分。例如將 VR 資訊系統進行服務拆分,分為資訊系統、話題系統、日報系統、百科系統四個微服務系統。

服務端指南 | 微服務初級設計指南

但是,很多情況下,服務的拆分圍繞業務模組進行拆分是一種理想狀態下的拆分方法,換句話說,我們在架構設計之初就假定我們可以掌握一切。然而,不同的服務可能由不同的團隊開發與維護,實際場景下,微服務的便利性更多的在於團隊內部能夠產生閉環,換句話說,團隊內部可以易於開發與維護,便於溝通與協作,但是對於外部團隊就存在很大的溝通成本與協作成本。現在,我們來看一個案例。團隊 A 考慮到功能的複用性而開發了一個“互動元件”,其中包括 “評論模組”功能。此時,團隊 B 並不知情也開發了一個類似的“互動元件”。而團隊 C 也有這個需求,它知道團隊 A 有這個“互動元件”,希望可以複用,但是由於這個“互動元件”在設計的時候更多地考慮了團隊 A 的當前業務,沒有很好的複用性,例如不支援“評論蓋樓”功能,而由於團隊 A 出於當前其他專案的進度原因無法馬上提供支援,團隊 B 評估後決定花一週時間自己開發一個符合自己業務需求的“互動元件”。此時,各個專案團隊各自維護了一個“互動元件”。此外,我們再來看一個案例。一個 OA 系統擁有“使用者管理”、“檔案管理”、“公告管理”、“政策管理”、“公文管理”、“任務管理”、“審批管理”等功能,如果按照微服務架構思想可以圍繞業務模組進行拆分,但是事實上這個 OA 系統的終端使用者只有 30 多人,使用微服務架構可能有點“殺雞用牛刀”的感覺了。回顧下,第一個案例中,由於團隊之間的職責與邊界導致了服務的複用存在侷限性,甚至造成各自為戰的局面,這種情況一般需要公司層面進行規劃和統籌。第二案例中,由於使用者量不大,系統也不復雜,使用微服務反而帶來了不必要的設計和運維難度,同時也帶來了一些技術的複雜度。此外,我們還需要考慮服務依賴,鏈式呼叫、資料一致性、分散式事務等問題。

總結下,服務的拆分是一個非常有學問的技術活,要圍繞業務模組進行拆分,拆分粒度應該保證微服務具有業務的獨立性與完整性,儘可能少的存在服務依賴,鏈式呼叫。但是,在實際開發過程中,有的時候單體架構更加適合當前的專案。實際上,微服務的設計並不是一蹴而就的,它是一個設計與反饋過程。因此,我們在設計之初可以將服務的粒度設計的大一些,並考慮其可擴充套件性,隨著業務的發展,進行動態地拆分也是一個不錯的選擇。

論微服務的資料庫管理

通常情況下,在每個服務都有自己的快取和資料庫,並且快取和資料庫是相互獨立且透明的。因此,共享快取與共享資料庫是不對的。那如果服務 A 需要獲取服務 B 的資料怎麼辦?請讀者思考,這種情況下,可以直接在服務 A 建立兩個資料來源(一個是服務 A 的資料庫,一個是服務 B 的資料庫)進行資料操作麼?事實上,這個方案是不提倡的,因為它破壞了微服務之間的資料獨立性。因此,更好的做法是:服務 B 提供一個獲取該資料的 API 介面,而服務 A 通過呼叫該介面進行業務組裝。但是,凡事無絕對,有一種特殊的場景可能需要共享資料庫,那就是舊的服務過度到新的服務的場景,新的服務複用舊的服務的資料庫從而到達功能與資料過度的需求。

服務多版本指南

微服務的 API 介面應該儘量相容之前的版本,換句話說,微服務可以支援多版本,但是需要儘量確保向下相容。如果服務無法相容舊版本,則需要升級版本號。為了解決這個版本不相容問題,在設計 RESTful API 的一種實用的做法是使用版本號。一般情況下,我們會在 url 中保留版本號,並同時相容多個版本。

【GET】  /v1/users/{user_id}  // 版本 v1 的查詢使用者列表的 API 介面
【GET】  /v2/users/{user_id}  // 版本 v2 的查詢使用者列表的 API 介面複製程式碼

在 Java 語言的 Spring 框架中實現,如下所示。

@RestController
@RequestMapping({"v1/c/users"})
public class SysUserV1Controller {
    @RequestMapping(value={"/{userId:\d+}"}, method=RequestMethod.GET)
    public SysUser findOne(@PathVariable long userId){  
        // 業務實現
    }
}

@RestController
@RequestMapping({"v2/c/users"}) 
public class SysUserV2Controller {
    @RequestMapping(value={"/{userId:\d+}"}, method=RequestMethod.GET)
    public SysUser findOne(@PathVariable long userId){  
        // 業務實現
    }
}複製程式碼

此時,客戶端的產品的新功能將請求新的服務端的 API 介面地址。

服務端指南 | 微服務初級設計指南

雖然服務端會同時相容多個版本,但是同時維護太多版本對於服務端而言是個不小的負擔,因為服務端要維護多套程式碼。這種情況下,常見的做法不是維護所有的相容版本,而是隻維護最新的幾個相容版本,例如維護最新的三個相容版本。在一段時間後,當絕大多數使用者升級到較新的版本後,廢棄一些使用量較少的服務端的老版本API 介面版本,並要求使用產品的非常舊的版本的使用者強制升級。

此外,在微服務的多版本並存的場景下,這些服務的版本可以在同一個服務中定義,這種情況一般出現在服務的內容變更不是很大的場景。另一種方式,服務的版本可以在不同的服務中定義,例如“資訊v1服務”、“資訊v2服務”, 這種情況一般出現在服務框架重構中,它將抽離出一個新的工程來提供新的服務介面。

應對微服務的鏈式呼叫異常

一般情況下,每個微服務之間是獨立的,如果某個服務當機,只會影響到當前服務,而不會對整個業務系統產生影響。但是,服務端可能會在多個微服務之間產生一條鏈式呼叫,並把整合後的資訊返回給客戶端。在呼叫過程中,如果某個服務當機或者網路不穩定可能造成整個請求失敗。因此,為了應對微服務的鏈式呼叫異常,我們需要在設計微服務呼叫鏈時不宜過長,以免客戶端長時間等待,以及中間環節出現錯誤造成整個請求失敗。此外,可以
考慮使用訊息佇列進行業務解耦,並且使用快取避免微服務的鏈式呼叫從而提高該介面的可用性。

是否需要提供外觀介面

外觀介面,指的是將多個服務的介面進行業務封裝與整合並提供一個簡單的呼叫介面給客戶端使用。這種設計的好處在於,客戶端不再需要知道那麼多服務的介面,只需要呼叫這個外觀介面即可。但是,壞處也是顯而易見的,即增加了服務端的業務複雜度,介面效能不高,並且複用性不高。因此,筆者的建議是服務端的介面設計儘可能保證職責單一,而在客戶端進行“樂高式”組裝。如果存在 SEO 優化的產品,需要被類似於百度這樣的搜尋引擎收錄,可以當首屏的時候,通過服務端渲染生成 HTML,使之讓搜尋引擎收錄,若不是首屏的時候,可以通過客戶端呼叫服務端 RESTful API 介面進行頁面渲染。

如何快速追蹤與定位問題

在微服務複雜的鏈式呼叫中,我們會比單體架構更難以追蹤與定位問題。因此,在設計的時候,需要特別注意。一種比較好的方案是,當 RESTful API 介面出現非 2xx 的 HTTP 錯誤碼響應時,採用全域性的異常結構響應資訊。其中,code 欄位用來表示某類錯誤的錯誤碼,在微服務中應該加上“{biz_name}/”字首以便於定位錯誤發生在哪個業務系統上。我們來看一個案例,假設“使用者中心”某個介面沒有許可權獲取資源而出現錯誤,我們的業務系統可以響應“UC/AUTH_DENIED”,並且通過自動生成的 UUID 值的 request_id 欄位,在日誌系統中獲得錯誤的詳細資訊。

HTTP/1.1 400 Bad Request
Content-Type: application/json
{
    "code": "INVALID_ARGUMENT",
    "message": "{error message}",
    "cause": "{cause message}",
    "request_id": "01234567-89ab-cdef-0123-456789abcdef",
    "host_id": "{server identity}",
    "server_time": "2014-01-01T12:00:00Z"
}複製程式碼

此外,我們需要在記錄日誌時,標記出錯誤來源以及錯誤詳情便於更好地分析與定位問題。

微服務的安全

OAuth 是一個關於授權的開放網路標準,它允許第三方網站在使用者授權的前提下訪問使用者在服務商那裡儲存的各種資訊。實際上,OAuth 2.0 允許使用者提供一個令牌給第三方網站,一個令牌對應一個特定的第三方網站,同時該令牌只能在特定的時間內訪問特定的資源。使用者在客戶端使用使用者名稱和密碼在使用者中心獲得授權,然後客戶端在訪問應用是附上 Token 令牌。此時,應用接收到客戶端的 Token 令牌到使用者中心進行認證。

服務端指南 | 微服務初級設計指南

一般情況下,access token 會新增到 HTTP Header 的 Authorization 引數中使用,其中經常使用到的是 Bearer Token 與 Mac Token。其中,Bearer Token 適用於安全的網路下 API 授權。MAC Token 適用於不安全的網路下 API 授權。

微服務的資料一致性

如何保證多個微服務的資料的一致性是一個必須面對的問題。例如,“話題系統”的資料變更的同時要保證“使用者動態系統”的資料級聯更新。如果,這個時候“使用者動態系統”發生當機,或者網路連線異常、網路超時,就會導致資料的不一致。目前,分散式事務並沒有很好的解決方案,難以滿足資料強一致性,一般情況下,保證系統經過一段較短的時間的自我恢復和修正,資料最終達到一致。這個自我恢復和修正的方式,可以在每次更新的時候進行修復,或者採取週期性的進行校驗操作來保證。

我們還可以引入可靠的訊息佇列,只要保證當前“話題系統”的可靠事件投遞並且訊息中介軟體確保事件傳遞至少一次,那麼訂閱這個事件的消費者(使用者動態系統)保證事件能夠在自己的業務內被消費即可。在消費者(使用者動態系統)處理的過程中出現異常,可以將事件放入重試佇列並根據具體的策略進行失敗重試,如果多次重試失敗可以寫入錯誤日誌並主動通知開發人員進行手工介入。注意的是,這個過程要保證可靠事件投遞與避免重複消費,其中尤其重要是介面要保證冪等性,例如支付系統不能因為重複收到支付事件而導致多次支付。

服務端指南 | 微服務初級設計指南

此外,我們可以採用業務補償等方式保證資料的一致性。

(完)

更多精彩文章,盡在「服務端思維」微信公眾號!

服務端指南 | 微服務初級設計指南

相關文章