[关闭]
@levinzhang 2021-01-16T16:04:02.000000Z 字数 10367 阅读 467

Spring Cloud的新时代

摘要

2020年12月,Spring在其博客站点正式发布2020.0.0版本(即Ilford),该版本移除了多项之前处于维护模式的Netflix组件,如Ribbon、Hystrix和Zuul。本文基于Spring Cloud的新组件,阐述了如何构建微服务架构。


本文最初发表于作者Piotr Mińkowski的个人站点,经作者许可由InfoQ中文站编译分享。

2020年12月22日,Spring在其博客站点正式发布2020.0.0版本(即Ilford),这是一个采用新命名规范的一个版本,但是更令人关注的是该版本移除了多项之前处于维护模式的Netflix组件,如Ribbon、Hystrix和Zuul。唯一剩余的模块是Netflix的服务发现服务器,即Eureka。这项变化对于Spring Cloud来说是很重要的,因为从诞生之初,Spring Cloud就是因为与Netflix组件的集成得到了认可。此外,Spring Cloud Netflix仍然是GitHub上最受欢迎的Spring Cloud项目(约有4200颗星)。

在将Netflix组件转入维护模式的同时,Spring 团队就已经开始着手准备替代方案了。因此,Ribbon将被Spring Cloud Load Balancer取代,Hystrix将会被建立在Resilience4J库之上的Spring Cloud Circuit Breaker取代。Spring Cloud Gateway是Zuul的竞争解决方案,目前已经是一个很受欢迎的项目,在Ilford版本之后,它将是API网关方面的唯一方案。

这篇文章的主要目的是指导你使用新的Spring Cloud组件构建微服务架构,替换废弃的Netflix项目。示例应用的源码可以在GitHub上的仓库中找到

架构

下面的图片展现了我们样例系统的架构。在这里,我们包含了微服务的特征元素,如API网关、服务发现服务器以及配置服务器。在本文接下来的内容中,我将会展示如何使用提供了这些模式的Spring Cloud组件。目前,向系统中添加API网关的主要组件是Spring Cloud Gateway

Spring Cloud提供了与多个可用作服务发现服务器的方案的集成,包括Netflix Eureka、HashiCorp Consul、Alibaba Nacos和Apache ZooKeeper。其中,最流行的是前两者。Spring Cloud Netflix Eureka专门用于服务发现,而Spring Cloud Consul可以同时通过Consul Services实现服务发现,通过Consul Key/Value引擎实现分布式跟踪特性。

Spring Cloud Config只负责提供一个配置管理的机制。但是,它也可以与第三方工具集成,如来自HashiCorp的Vault。我们将会以两个简单的Spring Boot应用callme-servicecaller-service为例阐述如何将我们的应用与服务发现和配置服务器进行集成。我们还会在caller-service上使用Spring Cloud Load Balancer启用客户端负载均衡,并使用基于Resilience4J构建的Spring Cloud Circuit Breaker启用断路器功能。

a-new-era-of-spring-cloud.png?resize=696%2C448未知大小

服务发现

借助Spring Cloud的DiscoveryClient抽象,在客户端切换不同的服务发现服务器非常容易。这种切换只需要替换Maven pom.xml文件中的一个依赖项。如果你想要使用Eureka的话,只需要添加如下的starter到微服务中。

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  4. </dependency>

而如果你想要使用Consul的话,那么需要添加如下的starter到微服务中。

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-consul-discovery</artifactId>
  4. </dependency>

如果你想要为服务发现的客户端定义一些非默认的配置的话,那情况就有点复杂了。在这种情况下,你需要使用特定于Eureka或Consul的属性。例如,如果你想要在同一个主机上运行同一个应用的多个实例,并启用动态HTTP服务器端口(选项server.port=0),那么你需要为每个实例设置一个唯一的id。如下是Eureka客户端中所使用的属性。

  1. eureka:
  2. instance:
  3. instanceId: ${spring.cloud.client.hostname}:${spring.application.name}:${random.value}

对于Consul客户端来说,相同的配置如下所示。

  1. spring:
  2. cloud:
  3. consul:
  4. discovery:
  5. instanceId: ${spring.cloud.client.hostname}:${spring.application.name}:${random.value}

在微服务架构中,我们可以很容易地通过Spring Cloud Netflix Eureka Server模块配置和运行Eureka服务发现。我们只需要创建包含该模块的Spring Boot应用即可。

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
  4. </dependency>

