1. 程式人生 > >寫給大忙人的spring cloud 1.x學習指南

寫給大忙人的spring cloud 1.x學習指南

enter pla 聯系 logback 加載 bubuko request 線程 ride

這幾天抽空搞了下spring cloud 1.x(2.0目前應該來說還不成熟),因為之前項目中使用dubbo以及自研的rpc框架,所以總體下來還是比較順利,加上spring boot,不算筆記整理,三天不到一點圍繞spring boot reference和spring microservice in action主要章節都看完並完整的搭建了spring cloud環境,同時仔細的思考並解決了一些spring cloud和書籍作者理想化假設的問題,有些在網上和官方文檔中沒有明確的答案,比如spring cloud sleuth如何使用log4j 2同時保留MDC。本文還會列出spring cloud和dubbo的一些異同和各自優劣勢總結,兩者應該來說各有優劣勢,理想的架構如果各方面條件允許的話,其實可以結合spring cloud+dubbo或者自研的rpc。當然本文不會完完整整的講解spring cloud整個技術棧的詳細細節,但是對於核心要點以及關鍵的特性/邏輯組件會更多的評述組件的設計是否合理,如何解決,必要的會引述第三方資源,這和其他系列如出一轍。

在開始介紹spring cloud的架構之前,筆者打算先梳理下spring cloud生態的各個組件,因為對於很多新人甚至老鳥來說,初看起來,spring cloud的組件以及版本很亂,查看官方文檔https://projects.spring.io/spring-cloud/(http://cloud.spring.io/spring-cloud-static/Edgware.SR3/single/spring-cloud.html,註:spring 5.0出來之後,pdf就沒有了:(),我們可以發現如下:

技術分享圖片

