美利好車的微服務實踐
前言
美麗好車的微服務實踐是基于 Spring Cloud 體系來做的,在具體的開發過程中遇到了不少問題,踩了不少坑,對于微服務也有了實際的切身體會和理解,而不再是泛泛而談。在整個 Spring Cloud 技術棧中,基于不同職責需要,我們選擇了相應組件來支持我們的服務化,同時配合 Swagger 和 Feign 實現接口的文檔化和聲明式調用,在實際開發過程中極大地降低了溝通成本,提高了研發聯調和測試的效率。
從應用架構來看,正是由于基于 Spring Cloud 來實現,整個系統完全秉承了微服務的原則,無論是 Spring Cloud 組件還是業務系統,都體現了服務即組件、獨立部署、去中心化的特性,由此提供了快速交付和彈性伸縮的能力。
接下來我們基于各個組件具體介紹一下美利好車的微服務實踐,首先最基本的就是 Eureka,其承載著微服務中的服務注冊和服務發現的職責,是最基礎的組件,必然有高可用的要求。
基于高可用的 Eureka 集群實現服務發現
美利好車在生產實踐中部署了一個三節點的 Eureka Server 的集群,每個節點自身也同時基于 Eureka Client 向其它 Server 注冊,節點之間兩兩復制,實現了高可用。在配置時指定所有節點機器的 hostname 既可,即做到了配置部署的統一,又簡單實現了 IP 解耦,不需要像官方示例那樣用 profile 機制區分節點配置。這主要是由于 Eureka 節點在復制時會剔除自身節點,向其它節點復制實例信息,保證了單邊同步原則:只要有一條邊將節點連接,就可以進行信息傳播和同步。在生產環境中并不要過多調整其它配置,遵循默認的配置既可。
服務發現
作為服務提供者的 Eureka Client 必須配置 register-with-eureka 為 true,即向 Eureka Server 注冊服務,而作為服務消費者的 Eureka Client 必須配置 fetch-registry=true,意即從 Eureka Server 上獲取服務信息。如果一個應用服務可能既對外提供服務,也使用其它領域提供的服務,則兩者都配置為 true,同時支持服務注冊和服務發現。由于 Ribbon 支持了負載均衡,所以作為服務提供者的應用一般都是采用基于 IP 的方式注冊,這樣更靈活。
健康檢查
在開發測試環境中,常常都是 standlone 方式部署,但由于 Eureka 自我保護模式以及心跳周期長的原因,經常會遇到 Eureka Server 不剔除已關停的節點的問題,而應用在開發測試環境的啟停又比較頻繁,給聯調測試造成了不小的困擾。為此我們調整了部分配置讓 Eureka Server 能夠迅速有效地踢出已關停的節點,主要包括在 Server 端配置關閉自我保護 (eureka.server.enableSelfPreservation=false),同時可以縮小 Eureka Server 清理無效節點的時間間隔(eureka.server.evictionIntervalTimerInMs=1000)等方式。
另外在 Client 端開啟健康檢查,并同步縮小配置續約更新時間和到期時間 (eureka.instance.leaseRenewalIntervalInSeconds=10 和 eureka.instance.leaseExpirationDurationInSeconds=20)。
健康檢查機制也有助于幫助 Eureka 判斷 Client 端應用的可用性。沒有健康檢查機制的 Client 端,其應用狀態會一直是 UP,只能依賴于 Server 端的定期續約和清理機制判斷節點可用性。配置了健康檢查的 Client 端會定時向 Server 端發送狀態心跳,同時內置支持了包括 JDBC、Redis 等第三方組件的健康檢查,任何一個不可用,則應用會被標為 DOWN 狀態,將不再提供服務。在生產環境下也是開啟了客戶端健康檢查的機制,但沒有調節配置參數。
Eureka 的一致性
在 CAP 原則中,Eureka 在設計時優先保證 AP。Eureka 各個節點都是平等的,幾個節點掛掉不會影響正常節點的工作,剩余的節點依然可以提供注冊和查詢服務。而 Eureka 的客戶端在向某個 Eureka 注冊時如果發現連接失敗,則會自動切換至其它節點,只要有一臺 Eureka 還在,就能保證注冊服務可用 (保證可用性),只不過查到的信息可能不是最新的 (不保證強一致性)。除此之外,Eureka 還有一種自我保護機制:如果在 15 分鐘內超過 85% 的節點都沒有正常的心跳,那么 Eureka 就認為客戶端與注冊中心出現了網絡故障,開啟自我保護,支持可讀不可寫。
Eureka 為了保證高可用,在應用存活、服務實例信息、節點復制等都采用了緩存機制及定期同步的控制策略,比如客戶端的定期獲取(eureka.client.registryFetchIntervalSeconds),實例信息的定期復制(eureka.client.instanceInfoReplicationIntervalSeconds),Server 的定期心跳檢查 (eureka.instance.leaseExpirationDurationInSeconds),客戶端定期存活心跳(eureka.instance.leaseRenewalIntervalInSeconds)等等,加強了注冊信息的不一致性。服務消費者應用可以選擇重試或者快速失敗的方式,但作為服務提供者在基于 Spirng Cloud 的微服務機制下應當保證服務的冪等性,支持重試。因此如果對一致性的要求較高,可以適當調整相應參數,但明顯這樣也增加了通信的頻率,這種平衡性的考慮更多地需要根據生產環境實際情況來調整,并沒有最優的設置。
Config 的高可用及實時更新
高可用方案
Config 的高可用方案比較簡單,只需將 Config Server 作為一個服務發布到注冊中心上,客戶端基于 Eureka Client 完成服務發現,既可實現配置中心的高可用。這種方式要求客戶端應用必須在 bootstrap 階段完成發現配置服務并獲取配置,因此關于 Eureka Client 的配置也必須在 bootstrap 的配置文件中存在。同時我們引入了 Spring Retry 支持重試,可多次從 Server 端拉取配置,提高了容錯能力。另外,在啟動階段,我們配置了 failFast=true 來實現快速失敗的方式檢查應用啟動狀態,避免對于失敗的無感知帶來應用不可用的情況。
配置實時同步
在實際的生產中,我們同時基于 Spring Cloud Bus 機制和 Kafka 實現了實時更新,當通過 git 提交了更新的配置文件后,基于 webhook 或者手動向 Config Server 應用發送一個 /bus/refresh 請求,Config Server 則通過 Kafka 向應用節點發送了一個配置更新的事件,應用接收到配置更新的事件后,會判斷該文件的 version 和 state,如果任一個發生變化,則從 Config Server 新拉取配置,內部基于 RefreshRemoteApplicationEvent 廣播更新 RefreshScope 標注的配置。默認的 Kafka 的 Topic 為 springCloudbus,同時需要注意的是應用集群的節點不能采用 consumer group 的方式消費,應采用廣播模式保證每個節點都消費配置更新消息。Spring CloudBus 又是基于 Spring Cloud Stream 機制實現的,因此配置需要按照 Steam 的方式設置。具體為:
spring.cloud.stream.kafka.binder.brokers=ip:port spring.cloud.stream.kafka.binder.zk-nodes=ip:port spring.cloud.stream.bindings.springCloudBusInput.destination=springCloudbus.dev
如果需要重定義 Topic 名稱,則需要如上所示進行調整,由于多套開發環境的存在,而 Kafka 只有一套,我們對 Topic 進行了不同環境的重定義。
但需要注意的一點是,這種實時刷新會導致拒絕任務的異常 (RejectedExecutionException),必現(當前 Edgware.RELEASE 版本)但不影響實際刷新配置,已被證實是個 Bug,具體參見 https://github.com/spring-cloud/spring-cloud-netflix/issues/2228,可簡單理解為在刷新時會關閉 context 及關聯的線程池重新加載,但刷新事件又同時提交了一個新的任務,導致拒絕執行異常。
Zuul 的網關的安全及 session
安全處理
針對外網請求,必然需要一個網關系統來進行統一的安全校驗及路由請求,Zuul 很好地支持了這一點,在實際生產中,我們盡量讓 gateway 系統不集成任何業務邏輯,基于 EnableZuulProxy 開啟了服務發現模式實現了服務路由。且只做安全和路由,降低了網關系統的依賴和耦合,也因此使 gateway 系統可以線性擴展,無壓力和無限制地應對流量和吞吐的擴張。
需要注意的是,重定向的問題需要配置 add-host-header=true 支持;為了安全保障,默認忽略所有服務(ignored-services='*'),基于白名單進行路由,同時開啟 endpoints 的安全校驗,以避免泄露信息,還要通過 ignored-patterns 關閉后端服務的 endpoints 訪問請求。
Session 管理
Zuul 支持自定義 Http Header,我們借助于該機制,實現了 Session 從網關層向后端服務的透傳。主要是基于 pre 類型的 ZuulFilter,通過 RequestContex.addZuulRequestHeader 方法可實現請求轉發時增加自定義 Header,后端基于 SpringMVC 的攔截器攔截處理即可。
ZuulFilter 不像 SpringMVC 的攔截器那么強大,它是不支持請求路徑的過濾的。Zuul 也沒有集成 SpringMVC 的攔截器,這就需要我們自己開發實現類似的功能。如果需要支持 SpringMVC 攔截器,只需要繼承 InstantiationAwareBeanPostProcessorAdapter 重寫初始化方法 postProcessAfterInstantiation,向 ZuulHandlerMapping 添加攔截器既可。為了支持請求的過濾,還可以將攔截器包裝為 MappedInterceptor,這就可以像 SpringMVC 的攔截器一樣支持 include 和 exclude。具體代碼示例如下:
- public static class ZuulHandlerBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter {
- @Value("${login.patterns.include}")
- private String includePattern;
- @Value("${login.patterns.exclude}")
- private String excludePattern;
- @Autowired
- private AuthenticateInterceptor authenticateInterceptor;
- public MappedInterceptor pattern(String[] includePatterns, String[] excludePatterns, HandlerInterceptor interceptor) {
- return new MappedInterceptor(includePatterns, excludePatterns, interceptor);
- }
- @Override
- public boolean postProcessAfterInstantiation(final Object bean, final String beanName) throws BeansException {
- if (bean instanceof ZuulHandlerMapping) {
- ZuulHandlerMapping zuulHandlerMapping = (ZuulHandlerMapping) bean;
- String[] includePatterns = Iterables.toArray(Splitter.on(",").trimResults().omitEmptyStrings().split(includePattern), String.class);
- String[] excludePatterns = Iterables.toArray(Splitter.on(",").trimResults().omitEmptyStrings().split(excludePattern), String.class);
- zuulHandlerMapping.setInterceptors(pattern(includePatterns, excludePatterns, authenticateInterceptor));
- }
- return super.postProcessAfterInstantiation(bean, beanName);
- }
- }</pre>
關閉重試機制
Zuul 底層是基于 Ribbon 和 Hystrix 實現的,因此超時配置需要注意,如果基于服務發現的方式,則超時主要受 Ribbon 控制。另外由于 Spring Config 引入了 Spring Retry 導致 Zuul 會至少進行一次失敗請求的重試,各種開關配置都不生效,最后通過將 Ribbon 的 MaxAutoRetries 和 MaxAutoRetriesNextServer 同時設置為 0,避免了重試。在整個微服務調用中,由于不能嚴格保證服務的冪等性,我們是關閉了所有的重試機制的,包括 Feign 的重試,只能手動進行服務重試調用,以確保不會產生臟數據。
基于 Sleuth 的服務追蹤
Zipkin 是大規模分布式跟蹤系統的開源實現,基于 2010 年 Google 發表的 Dapper 論文開發的,Spring Cloud Sleuth 提供了兼容 Zipkin 的實現,可以很方便地集成,提供了較為直觀的可視化視圖,便于人工排查問題。美利好車系統在實際的生產實踐中,將日志同步改為適用于 Zipkin 格式的 pattern,這樣后端 ELK 組件日志的收集查詢也兼容,基于 traceId 實現了服務追蹤和日志查詢的聯動。
在日志的上報和收集上我們仍然基于 spring-cloud-starter-bus-kafka 來實現。
Swagger 支持的接口文檔化和可測性
在前后端分離成為主流和現狀的情況下,前后端就接口的定義和理解的不一致性成為開發過程中效率的制約因素,解決這個問題可以幫助前后端團隊很好地協作,高質量地完成開發工作。我們在實際開發過程中使用了 Swagger 來生成在線 API 文檔,讓 API 管理和使用變得極其簡單,同時提供了接口的簡單測試能力。雖然這帶來了一定的侵入性,但從實際生產效率來說遠超出了預期,因此也特別予以強調和推薦。
實際開發過程中,我們仍然提供了 API 的 SDK 讓調用方接入,雖然這個方式是微服務架構下不被推崇的,但現階段我們認為 SDK 可以讓調用 API 更簡單、更友好。版本問題基于 Maven 的 snapshot 機制實現實時更新,唯一需要注意的是要保證向后兼容。
以上就是美利好車系統微服務實施的一些實踐,有些地方可能不是特別恰當和正確,但在當前階段也算基本滿足了開發需要,而且我們秉承擁抱變化的態度,對整個體系結構也在持續進行改善和優化,積極推動美利好車架構的演進,希望能更好地支持美利好車的業務需求。
來自:http://www.infoq.com/cn/articles/meilihaoche-microservice-practice