vlambda博客
学习文章列表

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

Chapter 8. Circuit Breakers and Security

在上一章中,我们配置了将在我们的基础设施中运行的微服务,并且我们创建了一个 Eureka 服务器来作为我们解决方案的服务发现。此外,我们创建了一个配置服务器应用程序,它将作为我们微服务的配置。

在本章中,我们将创建微服务来与我们之前的基础设施进行交互。我们将了解如何为我们的业务微服务应用服务发现功能,并了解断路器模式如何帮助我们为应用程序带来弹性。 

在本章中,我们将了解微服务如何通过 Spring WebFlux 客户端支持的 HTTP 异步调用与其他服务进行通信。

在本章结束时,我们将学会如何:

  • Connect microservices with service discovery
  • Pull the configuration from the configuration server
  • Understand how Hystrix brings resilience to microservices
  • Show the Edge API strategy
  • Present the Spring Boot Admin

Understanding the service discovery power


我们将根据业务需求创建我们的第一个 微服务。我们将创建一个 planes 微服务,它将维护有关公司飞机的数据,例如特征、模型和其他一些属性。

planes 微服务将用于为我们的第二个微服务 flights 微服务提供平面特征。它需要获取一些飞机信息才能创建航班,例如座位数。

planes 微服务是一个很好的起点 因为不需要创建与业务相关的依赖项。

我们的 planes 微服务很快就会派上用场。是时候创造它了。我们走吧。

Creating the planes microservice

正如我们在 previous 章节中所做的那样,我们将为此使用 Spring Initializr。应选择以下依赖项,如以下屏幕截图所示:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

有一些必要的依赖关系。 Stream Binder RabbitSleuth Stream 依赖项是必要的,以使我们能够发送数据跨度,并启用跨 RabbitMQ 消息代理的应用程序跟踪。我们将使用 MongoDB 作为这个特定应用程序的数据库,因此我们需要 Reactive MongoDBConfig Client 对于解决方案中存在的所有微服务都是必需的。我们不会在类路径上进行任何应用程序配置。 Actuator 提供生产就绪的指标和有关正在运行的应用程序的信息;它是微服务架构风格的基本特征。此外, Zuul 对于让我们将应用程序与 Edge API 连接起来至关重要。我们将在本章的课程中了解更多有关它的信息。

我们现在可以按 Generate Project按钮下载项目。在 IDE 上打开项目。

planes 微服务将使用 Spring Boot 2 框架创建,因为我们有兴趣为我们的平面服务实现反应式基础。

此外,我们还需要包含一个依赖项,可以使用 pom.xml 中的以下代码段来完成:

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

spring-cloud-starter-netflix-eureka-client 启用服务发现,由我们应用程序中的 Eureka 服务器提供支持。

Coding the planes microservice

我们将在应用程序上添加一些功能。对于这个特定的 应用程序,我们将使用Spring Reactive WebFlux 创建CRUD 功能。

 Plane 类代表我们微服务中的平面模型,类应该是这样的:

package springfive.airline.airlineplanes.domain;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import java.util.Set;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import springfive.airline.airlineplanes.resource.data.PlaneRequest;

@Data
@Document(collection = "planes")
@JsonInclude(Include.NON_NULL)
public class Plane {

@Id
String id;

String owner;

PlaneModel model;

Set<Seat> seats;

String notes;

@Builder
public static Plane newPlane(String owner,PlaneModel planeModel,Set<Seat> seats,String notes){
    Plane plane = new Plane();
plane.owner = owner;
plane.model = planeModel;
plane.seats = seats;
plane.notes = notes;
    return plane;
}

public Plane fromPlaneRequest(@NonNull PlaneRequest planeRequest){
this.owner = planeRequest.getOwner();
    this.model = planeRequest.getModel();
    this.seats = planeRequest.getSeats();
    this.notes = planeRequest.getNotes();
    return this;
}

}