我们还需要为应用启用Eureka,只需在主类上添加@EnableEurekaServer注解。

  1. @SpringBootApplication
  2. @EnableEurekaServer
  3. class DiscoveryServerApplication
  4. fun main(args: Array<String>) {
  5. runApplication<DiscoveryServerApplication>(*args)
  6. }

在本地机器上运行Consul的最简便方式是使用它的Docker镜像。我们可以通过执行如下的命令以开发模式在Docker容器中运行Consul:

  1. $ docker run -d --name=consul -e CONSUL_BIND_INTERFACE=eth0 -p 8500:8500 consul:1.7.2

使用Spring cloud进行分布式配置

在我们的架构中,下一个重要的元素就是配置服务器。在Spring Cloud中,能够提供分布式配置机制的最流行方案就是Spring Cloud Config。Spring Cloud Config为分布式系统中的外部化配置提供了服务器端和客户端的支持。

借助配置服务器,我们能够有一个中心化的位置管理应用在所有环境下的外部属性。有一些其他的方案可以用作基于微服务的架构的配置服务器,如Consul、ZooKeeper和Alibaba Nacos。但是,严格意义上来讲,所有的这些方案都不是专门的分布式配置,它们也可以用作服务发现的服务器。Spring Cloud Config可能会集成不同的工具来存储数据。服务器存储后端的默认实现是使用git,但是我们也可以使用像HashiCorp Vault这样的工具来管理secret和保护敏感数据,也可以使用简单的文件系统。在一个配置服务器应用中,可能会将不同的后端组合在一起。我们只需要在应用属性文件中通过spring.profiles.active激活对应的profile即可。我们可以覆盖一些默认值,比如修改Vault服务器的地址或设置认证token。

  1. spring:
  2. application:
  3. name: config-server
  4. profiles:
  5. active: native,vault
  6. cloud:
  7. config:
  8. server:
  9. native:
  10. searchLocations: classpath:/config-repo
  11. vault:
  12. host: 192.168.99.100
  13. authentication: TOKEN
  14. token: spring-microservices-course

对于Consul来讲,同样如此,在开发模式下,我们应该使用Docker来运行Vault实例。我们可以使用环境变量VAULT_DEV_ROOT_TOKEN_ID设置一个静态的根token用于进行认证:

  1. $ docker run -d --name vault --cap-add=IPC_LOCK -e 'VAULT_DEV_ROOT_TOKEN_ID=spring-microservices-course' -p 8200:8200 vault:1.4.0

当Spring Cloud Config与服务发现一起使用的时候,我们可以在两种可用的方式之间做出选择,也就是Config First BootstrapDiscovery First Bootstrap。在Discovery First Bootstrap中,配置服务器会将自身注册到发现服务中。借助这一点,每个微服务都可以基于配置服务器的注册id找到它。

因为配置是在引导阶段注入的,我们需要使用bootstrap.yml在客户端设置属性。为了在客户端启用配置服务器的“发现”功能,我们需要将spring.cloud.config.discovery.enabled属性设置为true。如果配置服务器的注册服务id与自动配置的configserver(在我们的样例中是config-server)不同的话,我们还应该覆盖它。当然,我们还可以使用Consul作为配置属性源。

  1. spring:
  2. application:
  3. name: callme-service
  4. cloud:
  5. config:
  6. discovery:
  7. enabled: true
  8. serviceId: config-server
  9. consul:
  10. host: 192.168.99.100
  11. config:
  12. format: YAML

服务间通信

目前,有三种基于HTTP的Spring组件可以用于服务间的通信,它们都与服务发现进行了集成:同步的RestTemplate、反应式的WebClient以及声明式的REST客户端OpenFeign。RestTemplate组件可以通过Spring Web模块获取,WebClient可以通过Spring WebFlux模块获取。要包含Spring Cloud OpenFeign的话,我们需要一个专门的starter。

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-webflux</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.cloud</groupId>
  7. <artifactId>spring-cloud-starter-openfeign</artifactId>
  8. </dependency>

