写给大忙人的spring cloud 1.x学习指南

时间:2022-11-18 01:07:27

这几天抽空搞了下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 cloud 1.x学习指南

相对于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(Feign走的是HTTP协议,如果协议能够优化到和主流rpc如dubbo/grpc一样的序列化及延时,抑或在15%甚至20%以内,其实走http还是完全可以接受的)、路由管理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 1.x学习指南

注:spring-cloud-dependencies是个应用一定会引入到dependencyManagement的依赖,它包含了特定版本的spring cloud组件的版本管理,直接引入可以省事很多。

从上述对spring cloud各组件的梳理,我们可以知道完整的spring cloud架构如下:

写给大忙人的spring cloud 1.x学习指南

最简的spring cloud架构如下:

写给大忙人的spring cloud 1.x学习指南

现在,我们来看下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机制的服务路由机制
写给大忙人的spring cloud 1.x学习指南
这也是早期主流自定义RPC框架的实现模式,它的最主要缺点是LB的单点失败和扩展能力受限。在极高性能要求下,对于很多简单频繁调用的微服务来说,响应时间增加(因为网络多了一个节点)。
基于服务注册中心的服务路由机制
写给大忙人的spring cloud 1.x学习指南
在这种情况下,服务发现层并不是微服务调用本身需要的,只是一个注册和查找中心。避免了单点失败和扩展性问题。
注:如果客户端是使用http直接访问的,而不是其他微服务来访问,也就是本篇开头的spring cloud架构图。在这种架构中,zuul相当于智能化的nginx代理,只不过原生的nginx是下游服务节点需要人工静态配置,zuul增加了功能,和注册中心保持联系,使得每个微服务实例可以主动连接到注册中心,发布本实例的服务列表,就可以感知到,从这一点上来说,在nginx上增加一个动态反向注册的模块是合理的,这样的话,从完整的架构角度来说,zuul就不需要了。因为现在应用几乎全部前端功能单独运行在nginx里面(除非完全以API方式对外提供服务),nginx几乎在任何应用中都是必备的。所以,最佳的方式是nginx增加一个动态反向注册的模块,如果没有能力,退而次之,就只能nginx+zuul。虽然服务可以规划的比较好,也就是按照命名空间规划,而且新增、合并命名空间的概率较低,但是对于高并发系统来说,增加或者减少不同服务的实例数量,这是一个比较常见的操作。而且,原生的nginx有太多的静态配置,这也是比较诟病的。虽然可以要求前端区分API资源和非API资源,对于api直接访问zuul对应的域名,哪怕通过二级域名,不过这在推动上可能遇到比较大的阻力,除非一开始就是这么规划的。
Spring Cloud支持多种类型的服务注册中心,Netflix Eureka是集成最紧密(亲儿子)的,其次是consul(为什么吐槽某些写书的,其实这就是一例,没见到那本书里面讲了使用consul作为服务中心,其它例子还很多),各种注册中心的比较可参考https://luyiisme.github.io/2017/04/22/spring-cloud-service-discovery-products/,这里引用如下:
写给大忙人的spring cloud 1.x学习指南

现在来看下服务的注册和调用。

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,如下:
写给大忙人的spring cloud 1.x学习指南

通过为controller定义要实现的接口,就做到了一次定义,多次引用(这和我们使用传统的spring mvc开发不同,建议把RequestMapper定义在接口上)。

所以,从使用上来说,Feign很简单,对于有过其他RPC开发经验的同学来说,就是换个注解而已。

熔断、降级和服务隔离Netflix Hystrix

我记得dubbo和其他rpc在这一块做的不是特别好,虽然spring cloud提供了该特性、而且很灵活,但是它有个关键设计很鸡肋,后面会讲到。

在spring cloud的微服务架构中,一个请求调用经过的节点内关键步骤如下:

写给大忙人的spring cloud 1.x学习指南

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,只要是远程调用都算(它的原理或者判断依据是???),如下所示:
写给大忙人的spring cloud 1.x学习指南

合理的隔离机制应该是可以自定义线程池数量,以及哪些服务放在哪个线程池。如下:

写给大忙人的spring cloud 1.x学习指南

自然,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的默认计算规则为:
写给大忙人的spring cloud 1.x学习指南
控制熔断的行为是在@HystrixCommand注解的commandPoolProperties属性中设置,其中包含的是@HystrixProperty属性数组。Hystrix的完整参数列表可见https://github.com/Netflix/Hystrix/wiki/Configuration
 

服务路由zuul

为什么使用路由以及是否需要使用路由的原因,在上面已经讲过了,所以,这里还是看下核心部分。其在spring cloud架构中的角色如下:
写给大忙人的spring cloud 1.x学习指南
通过添加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粒度比较粗,是根据应用名而不是严格意义上的服务来区别的。例如:
是根据organizationservice来判断下游服务,而不是通过具体的API来判断的,这种情况下,如果两个应用相同名称,但是提供的服务有差别,就坑了。
访问Zuul服务器的/routes服务可以查看所有的服务。如下所示:
写给大忙人的spring cloud 1.x学习指南
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

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的标记

写给大忙人的spring cloud 1.x学习指南

写给大忙人的spring cloud 1.x学习指南

通过将日志聚合到ELK,我们就可以很方便的进行搜索了。
Spring Cloud Sleuth和ELK的整合还是比较简单的,因为目前ELK框架推荐在应用端部署Filebeat的方式,只要Filebeat配置好格式即可。参考ELK最新版6.2.4学习笔记-Logstash和Filebeat解析(java异常堆栈下多行日志配置支持)
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/下包含如下配置文件:

写给大忙人的spring cloud 1.x学习指南

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 1.x学习指南

这样,基于文件存储的配置中心就搭建好了。

目前,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 boot admin,相当于spring boot版的jvisualvm类似了,参见https://github.com/codecentric/spring-boot-admin。
 
参考:
spring microservice in action
http://cloud.spring.io/spring-cloud-static/Edgware.SR3/index.html
https://mp.weixin.qq.com/s?__biz=MzIwMzg1ODcwMw==&mid=2247487921&idx=1&sn=1a1d9864e58fbea3f07c98d447b90e44&chksm=96c9a7d1a1be2ec752882590a53c37d59bb291db73187f097702235c606da9feddd6cb9990a4&scene=27#wechat_redirect
 
待下一系列补充、完善的要点:
Eureka 服务中心高可用
Spring Cloud Config高可用
配置中心推送机制
Spring Cloud使用Consul作为服务中心