有趣的是 @Document 注释。它使我们能够配置的名称 我们域的 MongoDB 集合。 @Builder 注释使用带注释的方法创建 Builder 模式的实现。  Project Lombok 库提供 this 功能(https://projectlombok.org)。此外,该项目还有一些令人兴奋的功能,例如 @Data,它 创建了getters/settersequalshashCode 自动为带注释的类实现。

我们可以看到,这个类中有一些 domain 模型。这些模型在这里不需要解释,完整的源代码可以在GitHub项目中找到 https://github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter08/airline-planes

The reactive repository

我们的 Plane 类需要一个 repository 来将数据持久化到数据库中。我们将使用 Spring Reactive MongoDB 实现为 MongoDB 提供的响应式存储库。我们将使用 ReactiveCrudRepository 因为它使我们的存储库具有反应性。我们的存储库应该是这样的:

package springfive.airline.airlineplanes.repository;

import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import springfive.airline.airlineplanes.domain.Plane;

public interface PlaneRepository extends ReactiveCrudRepository<Plane,String>{
}

除了新的响应式接口外,实现与之前的 Spring Data 版本相同。现在,我们可以在下一节中创建我们的服务层。

Creating the Plane service

我们的 PlaneServiceresponsible 在code class="literal">PlaneRepository 和 PlaneResource;后一个我们将在下一节中创建。实现应该是这样的:

package springfive.airline.airlineplanes.service;

import lombok.NonNull;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import springfive.airline.airlineplanes.domain.Plane;
import springfive.airline.airlineplanes.repository.PlaneRepository;
import springfive.airline.airlineplanes.resource.data.PlaneRequest;

@Service
public class PlaneService {

private final PlaneRepository planeRepository;

  public PlaneService(PlaneRepository planeRepository) {
this.planeRepository = planeRepository;
}

public Flux<Plane> planes(){
return this.planeRepository.findAll();
}

public Mono<Plane> plane(@NonNull String id){
return this.planeRepository.findById(id);
}

public Mono<Void> deletePlane(@NonNull Plane plane){
return this.planeRepository.delete(plane);
}

public Mono<Plane> create(@NonNull PlaneRequest planeRequest){
final Plane plane = Plane.builder().owner(planeRequest.getOwner())
        .planeModel(planeRequest.getModel()).seats(planeRequest.getSeats())
        .notes(planeRequest.getNotes()).build();
    return this.planeRepository.save(plane);
}

public Mono<Plane> update(@NonNull String id,@NonNull PlaneRequest planeRequest){
return this.planeRepository.findById(id)
        .flatMap(plane -> Mono.just(plane.fromPlaneRequest(planeRequest)))
        .flatMap(this.planeRepository::save);
}

}

 

 

这个类没有什么特别的,PlaneService会调用PlaneRepository来持久化数据库中的平面。正如我们所见,我们已经广泛使用了 lambda。 Java 8 是运行 Spring Boot 2 应用程序的要求。

看看 Builder 模式如何使我们能够编写干净的代码。阅读这段代码要容易得多;我们使用 Lombok 提供的 chaining 方法来实现。

The REST layer

我们将使用 Spring WebFlux 来公开我们的 REST 端点,然后我们需要在我们的方法中返回 MonoFlux。 REST 实现 应该是这样的:

package springfive.airline.airlineplanes.resource;

import java.net.URI;
import javax.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import springfive.airline.airlineplanes.domain.Plane;
import springfive.airline.airlineplanes.resource.data.PlaneRequest;
import springfive.airline.airlineplanes.service.PlaneService;

@RestController
@RequestMapping("/planes")
public class PlaneResource {

private final PlaneService planeService;

  public PlaneResource(PlaneService planeService) {
this.planeService = planeService;
}

@GetMapping
public Flux<Plane> planes() {
return this.planeService.planes();
}

@GetMapping("/{id}")
public Mono<ResponseEntity<Plane>> plane(@PathVariable("id") String id) {
return this.planeService.plane(id).map(ResponseEntity::ok)
        .defaultIfEmpty(ResponseEntity.notFound().build());
}

@PostMapping
public Mono<ResponseEntity<Void>> newPlane(
@Valid @RequestBody PlaneRequest planeRequest, UriComponentsBuilder uriBuilder) {
return this.planeService.create(planeRequest).map(data -> {
URI location = uriBuilder.path("/planes/{id}")
          .buildAndExpand(data.getId())
          .toUri();
      return ResponseEntity.created(location).build();
});
}

@DeleteMapping("/{id}")
public Mono<ResponseEntity<Object>> deletePlane(@PathVariable("id") String id) {
return this.planeService.plane(id).flatMap(data -> this.planeService.deletePlane(data)
        .then(Mono.just(ResponseEntity.noContent().build())))
        .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}

@PutMapping("/{id}")
public Mono<ResponseEntity<Object>> updatePlane(@PathVariable("id") String id,@Valid @RequestBody PlaneRequest planeRequest) {
return this.planeService.update(id,planeRequest)
        .then(Mono.just(ResponseEntity.ok().build()));
}

}

看看 plane 方法。当 planeService.plane(id) 返回空的 Mono 时,REST 端点将返回 notFound,如下实现: <代码类="literal">ResponseEntity.notFound().build()。它使代码非常容易理解。

newPlane 方法中,我们将返回带有最近创建的新实体 ID 的 location HTTP 标头。

Running the plane microservice

在我们运行平面微服务之前,我们将创建 plane 微服务的main 类。 负责 启动应用程序。为此,我们需要包含几个 Spring Annotations。类实现可以是这样的:

package springfive.airline.airlineplanes;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@EnableEurekaClient
@SpringBootApplication
public class AirlinePlanesApplication {

public static void main(String[] args) {
  SpringApplication.run(AirlinePlanesApplication.class, args);
}

}

Spring Annotations 将与 Zuul 代理连接。此外,我们需要将应用程序与 Eureka 服务器连接并自动配置应用程序。这些行为可以使用 @EnableZuulProxy@EnableEurekaClient@SpringBootApplication

现在,我们将创建一个 bootstrap.yaml 文件来指示 Spring Framework 搜索上一章创建的 Config Server 上的配置文件。该文件应该是这样的:

spring:
  application:
    name: planes
cloud:
    config:
      uri: http://localhost:5000
label: master

我们已经配置了 Config Server 地址;这是小菜一碟。

现在,我们需要在 GitHub 存储库中添加 application.yaml 文件,因为 Config Server 会尝试在存储库中查找该文件。

该文件可以在 GitHub 上找到 https://github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/config-files/flights.yaml

我们可以在 IDE 上或通过命令行运行应用程序;它是由你决定。在尝试运行之前检查配置服务器、Eureka、MongoDB 和 RabbitMQ 是否已启动并运行。

让我们检查一下输出。我们可以通过不同的 方式检查它:在控制台上和在Eureka 服务器上。我们开始做吧。

检查控制台。让我们试着找到关于 DiscoveryClient 的一行。 planes 微服务正在尝试连接到 Eureka 服务器:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

这里有一些关于日志文件的重要信息。第一行指示哪个应用程序正在尝试向 Eureka 服务器注册。接下来的四行是关于侦探的。 Sleuth 框架正在注册 RabbitMQ 队列和通道。

我们需要找到以下行:

Started AirlinePlanesApplication in 17.153 seconds (JVM running for 18.25)

此外,我们可以检查 Eureka 服务器,我们可以在其中看到 PLANES 应用程序,如下所示:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

太棒了,我们的平面微服务可以运行了。

Note

我们可以使用 Postman 来尝试我们的微服务。这个应用程序使我们能够使用直观的 IDE 调用我们的 API 来与我们的微服务进行交互。该应用程序允许我们将一些 HTTP 调用分组到集合中。平面集合可以在 GitHub 上的 https://github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/postman/planes.postman_collection。 

我们已经完成了我们的第一个微服务。在下一节中,我们将创建我们的 flights 微服务,它将使用飞机的数据。

Flights microservice


我们飞机的微服务已经启动并运行。这将是important,因为航班的微服务需要获取飞机的数据来创建航班的实体。

我们将介绍 Netflix Ribbon,它将充当我们应用程序的客户端负载均衡器,我们将使用服务发现来从服务注册表中查找服务的地址。

Cloning the Flight microservice project

我们在previous 章节中多次执行此任务。我们可以在 GitHub 上下载项目源代码 https://github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter08/airline-flights。在下一节中,我们将深入研究 Ribbon 以及它如何在分布式系统上为我们提供帮助。

Netflix Ribbon

Ribbon 是由 Netflix 公司创建和维护的开源项目。该项目在 Apache 2.0 下获得许可,可用于商业目的。

功能区为IPC<提供客户端软件负载平衡 /strong>进程间通信)。该项目以异步方式支持大多数流行的协议,例如 TCP、UDP 和 HTTP。