相對於spring framework來說,spring cloud的組織更像是spring template各系列,獨立發展,除了核心部分外,幾乎各組件沒有關聯或者關聯性很弱,他們只是基於這個框架,除非應用架構需要其特性,否則都不需要關心這些組件。對於微服務架構(假設使用 spring cloud的rpc)來說,只有兩個必備:

  • spring boot。spring cloud基於spring boot打包方式,所以spring boot是必備的,後面我們會詳細講解spring boot,實際上spring boot中的很多特性跟spring boot毫無關系,純粹是設計者有意推廣而不放在spring context或者spring context support中。
  • spring cloud netflix。spring cloud netflix是netflix開源的一套微服務框架,它提供了微服務架構的核心功能,包括服務註冊和發現中心Eureka、客戶端負載均衡器Ribbon、聲明式RPC調用Feign、路由管理Zuul、熔斷和降級管理Hystrix。對於大部分的RPC框架來說,基本上都會提供除了熔斷和降級管理外的所有特性,比如dubbo(http://dubbo.apache.org/)以及筆者在之前公司自行研發的rpc框架(https://gitee.com/zhjh256/io-spider)。
所以,spring cloud可以說就是圍繞netflix的這套組件為主,其他都是輔助。

除了這兩個核心組件外,下列組件通常在大型系統中會一起使用(中小型系統可能不會采用):

  • spring cloud config。spring cloud config提供了集中式的配置管理中心,其存儲支持文件系統和git。
  • spring cloud sleuth/zipkin。spring cloud sleuth解決了分布式系統環境中日誌的上下文關聯和鏈路追蹤問題。
  • spring cloud consul。spring cloud consul提供了另一種服務中心、配置中心選擇。

在開始正式講解spring cloud前,還不得不提下spring cloud組件的版本,由於spring cloud組件眾多,且由不同的社區主導開發,因此spring cloud的版本命名跟eclipse類似,不是使用數字遞增,而是采用城市名命名,每個spring cloud包含一系列的組件,通過查看spring-cloud-dependencies maven構件的定義,我們知道各自的版本。例如Edgware.SR3版本依賴的各組件版本如下:

技術分享圖片

註:spring-cloud-dependencies是個應用一定會引入到dependencyManagement的依賴,它包含了特定版本的spring cloud組件的版本管理,直接引入可以省事很多。

從上述對spring cloud各組件的梳理,我們可以知道完整的spring cloud架構如下:

技術分享圖片

最簡的spring cloud架構如下:

技術分享圖片

現在,我們來看下spring cloud的主要組件的核心功能以及dubbo中對應的特性。

  • 在dubbo微服務框架內,ribbon/hystrix集成到了dubbo核心包中,turbine則在dubbo-admin和dubbo-monitor中。
  • zuul proxy就是網關AR(我們原來自研發的spider提供了該特性),這個組件在dubbo中沒有對應的實現。
  • spring cloud config是分布式配置中心,dubbo開源版沒有提供,阿裏內部有個供HSF使用的diamon配置中心。Spring Cloud Config有自帶的配置管理庫,也可以和開源項目集成,包括:Consul,Eureka,zk(後面我們會看到各配置中心的優劣勢)。 Spring Cloud Config其實是一個基於spring boot的REST應用,不是一個單獨第三方的服務器,可以嵌入在Spring Boot應用中,或者單獨啟動一個Spring Boot應用,其數據可以存儲在文件系統中或者git倉庫中。spring cloud配置中心的客戶端實現原理比較簡單,我們知道在spring框架中,是通過PlaceholderConfigurerSupport實現配置文件加載的,如果不使用spring cloud的配置中心,我們完全可以自己擴展PlaceholderConfigurerSupport,根據啟動參數,從遠程配置中心進行加載。
  • dubbo使用zk和dubbo-admin作為註冊和治理中心,所以spring cloud netflix eureka就相當於zk和dubbo-admin,spring cloud也集成了使用zk作為服務註冊和查找中心的組件。
  • 在dubbo中,如果需要鏈路跟蹤,我們需要自定義dubbo filter集成zipkin,dubbo自身沒有提供這個機制。在spring cloud中,提供了可集成zipkin的Spring Cloud Sleuth,它的其中一個特性是增加跟蹤ID到Slf4J MDC,這一點和筆者前面設計的日誌框架出入一轍,無法做到跨節點追蹤的集中日誌平臺都是耍流氓。
  • 在dubbo中,我們可以通過聲明式的@Reference註解來直接調用rpc服務,在spring cloud中,通過Feign,可以實現聲明式調用,對於微服務開發來說,提供聲明式的服務調用機制對開發效率是很重要的,它可以在編譯階段確保調用方和服務方接口一致。從技術實現來說,現代RESTFUL接口一般簽名上出入參都是對象,如果把controller同時當做接口來用的話,實現聲明式調用REST微服務也不是很難的事,關鍵是代碼實現上,我們需要在編寫controller接口的時候做些調整,跟service一樣,實現接口的方式,後面我們會詳細講到。
除此之外,同正常開發不一樣的點在於spring cloud依賴於spring boot,這是同標準化開發最大的區別,Spring Boot(實際上,spring boot本身簡單地說就是另一種開發時打包方式)被認為是spring cloud下微服務實現的核心技術。 在協議上,spring cloud是基於http的,這一點上會不會對延遲造成影響需要在評估下。 對spring cloud的大體介紹就到這裏,後面我們開始進入正文部分,spring cloud的學習。

=====================================

再重復一遍,spring cloud依賴於spring boot,所以不熟悉spring boot的同學,先掌握下spring boot,可參考筆者的寫給大忙人spring boot 1.x學習指南。

有些書籍一開始就講spring cloud config,有些書籍則幾乎可以認為把官方文檔翻譯一遍 ,官方文檔很多情況下對某些假設是很理想化的,所以,個人覺得有些時候就該有得放矢,不要追求大而全,很簡單的就不要大談特談了。

服務註冊與發現spring cloud netflix eureka

筆者建議不要急著動手,我們先來了解下服務中心的相關概念和優劣勢。 傳統基於ESB或者類DNS機制的服務路由機制 技術分享圖片

這也是早期主流自定義RPC框架的實現模式,它的最主要缺點是LB的單點失敗和擴展能力受限。在極高性能要求下,對於很多簡單頻繁調用的微服務來說,響應時間增加(因為網絡多了一個節點)。 基於服務註冊中心的服務路由機制 技術分享圖片

在這種情況下,服務發現層並不是微服務調用本身需要的,只是一個註冊和查找中心。避免了單點失敗和擴展性問題。 註:如果客戶端是使用http直接訪問的,而不是其他微服務來訪問,也就是本篇開頭的spring cloud架構圖。在這種架構中,zuul相當於智能化的nginx代理,只不過原生的nginx是下遊服務節點需要人工靜態配置,zuul增加了功能,和註冊中心保持聯系,使得每個微服務實例可以主動連接到註冊中心,發布本實例的服務列表,就可以感知到,從這一點上來說,在nginx上增加一個動態反向註冊的模塊是合理的,這樣的話,從完整的架構角度來說,zuul就不需要了。因為現在應用幾乎全部前端功能單獨運行在nginx裏面(除非完全以API方式對外提供服務),nginx幾乎在任何應用中都是必備的。所以,最佳的方式是nginx增加一個動態反向註冊的模塊,如果沒有能力,退而次之,就只能nginx+zuul。雖然服務可以規劃的比較好,也就是按照命名空間規劃,而且新增、合並命名空間的概率較低,但是對於高並發系統來說,增加或者減少不同服務的實例數量,這是一個比較常見的操作。而且,原生的nginx有太多的靜態配置,這也是比較詬病的。雖然可以要求前端區分API資源和非API資源,對於api直接訪問zuul對應的域名,哪怕通過二級域名,不過這在推動上可能遇到比較大的阻力,除非一開始就是這麽規劃的。 nginx動態upstream模塊的開發,可以參考https://www.jianshu.com/p/35b03c82f9fd,https://github.com/yzprofile/ngx_http_dyups_module以及https://github.com/weibocom/nginx-upsync-module Spring Cloud支持多種類型的服務註冊中心,Netflix Eureka是集成最緊密(親兒子)的,其次是consul(為什麽吐槽某些寫書的,其實這就是一例,沒見到那本書裏面講了使用consul作為服務中心,其它例子還很多),各種註冊中心的比較可參考https://luyiisme.github.io/2017/04/22/spring-cloud-service-discovery-products/,這裏引用如下: 技術分享圖片

現在來看下服務的註冊和調用。

Spring Cloud提供了三種調用微服務的方法:
  • DiscoveryClient,最底層
  • Ribbon-aware Spring RestTemplate,中間層
  • Netflix Feign,最抽象,也是最高效的 (註:我們一般自研rpc框架的時候,也是這個思路,不過一般是兩層,而不是三層)

由於在實際開發中,我們基本上使用Feign開發,所以,這裏我們重點看Feign方式的RPC調用。

在使用Feign聲明式調用前,開發者需要先定義一個接口,然後使用Spring Cloud的@FeignClient註解該接口,告訴Ribbon調用哪個Eureka服務,其實原理上就是根據接口動態生成代理,跟我們自定義RPC/dubbo的實現是一樣的原理,一般來說,該接口應該由服務方提供比較合適。可以像下面這樣定義:
package com.thoughtmechanix.org.api;

import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@FeignClient("organizationservice")
public interface OrganizationService {
    @RequestMapping(value="/v1/organizations/{organizationId}",method = RequestMethod.GET)
    public Organization getOrganization( @PathVariable("organizationId") String organizationId);
}

然後OrganizationService就可以被當做正常的spring bean使用了,如下:

package com.thoughtmechanix.licenses.controllers;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import com.thoughtmechanix.licenses.model.License;
import com.thoughtmechanix.org.api.Organization;
import com.thoughtmechanix.org.api.OrganizationService;

@RestController
public class LicenseServiceController implements LicenseService {
    
    private static final Logger logger = LoggerFactory.getLogger(LicenseServiceController.class);
    
    @Autowired
    private OrganizationService organizationService;

    @Override
@RequestMapping(value = "/v2/organizations/{organizationId}/licenses/{licenseId}", method = RequestMethod.GET)
public Organization getLicensesInterface(@PathVariable("organizationId")String organizationId, @PathVariable("licenseId")String licenseId) { logger.info("調用遠程Eureka服務!"); return organizationService.getOrganization(organizationId); } }
要啟用Feign客戶端,還需要在主應用類中加上@EnableFeignClients註解。
package com.thoughtmechanix.licenses;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;

@RefreshScope
@EnableFeignClients("com.thoughtmechanix.org.api")
@EnableEurekaClient
@SpringBootApplication
@EnableCircuitBreaker
@ComponentScan({"com.thoughtmechanix.licenses","com.thoughtmechanix.xyz.api"})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

註:這裏有個特殊點,Feign的接口掃描路徑定義在@EnableFeignClients註解的beanPackage屬性上,而不是@ComponentScan註解上,否則如果Feign的接口不在主應用類所在的包或者子包下,就在啟動時包bean找不到,如下所示:

Description:

Field organizationService in com.thoughtmechanix.licenses.controllers.LicenseServiceController required a bean of type ‘com.thoughtmechanix.org.api.OrganizationService‘ that could not be found.


Action:

Consider defining a bean of type ‘com.thoughtmechanix.org.api.OrganizationService‘ in your configuration.

[WARNING] 
java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.boot.maven.AbstractRunMojo$LaunchRunner.run(AbstractRunMojo.java:527)
	at java.lang.Thread.run(Thread.java:745)
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name ‘licenseServiceController‘: Unsatisfied dependency expressed through field ‘organizationService‘; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type ‘com.thoughtmechanix.org.api.OrganizationService‘ available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:588)
	at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:366)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1264)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
	at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:761)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543)
	at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:693)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:360)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:303)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1118)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1107)
	at com.thoughtmechanix.licenses.Application.main(Application.java:19)
	... 6 more
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type ‘com.thoughtmechanix.org.api.OrganizationService‘ available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1493)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1104)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1066)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:585)
	... 25 more

