這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
有贊技術發展曆程
2014年公司所有業務(交易,商品,ump,支付等等)都在一個單體應用中完成,使用php開發,滿足了公司快速發展(我們姑且稱為v1.0)。
2015年到2016期間,隨著業務流量增長,現有架構模式遇到了挑戰,公司開始朝著業務拆分和服務化方向邁進。開始採用java作為開發語言,服務化架構使用公司改進過的dubbox,支援跨語言服務調用的nova架構(v2.0)。
2017年在服務化的基礎上我們更近一步,向微服務架構漸層。擁抱社區提供的豐富組件(v3.0)。
我們遇到的問題
隨著業務的發展,團隊規模也在增長,這時候v1.0單體架構遇到了挑戰
- 系統變的複雜和脆弱,架構需要升級
- 開發成本高,學習成本高,加大了merge衝突,排隊等待,故障發生率等
- 測試成本高,修改一處,也需要迴歸測試
- 應用難以做水平拓展,難以進行容量規則
第一個問題很棘手,我們先來說說架構升級會做些什麼.
要明白做什麼,首先需要考慮目標是什嗎?軟體架構的目標是要設計軟體系統來解決問題,所以架構要做的事從抽象的維度上看,就是:
- 根據問題域,界定系統的邊界(Eric Evans的領域驅動設計,劃定bounded context)
- 對系統進行切分,切分的目的是分工與協作(可以並行,以獲得效率提升)
- 被切分的各部分之間建立協作與溝通的原則和機制
- 將各個部分串連合并成一個整體,完成系統的目標
上面是大的抽象原則,更具體一些來說,架構做得就是結構設計,在不同維度和層次上:
- 高維度:是系統、子系統或服務的切分與互動結構
- 中維度:是系統或服務內部的領域模組劃分
- 低緯度:是代碼結構、資料結構、表結構,技術方案選擇,開發用的爽不爽
架構執行過程中可能會出現一些新問題,是在當初的架構設計中未能考慮到的,需要對此做分析判斷,並形成新的決策調整。而另一些問題,也許是執行過程中的走樣,導致和當初的決策形成了偏差。架構師需要考慮所有這些關注點,並和開發工程師找到解決這些關注點的各種選項,在適當的時候根據真實環境的情景去採取合適的行動。有時,我們稱這些行動叫作:重構或最佳化。當一箇舊系統長期沒有這樣的行動,積累久了後,我們將迫不得已採取另外一種行動,我們稱之為 —— 架構升級。
軟體系統或架構,不像建築物會因為時間的流逝而自然耗損腐壞,它只會因為變化而腐壞。一開始清晰整潔的架構與實現隨著需求的變化而不斷變得渾濁、混亂。電腦科學都愛借用一個物理學的術語「熵」,它表達體系的混亂程度,而軟體系統的「熵」很容易不經意間隨著需求的變化而變得更高。
軟體系統「熵」有個臨界值,當達到並超過臨界值後,軟體系統的生命也基本到頭了。這時,我們就要採取那個迫不得已的行動了。圖例展示了軟體系統「熵」值的生命週期變化。
所以,不是所有的大型系統都是被很好的設計的,想要設計好一個巨型系統是非常困難的,而隨著業務功能的疊加,原先的設計也會被堆砌的代碼所淹沒,以至打破原先的設計。我們所能掌控的是一個有著特定邊界的系統,所以根據業務屬性拆分系統,將其限定在一個有邊界的上下文中(Bouded Context),是一個最直觀也是最有效方法。這也是領域驅動設計所追求的。在DDD歐洲大會上Eric也認可近年流行的微服務架構有個很大的優勢,服務粒度合適,服務物理隔離,單個服務的「熵」增問題被局限在單個微服務內部。單個微服務的替換與重構成本十分有限,使得「熵」增問題局部化,不容易傳染全域,以致失控。當然這有個前提,就是微服務的拆分和介面互動要合理,合理的檢驗標準就是隨需求變化,總是實現變化或介面新增,而非總是調整介面互動。架構始於系統生命之初,並伴隨系統生命週期全程。每次需求變化帶來的變動都應進行一次或大或小的重新架構過程。架構的關注點在於控制軟體系統變動時「熵」值的變化。
按照業務領域拆分後,已經能很好地滿足了業務發展。但是v2.0對開發人員負擔過重,需要做一些方便架構執行的工作
- 現有的java架構不能讓開發人員只關注業務實現,架構本身沒有提供一些開箱即用的三方的和公司的組件,需要大量地配置
- 架構沒有提供一些common patterns指導開發人員編寫代碼
- 混亂的版本依賴
- 項目結構不標準化
- 應用健全狀態檢查不標準化
- 編寫測試複雜,難以持續整合
為什麼選擇spring boot
- 開發體檢極大地提升
- 使應用配置變簡單
- 使編碼變簡單
- 使編寫測試代碼更簡單
- 本地啟動,方便開發調試
- 使部署變簡單,內嵌容器
- 簡單強大的spi機制,很容易拓展自訂的autoconfiguer
- spring-boot-starter-actuator使監控更簡單
- 完善的生態圈,對主流架構無縫整合
- 社區活躍,迭代迅速
- 符合我們的微服務目標,方便未來容器化
解決問題
youzan pom and youzan-boot-parent
借鑒spring bom的做法,建立youzan bom,版本統一管理,徹底解決版本混亂問題。針對各個應用中重複配置問題,建立youzan-boot-parent,消除重複,無需各個應用間copy。另外還額外帶來一個好處,方便統一升級。
另外,針對我們現有的營運環境,標準化了4套環境:開發,測試,預發,線上。
我們針對publish api jar deploy到maven倉庫中做了嚴格的限制,api jar本應只包含一些DTO和一些介面,但由於開門人員經常是複製粘貼,也會把各種不需要的依賴(比如spring,各種log架構等)加入到api中,導致使用該jar的應用方發生依賴衝突,通過會花一些不必要的時間來找到衝突並解決衝突,我們希望通過技術手段來做最後一道防線,從源頭上解決因為依賴其他系統api jar而導致的依賴衝突。
youzan application
我們希望應用對一些開源組件和公司自己開發的組件使用起來更簡單,降低接入成本。為此我們拓展了spring boot的autoconfiger,添加了各種starter。
- youzan-boot-dependencies
- youzan-boot-parent
- youzan-boot
- nova-spring-boot-starter
- nsq-spring-boot-starter
- druid-spring-boot-starter
- mybatis-spring-boot-starter
- chameleon-spring-boot-starter
- youzan-boot-starter-test
舉個例子,如果我們想使用nova架構,只需一個註解@EnableNova即可
應用標準化的健全狀態檢查
curl http://127.0.0.1:8080/health
{ status: "UP", diskSpace: { status: "UP", total: 249779191808, free: 61591195648, threshold: 10485760 }, redis: { status: "UP", version: "3.0.3" }, db: { status: "UP", database: "MySQL", hello: 1 }, refreshScope: { status: "UP" }, hystrix: { status: "UP" }}
面向失敗設計(CircuitBreaker)
分布式系統設計中,有一條很重要的原則就是:為失敗而設計,錯誤一定會發生。為了防止系統出現級連失敗,我們需要對依賴的服務所能夠使用的資源做一定限制,保護應用本身。通常以下目標都是要考慮的:
- 保護調用方,不因為依賴的服務(特別是網路服務)的問題(高延時和失敗)而影響到調用方應用
- 在複雜的分布式系統中阻止級聯失敗
- 即時失敗(fail fast)和快速恢複
- fallback和優雅降級,如果可以的話
- 能夠即時的監控,動態調整參數和相關操作
下面簡單介紹下hystrix實現原理,具體內容請參考官方文檔
- 使用HystrixCommand 或者 HystrixObservableCommand 來封裝外部系統調用,通常是在單獨的一個線程中執行
- 每個dependency設定逾時調用,可以自訂逾時時間,也可以動態調整,通常逾時時間設定的比測量的第99.5個百分位的值稍高一些
- 為每個dependency維護一個小的線程池,當線程池滿了,直接拒絕請求
- 測量記錄請求成功數,失敗數,逾時數和被拒絕數
- 觸發斷路器,針對某個特定的服務阻止一切請求一段時間(可以手動觸發也可以自動觸發,自動觸發規則是這個dependency的錯誤百分比超過閥值)
- 當請求失敗,逾時,或者短路時執行fallback邏輯
- 監控測量值和准即時配置變更
體現在代碼上
@HystrixCommand(groupKey = "RiskGroup", commandKey = "RiskClient-containsSensitiveWord", fallbackMethod = "fallback", commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000") }) public boolean containsSensitiveWord(List<String> words) { if(skipSensitiveWordValidation()){ return false; } PlainResult<Boolean> result = sensitiveWordFilter.containSensitiveWord(words, RECEIPT_SCENE); LazyLogs.info(logger, "RiskClient.containsSensitiveWord({}), result={}", () -> words, () -> JSON.toJSONString(result)); return result.getData(); } /** * 風控介面調用出現異常,則降級,是弱依賴 */ public boolean fallback(List<String> words, Throwable e) { logger.warn("RiskClient.containsSensitiveWord({}) fallback", words, e); return false; }
通過配置中心可以動態控制hystrix的參數
關於api文檔的更新
作為api的使用者的開發經常會發現文檔是到期的,甚至是錯誤的,據說程式員都不喜歡寫文檔,因為他們喜歡寫代碼,所以最好通過代碼來自動產生文檔。利用spring restdocs可以通過測試代碼自動產生文檔,還有一個好處時,如果介面中增加或減少欄位時,如果不同步更新測試的話,測試就不會通過,這樣就可以保證文檔始終是最新的。
測試代碼
@Test public void withdrawSummary() throws Exception { given(withdrawQueryService.queryWithdrawStatus()) .willReturn(PlainResults.success(WithdrawStatus.getStatusMap())); this.mockMvc.perform(get("/withdraw/queryWithdrawStatus")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("success").value(true)) .andExpect(jsonPath("code").value(0)) .andExpect(jsonPath("message").value("")) .andDo(document("withdraw-status", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), responseFields( subsectionWithPath("requestId").ignored().optional(), subsectionWithPath("success").description("請求結果"), subsectionWithPath("code").description("錯誤碼,0表示無錯誤"), subsectionWithPath("message").description("錯誤提示訊息,如果有錯誤的話"), subsectionWithPath("data").description("提現狀態列表") ))); }
產生的api文檔
關於api提供者的煩惱
在公司或組織內部,api提供者最大的煩惱莫過於找不到消費者到底有哪些,以及消費者是如何使用他們api的。更不要說經過一些公司人員變動之後的情況。有個真實的案例,開發人員將某個欄位單詞拼字錯誤修正回來,結果發布上線後,有個依賴方因為使用到該欄位,而導致依賴方服務不可用。其實這種情境和經曆發生多次後,開發人員就會畏手畏腳,對原先一些不合理的設計和錯誤就會不去改進它,聽之任之。
其實這種問題根本原因是服務提供者與消費者協作模式的問題,我們希望有某種機制來減輕這種問題。如果服務消費方能夠把使用api的情境通知給服務提供者,並落實在測試代碼上,那是不是就可以讓服務提供者感知到各個依賴方api使用情境。其實這是一種契約精神,服務提供者與依賴方要多多溝通交流,並將溝通交流的成果落實到測試代碼上。當然了得有些得力的工具和架構來支援我們這種設想,契約測試(Contract Testing)就是來達成這些目標的。具體使用文檔請參考 Spring Cloud Contract
持續整合
持續整合的好處不用多說,關鍵在於執行下去。
更多的收益
curl -i -X POST -H 'Content-Type: application/json' -d '{"configuredLevel": "DEBUG"}' http://localhost:8080/loggers/com.youzan.pay
- 更方便地收集系統metrics資料,方便監控
- spring cloud config/consul 配置中心
- 服務發現/consul
- spring cloud zuul api網關
- spring cloud sluth & distributed tracing/zipkin 分布式tracing
參考
Eric Evans — Tackling Complexity in the Heart of Software
spring boot
spring cloud
https://martinfowler.com/microservices
microXchg 2017 - Juven Xu: AliExpress' Way to Microservices
Microservices at Netflix Scale
https://jenkins.io/
BoundedContext