要使用RestTemplateWebClient进行支持服务发现的通信,我们需要注册bean并为它们添加@LoadBalanced注解。我们最好还要为这样的通信设置恰当的超时时间,当不使用断路器的时候,更应如此。

  1. @SpringBootApplication
  2. @EnableFeignClients
  3. class InterCallerServiceApplication {
  4. @Bean
  5. @LoadBalanced
  6. fun template(): RestTemplate = RestTemplateBuilder()
  7. .setReadTimeout(Duration.ofMillis(100))
  8. .setConnectTimeout(Duration.ofMillis(100))
  9. .build()
  10. @Bean
  11. @LoadBalanced
  12. fun clientBuilder(): WebClient.Builder {
  13. val tcpClient: TcpClient = TcpClient.create()
  14. .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 100)
  15. .doOnConnected { conn ->
  16. conn.addHandlerLast(ReadTimeoutHandler(100, TimeUnit.MILLISECONDS))
  17. }
  18. val connector = ReactorClientHttpConnector(HttpClient.from(tcpClient))
  19. return WebClient.builder().clientConnector(connector)
  20. }
  21. }

Spring Cloud LoadBalancer提供了自己的抽象和实现。为了实现负载均衡机制,Spring Cloud添加了ReactiveLoadBalancer接口,并支持基于Round-Robin和Random的实现。

目前,对于负载均衡的定制化并没有太多的选项。不过,选项之一就是配置客户端缓存的能力。默认情况下,每个客户端缓存目标服务的列表并且每隔30秒刷新一次。在你的场景下,这样的间隔可能会有些长。在配置中,我们可以很容易地修改它,如下面的样例,我们将其设置成了1秒。如果你的负载均衡器与Eureka服务发现进行集成的话,还需要减少获取注册表的时间间隔,默认它是30秒钟。在修改配置之后,这两个对客户端的变更能够几乎立即刷新当前运行的服务的列表。

  1. spring:
  2. cloud:
  3. loadbalancer:
  4. cache:
  5. ttl: 1s
  6. ribbon:
  7. enabled: false
  8. eureka:
  9. client:
  10. registryFetchIntervalSeconds: 1

断路器

断路器是微服务架构中一个很流行的设计模式。它被设计用来探测失败并封装阻止失败不断重复出现的逻辑。Spring Cloud提供了一个使用不同断路器的实现。针对Resilience4J,有两个实现,分别用于反应式应用和非反应式应用。要启用非反应式的实现,我们要包含如下的依赖。

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
  4. </dependency>

如下是负责注册Customizer bean的代码,这个bean配置了断路器的行为。

  1. @Bean
  2. fun defaultCustomizer(): Customizer<Resilience4JCircuitBreakerFactory> {
  3. return Customizer { factory: Resilience4JCircuitBreakerFactory ->
  4. factory.configureDefault { id: String? ->
  5. Resilience4JConfigBuilder(id)
  6. .timeLimiterConfig(TimeLimiterConfig.custom()
  7. .timeoutDuration(Duration.ofMillis(500))
  8. .build())
  9. .circuitBreakerConfig(CircuitBreakerConfig.custom()
  10. .slidingWindowSize(10)
  11. .failureRateThreshold(33.3F)
  12. .slowCallRateThreshold(33.3F)
  13. .build())
  14. .build()
  15. }
  16. }
  17. }

断路器的设置通过如下的图片进行了可视化。滑动窗口的大小设置了用于计算错误率的请求数。如果我们在大小为10的窗口中出现了3个以上的错误,那么断路器就会打开。

circuit-breaker.png?resize=696%2C237未知大小

下一步,我们需要使用Resilience4JCircuitBreakerFactory创建一个断路器实例并为HTTP客户端启用它,如下所示。

  1. @RestController
  2. @RequestMapping("/caller")
  3. class CallerController(private val template: RestTemplate, private val factory: Resilience4JCircuitBreakerFactory) {
  4. private var id: Int = 0
  5. @PostMapping("/random-send/{message}")
  6. fun randomSend(@PathVariable message: String): CallmeResponse? {
  7. val request = CallmeRequest(++id, message)
  8. val circuit = factory.create("random-circuit")
  9. return circuit.run { template.postForObject("http://inter-callme-service/callme/random-call",
  10. request, CallmeResponse::class.java) }
  11. }
  12. }

Spring Cloud API网关

在我们的微服务架构中,缺失的最后一个元素就是API网关。Spring Cloud Gateway能够帮助我们实现这一组件。目前,它是Spring Cloud中仅次于Spring Cloud Netflix,欢迎程度排在第二名项目。在GitHub上它有2800多颗星。它构建在Spring WebFlux和Reactor项目之上,它以反应式的方式运行,需要Netty作为运行时框架。