現在來調用http://localhost:8080/v2/organizations/yidooo/licenses/x0009999,如下: 技術分享圖片

通過為controller定義要實現的接口,就做到了一次定義,多次引用(這和我們使用傳統的spring mvc開發不同,建議把RequestMapper定義在接口上)。

所以,從使用上來說,Feign很簡單,對於有過其他RPC開發經驗的同學來說,就是換個註解而已。

熔斷、降級和服務隔離Netflix Hystrix

我記得dubbo和其他rpc在這一塊做的不是特別好,雖然spring cloud提供了該特性、而且很靈活,但是它有個關鍵設計很雞肋,後面會講到。

在spring cloud的微服務架構中,一個請求調用經過的節點內關鍵步驟如下:

技術分享圖片

Hystrix和Spring Cloud使用@HystrixCommand標記java方法由Hystrix電路中斷器管理,當spring框架看到@HystrixCommand註解的時候,它會為方法生成一個代理,並使用特定的線程池執行調用(這會導致個問題,就是log4j的MDC是基於線程上下文的)。需要註意的是,整個被註解的方法都會由Hystrix電路中斷器管理,默認情況下,只要執行超過1秒就會觸發com.nextflix.hystrix.exception.HystrixRuntimeException異常,哪怕該方法內部調用了其他很多RPC服務,實際上控制應該放在調用具體遠程eureka服務上,而不是整個本地方法上(這個是實現不合理的,註:通過為每個遠程服務包裝一個本地代理調用,實現細粒度控制,這估計得靠代理來實現,不能一個個手寫)。同時,默認情況下,由@HystrixCommand標記的所有方法都會在一個線程池中執行,這也是不合理的(雖然為不同的遠程服務定義不同的線程池)。 @HystrixCommand註解定義了很多屬性控制其行為和線程池等,其中commandProperties屬性控制電路中斷器的行為,具體可見javadoc(https://netflix.github.io/Hystrix/javadoc/com/netflix/hystrix/HystrixCommand.html)。 服務不可用回調,@HystrixCommand的fallbackMethod屬性用來設置如果調用Hystrix失敗,回調本類的哪個方法,回調方法和原方法的簽名必須相同,因為調用回調方法時會把所有參數都傳遞過去。 Hystrix使用線程池執行所有遠程調用服務,默認情況下,這個線程池有10個線程,任何遠程調用都會放在這個線程池中執行,包括jdbc,只要是遠程調用都算(它的原理或者判斷依據是???),如下所示: 技術分享圖片

合理的隔離機制應該是可以自定義線程池數量,以及哪些服務放在哪個線程池。如下:

技術分享圖片

自然,Hystrix提供了按需配置線程池的接口。@HystrixCommand註解的threadPoolKey和threadPoolProperties屬性就是用來指定線程池的,包括線程池名稱、大小、隊列長度(就線程池而言,最重要的就是名稱,核心大小,最大大小,隊列長度)。如下:

@HystrixCommand(fallbackMethod = "buildFallbackLicenseList",
          threadPoolKey
= "licenseByOrgThreadPool",
          threadPoolProperties
= {
            @HystrixProperty(name
= "coreSize",value="30"),
            @HystrixProperty(name
="maxQueueSize", value="10")
          })
public List<License> getLicensesByOrg(String organizationId){   return licenseRepository.findByOrganizationId(organizationId);
)
註:默認情況下,線程池任務隊列使用的數據結構是LinkedBlockingQueue,因為任務通常是先進先出的策略。 因為@HystrixCommand標記的方法在另外的線程中執行,這意味著默認情況下ThreadLocal中的值在@HystrixCommand標記的方法內是取不到值的。所以,Hystrix和Spring Cloud提供了一種機制來使得可以將父線程中的上下文傳遞給Hystrix Thread pool。這種機制稱為HystrixConcurrencyStrategy。HystrixConcurrencyStrategy是一個抽象類,通過實現它可以將父線程上下文中的信息註入到Hystrix管理的線程中,它包括三個步驟:
  1. 自定義Hystrix Concurrency Strategy類
  2. 定義一個Callable類,將UserContext註入Hystrix Command
  3. 配置Spring Cloud使用自定義的Hystrix Concurrency Strategy類
實現細節可以參考Spring microservice in action 5.9.2 The HystrixConcurrencyStrategy in action。 需要註意的是:spring cloud內置了HystrixConcurrencyStrategy處理spring secuiry相關問題,各HystrixConcurrencyStrategy之間是過濾器鏈設計模式,所以實現自定義HystrixConcurrencyStrategy的時候,需要確保調用了已存在的HystrixConcurrencyStrategy,這和我們自定義過濾器或者框架相關的生命周期事件的模式一樣。

線路熔斷

默認情況下,雖然可以配置超過多少時間之後,服務拋異常,但是很多時候,如果目標系統非常忙了,這個時候請求在源源不斷的發過去是沒有意義的,只會讓目標系統更慢,此時我們需要一些更加靈活或者智能的機制來確定什麽時候不在調用到目標物理地址的服務。Hystrix的默認計算規則為: 技術分享圖片

控制熔斷的行為是在@HystrixCommand註解的commandPoolProperties屬性中設置,其中包含的是@HystrixProperty屬性數組。Hystrix的完整參數列表可見https://github.com/Netflix/Hystrix/wiki/Configuration。

服務路由zuul

為什麽使用路由以及是否需要使用路由的原因,在上面已經講過了,所以,這裏還是看下核心部分。其在spring cloud架構中的角色如下: 技術分享圖片

通過添加spring-cloud-starter-zuul依賴以及在主應用類中添加@EnableZuulProxy註解就可以啟用zuul服務器功能。添加依賴:
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
@SpringBootApplication
@EnableZuulProxy
public class ZuulServerApplication {
  public static void main(String[] args) {
    SpringApplication.run(ZuulServerApplication.class,args);
  }
}
註:除了EnableZuulProxy註解外,還有一個@EnableZuulServer註解,該註解會創建Zuul Server,但是不會加載任何Zuul反向代理過濾器,也不會使用Netflix Eureka作為服務中心,它主要用於自建路由服務,所以一般情況下不需要。 Zuul設計為默認使用Spring Cloud的其他服務,所以,默認使用Eureka作為服務中心,Netflix Ribbon作為客戶端負載均衡代理。連接eureka服務中心的配置都一樣。如下:
eureka:
  instance:
    preferIpAddress: true
  client:
    registerWithEureka: true
    fetchRegistry: true
  serviceUrl:
    defaultZone: http://localhost:8761/eureka/
這樣,運行maven spring-boot:run之後,就可以啟動Zuul了。

Zuul路由配置

Zuul支持三種類型的路由機制:
  • 基於服務中心自動路由(大規模使用)
  • 使用服務中心手工路由(A/B測試使用)
  • 根據靜態url路由(歷史兼容使用)
Zuul所有的路由映射都維護在application.yml文件中,默認情況下,不需要做任何配置,Zuul會自動使用Eureka中的服務ID尋找下遊的實例,這也是dubbo的做法。需要註意的是,實際上Zuul粒度比較粗,是根據應用名而不是嚴格意義上的服務來區別的。例如: http://localhost:5555/organizationservice/v1/organizations/e254f8c-c442-4ebea82a- e2fc1d1ff78a 是根據organizationservice來判斷下遊服務,而不是通過具體的API來判斷的,這種情況下,如果兩個應用相同名稱,但是提供的服務有差別,就坑了。 訪問Zuul服務器的/routes服務可以查看所有的服務。如下所示: 技術分享圖片

Zuul的真正強大在於過濾器,可以用來設置全局請求ID,因為spring cloud使用http協議,所以這是可以做到的,只要在http head中進行註入即可。也可以設置路由過濾器,不僅僅根據路由定義來決定路由,比如說灰度升級或者A/B測試(https://baike.baidu.com/item/AB%E6%B5%8B%E8%AF%95/9231223)的時候,就可以使用路由過濾器通過某個請求參數來判斷應該路由到什麽節點,比如說某個區域、某個級別的客戶等等。為了做到應用代碼一致性,我們需要根據上下文而不是服務名稱來確定路由邏輯。在zuul中,通過繼承com.netflix.zuul.ZuulFilter,可以實現自定義過濾器。 不過動態路由除了zuul應該提供外,ribbon也應該提供,因為通常來說,內部服務也會相互調用,思路可以參考https://github.com/jmnarloch/ribbon-discovery-filter-spring-cloud-starter。

分布式日誌聚合Spring Cloud Sleuth

日誌系統的架構以及與ELK的集成可以參考寫給大忙人的CentOS 7下最新版(6.2.4)ELK+Filebeat+Log4j日誌集成環境搭建完整指南、寫給大忙人的ELK最新版6.2.4學習筆記-Logstash和Filebeat解析(java異常堆棧下多行日誌配置支持)以及寫給大忙人spring boot 1.x學習指南的日誌部分,就可以搭建最完善、實用的分布式架構日誌體系。

Spring Cloud Sleuth是一個Spring Cloud項目的一部分,它可以在http調用上設置相關ID,同時提供了鉤子將跟蹤數據發往OpenZipkin。這是通過Zuul過濾器以及其他spring組件使得相關ID可以在系統調用見透傳。 Zipkin主要用來鏈路跟蹤,以及各節點之間服務調用的性能分析。 具體實現上,可以使用Zuul過濾器檢查HTTP請求,如果請求中沒有相關ID,就可以註入進去。相關ID存在之後,就可以使用自定義的Spring HTTP過濾器將相關ID註入到自定義的UserContext對象,然後將相關ID增加到Spring’s Mapped Diagnostic Context (MDC),也可以編寫一個Spring Interceptor將相關ID通過HTTP頭傳遞出去,這兩種應該來說都可以實現,其實最主要是他們使用了HTTP協議。同時確保Hystrix線程上下文能夠感知到。上述這些特性就是Spring Cloud Sleuth提供的主要功能:
  • 透明在服務調用上創建和註入相關ID(dubbo沒有提供現成的功能,需要自行基於dubbo filter實現)
  • 在服務調用之間透傳相關ID
  • 增加相關ID到Spring’s MDC,Spring Boots的默認SL4J和Logback會自動包含相關ID,log4j則不會自動包含(參考本博客spring boot系列的日誌部分)。
  • 可選的,發布跟蹤信息到Zipkin

要啟用Spring Cloud sleuth,只要在pom文件中包含下列依賴即可:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
Spring Cloud Sleuth信息分為4段組成,如下:
  • 服務的應用名
  • 全局跟蹤ID
  • 當前請求段ID
  • 是否發送到zipkin的標記

技術分享圖片

技術分享圖片

通過將日誌聚合到ELK,我們就可以很方便的進行搜索了。 Spring Cloud Sleuth和ELK的整合還是比較簡單的,因為目前ELK框架推薦在應用端部署Filebeat的方式,只要Filebeat配置好格式即可。參考ELK最新版6.2.4學習筆記-Logstash和Filebeat解析(java異常堆棧下多行日誌配置支持)。 http://cloud.spring.io/spring-cloud-static/spring-cloud-sleuth/1.3.3.RELEASE/single/spring-cloud-sleuth.html zipkin自身包含spring boot版本的可執行包下載,也可以自己創建一個spring boot應用,對於zipkin而言,唯一需要考慮的是數據存儲在哪裏,默認情況下數據存儲在內存中,這意味著如果zipkin重啟了,之前的監控數據就沒有了。zipkin目前支持mysql、Cassandra以及Elasticsearch,因為ELK數據存儲已經在ES中了,所以也建議配置到ES中,只要劃一個index出來給zipkin就可以了,zipkin配置ES存儲可以參考https://github.com/openzipkin/zipkin/tree/master/zipkin-server。 因為zipkin的數據總體來說是用來分析性能的,所以Zipkin默認會將1/10的數據寫到Zipkin服務器,配置參數spring.sleuth.sampler.percentage可以用來控制發送的比例,取值為0-1之間。 最後我們來看下分布式配置的使用,其原理我們在前面已經講過了,就不重復闡述。

分布式配置中心spring cloud config

spring cloud config和spring cloud eureka一樣,也是一個spring boot應用,只要添加maven依賴以及在主應用類上添加@EnableConfigServer註解即可,如下:
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
package com.thoughtmechanix.confsvr;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}

然後在application.yml中設置存儲信息,如下:

server:
  port: 8888
spring:
  profiles:
    active: native
  cloud:
    config:
      server:
        native:
          searchLocations: file:///D:/spring-cloud-example/config/

這樣運行spring-boot:run就可以啟動配置中心服務了。

註意,這裏需要註意點的是,路徑大小寫敏感,否則可能出現一直訪問不到配置文件,但是沒有報錯信息。

D:/spring-cloud-example/config/下包含如下配置文件:

技術分享圖片

tracer.property: "I AM THE DEFAULT FROM CONFIG CENTER"
spring.jpa.database: "POSTGRESQL"
spring.datasource.platform: "postgres"
spring.jpa.show-sql: "true"
spring.database.driverClassName: "org.postgresql.Driver"
spring.datasource.url: "jdbc:postgresql://database:5432/eagle_eye_local"
spring.datasource.username: "postgres"

我們可以使用postman訪問如下:

技術分享圖片

這樣,基於文件存儲的配置中心就搭建好了。

目前,spring cloud config支持使用文件系統和git作為存儲,git的配置可以參考官方文檔。

由於存在profile、label等概念,因此配置中心http請求地址和資源文件之間存在一個映射關系(如果請求的時候訪問不到的時候可以幫助排查),如下:
  • /{application}/{profile}[/{label}]
  • /{application}-{profile}.yml
  • /{label}/{application}-{profile}.yml
  • /{application}-{profile}.properties
  • /{label}/{application}-{profile}.properties
對於非git存儲而言,label不存在。這一點通過訪問配置中心資源可以看出,見上圖。 要配置客戶端使用配置中心,只要加上依賴即可:
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>

因為所有的配置信息都不在本地,所以我們需要一種機制告訴spring boot去哪裏找配置中心,因此spring boot提供了一個bootstrap.yml配置文件,其中定義了使用哪個應用、哪個profile的配置,以及服務器地址。如下所示:

spring:
 application:
   name: licensingservice
 profiles:
  active: default
 cloud:
   config:
     uri: http://localhost:8888

在spring boot應用啟動的時候,在執行任何bean的初始化前,會先加載bootstrap.yml文件,讀取配置,然後再進行其他初始化和加載工作。

這樣配置中心的配置就和原來properties中一樣,被加載到Environment中了,@Value就可以正常註入了。

雖然Spring Cloud配置中心能夠感知底層數據源的修改,無論是否直接修改了底層文件系統或者git倉庫的配置值,Spring Cloud配置中心總是可以獲取最新的值。但是Spring Boot應用則是在啟動時讀取配置的,這意味著默認情況下,對配置中心的修改不會通知spring boot應用,哪怕spring boot應用需要知道配置變更的時候也如此,比如說log4j2配置臨時調整。不過Spring Boot Actuator(https://docs.spring.io/spring-boot/docs/1.5.7.RELEASE/reference/htmlsingle/#production-ready)提供了一個@RefreshScope註解允許spring boot應用訪問/refresh來強制重新讀取配置,不過這對於@Value直接註入來說是合理的,但是對於一些spring boot啟動時就已經註入並且創建單例了的情況,比如jdbc/redis配置等就不適用了。只要在spring主應用類上增加@RefreshScope註解即可,如下:
@SpringBootApplication 
@RefreshScope 
public class Application { 
  public static void main(String[] args) { 
    SpringApplication.run(Application.class, args); 
  } 
}
同時在配置文件application.yml中增加
management:
 security:
  enabled: false
否則,spring boot 1.5.x版本在執行/refresh的時候會報401,"error":"Unauthorized","message":"Full authentication is required to access this resource." 執行post /refresh,查看spring boot日誌: 2018-06-13 09:29:23.490 INFO 5288 --- [nio-8080-exec-5] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at: http://localhost:8888 2018-06-13 09:29:24.953 INFO 5288 --- [nio-8080-exec-5] c.c.c.ConfigServicePropertySourceLocator : Located environment: name=licensingservice, profiles=[default], label=null, version=null, state=null 2018-06-13 09:29:24.954 INFO 5288 --- [nio-8080-exec-5] b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource [name=‘configService‘, propertySources=[MapPropertySource {name=‘file:///D:/spring-cloud-example/config/licensingservice.yml‘}]] 2018-06-13 09:29:24.956 INFO 5288 --- [nio-8080-exec-5] o.s.boot.SpringApplication : The following profiles are active: default spring cloud配置中心存在一個問題是沒有提供原生的配置變更發布功能,雖然提供了Spring Cloud Bus模塊來基於MQ發布配置變更,但這引入了額外的維護工作。可以說zk最有價值的特性之一就是根據特定節點訂閱變更、刪除、其下節點變更的事件。 參考: spring microservice in action http://cloud.spring.io/spring-cloud-static/Edgware.SR3/index.html 待下一系列補充、完善的要點: Eureka 服務中心高可用 Spring Cloud Config高可用 配置中心推送機制 Spring Cloud使用Consul作為服務中心

寫給大忙人的spring cloud 1.x學習指南