还有更多有趣的功能,例如服务发现集成,它支持在云等动态和弹性环境中进行集成。为此,我们将查看我们的 Eureka 服务器。这两个项目都由 Netflix 团队维护。它非常适合我们的用例。

另一个有趣的特性是容错。 Ribbon 客户端可以在配置的列表中找到实时服务器并发送请求。此外,宕机的服务器不会收到任何请求。

下图解释了 Ribbon 的工作原理:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

正如我们所见,Ribbon Client 可以与 Eureka 通信,然后将请求重定向到所需的微服务。在我们的例子中,flights 微服务将使用 Ribbon 客户端并从 Eureka 获取服务注册表并将调用重定向到实时 planes< /code> 微服务实例。这听起来像是一个了不起的解决方案。

Understanding the discovery client

现在,我们将了解服务 discovery 以及它如何在复杂和动态的环境中工作。服务发现的基本思想是维护服务存储库并为调用者提供服务地址。

它需要一些复杂的任务来实现这个目标。有两个主要的行为需要理解:

  • The first one is the register. As we know, the service discovery needs to store the services information, such as the address and name, and then during the service bootstrap, it needs to send the information to the service registry. 
  • In the the second operation, the service discovery clients need to query the service registry, asking for the desired service name, for instance. Then the service registry will send the service information to the client.

