本次分享主要介紹了愛油科技基於Docker和Spring Cloud將總體業務微服務化的一些實踐經驗,主要包括:node
對於單體應用來講,優勢不少,例如:git
然而隨着業務複雜性的上升,業務規模的擴大,缺點也顯現出來,例如:github
所以微服務技術做爲一項對分佈式服務治理的架構模式,逐漸被你們認識了。算法
實施微服務,首先對咱們的架構進行了拆分:按行分層,按列分業務spring
在咱們的微服務體系中,全部的服務被劃分爲了三個層次:docker
實踐中咱們主要關注業務服務層和接入層,對於沒有足夠運維力量的咱們,基礎設施使用雲服務是省事省力的選擇。數據庫
業務服務層咱們給他起名叫做Epic,接入層咱們起名Rune,創建之初便訂立了以下原則:json
業務邏輯層咱們主要使用使用Java,接入層咱們主要使用PHP或Node。後來隨着團隊的成長,逐步將接入層所有遷移至Node。後端
愛油科技做爲一家成品油行業的初創型公司,須要面對很是複雜的業務場景,並且隨着業務的發展,變化的可能性很是高。因此在微服務架構設計之初,咱們就指望咱們的微服務體系能:api
目前常見的微服務相關框架:
這些常見的框架中,Dubbo幾乎是惟一能被稱做全棧微服務框架的「框架」,它包含了微服務所需的幾乎全部內容,而DubboX做爲它的加強,增長了REST支持。
它優勢不少,例如:
不過遺憾的是:
Motan是微博平臺微服務框架,承載了微博平臺千億次調用業務。
優勢是:
不過:
Apache Thrift、gRPC等雖然優秀,並不能算做微服務框架,自身並不包括服務發現等必要特性。
若是說微服務少不了Java,那麼必定少不了Spring,若是說少不了Spring,那麼微服務「官配」Spring Cloud固然是值得斟酌的選擇。
Spring Cloud優勢:
固然它有不少不足之處,例如:
根據咱們的目標,咱們最終選擇了Spring Cloud做爲咱們的微服務框架,緣由有4點:
Apache Thrift
和gRPC
自研,投入產出比不好;Motan
尚未發佈。所以Spring Cloud成爲了理性的選擇。
Spring Cloud是一個集成框架,將開源社區中的框架集成到Spring體系下,幾個重要的家族項目:
spring-boot
,一改Java應用程序運行難、部署難,甚至無需Web容器,只依賴JRE便可spring-cloud-netflix
,集成Netflix優秀的組件Eureka、Hystrix、Ribbon、Zuul,提供服務發現、限流、客戶端負載均衡和API網關等特性支持spring-cloud-config
,微服務配置管理spring-cloud-consul
,集成Consul支持固然,SpringCloud下子項目很是多,這裏就不一一列出介紹了。
Spring Cloud Netflix提供了Eureka服務註冊的集成支持,不過沒選它是由於:
Docker做爲支撐平臺的重要技術之一,Consul幾乎也是咱們的必選服務。所以咱們以爲一事不煩二主,理所應當的Consul成爲咱們的服務註冊中心。
Consul的優點:
也就是說,Consul能夠一次性解決咱們對服務註冊發現、配置管理的需求,並且長期來看也更適合跟不一樣平臺的系統,包括和Docker調度系統進行整合。
最初打算本身開發一個Consul和Spring Cloud整合的組件,不過幸運的是,咱們作出這個決定的時候,spring-cloud-consul
剛剛發佈了,咱們能夠拿來即用,這節約了不少的工做量。
所以藉助Consul和spring-cloud-consul
,咱們實現了
srping-cloud-consul
的項目能夠自動註冊服務,也能夠經過HTTP接口手動註冊,Docker容器也能夠自動註冊固然也踩到了一些坑:
spring-cloud-consul
服務註冊時不能正確選判本地ip地址。對於咱們的環境來講,不管是在服務器上,仍是Docker容器裏,都有多個網絡接口同時存在,而spring-cloud-consul
在註冊服務時,須要先選判本地服務的IP地址,判斷邏輯是以第一個非本地地址爲準,經常錯判。所以在容器中咱們利用entrypoint腳本獲取再經過環境變量強制指定。
#!/usr/bin/env bash set -e # If service runs as Rancher service, auto set advertise ip address # from Rancher metadata service. if [ -n "$RUN_IN_RANCHER" ]; then echo "Waiting for ip address..." # Waiting for ip address sleep 5 RANCHER_MS_BASE=http://rancher-metadata/2015-12-19 PRIMARY_IP=`curl -sSL $RANCHER_MS_BASE/self/container/primary_ip` SERVICE_INDEX=`curl -sSL $RANCHER_MS_BASE/self/container/service_index` if [ -n "$PRIMARY_IP" ]; then export SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME=$PRIMARY_IP fi echo "Starting service #${SERVICE_INDEX-1} at $PRIMARY_IP." fi exec "$@"
咱們的容器運行在Rancher中,因此能夠利用Rancher的metadata服務來獲取容器的IP地址,再經過SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME
環境變量來設置服務發現的註冊地址。基於其餘容器調度平臺也會很類似。
爲了方便開發人員使用,微服務框架應當簡單容易使用。對於不少微服務框架和RPC框架來講,都提供了很好的機制。在Spring Cloud中經過OpenFeign
實現微服務之間的快速集成:
服務方聲明一個Restful的服務接口,和普通的Spring MVC控制器幾乎別無二致:
@RestController @RequestMapping("/users") public class UserResource { @RequestMapping(value = "{id}", method = RequestMethod.GET, produces = "application/json") public UserRepresentation findOne(@PathVariable("id") String id) { User user = this.userRepository.findByUserId(new UserId(id)); if (user == null || user.getDeleted()) { throw new NotFoundException("指定ID的用戶不存在或者已被刪除。"); } return new UserRepresentation(user); } }
客戶方使用一個微服務接口,只須要定義一個接口:
@FeignClient("epic-member-microservice") public interface UserClient { @Override @RequestMapping(value = "/users/{id}", method = RequestMethod.GET, produces = "application/json") User findOne(@PathVariable("id") String id); }
在須要使用UserClient
的Bean中,直接注入UserClient
類型便可。事實上,UserClient
和相關VO類,能夠直接做爲公共接口封裝在公共項目中,供任意須要使用的微服務引用,服務方Restful Controller直接實現這一接口便可。
OpenFeign
提供了這種簡單的方式來使用Restful服務,這大大下降了進行接口調用的複雜程度。
對於錯誤的處理,咱們使用HTTP狀態碼做爲錯誤標識,並作了以下規定:
對於服務器端,只須要在一個異常類上添加註解,便可指定該異常的HTTP響應狀態碼,例如:
@ResponseStatus(HttpStatus.NOT_FOUND) public class NotFoundException extends RuntimeException { public NotFoundException() { super("查找的資源不存在或者已被刪除。"); } public NotFoundException(String message) { super(message); } public NotFoundException(String message, Throwable cause) { super(message, cause); } }
對於客戶端咱們實現了本身的FeignClientExceptionErrorDecoder
來將請求異常轉換爲對於的異常類,示例以下:
@Component public class FeignClientExceptionErrorDecoder implements ErrorDecoder { private final ErrorDecoder delegate = new ErrorDecoder.Default(); @Override public Exception decode(String methodKey, Response response) { // Only decode 4xx errors. if (response.status() >= 500) { return delegate.decode(methodKey, response); } // Response content type must be json if (response.headers().getOrDefault("Content-Type", Lists.newArrayList()).stream() .filter(s -> s.toLowerCase().contains("json")).count() > 0) { try { String body = Util.toString(response.body().asReader()); // 轉換並返回異常對象 ... } catch (IOException ex) { throw new RuntimeException("Failed to process response body.", ex); } } return delegate.decode(methodKey, response); } }
須要注意的是,decode
方法返回的4xx狀態碼異常應當是HystrixBadRequestException
的子類對象,緣由在於,咱們把4xx異常視做業務異常,而不是因爲故障致使的異常,因此不該當被Hystrix計算爲失敗請求,並引起斷路器動做,這一點很是重要。
在UserClient.findOne
方法的調用代碼中,便可直接捕獲相應的異常了:
try { User user = this.userClient.findOne(new UserId(id)); } catch(NotFoundException ex) { ... }
經過OpenFeign
,咱們大大下降了Restful接口進行服務集成的難度,幾乎作到了無額外工做量的服務集成。
微服務架構下,因爲調用須要跨系統進行遠程操做,各微服務獨立運維,因此在設計架構時還必須考慮伸縮性和容錯性,具體地說主要包括如下幾點要求:
spring-cloud-netflix
和相關組件爲咱們提供了很好的解決方案:
下面主要介紹一下,各個組件在進行服務質量保證中是如何發揮做用的。
Consul中註冊了一致性的可用的服務列表,並經過健康檢查保證這些實例都是存活的,服務註冊和檢查的過程以下:
spring-cloud-consul
經過Consul接口發起服務註冊,將服務的/health
做爲健康檢查端點;/health
,檢查當前微服務是否爲UP
狀態;/health
將會收集微服務內各個儀表收集上來的狀態數據,主要包括數據庫、消息隊列是否連通等;這樣可以保證Consul中列出的全部微服務狀態都是健康可用的,各個微服務會監視微服務實例列表,自動同步更新他們。
Hystrix提供了斷路器模式的實現,主要在三個方面能夠說明:
圖片來自Hystrix項目文檔
首先Hystrix提供了降級方法,斷路器開啓時,操做請求會快速失敗再也不向後投遞,直接調用fallback方法來返回操做;當操做失敗、被拒或者超時後,也會直接調用fallback方法返回操做。這能夠保證在系統過載時,能有後備方案來返回一個操做,或者優雅的提示錯誤信息。斷路器的存在能讓故障業務被隔離,防止過載的流量涌入打死後端數據庫等。
而後是基於請求數據統計的斷路開關,在Hystrix中維護一個請求統計了列表(默認最多10條),列表中的每一項是一個桶。每一個桶記錄了在這個桶的時間範圍內(默認是1秒),請求的成功數、失敗數、超時數、被拒數。其中當失敗請求的比例高於某一值時,將會觸發斷路器工做。
最後是不一樣的請求命令(HystrixCommand
)可使用彼此隔離的資源池,不會發生相互的擠佔。在Hystrix中提供了兩種隔離機制,包括線程池和信號量。線程池模式下,經過線程池的大小來限制同時佔用資源的請求命令數目;信號量模式下經過控制進入臨界區的操做數目來達到限流的目的。
這裏包括了Hystrix的一些重要參數的配置項:
參數 | 說明 |
---|---|
circuitBreaker.requestVolumeThreshold | 至少在一個統計窗口內有多少個請求後,才執行斷路器的開關,默認20 |
circuitBreaker.sleepWindowInMilliseconds | 斷路器觸發後多久後才進行下一次斷定,默認5000毫秒 |
circuitBreaker.errorThresholdPercentage | 一個統計窗口內百分之多少的請求失敗才觸發熔斷,默認是50% |
execution.isolation.strategy | 運行隔離策略,支持Thread ,Semaphore ,前者經過線程池來控制同時運行的命令,後者經過信號來控制,默認是Thread |
execution.isolation.thread.interruptOnTimeout | 命令執行的超時時間,默認1000毫秒 |
coreSize | 線程池大小,默認10 |
keepAliveTimeMinutes | 線程存活時間,默認爲1分鐘 |
maxQueueSize | 最大隊列長度,-1使用SynchronousQueue,默認-1。 |
queueSizeRejectionThreshold | 容許隊列堆積的最大數量 |
Ribbon使用Consul提供的服務實例列表,能夠經過服務名選取一個後端服務實例鏈接,並保證後端流量均勻分佈。spring-cloud-netflix
整合了OpenFeign、Hystrix和Ribbon的負載均衡器,整個調用過程以下(返回值路徑已經省略):
在這個過程當中,各個組件扮演的角色以下:
Feign負責提供客戶端接口收調用,把發起請求操做(包括編碼、解碼和請求數據)封裝成一個Hystrix命令,這個命令包裹的請求對象,會被Ribbon的負載均衡器處理,按照負載均衡策略選擇一個主機,而後交給請求對象綁定的HTTP客戶端對象發請求,響應成功或者不成功的結果,返回給Hystrix。
spring-cloud-netflix
中默認使用了Ribbon的ZoneAwareLoadBalancer
負載均衡器,它的負載均衡策略的核心指標是平均活躍請求數(Average Active Requests)。ZoneAwareLoadBalancer
會拉取全部當前可用的服務器列表,而後將目前因爲種種緣由(好比網絡異常)響應過慢的實例暫時從可用服務實例列表中移除,這樣的機制能夠保證故障實例被隔離,以避免繼續向其發送流量致使集羣狀態進一步惡化。不過因爲目前spring-cloud-consul
還不支持經過consul來指定服務實例的所在區,咱們正在努力將這一功能完善。除了選區策略外,Ribbon中還提供了其餘的負載均衡器,也能夠自定義合適的負載均衡器。
關於區域的支持,我提交的PR已經Merge到spring-cloud-consul項目中,預計下個版本將會包含這項特性。
總的來看,spring-cloud-netflix
和Ribbon中提供了基本的負載均衡策略,對於咱們來講已經足夠用了。但實踐中,若是須要進行灰度發佈或者須要進行流量壓測,目前來看還很難直接實現。而這些特性在Dubbo則開箱即用。
Zuul爲使用Java語言的接入層服務提供API網關服務,既能夠根據配置反向代理指定的接口,也能夠根據服務發現自動配置。Zuul提供了相似於iptables的處理機制,來幫助咱們實現驗證權鑑、日誌等,請求工做流以下所示:
圖片來自Zuul官方文檔。
使用Zuul進行反向代理時,一樣會走與OpenFeign相似的請求過程,確保API的調用過程也能經過Hystrix、Ribbon提供的降級、控流機制。
Hystrix會統計每一個請求操做的狀況來幫助控制斷路器,這些數據是能夠暴露出來供監控系統熱點。Hystrix Dashboard能夠將當前接口調用的狀況以圖形形式展現出來:
圖片來自Hystrix Dashboard官方示例
Hystrix Dashboard既能夠集成在其餘項目中,也能夠獨立運行。咱們直接使用Docker啓動一個Hystrix Dashboard服務便可:
docker run --rm -ti -p 7979:7979 kennedyoliveira/hystrix-dashboard
爲了實現能對整個微服務集羣的接口調用狀況彙總,可使用spring-cloud-netflix-turbine
來將整個集羣的調用狀況聚集起來,供Hystrix Dashboard展現。
微服務的日誌直接輸出到標準輸出/標準錯誤中,再由Docker經過syslog日誌驅動將日誌寫入至節點機器機的rsyslog中。rsyslog在本地暫存並轉發至日誌中心節點的Logstash中,既歸檔存儲,又經過ElasticSearch進行索引,日誌能夠經過Kibana展現報表。
在rsyslog的日誌收集時,須要將容器信息和鏡像信息加入到tag中,經過Docker啓動參數來進行配置:
--log-driver syslog --log-opt tag="{{.ImageName}}/{{.Name}}/{{.ID}}"
不過rsyslog默認只容許tag不超過32個字符,這顯然是不夠用的,因此咱們自定義了日誌模板:
template (name="LongTagForwardFormat" type="string" string="<%PRI%>%TIMESTAMP:::date-rfc3339% %HOSTNAME% %syslogtag%%msg:::sp-if-no-1st-sp%%msg%")
在實際的使用過程當中發現,當主機內存負載比較高時,rsyslog會發生日誌沒法收集的狀況,報日誌數據文件損壞。後來在Redhat官方找到了相關的問題,確認是rsyslog中的一個Bug致使的,當開啓日誌壓縮時會出現這個問題,咱們選擇暫時把它禁用掉。
領域驅動設計可以很大程度上幫助咱們享用微服務帶來的優點,因此咱們使用領域驅動設計(DDD)的方法來構建微服務,由於微服務架構和DDD有一種自然的契合。把全部業務劃分紅若干個子領域,有強內在關聯關係的領域(界限上下文)應當被放在一塊兒做爲一個微服務。最後造成了界限上下文-工做團隊-微服務一一對應的關係:
在設計單個微服務(Epic層的微服務)時,咱們這樣作:
這給咱們帶來了顯著的好處:
從單體應用遷移到微服務架構時,不得不面臨的問題之一就是事務。在單體應用時代,全部業務共享同一個數據庫,一次請求操做可放置在同一個數據庫事務中;在微服務架構下,這件事變得很是困難。然而事務問題不可避免,很是關鍵。
解決事務問題時,最早想到的解決方法一般是分佈式事務。分佈式事務在傳統系統中應用的比較普遍,主要基於兩階段提交的方式實現。然而分佈式事務在微服務架構中可行性並不高,主要基於這些考慮:
根據CAP理論,分佈式系統不可兼得一致性、可用性、分區容錯性(可靠性)三者,對於微服務架構來說,咱們一般會保證可用性、容錯性,犧牲一部分一致性,追求最終一致性。因此對於微服務架構來講,使用分佈式事務來解決事務問題不管是從成本仍是收益上來看,都不划算。
對微服務系統來講解決事務問題,CQRS+Event Sourcing是更好的選擇。
CQRS是命令和查詢職責分離的縮寫。CQRS的核心觀點是,把操做分爲修改狀態的命令(Command),和返回數據的查詢(Query),前者對應於「寫」的操做,不能返回數據,後者對應於「讀」的操做,不形成任何影響,由此領域模型被一分爲二,分而治之。
Event Sourcing一般被翻譯成事件溯源,簡單的來講就是某一對象的當前狀態,是由一系列的事件疊加後產生的,存儲這些事件便可經過重放得到對象在任一時間節點上的狀態。
經過CQRS+Event Sourcing,咱們很容易得到最終一致性,例如對於一個跨系統的交易過程而言:
PlaceOrderEvent
,訂單狀態PENDING
;PaidEvent
;PaidEvent
,將訂單標記爲CREATED
;InsufficientEvent
,交易微服務消費將訂單標記爲CANCELED
。咱們只要保證領域事件能被持久化,那麼即便出現網絡延遲或部分系統失效,咱們也能保證最終一致性。
實踐上,咱們利用Spring從4.2版本開始支持的自定義應用事件機制將本地事務和事件投遞結合起來進行:
到目前爲止咱們已經有數十個微服務運行於線上了,微服務數目甚至多過了團隊人數。若是沒有DevOps支持,運維這些微服務將是一場災難。
咱們使用Docker鏡像做爲微服務交付的標準件:
因爲時間所限,這裏就不展開贅述了。
基於spring-cloud-consul
的配置管理仍然須要完善,對於大規模應用的環境中,配置的版本控制、灰度、回滾等很是重要。SpringCloud提供了一個核,可是具體的使用還要結合場景、需求和環境等,再作一些工做。
對於非JVM語言的微服務和基於SpringCloud的微服務如何協同治理,這一問題仍然值得探索。包括像與Docker編排平臺,特別是與Mesos協同進行伸縮的服務治理,還須要更多的實踐來支持。
Q:大家是部署在公有云,仍是託管機房。
A:咱們部署在阿里雲上,使用了不少阿里雲服務做爲基礎設施,這一點是爲了節約運維成本。
Q:怎麼解決服務過多依賴問題,開發也會有麻煩,由於要開發一個功能,爲了把服務跑起來,可能要跑不少服務。
A:在咱們的實際開發過程當中,也遇到了這個問題。主要的是經過部署幾個不一樣的仿真環境,一組開發者能夠共用這組環境。本地開發也很簡單,只須要把consul指向到這個集羣的consul上便可。
Q:大家微服務業務調用最深有幾層?restful接口調用鏈的效率如何?比單體結構慢多少?
A:通常不超過3層,這是領域驅動設計給咱們帶來的優點,單個服務幾乎本身就能完成職責範圍內的任務,沒有出現RPC災難,一個業務咱們也不傾向於拆分紅若干個遠程操做進行。
Q:你好 咱們單位從6月份 開始實施 微服務化(O2O業務),使用的是dubbo,使用事務型消息來作最終一致性,請問CQRS+Event Sourcing相對於事務型消息隊列來處理一致性問題 有什麼優點麼。
A:其實CQRS+Event Sourcing是一種觀念的轉變,落地仍是須要靠存儲和消息隊列,優點在於能夠消除系統中的鎖點,性能會更好。
Q:有沒有考慮過用kubernetes來實現微服務治理?
A:考慮過,可是咱們團隊規模有限,很難快速落地。
Q:其餘語言有沒有接入spring cloud config?例如PHP和node? 開發人員對微服務進行開發時,是用dockercompose吧服務都起起來,仍是要接入公共的rancher?
A:咱們沒有使用spring-cloud-config,目前有接入層的node服務在使用consul下發配置。開發時本機跑本身的服務,連Rancher環境的Consul。
Q:關於領域事件,若是本地事務提交後,下游的服務報錯,是否只能在業務層面再發起一個補償的事件,讓本地事務達到最終一致性呢?
A:若是下游服務報錯,那麼事件不會被消費。會以退避重試的方式重發事件。
Q:分享很棒,請問大家的docker的部署是基於原生的docker和swarm,仍是kubernetes來作的?
A:謝謝,咱們使用Rancher來管理集羣。沒選Kubernetes的緣由是由於團隊資源有限,Swarm最初試過,調度不夠完善。後來Docker 1.12之後的Swarmkit應該是更好的選擇。
Q:微服務開發測試用例相比於單體應用是否是更復雜一些?大家是怎樣保證測試覆蓋率的?
A:事實上對於單元測試來說,反而更容易進行了。由於系統拆分以後,把原來很難測試的一些節點給疏通了。
Q:你好請教一下,當微服務之間的依賴關係比較多,且層次比較深時,服務的啓動,中止,以及升級之間的關係如何處理?
A:目前還幾乎沒出現過須要完全冷啓動的狀況。並且啓動服務時並不須要依賴服務也啓動,只須要發生業務時,依賴服務啓動便可。
Q:有個問題, zuul作api網關時如何配置consul呢? 文章中貌似沒交待清楚
A:在網關服務中經過@EnableZuulProxy來啓用Zuul反向代理consul中已註冊的服務。也能夠同時經過配置來自定義,詳見http://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/1.2.4.RELEASE/文檔中的Embedded Zuul Reverse Proxy章節。