API网关的主要目标是提供一个有效的方式路由至API,从而为外部客户端隐藏微服务系统的复杂性,但是它也能解决一些安全性和可靠性相关的问题。用来配置Spring Cloud Gateway的主要组件是路由。

它由一个ID、一个目标URI、一个断言的集合和一个过滤器的集合组成。如果断言聚合为true的话,则会匹配路由。通过过滤器,我们则可以在发送下游请求之前或之后修改请求和响应。

通过预定义的网关过滤器集合,我们可以实现路径重写、速率限制、发现客户端、断路器、fallback或路由指标等机制。为了在网关上启用所有这些功能,我们首先需要包含以下依赖关系。

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-gateway</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-actuator</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.jetbrains.kotlin</groupId>
  11. <artifactId>kotlin-reflect</artifactId>
  12. </dependency>
  13. <dependency>
  14. <groupId>org.jetbrains.kotlin</groupId>
  15. <artifactId>kotlin-stdlib</artifactId>
  16. </dependency>
  17. <dependency>
  18. <groupId>org.springframework.cloud</groupId>
  19. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  20. </dependency>
  21. <dependency>
  22. <groupId>org.springframework.boot</groupId>
  23. <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
  24. </dependency>
  25. <dependency>
  26. <groupId>org.springframework.cloud</groupId>
  27. <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
  28. </dependency>

为了实现上述所列的所有特性,我们并不需要编写太多的代码。几乎所有的内容都是通过应用属性配置的。

  1. spring:
  2. application:
  3. name: api-gateway
  4. cloud:
  5. gateway:
  6. discovery:
  7. locator:
  8. enabled: true
  9. lowerCaseServiceId: true
  10. routes:
  11. - id: inter-callme-service
  12. uri: lb://inter-callme-service
  13. predicates:
  14. - Path=/api/callme/**
  15. filters:
  16. - RewritePath=/api(?/?.*), $\{path}
  17. - name: RequestRateLimiter
  18. args:
  19. redis-rate-limiter.replenishRate: 20
  20. redis-rate-limiter.burstCapacity: 40
  21. - name: CircuitBreaker
  22. args:
  23. name: sampleSlowCircuitBreaker
  24. fallbackUri: forward:/fallback/test
  25. - id: inter-caller-service
  26. uri: lb://inter-caller-service
  27. predicates:
  28. - Path=/api/caller/**
  29. filters:
  30. - StripPrefix=1
  31. - name: RequestRateLimiter
  32. args:
  33. redis-rate-limiter.replenishRate: 20
  34. redis-rate-limiter.burstCapacity: 40
  35. loadbalancer:
  36. ribbon:
  37. enabled: false
  38. redis:
  39. host: 192.168.99.100
  40. management:
  41. endpoints.web.exposure.include: '*'
  42. endpoint:
  43. health:
  44. show-details: always

有些设置依然需要在代码中进行配置,也就是断路器的配置,它基于Resilience4J项目,我们需要注册Customizer<reactiveresilience4jcircuitbreakerfactory></reactiveresilience4jcircuitbreakerfactory> bean。我们还需要定义一个速率限制的key,它用来设置为限制计数选择请求的策略。

  1. @SpringBootApplication
  2. class ApiGatewayApplication {
  3. @Bean
  4. fun keyResolver(): KeyResolver = KeyResolver { _ -> Mono.just("1") }
  5. @Bean
  6. fun defaultCustomizer(): Customizer<ReactiveResilience4JCircuitBreakerFactory> {
  7. return Customizer { factory: ReactiveResilience4JCircuitBreakerFactory ->
  8. factory.configureDefault { id: String? ->
  9. Resilience4JConfigBuilder(id)
  10. .timeLimiterConfig(TimeLimiterConfig.custom()
  11. .timeoutDuration(Duration.ofMillis(500))
  12. .build())
  13. .circuitBreakerConfig(CircuitBreakerConfig.custom()
  14. .slidingWindowSize(10)
  15. .failureRateThreshold(33.3F)
  16. .slowCallRateThreshold(33.3F)
  17. .build())
  18. .build()
  19. }
  20. }
  21. }
  22. }
  23. fun main(args: Array<String>) {
  24. runApplication<ApiGatewayApplication>(*args)
  25. }

结论

在本文中,我们快速了解了如何使用最新的Spring Cloud组件构建微服务架构。关于这些组件的更多详情,读者可以参阅作者在YouTube上发布的视频以及Spring Cloud的最新文档

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注