现在我们了解了基础知识,如下图所示:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

如上图所示:

  1. The first part is the service registration.
  2. At the second stage, the service client will get the service address from the Eureka server.
  3. Then the client can call based on the service information. 

让我们在代码中进行。

Service discovery and load balancing in practice

现在我们将编写一些代码来与我们的service 发现和负载平衡基础设施进行交互。现在我们知道它是如何工作的,它将帮助我们理解源代码。

我们将创建一个 DiscoveryService 类,该类将从请求的 service 名称。类代码应该是这样的:

package springfive.airline.airlineflights.service;

import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
public class DiscoveryService {

private final LoadBalancerClient lbClient;

  private final DiscoveryClient dClient;

  public DiscoveryService(LoadBalancerClient lbClient, DiscoveryClient dClient) {
this.lbClient = lbClient;
    this.dClient = dClient;
}

public Flux<String> serviceAddressFor(String service) {
return Flux.defer(() ->  Flux.just(this.dClient.getInstances(service)).flatMap(srv ->
        Mono.just(this.lbClient.choose(service))
    ).flatMap(serviceInstance ->
        Mono.just(serviceInstance.getUri().toString())
    ));
}

}

可以看到,我们注入了两个对象:LoadBalanceClient,它充当客户端负载均衡器,即Netflix Ribbon;和 DiscoveryClient,它将从请求的服务中找到实例。

我们使用 lambda Flux.defer() 来组织流程,然后我们将从 Eureka 服务器中查找服务实例。为此,我们使用 this.dClient.getInstances(service)。在我们从负载均衡中查找服务 URI 后,它将返回一个服务名称列表。这将使用 this.lbClient.choose(service). 然后我们将返回服务实例的Flux地址。

是时候看看客户端代码如何使用 DiscoveryService 对象了。客户端代码可以是这样的:

public Mono<Plane> plane(String id) {
return discoveryService.serviceAddressFor(this.planesService).next().flatMap(
      address -> this.webClient.mutate().baseUrl(address + "/" + this.planesServiceApiPath + "/" + id).build().get().exchange()
      .flatMap(clientResponse -> clientResponse.bodyToMono(Plane.class)));
}

此代码可以在项目的PlaneService 类中找到。记住 serviceAddressFor() 方法返回一个 Flux 的服务地址。我们将使用 next() 方法获得第一个。然后我们可以将服务地址转换为有效地址以到达平面微服务。

