美麗好車的微服務(wù)實踐是基于 Spring Cloud 體系來做的,在具體的開發(fā)過程中遇到了不少問題,踩了不少坑,對于微服務(wù)也有了實際的切身體會和理解,而不再是泛泛而談。在整個 Spring Cloud 技術(shù)棧中,基于不同職責(zé)需要,我們選擇了相應(yīng)組件來支持我們的服務(wù)化,同時配合 Swagger 和 Feign 實現(xiàn)接口的文檔化和聲明式調(diào)用,在實際開發(fā)過程中極大地降低了溝通成本,提高了研發(fā)聯(lián)調(diào)和測試的效率。
從應(yīng)用架構(gòu)來看,正是由于基于 Spring Cloud 來實現(xiàn),整個系統(tǒng)完全秉承了微服務(wù)的原則,無論是 Spring Cloud 組件還是業(yè)務(wù)系統(tǒng),都體現(xiàn)了服務(wù)即組件、獨立部署、去中心化的特性,由此提供了快速交付和彈性伸縮的能力。
接下來我們基于各個組件具體介紹一下美利好車的微服務(wù)實踐,首先最基本的就是 Eureka,其承載著微服務(wù)中的服務(wù)注冊和服務(wù)發(fā)現(xiàn)的職責(zé),是最基礎(chǔ)的組件,必然有高可用的要求。
美利好車在生產(chǎn)實踐中部署了一個三節(jié)點的 Eureka Server 的集群,每個節(jié)點自身也同時基于 Eureka Client 向其它 Server 注冊,節(jié)點之間兩兩復(fù)制,實現(xiàn)了高可用。在配置時指定所有節(jié)點機器的 hostname 既可,即做到了配置部署的統(tǒng)一,又簡單實現(xiàn)了 IP 解耦,不需要像官方示例那樣用 profile 機制區(qū)分節(jié)點配置。這主要是由于 Eureka 節(jié)點在復(fù)制時會剔除自身節(jié)點,向其它節(jié)點復(fù)制實例信息,保證了單邊同步原則:只要有一條邊將節(jié)點連接,就可以進(jìn)行信息傳播和同步。在生產(chǎn)環(huán)境中并不要過多調(diào)整其它配置,遵循默認(rèn)的配置既可。
作為服務(wù)提供者的 Eureka Client 必須配置 register-with-eureka 為 true,即向 Eureka Server 注冊服務(wù),而作為服務(wù)消費者的 Eureka Client 必須配置 fetch-registry=true,意即從 Eureka Server 上獲取服務(wù)信息。如果一個應(yīng)用服務(wù)可能既對外提供服務(wù),也使用其它領(lǐng)域提供的服務(wù),則兩者都配置為 true,同時支持服務(wù)注冊和服務(wù)發(fā)現(xiàn)。由于 Ribbon 支持了負(fù)載均衡,所以作為服務(wù)提供者的應(yīng)用一般都是采用基于 IP 的方式注冊,這樣更靈活。
在開發(fā)測試環(huán)境中,常常都是 standlone 方式部署,但由于 Eureka 自我保護(hù)模式以及心跳周期長的原因,經(jīng)常會遇到 Eureka Server 不剔除已關(guān)停的節(jié)點的問題,而應(yīng)用在開發(fā)測試環(huán)境的啟停又比較頻繁,給聯(lián)調(diào)測試造成了不小的困擾。為此我們調(diào)整了部分配置讓 Eureka Server 能夠迅速有效地踢出已關(guān)停的節(jié)點,主要包括在 Server 端配置關(guān)閉自我保護(hù) (eureka.server.enableSelfPreservation=false),同時可以縮小 Eureka Server 清理無效節(jié)點的時間間隔(eureka.server.evictionIntervalTimerInMs=1000)等方式。
另外在 Client 端開啟健康檢查,并同步縮小配置續(xù)約更新時間和到期時間 (eureka.instance.leaseRenewalIntervalInSeconds=10 和 eureka.instance.leaseExpirationDurationInSeconds=20)。
健康檢查機制也有助于幫助 Eureka 判斷 Client 端應(yīng)用的可用性。沒有健康檢查機制的 Client 端,其應(yīng)用狀態(tài)會一直是 UP,只能依賴于 Server 端的定期續(xù)約和清理機制判斷節(jié)點可用性。配置了健康檢查的 Client 端會定時向 Server 端發(fā)送狀態(tài)心跳,同時內(nèi)置支持了包括 JDBC、Redis 等第三方組件的健康檢查,任何一個不可用,則應(yīng)用會被標(biāo)為 DOWN 狀態(tài),將不再提供服務(wù)。在生產(chǎn)環(huán)境下也是開啟了客戶端健康檢查的機制,但沒有調(diào)節(jié)配置參數(shù)。
在 CAP 原則中,Eureka 在設(shè)計時優(yōu)先保證 AP。Eureka 各個節(jié)點都是平等的,幾個節(jié)點掛掉不會影響正常節(jié)點的工作,剩余的節(jié)點依然可以提供注冊和查詢服務(wù)。而 Eureka 的客戶端在向某個 Eureka 注冊時如果發(fā)現(xiàn)連接失敗,則會自動切換至其它節(jié)點,只要有一臺 Eureka 還在,就能保證注冊服務(wù)可用 (保證可用性),只不過查到的信息可能不是最新的 (不保證強一致性)。除此之外,Eureka 還有一種自我保護(hù)機制:如果在 15 分鐘內(nèi)超過 85% 的節(jié)點都沒有正常的心跳,那么 Eureka 就認(rèn)為客戶端與注冊中心出現(xiàn)了網(wǎng)絡(luò)故障,開啟自我保護(hù),支持可讀不可寫。
Eureka 為了保證高可用,在應(yīng)用存活、服務(wù)實例信息、節(jié)點復(fù)制等都采用了緩存機制及定期同步的控制策略,比如客戶端的定期獲?。╡ureka.client.registryFetchIntervalSeconds),實例信息的定期復(fù)制(eureka.client.instanceInfoReplicationIntervalSeconds),Server 的定期心跳檢查 (eureka.instance.leaseExpirationDurationInSeconds),客戶端定期存活心跳(eureka.instance.leaseRenewalIntervalInSeconds)等等,加強了注冊信息的不一致性。服務(wù)消費者應(yīng)用可以選擇重試或者快速失敗的方式,但作為服務(wù)提供者在基于 Spirng Cloud 的微服務(wù)機制下應(yīng)當(dāng)保證服務(wù)的冪等性,支持重試。因此如果對一致性的要求較高,可以適當(dāng)調(diào)整相應(yīng)參數(shù),但明顯這樣也增加了通信的頻率,這種平衡性的考慮更多地需要根據(jù)生產(chǎn)環(huán)境實際情況來調(diào)整,并沒有最優(yōu)的設(shè)置。
Config 的高可用方案比較簡單,只需將 Config Server 作為一個服務(wù)發(fā)布到注冊中心上,客戶端基于 Eureka Client 完成服務(wù)發(fā)現(xiàn),既可實現(xiàn)配置中心的高可用。這種方式要求客戶端應(yīng)用必須在 bootstrap 階段完成發(fā)現(xiàn)配置服務(wù)并獲取配置,因此關(guān)于 Eureka Client 的配置也必須在 bootstrap 的配置文件中存在。同時我們引入了 Spring Retry 支持重試,可多次從 Server 端拉取配置,提高了容錯能力。另外,在啟動階段,我們配置了 failFast=true 來實現(xiàn)快速失敗的方式檢查應(yīng)用啟動狀態(tài),避免對于失敗的無感知帶來應(yīng)用不可用的情況。
在實際的生產(chǎn)中,我們同時基于 Spring Cloud Bus 機制和 Kafka 實現(xiàn)了實時更新,當(dāng)通過 git 提交了更新的配置文件后,基于 webhook 或者手動向 Config Server 應(yīng)用發(fā)送一個 /bus/refresh 請求,Config Server 則通過 Kafka 向應(yīng)用節(jié)點發(fā)送了一個配置更新的事件,應(yīng)用接收到配置更新的事件后,會判斷該文件的 version 和 state,如果任一個發(fā)生變化,則從 Config Server 新拉取配置,內(nèi)部基于 RefreshRemoteApplicationEvent 廣播更新 RefreshScope 標(biāo)注的配置。默認(rèn)的 Kafka 的 Topic 為 springCloudbus,同時需要注意的是應(yīng)用集群的節(jié)點不能采用 consumer group 的方式消費,應(yīng)采用廣播模式保證每個節(jié)點都消費配置更新消息。Spring CloudBus 又是基于 Spring Cloud Stream 機制實現(xiàn)的,因此配置需要按照 Steam 的方式設(shè)置。具體為:
spring.cloud.stream.kafka.binder.brokers=ip:portspring.cloud.stream.kafka.binder.zk-nodes=ip:portspring.cloud.stream.bindings.springCloudBusInput.destination=springCloudbus.dev
如果需要重定義 Topic 名稱,則需要如上所示進(jìn)行調(diào)整,由于多套開發(fā)環(huán)境的存在,而 Kafka 只有一套,我們對 Topic 進(jìn)行了不同環(huán)境的重定義。
但需要注意的一點是,這種實時刷新會導(dǎo)致拒絕任務(wù)的異常 (RejectedExecutionException),必現(xiàn)(當(dāng)前 Edgware.RELEASE 版本)但不影響實際刷新配置,已被證實是個 Bug,具體參見 https://github.com/spring-cloud/spring-cloud-netflix/issues/2228,可簡單理解為在刷新時會關(guān)閉 context 及關(guān)聯(lián)的線程池重新加載,但刷新事件又同時提交了一個新的任務(wù),導(dǎo)致拒絕執(zhí)行異常。
針對外網(wǎng)請求,必然需要一個網(wǎng)關(guān)系統(tǒng)來進(jìn)行統(tǒng)一的安全校驗及路由請求,Zuul 很好地支持了這一點,在實際生產(chǎn)中,我們盡量讓 gateway 系統(tǒng)不集成任何業(yè)務(wù)邏輯,基于 EnableZuulProxy 開啟了服務(wù)發(fā)現(xiàn)模式實現(xiàn)了服務(wù)路由。且只做安全和路由,降低了網(wǎng)關(guān)系統(tǒng)的依賴和耦合,也因此使 gateway 系統(tǒng)可以線性擴展,無壓力和無限制地應(yīng)對流量和吞吐的擴張。
需要注意的是,重定向的問題需要配置 add-host-header=true 支持;為了安全保障,默認(rèn)忽略所有服務(wù)(ignored-services='*'),基于白名單進(jìn)行路由,同時開啟 endpoints 的安全校驗,以避免泄露信息,還要通過 ignored-patterns 關(guān)閉后端服務(wù)的 endpoints 訪問請求。
Zuul 支持自定義 Http Header,我們借助于該機制,實現(xiàn)了 Session 從網(wǎng)關(guān)層向后端服務(wù)的透傳。主要是基于 pre 類型的 ZuulFilter,通過 RequestContex.addZuulRequestHeader 方法可實現(xiàn)請求轉(zhuǎn)發(fā)時增加自定義 Header,后端基于 SpringMVC 的攔截器攔截處理即可。
ZuulFilter 不像 SpringMVC 的攔截器那么強大,它是不支持請求路徑的過濾的。Zuul 也沒有集成 SpringMVC 的攔截器,這就需要我們自己開發(fā)實現(xiàn)類似的功能。如果需要支持 SpringMVC 攔截器,只需要繼承 InstantiationAwareBeanPostProcessorAdapter 重寫初始化方法 postProcessAfterInstantiation,向 ZuulHandlerMapping 添加攔截器既可。為了支持請求的過濾,還可以將攔截器包裝為 MappedInterceptor,這就可以像 SpringMVC 的攔截器一樣支持 include 和 exclude。具體代碼示例如下:
1. public static class ZuulHandlerBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter { 2. 3. @Value('${login.patterns.include}') 4. private String includePattern; 5. @Value('${login.patterns.exclude}') 6. private String excludePattern; 7. 8. @Autowired 9. private AuthenticateInterceptor authenticateInterceptor; 10. 11. 12. public MappedInterceptor pattern(String[] includePatterns, String[] excludePatterns, HandlerInterceptor interceptor) { 13. return new MappedInterceptor(includePatterns, excludePatterns, interceptor); 14. } 15. 16. @Override 17. public boolean postProcessAfterInstantiation(final Object bean, final String beanName) throws BeansException { 18. if (bean instanceof ZuulHandlerMapping) { 19. ZuulHandlerMapping zuulHandlerMapping = (ZuulHandlerMapping) bean; 20. String[] includePatterns = Iterables.toArray(Splitter.on(',').trimResults().omitEmptyStrings().split(includePattern), String.class); 21. String[] excludePatterns = Iterables.toArray(Splitter.on(',').trimResults().omitEmptyStrings().split(excludePattern), String.class); 22. zuulHandlerMapping.setInterceptors(pattern(includePatterns, excludePatterns, authenticateInterceptor)); 23. } 24. return super.postProcessAfterInstantiation(bean, beanName); 25. } 26. 27. }
Zuul 底層是基于 Ribbon 和 Hystrix 實現(xiàn)的,因此超時配置需要注意,如果基于服務(wù)發(fā)現(xiàn)的方式,則超時主要受 Ribbon 控制。另外由于 Spring Config 引入了 Spring Retry 導(dǎo)致 Zuul 會至少進(jìn)行一次失敗請求的重試,各種開關(guān)配置都不生效,最后通過將 Ribbon 的 MaxAutoRetries 和 MaxAutoRetriesNextServer 同時設(shè)置為 0,避免了重試。在整個微服務(wù)調(diào)用中,由于不能嚴(yán)格保證服務(wù)的冪等性,我們是關(guān)閉了所有的重試機制的,包括 Feign 的重試,只能手動進(jìn)行服務(wù)重試調(diào)用,以確保不會產(chǎn)生臟數(shù)據(jù)。
Zipkin 是大規(guī)模分布式跟蹤系統(tǒng)的開源實現(xiàn),基于 2010 年 Google 發(fā)表的 Dapper 論文開發(fā)的,Spring Cloud Sleuth 提供了兼容 Zipkin 的實現(xiàn),可以很方便地集成,提供了較為直觀的可視化視圖,便于人工排查問題。美利好車系統(tǒng)在實際的生產(chǎn)實踐中,將日志同步改為適用于 Zipkin 格式的 pattern,這樣后端 ELK 組件日志的收集查詢也兼容,基于 traceId 實現(xiàn)了服務(wù)追蹤和日志查詢的聯(lián)動。
在日志的上報和收集上我們?nèi)匀换?spring-cloud-starter-bus-kafka 來實現(xiàn)。
在前后端分離成為主流和現(xiàn)狀的情況下,前后端就接口的定義和理解的不一致性成為開發(fā)過程中效率的制約因素,解決這個問題可以幫助前后端團隊很好地協(xié)作,高質(zhì)量地完成開發(fā)工作。我們在實際開發(fā)過程中使用了 Swagger 來生成在線 API 文檔,讓 API 管理和使用變得極其簡單,同時提供了接口的簡單測試能力。雖然這帶來了一定的侵入性,但從實際生產(chǎn)效率來說遠(yuǎn)超出了預(yù)期,因此也特別予以強調(diào)和推薦。
實際開發(fā)過程中,我們?nèi)匀惶峁┝?API 的 SDK 讓調(diào)用方接入,雖然這個方式是微服務(wù)架構(gòu)下不被推崇的,但現(xiàn)階段我們認(rèn)為 SDK 可以讓調(diào)用 API 更簡單、更友好。版本問題基于 Maven 的 snapshot 機制實現(xiàn)實時更新,唯一需要注意的是要保證向后兼容。
以上就是美利好車系統(tǒng)微服務(wù)實施的一些實踐,有些地方可能不是特別恰當(dāng)和正確,但在當(dāng)前階段也算基本滿足了開發(fā)需要,而且我們秉承擁抱變化的態(tài)度,對整個體系結(jié)構(gòu)也在持續(xù)進(jìn)行改善和優(yōu)化,積極推動美利好車架構(gòu)的演進(jìn),希望能更好地支持美利好車的業(yè)務(wù)需求。
王文堯,曾在京東等多家知名互聯(lián)網(wǎng)電商領(lǐng)域公司任職,也創(chuàng)業(yè)做過垂直電商平臺,現(xiàn)任美利金融好車技術(shù)部架構(gòu)師。對電商領(lǐng)域的業(yè)務(wù)比較了解,同時對敏捷、領(lǐng)域建模、高并發(fā)高可用的分布式系統(tǒng)設(shè)計及服務(wù)化等方面有較為深入的研究和實踐。
2017 年軟件研發(fā)領(lǐng)域有很多新的變化,比如 Java 9 正式發(fā)布;Kotlin 得到 Google Android 正式支持;Spark、Kafka 等框架紛紛引入流式計算能力;AI 技術(shù)蓬勃發(fā)展等等。
新一年的技術(shù)新趨勢 QCon 全球軟件開發(fā)大會也與你一同關(guān)注。目前,我們已經(jīng)確認(rèn)多位技術(shù)專家:Netflix 工程總監(jiān) Katharina Probst、Kotlin 團隊工程師 Roman Elizarov、Apache Spark Structured Streaming 的核心開發(fā)人員朱詩雄、愛奇藝科學(xué)家李典等老師將在現(xiàn)場分享前沿技術(shù)案例,共呈技術(shù)盛宴。
2018 北京站現(xiàn)在報名享 7 折優(yōu)惠,立減 2040 元。有任何問題可咨詢購票經(jīng)理 Hanna,電話:15110019061,微信:qcon-0410。