现在,我们将测试 service 连接。我们需要完成以下任务:

  1. Run the Config Server, Eureka, the planes microservice, and the flights microservice
  2. Create a plane entity on the planes microservice
  3. Create a flight entity on the flights microservice

检查前面列出的所有服务是否已启动并正在运行。然后我们将使用以下 JSON 创建一个 plane 实体:

{
  "owner" : "Spring Framework Company",
  "model" : {
    "factory" : "Pivotal",
    "model" : "5.0",
    "name" : "Spring 5.0",
    "reference_name" : "S5.0"
  },
  "seats" : [
    {
      "identity" : "1A",
      "row" : "1",
      "right_side" : { "seat_identity" : "2A"},
      "category" : {
        "id" : "A",
        "name": "First Class"
      }
    },
    {
      "identity" : "2A",
      "row" : "1",
      "left_side" : { "seat_identity" : "1A"},
      "category" : {
        "id" : "A",
        "name": "First Class"
      }
    },
    {
      "identity" : "3A",
      "row" : "1",
      "left_side" :{ "seat_identity" : "2A"},
      "category" : {
        "id" : "A",
        "name": "First Class"
      }
    }
    ],
  "notes": "The best company airplane"
}

我们需要使用 HTTP http://localhost:50001/planes 调用 planes 微服务文字">POST 方法。我们可以在 Postman 的Planes Collection 中找到创建平面的请求。当我们调用创建平面 API 后,我们将获得一个新的平面 ID。它可以在 HTTP 响应标头中找到,如 Postman 上的 following 图像所示:

Note

Postman 是一个帮助开发者测试 API 的工具。 Postman 提供了一个友好的 GUI ( Graphic User Interface )来制作要求。此外,该工具支持环境,有助于测试不同的环境,例如开发、测试和生产。

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

查看 location HTTP 响应标头。 HTTP 状态码也很重要。我们将使用刚刚创建的飞机 ID 5a6a6c636798a63817bed8b4 创建一个新航班。

Note

我们可以在 W3 Org (https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)。 记住这一点,因为遵循正确的状态代码非常重要。当我们创建 REST API 时,这被认为是最佳实践。

Flight Collection 可以在 GitHub 上找到 https://github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/postman/flights.postman_collection。 有一个 Create Flight 请求我们要执行,但在此之前,我们需要更改我们之前创建的飞机 ID。看看下面的截图:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

飞机 ID 已更改为我们之前创建的飞机的 ID。现在我们可以执行请求了。 flights 微服务与 planes 微服务具有相同的行为。它将返回带有新航班 ID 的位置响应。就我而言,生成的新 ID 如下图所示:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

现在,我们可以通过 ID 找到航班。该请求可在 Flight Collection; 名称为 Flight by Id 中找到。我们可以执行这个请求,结果应该是这样的:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

查看 plane JSON 节点。我们在 flight 微服务中没有任何关于飞机的数据。此信息来自 planes 微服务。我们使用了服务发现和客户端负载平衡。做得好!

我们来看看IDE提供的调试。我们要查看飞机服务地址:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

Variables 面板上,我们可以看到 address 变量。价值来自服务发现和客户端负载平衡。它是 服务 IP名称。 现在我们可以调用请求的服务来转换 URL。

太棒了,我们的基础设施运行良好,现在我们能够使用基础设施找到服务,但有一些重要的事情需要注意。我们将在下一节中发现它。

When the services fail, hello Hystrix


有时基础设施可能会出现故障,尤其是网络。它可能会导致微服务架构中的一些问题,因为通常服务之间存在许多连接。这意味着在运行时,微服务依赖于其他微服务。通常,这些连接是通过 HTTP 协议使用 REST API 完成的。

它可能导致称为级联故障的行为;也就是说,当微服务 system 的一部分失败时,由于依赖关系,它可以触发其他微服务失败。让我们来说明一下:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

如果 Service Y 失败,Service AService M 也可能会失败。

当这种情况发生时,我们有一个模式可以帮助我们:断路器。

Hystrix in a nutshell

Hystrix 是一个帮助开发人员管理服务之间交互的库​​。该项目是开源的,由社区维护,位于 Netflix GitHub 下。

断路器模式是一种有助于控制 system 集成的模式。这个想法很简单:我们将远程调用包装在一个函数或对象中,我们将监视这些调用以跟踪失败。如果呼叫达到限制,电路将打开。其行为类似于电气断路器的行为,其思想是相同的——保护某物以避免破坏电气系统:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

Hystrix 实现了断路器模式并具有一些有趣的行为,例如回退选项。 Hystrix 为我们的应用程序提供弹性。我们能够提供回退,停止级联故障,并提供操作控制。

该库提供高级配置,可以通过注解配置 "id325894901" class="indexterm"> 如果我们使用 Spring Cloud Hystrix

Note

断路器模式由 Martin Fowler 描述。您可以在 Martin Fowler 的页面上找到有关它的更多信息 https://martinfowler。 com/bliki/CircuitBreaker.html

Spring Cloud Hystrix

正如我们所料,Spring Boot 与 Netflix Hystrix 集成。 集成可以使用几个注解并通过使用 Hystrix 属性配置注解来完成。我们将保护 planes 我们在 flight 服务中编码的微服务交互。我们现在一个方法来尝试获取飞机的数据。

我们来看看那个方法:

@HystrixCommand(commandKey = "plane-by-id",groupKey = "airline-flights",fallbackMethod = "fallback",commandProperties = {
      @HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value="10"),
      @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "10"),
      @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value="10000"),
      @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "800"),
      @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "10000")
  })
public Mono<Plane> plane(String id) {
return discoveryService.serviceAddressFor(this.planesService).next().flatMap(
      address -> this.webClient.mutate().baseUrl(address + "/" + this.planesServiceApiPath + "/" + id).build().get().exchange()
      .flatMap(clientResponse -> clientResponse.bodyToMono(Plane.class)));
}

此命令有一些配置。第一个配置是 commandKey。 这里的基本思想是为命令创建一个名称。这对面板控制很有用。第二个, groupKey,是用于对命令进行分组的命令。它还有助于在仪表板上将命令数据分组在一起。有滚动窗口的概念。这个想法是在一段时间内对请求进行分组;它用于启用指标和统计信息。 

circuitBreaker.requestVolumeThreshold 配置滚动窗口中将在电路上跳闸的请求数。例如,如果我们有一个滚动窗口配置为打开 10 秒,如果我们在 10 秒的间隙中有 9 个请求,电路将不会打开,因为我们 在我们的命令中将其配置为 10。另一种配置是 circuitBreaker.sleepWindowInMilliseconds,其基本思想是在电路跳闸后给予一定的时间来拒绝请求,然后再次尝试以允许尝试。

最后一个是 execution.isolation.thread.timeoutInMilliseconds。 该属性配置命令的超时时间。这意味着如果达到配置的时间,断路器系统将执行回退逻辑并将命令标记为超时。

Note

Hystrix 库是高度可定制的,并且有很多属性可供使用。完整的文档可以在 https://github.com/Netflix/找到Hystrix/wiki/configuration。 我们可以将这些属性用于不同的用例。

Spring Boot Admin


Spring Boot Admin 项目是在生产环境中帮助 开发人员的工具。该工具在有条理的仪表板中显示 Spring Boot 应用程序指标,它使查看应用程序指标和更多信息变得非常容易。

该工具使用来自 Spring Boot Actuator 的数据作为信息源。该项目是开源的,有很多贡献者,也是社区中的一个活跃项目。

Running Spring Boot Admin

设置应用程序是小菜一碟。我们将需要 一个新的 Spring Boot 应用程序,并将这个新应用程序与我们的服务发现实现连接起来。让我们现在就做。

我们可以在 GitHub 上找到代码 https://github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter08/admin。如果您想创建一个新应用程序,请继续;该过程类似于我们在前几章中所做的。

该项目是一个 Spring Boot 常规应用程序,具有两个新的依赖项:

<dependency>
  <groupId>de.codecentric</groupId>
  <artifactId>spring-boot-admin-server</artifactId>
  <version>1.5.6</version>v
</dependency>

<dependency>
  <groupId>de.codecentric</groupId>
  <artifactId>spring-boot-admin-server-ui</artifactId>
  <version>1.5.6</version>
</dependency>

这些依赖关系是关于 admin-serveradmin-server-ui。该项目还不支持 Spring Boot 2,但这不是问题,因为我们不需要响应式的东西;它是一个监控工具。

我们已经配置了我们的强制依赖项。我们将需要一个服务发现,因为我们的基础设施中有一个。我们需要它来提供服务发现功能,并最小化我们的 Spring Boot Admin 应用程序的配置。让我们添加 Eureka 客户端依赖项:

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

太棒了,我们的依赖项配置正确。然后我们可以创建我们的主类。主类应该是这样的:

package springfive.airline.admin;

import de.codecentric.boot.admin.config.EnableAdminServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@EnableAdminServer
@EnableEurekaClient
@SpringBootApplication
public class AdminApplication {

public static void main(String[] args) {
    SpringApplication.run(AdminApplication.class, args);
}

}

这里的主要区别在于 @EnableAdminServer 将为我们配置 Spring Boot Admin 应用程序并设置服务器。正如我们所料,我们将使用 Config Server 应用程序来存储我们的 application.yaml。为了实现这一点,我们需要创建我们的 bootstrap.yaml,它应该是这样的:

spring:
  application:
    name: admin
cloud:
    config:
      uri: http://localhost:5000
label: master

完全没有区别,  bootstrap.yaml 被配置为从Config Server中查找配置文件。

是时候创建我们的 application.yaml 文件了,我们需要在其中添加一些配置来设置新的健康检查 URL,因为 Spring Boot 2 上的执行器已移动,前缀为执行器。我们新的健康检查 URL 应该是 /actuator/health

我们的配置文件应该是这样的:

server:
  port: 50015

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
spring:
  boot:
    admin:
      discovery:
        converter:
          health-endpoint-path: /actuator/health

我们已经配置了 Eureka 服务器地址并设置了健康检查 URL。

现在我们可以运行我们的主类 AdminApplication。 我们可以使用Java命令行或IDE;完全没有区别。

运行!

我们应该在日志文件中看到以下行:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

太棒了,我们的应用程序已经可以使用了。现在我们可以进入主页面了。进入http://localhost:50015/#/(主页面),我们可以看到如下页面:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

看看我们的微服务中的任何中断或奇怪行为是如何更容易看到的。请记住,微服务架构中的关键点是监控。为了有一个良好的环境,这是非常必要的。

Spring Cloud Zuul


Spring Cloud Gateway 是我们采用微服务架构的自然选择,但现在 Spring Cloud Gateway 并没有开启支持用于服务发现功能,例如 Eureka 服务器。这意味着我们必须逐个路由配置它。这听起来不太好。

我们将 Zuul 代理作为微服务环境的网关,但请记住,当项目支持服务发现时,Spring Cloud Gateway 是最佳选择。

让我们创建 Zuul 代理项目。

Understanding the EDGE service project

EDGE 服务是一种提供 动态路由、监控、弹性和安全性的服务。这里的基本思想是为我们的微服务创建一个反向代理。 

该服务将充当我们微服务的代理,并将作为中央访问点公开。 Spring Cloud Zuul 与 Eureka 服务器集成。它将增加我们的弹性,因为我们将使用 Eureka 服务器提供的服务发现功能。

下图展示了我们将如何在我们的架构中使用 Edge Service

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

正如我们所见,Zuul Server 将连接到服务发现服务器,以获取可用服务的列表。之后,Zuul 服务将重定向到请求的服务。 

看图。没有与客户端交互,即 MobileBrowser 和我们的微服务。

Spring Cloud Zuul 还支持 interesting 特性,例如:

  • pre: This can be used to set some data inRequestContext; it is executed before the request is routed
  • route: This handles the request routing
  • post: This filters which one acts after the request is routed
  • error: When some errors happen, we can use the error feature to handle the request

我们不会使用这些功能,但请记住它们可能非常有用。请记住,我们的 Zuul 服务器是我们通往互联网的门户。

Creating the EDGE server

我们将使用 Zuul 服务器作为我们应用程序的 API gateway。现在是时候创建我们的项目了。由于创建这个项目没有相关的区别,我们将看看具体的 Zuul 部分。

所需的依赖项是:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

它将为我们配置 Zuul 服务器依赖项。

现在我们可以添加项目的主类。类应该是这样的:

package springfive.airline.edge;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.stereotype.Controller;

@Controller
@EnableZuulProxy
@EnableEurekaClient
@SpringBootApplication
public class EdgeServerApplication {

public static void main(String[] args) {
    SpringApplication.run(EdgeServerApplication.class, args);
}

}

这里的新东西是 @EnableZuulProxy。它将设置一个 Zuul 服务器端点并配置反向代理过滤器。然后我们将能够将请求转发到微服务应用程序。 Zuul 集成了 Eureka 服务器,所以我们不需要手动配置。自动配置将在发现客户端实现时找到服务。

我们可以通过命令行或 IDE 运行应用程序,这取决于您。

然后我们可以看到配置的路由。转到 http://localhost:8888/routes,我们将能够看到路由:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

我们配置了一些路由。我们使用 application.yaml 文件来做到这一点。该文件应该是这样的:

zuul:
  routes:
    planes:
      path: /api/v1/planes/**
      serviceId: planes
    flights:
      path: /api/v1/flights/**
      serviceId: flights
    fares:
      path: /api/v1/fares/**
      serviceId: fares
    passengers:
      path: /api/v1/passengers/**
      serviceId: passengers

让我们了解一下这个配置。我们创建了一个名为 planes 的节点。该节点配置一个path (即URI),并通过serviceId配置服务名,注册在Eureka服务器。

让我们做一个简单的测试。我们将:

  • Configure the new URL path for the planes service
  • Test the request using the Zuul server

打开位于 planes 微服务项目中的 PlaneResource 类。

RequestMapping 配置如下:

@RequestMapping("/planes")

把它改成这样:

@RequestMapping("/")

请记住,我们可以将 Zuul 服务器用作路由器,因此我们不再需要此信息。通过源代码上的 URI 路径,我们可以使用配置文件。

再次运行 planes 微服务。需要运行以下服务:

  • Config Server
  • Eureka server
  • Planes microservice
  • API Edge

然后我们可以使用 Zuul 代理调用 planes 微服务。让我们使用 cURL 来实现:

curl http://localhost:8888/api/v1/planes

让我们稍微了解。端口8888指向Zuul Server,我们已经在application.yaml。当路径为 /api/v1/planes/**时,Zuul Server会重定向到 planes 微服务。基本流程是:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

请求到达 Zuul Server,然后是 Zuul Server 会将其重定向到请求的微服务。结果应该是这样的;就我而言,我在数据库中有一些飞机:

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》断路器和安全

太棒了,我们的 API 网关已全面运行。我们将它用于同一个端口中的所有服务,并且只有 URI 将被更改为指向所需的 serviceId

Note

我们可以像在其他 Spring Boot 应用程序中一样配置端口。在这种情况下,我们选择了 8888 port。

Summary


在本章中,我们了解了一些重要的微服务模式,以及它们如何帮助我们交付容错、弹性和容易出错的应用程序。

我们已经练习了如何使用 Spring Framework 提供的服务发现特性以及它在应用程序运行时是如何工作的,并且我们做了一些调试任务来帮助我们理解它是如何工作的。

由 Netflix 托管的 Hystrix 项目可以提高我们应用程序的弹性和容错能力。在处理远程调用时,在本节中,我们创建了一些 Hystrix 命令,并了解了 Hystrix 是如何成为断路器模式的有用实现的。

在本章的最后,我们能够了解微服务的缺点以及如何解决分布式环境中的常见问题。

现在我们知道了如何使用 Spring Framework 解决微服务架构风格的常见问题。

在下一章中,我们将完成我们的 Airline Ticket System,使用配置的工具来监控微服务的健康状况,并看看它如何帮助开发人员在微服务在生产阶段运行时的运行时间。

到时候那里见。