Quarkus-和-Kubernetes-的-Java-微服务高级教程-四-

107 阅读24分钟

Quarkus 和 Kubernetes 的 Java 微服务高级教程(四)

原文:Pro Java Microservices with Quarkus and Kubernetes

协议:CC BY-NC-SA 4.0

十三、构建 Kubernetized 微服务

介绍

第八章讨论了如何将数据驱动设计(DDD)应用到单体应用中。我们打破了在分析项目时揭示的边界上下文之间的关系。在 Stan4J 中,最终的代码结构如下所示:

img/509649_1_En_13_Figa_HTML.jpg

在本章中,您将实现三个微服务— ProductOrderCustomer。这些包依赖于commons包,因此,您需要在实现三个微服务之前实现它。

创建公共图书馆

commons JAR 库将包装commons包的内容。

我们将生成一个简单的 Maven 项目,其中我们将复制commons包的内容。

mvn archetype:generate -DgroupId=com.targa.labs.commons -DartifactId=quarkushop-commons \
    -DarchetypeArtifactId=maven-archetype-quickstart \
    -DarchetypeVersion=1.4 -DinteractiveMode=false

该命令生成一个包含以下内容的简单项目:

project
|-- pom.xml
`-- src
    |-- main/java
    |       `-- com.targa.labs.commons
    |           `-- App.java
    `-- test/java
            `-- com.targa.labs.commons
                `-- AppTest.java

img/509649_1_En_13_Figb_HTML.gif我们将删除App.javaAppTest.java,因为我们不再需要它们。

然后,我们将commons包的内容从单体应用复制/粘贴到我们的quarkushop-commons项目中。

不要害怕!当您粘贴复制的类时,您会看到许多错误和警告,但是下一步是添加缺少的依赖项以使 IDE 满意。img/509649_1_En_13_Figc_HTML.gif

让我们打开pom.xml文件并开始进行更改:

  1. 首先将maven.compiler.sourcemaven.compiler.target1.7改为11

  2. 如下定义依赖关系:

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.microprofile.openapi</groupId>
            <artifactId>microprofile-openapi-api</artifactId>
            <version>1.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.jboss.spec.javax.ws.rs</groupId>
            <artifactId>jboss-jaxrs-api_2.1_spec</artifactId>
            <version>2.0.1.Final</version>
        </dependency>
        <dependency>
            <groupId>jakarta.persistence</groupId>
            <artifactId>jakarta.persistence-api</artifactId>
            <version>2.2.3</version>
        </dependency>
        <dependency>
            <groupId>jakarta.enterprise</groupId>
            <artifactId>jakarta.enterprise.cdi-api</artifactId>
            <version>2.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.microprofile.health</groupId>
            <artifactId>microprofile-health-api</artifactId>
            <version>2.2</version>
        </dependency>
    
        <dependency>
            <groupId>org.eclipse.microprofile.config</groupId>
            <artifactId>microprofile-config-api</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.microprofile.metrics</groupId>
            <artifactId>microprofile-metrics-api</artifactId>
            <version>2.3</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.quarkus.security</groupId>
            <artifactId>quarkus-security</artifactId>
            <version>1.1.2.Final</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>postgresql</artifactId>
            <version>1.15.3</version>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-test-common</artifactId>
            <version>1.13.3.Final</version>
        </dependency>
    </dependencies>
    
    

哇哦!这些依赖来自哪里?我敢肯定你和我一样,不喜欢写自己不懂的东西。img/509649_1_En_13_Fige_HTML.gif不过不用担心,这些依赖项都是来自 Quarkus 框架。我使用 IDE 添加了缺少的依赖项:

img/509649_1_En_13_Figf_HTML.jpg

将出现 Maven 工件搜索窗口,如下所示:

img/509649_1_En_13_Figg_HTML.jpg

那么,您应该选择给定依赖项的哪个版本呢?您可以使用 IntelliJ 来确定在单体应用中使用哪些外部库:

img/509649_1_En_13_Figh_HTML.jpg

展开该部分并滚动以找到所需的库。版本将显示在groupIdartifactId之后,如下所示:

img/509649_1_En_13_Figi_HTML.jpg

在这里,您可以看到单体应用正在使用Lombok v1.18.12,因此在Commons项目中,您需要选择相同的版本。

img/509649_1_En_13_Figj_HTML.gif为了避免冲突,请确保quarkus-test-common依赖项与微服务具有相同的 Quarkus 版本。img/509649_1_En_13_Figk_HTML.gif

最后,您需要使用mvn clean install构建 Maven 项目。这个命令将构建 JAR,并使它在本地.m2目录中可用。这使您能够在以后的步骤中将它用作依赖项。

等等!你还没说完呢!你需要考虑一下测试!您需要将utils包从测试类复制到quarkushop-commons项目的主类中。

为了能够在quarkushop-commons库之外重用这些类,您需要将它们放在主目录中,就像任何其他普通类一样。属于测试目录的类只是用于测试目的,它们不打算被重用。

实施产品微服务

在这一部分,您开始做严肃的工作:创建Product微服务。让我们从代码中生成一个名为quarkushop-product的新 Quarkus 应用。夸库斯。io :

img/509649_1_En_13_Figl_HTML.jpg

以下是选定的扩展:

  • RESTEasy JAX-RS

  • 塞西·JSON-b

  • SmallRye OpenAPI

  • 冬眠的奥姆

  • Hibernate 验证程序

  • JDBC 驱动程序- PostgreSQL

  • 候鸟迁徙所经的路径

  • spring 数据 jpa api 的 quartus 扩展

  • 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj

  • SmallRye 健康

  • 忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈

  • 库比涅斯配置

  • 容器图像悬臂

通过单击 Generate Your Application 下载生成的应用 skull。

然后将代码导入您的 IDE。打开pom.xml文件,向其中添加 Lombok 和 TestContainers 依赖项:

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.16</version>
    </dependency>

    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>1.15.3</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>postgresql</artifactId>
        <version>1.15.3</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

接下来,添加最重要的依赖项——quarkushop-commons:

<dependency>
    <groupId>com.targa.labs</groupId>
    <artifactId>quarkushop-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

将代码从product包复制到quarkushop-product微服务。

img/509649_1_En_13_Figm_HTML.gif不要忘记将 banner.txt 文件从单体应用复制到Product微服务的src/main/resources目录中。

下一步是填充application.properties:

 1 # Datasource config properties
 2 quarkus.datasource.db-kind=postgresql
 3 quarkus.datasource.username=developer
 4 quarkus.datasource.password=p4SSW0rd
 5 quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/product
 6 # Flyway minimal config properties
 7 quarkus.flyway.migrate-at-start=true
 8 # HTTP config properties
 9 quarkus.http.root-path=/api
10 quarkus.http.access-log.enabled=true
11 %prod.quarkus.http.access-log.enabled=false
12 # Swagger UI
13 quarkus.swagger-ui.always-include=true
14 # Datasource config properties
15 %test.quarkus.datasource.db-kind=postgresql
16 # Flyway minimal config properties
17 %test.quarkus.flyway.migrate-at-start=true
18 # Define the custom banner
19 quarkus.banner.path=banner.txt
20 ### Security
21 quarkus.http.cors=true
22 quarkus.smallrye-jwt.enabled=true
23 # Keycloak Configuration
24 keycloak.credentials.client-id=quarkushop
25 # MP-JWT Config
26 mp.jwt.verify.publickey.location=http://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/certs
27 mp.jwt.verify.issuer=http://localhost:9080/auth/realms/quarkushop-realm
28 ### Health Check
29 quarkus.smallrye-health.ui.always-include=true
30 # Kubernetes ConfigMaps
31 quarkus.kubernetes-config.enabled=true
32 quarkus.kubernetes-config.config-maps=quarkushop-product-config

这些属性与单体应用几乎相同,这是合乎逻辑的,因为微服务是从单体应用上切下的一片。

img/509649_1_En_13_Fign_HTML.gif我更改了数据库的名称(第 5 行)和ConfigMap(第 34 行)。img/509649_1_En_13_Figo_HTML.gif

回想一下,我们更改了ConfigMap,所以现在我们需要创建它。见清单 13-1 。

1 apiVersion: v1
2 kind: ConfigMap
3 metadata:
4   name: quarkushop-product-config
5 data:
6   application.properties: |-
7     quarkus.datasource.jdbc.url=jdbc:postgresql://postgres:5432/product
8     mp.jwt.verify.publickey.location=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
9     mp.jwt.verify.issuer=http://keycloak-http.keycloak/auth/realms/quarkushop-realm

Listing 13-1quarkushop-product-config.yml

img/509649_1_En_13_Figp_HTML.gif我还在这个ConfigMap中更改了数据库的名称(第 7 行)。

我更改了数据库名称,因为正如您在第九章中了解到的,每个微服务拥有一个数据库是明智的。img/509649_1_En_13_Figq_HTML.gif这就是我为每个微服务创建专用模式的原因:

img/509649_1_En_13_Figr_HTML.jpg

因为我们在数据库上下文中,我们需要将 Flyway 脚本V1.0__Init_app.sqlV1.1__Insert_samples.sql从单体应用复制到Product微服务的src/main/resources/db/migration目录中。我们还需要清理 SQL 脚本,只保留产品绑定的上下文相关对象和样本数据。

确保正确清理脚本,否则部署将在应用引导过程中失败。

接下来,有一个非常重要的任务要做:将quarkushop-commons项目标识到quarkushop-product的 Quarkus 索引。

What is the Quarkus Index?

Quarkus 自动索引当前模块。但是,当您拥有包含 CDI beans、实体和序列化为 JSON 的对象的外部模块时,您需要显式地对它们进行索引。

可以通过多种方式建立索引:

  • 使用 Jandex Maven 插件

  • 添加一个空的META-INF/beans.xml文件

  • 使用 Quarkus 索引依赖属性,这是我最喜欢的选择

可以使用application.properties值完成该索引:

quarkus.index-dependency.commons.group-id=com.targa.labs
quarkus.index-dependency.commons.artifact-id=quarkushop-commons

img/509649_1_En_13_Figt_HTML.gif没有这个index-dependency配置,您就无法构建应用的本机二进制文件。

在构建项目之前,我们需要将相关测试从 monolith 复制到quarkushop-product微服务:

  • CategoryResourceIT

  • CategoryResourceTest

  • ProductResourceIT

  • ProductResourceTest

  • ReviewResourceIT

  • ReviewResourceTest

我们还需要将 Keycloak Docker 文件从src/main/docker复制到quarkushop-product微服务:

  • keycloak-test.yml文件

  • realms目录

为了能够执行测试,我们需要在test环境/概要文件中禁用 Kubernetes 支持:

%test.quarkus.kubernetes-config.enabled=false
quarkus.test.native-image-profile=test

img/509649_1_En_13_Figu_HTML.gif我们将原生映像测试配置文件定义为test,以便禁用 Kubernetes 对原生映像测试的支持。

接下来,我们需要执行测试,构建并推送quarkushop-product映像:

mvn clean install -Pnative \
    -Dquarkus.native.container-build=true \
    -Dquarkus.container-image.build=true

然后,我们将quarkushop-product图像推送到容器注册表,如下所示:

docker push nebrass/quarkushop-product:1.0.0-SNAPSHOT

我们现在创建quarkushop-product-config ConfigMap:

kubectl apply -f quarkushop-product/quarkushop-product-config.yml

并将Product微服务部署到 Kubernetes 集群:

kubectl apply -f quarkushop-product/target/kubernetes/kubernetes.json

太棒了!现在我们可以列出 pod 了:

$ kubectl get pods

NAME                                  READY   STATUS
postgres-69c47c748-pnbbf              1/1     Running
quarkushop-product-7748f9f74c-dqnqk   1/1     Running

我们可以在quarkushop-product上使用port-forward来测试应用:

$ kubectl port-forward quarkushop-product-7748f9f74c-dqnqk 8080:8080

Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Handling connection for 8080
Handling connection for 8080

然后一个curl命令可以计算存储在数据库中的产品数量:

$ curl -X GET "http://localhost:8080/api/products/count"

4

很好!对数据库的访问工作正常。img/509649_1_En_13_Figv_HTML.gif我们还可以通过使用curl -X GET "http://localhost:8080/api/health"命令来运行健康检查,以确保正确到达键盘锁:

{
    "status": "UP",
    "checks": [
        {
            "name": "Keycloak connection health check",
            "status": "UP"
        },
        {
            "name": "Database connections health check",
            "status": "UP"
        }
    ]
}

太棒了!一切都在按预期工作!img/509649_1_En_13_Figw_HTML.gif img/509649_1_En_13_Figx_HTML.gif我们现在可以转移到Order微服务。

实施订单微服务

在本节中,我们将生成与Product微服务具有相同扩展的Order微服务,并增加一个扩展:REST 客户端扩展。

我们知道Order微服务对Product微服务有通信依赖。这种通信可以作为从Order微服务到Product微服务的 REST API 调用来实现。这就是为什么我们在选择的依赖项中包含 REST 客户端扩展。

生成项目后,我们将运行与使用Product微服务时相同的任务:

  • 将代码从单体应用中的order包复制到新的Order微服务中

  • 添加 Lombok、AssertJ 和 TestContainers 依赖项

  • 添加quarkushop-commons依赖项

  • 从 monolith 复制banner.txt文件

  • 添加application.properties并更改数据库和ConfigMap名称

  • 创建quarkushop-product-config ConfigMap文件

  • 复制 Flyway 脚本并清理不相关的对象和数据

  • application.properties中添加quarkushop-commons的 Quarkus 索引依赖关系

此时,我们需要修复代码,因为我们在OrderItemService类中仍然有一个ProductRepository引用。

OrderItemService使用ProductRepository查找使用给定 ID 的产品。这个编程调用将被对Product微服务的 REST API 调用所取代。为此,我们需要创建一个ProductRestClient类,它将使用给定的 ID 获取产品数据:

  • ProductRestClient将指向/products URI。

  • @RegisterRestClient允许 Quarkus 知道这个接口可以作为 REST 客户端用于 CDI 注入。

  • findById()方法将在/products URI 上进行 HTTP GET。

@Path("/products")@RegisterRestClientpublic interface ProductRestClient {

    @GET
    @Path("/{id}")
    ProductDto findById(@PathParam Long id);    ③
}

但是Product微服务 API 的基 URL 是什么?为了正常工作,需要对ProductRestClient进行配置。可以使用这些属性完成配置:

  • ProductRestClient的基本 URL 配置。

  • ②将ProductRestClient bean 的范围定义为Singleton

1 product-service.url=http://quarkushop-product:8080/api
2 com.targa.labs.quarkushop.order.client.ProductRestClient/mp-rest/url=${product-service.url}     ①
3 com.targa.labs.quarkushop.order.client.ProductRestClient/mp-rest/scope=javax.inject.Singleton   ②

我们将重构OrderItemService类来改变:

@Inject
ProductRepository productRepository;

敬新的:

@RestClient
ProductRestClient productRestClient;

img/509649_1_En_13_Figy_HTML.gif @RestClient用于注入一个 REST 客户端。

然后,我们将productRepository.getOne()调用更改为productRestClient.findById()img/509649_1_En_13_Figz_HTML.gifJPA 存储库获取已经被 REST API 调用所取代。

因为我们对Product微服务有一个外部依赖,所以我们需要创建一个健康检查来验证Product微服务是否可达,就像我们对 PostgreSQL 和 Keycloak 所做的一样。ProductServiceHealthCheck看起来是这样的:

@Slf4j
@Liveness
@ApplicationScoped
public class ProductServiceHealthCheck implements HealthCheck {

    @ConfigProperty(name = "product-service.url", defaultValue = "false")
    Provider<String> productServiceUrl;

    @Override
    public HealthCheckResponse call() {

        HealthCheckResponseBuilder responseBuilder =
                HealthCheckResponse.named("Product Service connection health check");

        try {

            productServiceConnectionVerification();
            responseBuilder.up();

        } catch (IllegalStateException e) {
            responseBuilder.down().withData("error", e.getMessage());
        }

        return responseBuilder.build();
    }

    private void productServiceConnectionVerification() {
        HttpClient httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofMillis(3000))
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .GET()
                .uri(URI.create(productServiceUrl.get() + "/health"))
                .build();

        HttpResponse<String> response = null;

        try {
            response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        } catch (IOException e) {
            log.error("IOException", e);
        } catch (InterruptedException e) {
            log.error("InterruptedException", e);
            Thread.currentThread().interrupt();
        }

        if (response == null || response.statusCode() != 200) {
            throw new IllegalStateException("Cannot contact Product Service");
        }
    }
}

我们需要将这些相关测试从 monolith 复制到quarkushop-order微服务:

  • AddressServiceUnitTest

  • CartResourceIT

  • CartResourceTest

  • OrderItemResourceIT

  • OrderItemResourceTest

  • OrderResourceIT

  • OrderResourceTest

我们还需要将 Keycloak Docker 文件从src/main/docker复制到quarkushop-order微服务:

  • keycloak-test.yml文件

  • realms目录

然后我们添加执行测试所需的属性:

%test.quarkus.kubernetes-config.enabled=false
quarkus.test.native-image-profile=test

quarkushop-product不依赖任何其他微服务,所以我们为quarkushop-order所做的修改对quarkushop-product来说已经足够了。然而,quarkushop-product微服务上的quarkushop-order回复意味着测试也依赖于quarkushop-product微服务。

我们有许多解决方案,以下是我的两个选择:

  • 嘲笑RestClient

  • 添加一个quarkushop-product的测试实例

我将为quarkushop-order使用第二种选择,并为quarkushop-customer模拟RestClient,因为它在另一个微服务上回复。

回想一下,我们使用 TestContainers 框架为集成测试提供了一个 Keycloak 实例。该供应是使用一个docker-compose文件进行的,其中创建了一个keycloak服务。我们可以使用相同的方法来提供一个quarkushop-product实例,使用相同的docker-compose文件。但是我们必须首先创建一个新的QuarkusTestResourceLifecycleManager类,而不是使用KeycloakRealmResource,因为它只提供 Keycloak。

让我们将src/main/docker/keycloak-test.yml文件重命名为src/main/docker/context-test.yml。然后我们可以添加两个服务:quarkushop-productpostgresql-db

img/509649_1_En_13_Figaa_HTML.gif我们为什么要增加postgresql-db服务?答案很简单;img/509649_1_En_13_Figab_HTML.gif这是quarkushop-product所需要的(我们的微服务需要数据库存储和一个 Keycloak 租户来工作)。img/509649_1_En_13_Figac_HTML.gif

src/main/docker/ context-test.yml的内容如下:

version: '3'
services:
  keycloak:
    image: jboss/keycloak:latest
    command:
      [
        '-b','0.0.0.0',
        '-Dkeycloak.migration.action=import',
        '-Dkeycloak.migration.provider=dir',
        '-Dkeycloak.migration.dir=/opt/jboss/keycloak/realms',
        '-Dkeycloak.migration.strategy=OVERWRITE_EXISTING',
        '-Djboss.socket.binding.port-offset=1000',
        '-Dkeycloak.profile.feature.upload_scripts=enabled',
      ]
    volumes:
      - ./realms-test:/opt/jboss/keycloak/realms
    environment:
      - KEYCLOAK_USER=admin
      - KEYCLOAK_PASSWORD=admin
      - DB_VENDOR=h2
    ports:
      - 9080:9080
      - 9443:9443
      - 10990:10990
  quarkushop-product:
    image: nebrass/quarkushop-product:1.0.0-SNAPSHOT
    environment:
      - QUARKUS_PROFILE=test
      - QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgresql-db:5432/product
      - MP_JWT_VERIFY_PUBLICKEY_LOCATION=http://keycloak:9080/auth/realms/quarkushop-realm/protocol/openid-connect/certs
      - MP_JWT_VERIFY_ISSUER=http://keycloak:9080/auth/realms/quarkushop-realm
    depends_on:
      - postgresql-db
      - keycloak
    ports:
      - 8080:8080
  postgresql-db:
    image: postgres:13
    volumes:
      - /opt/postgres-volume:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=developer
      - POSTGRES_PASSWORD=p4SSW0rd
      - POSTGRES_DB=product
      - POSTGRES_HOST_AUTH_METHOD=trust
    ports:
      - 5432:5432

img/509649_1_En_13_Figad_HTML.gif注意,通过quarkushop-product的环境变量使用了可用的 Keycloak 实例。img/509649_1_En_13_Figae_HTML.gif

太棒了!现在,TestContainers将提供 Keycloak/PostgreSQL/ quarkushop-product实例,这是quarkushop-order在这些集成测试中所需要的。img/509649_1_En_13_Figaf_HTML.gif img/509649_1_En_13_Figag_HTML.gif

接下来,我们需要创建一个新的QuarkusTestResourceLifecycleManager类,名为ContextTestResource。这个类将提供 Keycloak 和quarkushop-product,并将它们的属性传递给应用。见清单 13-2 。

  • ①定义提供的服务。

  • ②定义 Keycloak 和quarkushop-product需要的属性。

public class ContextTestResource implements QuarkusTestResourceLifecycleManager {

    @ClassRule
    public static DockerComposeContainer ECOSYSTEM = new DockerComposeContainer(
            new File("src/main/docker/context-test.yml"))
            .withExposedService("quarkushop-product_1", 8080,   ①
                    Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)))
            .withExposedService("keycloak_1", 9080,             ①
                    Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)));

    @Override
    public Map<String, String> start() {
        ECOSYSTEM.start();

        String jwtIssuerUrl = String.format("http://%s:%s/auth/realms/quarkus-realm",
                ECOSYSTEM.getServiceHost("keycloak_1", 9080),
                ECOSYSTEM.getServicePort("keycloak_1", 9080)
        );

        TokenService tokenService = new TokenService();
        Map<String, String> config = new HashMap<>();

        try {

            String adminAccessToken = tokenService.getAccessToken(jwtIssuerUrl,
                    "admin", "test", "quarkus-client", "mysecret");
            String testAccessToken = tokenService.getAccessToken(jwtIssuerUrl, "test", "test", "quarkus-client", "mysecret");

            config.put("quarkus-admin-access-token", adminAccessToken);
            config.put("quarkus-test-access-token", testAccessToken);

        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }

        config.put("mp.jwt.verify.publickey.location", jwtIssuerUrl
                                  + "/protocol/openid-connect/certs");    ②

        config.put("mp.jwt.verify.issuer", jwtIssuerUrl);                  ②

        String productServiceUrl = String.format("http://%s:%s/api",
                ECOSYSTEM.getServiceHost("quarkushop-product_1", 8080),
                ECOSYSTEM.getServicePort("quarkushop-product_1", 8080)
        );
        config.put("product-service.url", productServiceUrl);              ②

        return config;
    }

    @Override
    public void stop() {
        ECOSYSTEM.stop();
    }

}

Listing 13-2src/test/java/com/targa/labs/quarkushop/order/util/ContextTestResource.java

然后我们重构测试以包含ContextTestResource:

@DisabledOnNativeImage
@QuarkusTest
@QuarkusTestResource(TestContainerResource.class)
@QuarkusTestResource(ContextTestResource.class)
class CartResourceTest {
...
}

img/509649_1_En_13_Figah_HTML.gif当运行重构步骤时,我删除了客户创建。我用的是随机生成的客户 ID。

让我们仔细看看微服务的创建过程:img/509649_1_En_13_Figai_HTML.gif

  • 构建quarkushop-order映像并将其推送到容器注册表中:

  • 创建quarkushop-order-config ConfigMap文件:

$ mvn clean install -Pnative \
    -Dquarkus.native.container-build=true \
    -Dquarkus.container-image.build=true

$ docker push nebrass/quarkushop-order:1.0.0-SNAPSHOT

  • 在 Kubernetes 中创建quarkushop-order-config ConfigMap:
1 apiVersion: v1
2 kind: ConfigMap
3 metadata:
4   name: quarkushop-order-config
5 data:
6   application.properties: |-
7     quarkus.datasource.jdbc.url=jdbc:postgresql://postgres:5432/order
8     mp.jwt.verify.publickey.location=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
9     mp.jwt.verify.issuer=http://keycloak-http.keycloak/auth/realms/quarkushop-realm

  • quarkushop-order应用部署到 Kubernetes:
kubectl apply -f quarkushop-order/quarkushop-order-config.yml

  • 使用健康检查、一个port-forward和一个简单的curl GET 命令对/health API 检查quarkushop-order pod:
kubectl apply -f quarkushop-order/target/kubernetes/kubernetes.json

{
    "status": "UP",
    "checks": [
        {
            "name": "Keycloak connection health check",
            "status": "UP"
        },
        {
            "name": "Product Service connection health check",
            "status": "UP"
        },
        {
            "name": "Database connections health check",
            "status": "UP"
        }
    ]
}

很好!Order微服务工作正常。接下来,我们对Customer微服务做同样的事情。img/509649_1_En_13_Figaj_HTML.gif

实施客户微服务

为了实现Customer微服务,我们将应用与Order微服务相同的步骤。同样,我们将:

  • 将代码从单体应用中的customer包复制到新的Customer微服务中

  • 添加 Lombok、AssertJ 和 TestContainers 依赖项

  • 添加quarkushop-commons依赖项

  • 从 monolith 复制banner.txt文件

  • 添加application.properties并更改数据库和ConfigMap名称

  • 复制 Flyway 脚本并清理不相关的对象和数据

  • application.properties中添加quarkushop-commons的 Quarkus 索引依赖关系

然后,我们需要将这些相关测试从 monolith 复制到quarkushop-customer微服务:

  • CustomerResourceIT

  • CustomerResourceTest

  • PaymentResourceIT

  • PaymentResourceTest

我们还需要将 Keycloak Docker 文件从src/main/docker复制到quarkushop-order微服务:

  • keycloak-test.yml文件

  • realms目录

然后我们添加执行测试所需的属性:

%test.quarkus.kubernetes-config.enabled=false
quarkus.test.native-image-profile=test

接下来,我们嘲弄一下OrderRestClient。我们创建一个名为MockOrderRestClient的新类,如清单 13-3 所示。

  • ①用于模拟测试中注入的 beans 的注释。

  • Mock类实现了RestClient接口。

  • ③实现了Mock方法,以返回适合测试的结果。

@Mock@ApplicationScoped
@RestClient
public class MockOrderRestClient implements OrderRestClient {   ②

    @Override
    public Optional<OrderDto> findById(Long id) {               ③
        OrderDto order = new OrderDto();
        order.setId(id);
        order.setTotalPrice(BigDecimal.valueOf(1000));
        return Optional.of(order);
    }

    @Override
    public Optional<OrderDto> findByPaymentId(Long id) {
        OrderDto order = new OrderDto();
        order.setId(5L);
        return Optional.of(order);
    }

    @Override
    public OrderDto save(OrderDto order) {
        return order;
    }
}

Listing 13-3src/test/java/com/targa/labs/quarkushop/customer/utils/MockOrderRestClient.java

仅此而已!模仿是非常容易的,被模仿的组件会被自动注入到测试中。img/509649_1_En_13_Figak_HTML.gif微服务创建流程如下:

  • 构建quarkushop-customer映像并将其推送到容器注册表中

  • 创建quarkushop-customer-config ConfigMap文件

  • 在 Kubernetes 中创建quarkushop-customer-config ConfigMap

  • 使用健康检查、port-forward和简单的curl GET 命令对/health API 检查quarkushop-customer pod

img/509649_1_En_13_Figal_HTML.gif你会在本书的img/509649_1_En_13_Figam_HTML.gif GitHub 资源库中找到所有的代码和资源。

实施用户微服务

什么,额外的微服务?我知道我在这一章的开始没有提到这一点。img/509649_1_En_13_Figan_HTML.gif

别担心,这不是一个巨大的微服务。它用于身份验证,并将保存 Quarkushop Swagger UI monolith 中列出的user部分 REST APIs:

img/509649_1_En_13_Figao_HTML.jpg

让我们用这些扩展生成一个新的 Quarkus 应用:

  • RESTEasy JAX-RS

  • 塞西·JSON-b

  • SmallRye OpenAPI

  • 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj

  • SmallRye 健康

  • 忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈

  • 库比涅斯配置

  • 容器图像悬臂

没有与持久性相关的扩展,因为在这个微服务中你不会与数据库交互。

实施User微服务的步骤如下:

  • 复制用户相关内容。这个微服务非常少,所以只会装UserResource级。

  • 添加 Lombok 依赖项。

  • 添加quarkushop-commons依赖项。

  • 从 monolith 复制banner.txt文件。

  • 添加application.properties,更改ConfigMap名称,并删除所有数据持久性属性(Flyway、JPA 等。).

  • application.properties中添加quarkushop-commons的 Quarkus 索引依赖关系。

  • 构建quarkushop-user映像并将其推送到容器注册中心。

  • 创建quarkushop-user-config ConfigMap文件。

  • 在 Kubernetes 中创建quarkushop-user-config ConfigMap

  • 使用健康检查、port-forward和简单的curl GET 命令对/health API 检查quarkushop-user pod。

img/509649_1_En_13_Figaq_HTML.gif你会在本书的img/509649_1_En_13_Figar_HTML.gif GitHub 资源库中找到所有的代码和资源。

在部署了quarkushop-user微服务之后,您可以使用它来获得一个access_token,并与其他三个微服务的安全 API 进行通信:

img/509649_1_En_13_Figas_HTML.png

很好!微服务已正确部署并正常工作!img/509649_1_En_13_Figat_HTML.gif

结论

在本章中,您学习了如何创建微服务并将它们部署到 Kubernetes。在这个繁重的任务中,您实现了在第九章中介绍的模式。但是您没有实现最需要的模式。这是你在下一章要做的。img/509649_1_En_13_Figau_HTML.gif

十四、与 Quarkus 和 Kubernetes 一起飞遍天空

介绍

从第九章开始,您学习了云原生模式,甚至实现了其中的一些模式:

  • 服务发现和注册:使用 Kubernetes DeploymentService对象完成

  • 外部化配置:使用 Kubernetes ConfigMapSecret对象制作

  • 每个服务的数据库:在使用 DDD 概念分割整体代码库时创建的

  • 应用度量:使用 SmallRye Metrics Quarkus 扩展实现

  • 健康检查 API :使用 SmallRye Health Quarkus 扩展实现

  • 服务间的安全性:使用 SmallRye JWT 夸库扩展和 Keycloak 实现

在本章中,您将学习如何实现更流行的模式:

  • 断路器

  • 日志聚合

  • 分布式跟踪

  • 应用编程接口网关

实现断路器模式

断路器模式对于使用错误通信协议(如 HTTP)的弹性微服务非常有用。该模式的思想是处理微服务之间的任何通信问题。

img/509649_1_En_14_Figa_HTML.gif断路器模式的这种实现只会影响OrderCustomer微服务,其中我们使用 REST 客户端进行外部调用。

在基于 Quarkus 的应用中实现这种模式非常容易。第一步是将这个依赖项添加到pom.xml文件中:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-fault-tolerance</artifactId>
</dependency>

现在,我们在 REST 客户端组件上启用断路器特性。假设我们希望 REST 客户机停止调用 API 15 秒钟,如果我们在最近 10 次请求中有 50%的请求失败。这可以使用下面这行代码来完成:

@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 15000)

Order微服务中:

@Path("/products")
@RegisterRestClient
public interface ProductRestClient {

    @GET
    @Path("/{id}")
    @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 15000)
    ProductDto findById(@PathParam Long id);
}

@CircuitBreaker有许多属性:

  • failOn:应被视为失败的异常类型列表;默认值为{Throwable.class}。由带注释的方法抛出的继承自Throwable的所有异常都被认为是失败的。

  • skipOn:不应被视为失败的异常类型列表;默认值为{}

  • delay:开路转变为半开状态的延迟时间;默认值为5000

  • delayUnit:延时的单位。默认值为ChronoUnit.MILLIS

  • requestVolumeThreshold:滚动窗口中连续请求的数量。默认值为20

  • failureRatio:滚动窗口内使电路跳闸断开的故障比率。默认值为0.50

  • successThreshold:半开电路再次闭合前成功执行的次数。默认值为1

img/509649_1_En_14_Figb_HTML.gif断路器模式有三种状态:

  • 所有的请求都是正常的

  • Half-open:进行验证以检查问题是否仍然出现的过渡状态

  • Open:所有请求都被禁用,直到延迟到期

Customer微服务中:

@Path("/orders")
@RegisterRestClient
public interface OrderRestClient {

    @GET
    @Path("/{id}")
    @CircuitBreaker(requestVolumeThreshold = 10, delay = 15000)
    Optional<OrderDto> findById(@PathParam Long id);

    @GET
    @Path("/payment/{id}")
    @CircuitBreaker(requestVolumeThreshold = 10, delay = 15000)
    Optional<OrderDto> findByPaymentId(Long id);

    @POST
    @CircuitBreaker(requestVolumeThreshold = 10, delay = 15000)
    OrderDto save(OrderDto order);
}

SmallRye 容错扩展提供了许多其他有用的选项来处理故障,以便成为具有强大弹性的微服务。例如,我们有这些机制:

  • 重试机制:用于调用失败时重试的次数;
@Path("/products")
@RegisterRestClient
public interface ProductRestClient {

    @GET
    @Path("/{id}")
    @CircuitBreaker(requestVolumeThreshold = 10, delay = 15000)
    @Retry(maxRetries = 4)
    ProductDto findById(@PathParam Long id);
}

如果调用失败,img/509649_1_En_14_Figc_HTML.gif @Retry(maxRetries = 4)将运行最多四次重试。

  • 超时机制:用于定义方法执行超时。这很容易实现:
@Path("/products")
@RegisterRestClient
public interface ProductRestClient {

    @GET
    @Path("/{id}")
    @CircuitBreaker(requestVolumeThreshold = 10, delay = 15000)
    @Retry(maxRetries = 4)
    @Timeout(500)
    ProductDto findById(@PathParam Long id);
}

如果findById()调用超过 500 毫秒,那么img/509649_1_En_14_Figd_HTML.gif @Timeout(500)会让应用抛出一个TimeoutException

  • 回退机制:用于在主方法失败时调用回退(或备份)方法。这里有一个注释来完成这项工作:
public class SomeClass {

    @Inject
    @RestClient
    ProductRestClient productRestClient;

    @Fallback(fallbackMethod = "fallbackFetchProduct")
    List<ProductDto> findProductsByCategory(String category){
        return productRestClient.findProductsByCategory(category);
    }

    public List<ProductDto> fallbackFetchProduct(String category) {
        return Collections.emptyList();
    }
}

img/509649_1_En_14_Fige_HTML.gif如果productRestClient.findProductsByCategory()失败,您将从fallbackFetchProduct()方法而不是findProductsByCategory()获得响应。您可以进一步调整这个强大的机制。例如,您可以将其配置为在定义异常或特定超时后切换到回退方法。

请注意,断路器模式和容错模式在 Quarkus 框架中得到了完美的实现。

实现日志聚合模式

对于日志聚合模式,我们使用著名的 ELK(弹性搜索、日志存储和 Kibana)堆栈。这是三个开源项目:

  • Elasticsearch 是一个搜索和分析引擎。

  • Logstash 是一个服务器端数据处理管道,它同时从多个来源获取数据,对其进行转换,然后将其发送到 Elasticsearch 这样的“stash”。

  • Kibana 允许用户在 Elasticsearch 中用图表和图形可视化数据。

总之,这些工具最常用于集中和分析分布式系统中的日志。ELK 堆栈很受欢迎,因为它满足了日志分析领域的需求。

本章中 ELK 堆栈的使用案例如下:

img/509649_1_En_14_Figf_HTML.jpg

所有的微服务都会将各自的日志推送到 Logstash,Logstash 会使用 Elasticsearch 对它们进行索引。索引日志可以在以后被 Kibana 使用。

Quarkus 有一个很棒的扩展叫做quarkus-logging-gelf,它被描述为“使用 Graylog 扩展日志格式的日志,并将您的日志集中在埃尔克或 EFK。”img/509649_1_En_14_Figg_HTML.gif

What is the Graylog Extended log Format?

基于 Graylog.org 网站:“Graylog 扩展日志格式(GELF)是一种独特方便的日志格式,旨在解决传统普通系统日志的所有缺点。这一企业功能允许您从任何地方收集结构化事件,然后在眨眼之间压缩并分块它们。”

很好!Logstash 本身支持 Graylog 扩展日志格式。你只需要在配置过程中激活它。

步骤 1:将 ELK 栈部署到 Kubernetes

如何在 Kubernetes 集群中安装 ELK 堆栈?img/509649_1_En_14_Figh_HTML.gif这是个大问题!

这是一个极其简单的任务:正如我们对 Keycloak 所做的那样,我们将使用 Helm 来安装 ELK 堆栈。img/509649_1_En_14_Figi_HTML.gif

首先将官方 ELK Helm 图表库添加到我们的 Helm 客户端:

helm repo add elastic https://helm.elastic.co

接下来,我们需要更新引用:

helm repo update

如果你像我一样在 Minikube 上,你需要创建一个elasticsearch-values.yaml文件,你将使用它来定制helm install。见清单 14-1 。

# Permit co-located instances for solitary minikube virtual machines.
antiAffinity: "soft"

# Shrink default JVM heap.
esJavaOpts: "-Xmx128m -Xms128m"

# Allocate smaller chunks of memory per pod.
resources:
  requests:
    cpu: "100m"
    memory: "512M"
  limits:
    cpu: "1000m"
    memory: "512M"

# Request smaller persistent volumes.
volumeClaimTemplate:
  accessModes: [ "ReadWriteOnce" ]
  storageClassName: "standard"
  resources:
    requests:
      storage: 100M

Listing 14-1elasticsearch-values.yaml

img/509649_1_En_14_Figj_HTML.gif该配置文件是在 Minikube 上安装 Elasticsearch 时推荐使用的配置: https://github.com/elastic/helm-charts/blob/master/elasticsearch/examples/minikube/values.yaml

现在我们安装 Elasticsearch:

helm install elasticsearch elastic/elasticsearch -f ./elasticsearch-values.yaml

我们可以看到使用该命令创建了什么:

$ kubectl get all -l release=elasticsearch

NAME                         READY   STATUS    RESTARTS
pod/elasticsearch-master-0   1/1     Running   0
pod/elasticsearch-master-1   1/1     Running   0
pod/elasticsearch-master-2   1/1     Running   0

NAME                                    TYPE        CLUSTER-IP    PORT(S)
service/elasticsearch-master            ClusterIP   10.103.91.46  9200/TCP,9300/TCP
service/elasticsearch-master-headless   ClusterIP   None          9200/TCP,9300/TCP

NAME                                    READY
statefulset.apps/elasticsearch-master   3/3

img/509649_1_En_14_Figk_HTML.gif这三个主机是为了实现高可用性。我知道我们现在没有问题,但是想想黑色星期五!img/509649_1_En_14_Figl_HTML.gif

pod 处于运行状态,因此我们可以测试 Elasticsearch 9200 端口。我们可以做一个port-forward和一个curl:

$ kubectl port-forward service/elasticsearch-master 9200
Forwarding from 127.0.0.1:9200 -> 9200
Forwarding from [::1]:9200 -> 9200

$ curl localhost:9200
{
  "name" : "elasticsearch-master-1",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "UkYbL4KsSeK4boVr4rOe2w",
  "version" : {
    "number" : "7.9.2",
    ...
    "lucene_version" : "8.6.2",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

太棒了!我们可以部署基巴纳:

helm install kibana elastic/kibana --set fullnameOverride=quarkushop-kibana

我们可以看到使用该命令创建了什么:

$ kubectl get all -l release=kibana

NAME                                     READY   STATUS
pod/quarkushop-kibana-696f869668-5tcvz   1/1     Running

NAME                        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)
service/quarkushop-kibana   ClusterIP   10.107.223.6   <none>        5601/TCP

NAME                                READY   UP-TO-DATE   AVAILABLE
deployment.apps/quarkushop-kibana   1/1     1            1

NAME                                           DESIRED   CURRENT   READY
replicaset.apps/quarkushop-kibana-696f869668   1         1         1

我们将在 Kibana 服务上做一个port-forward:

kubectl port-forward service/quarkushop-kibana 5601

然后打开 Kibana UI,检查一切是否正常工作:

img/509649_1_En_14_Figm_HTML.jpg

很好!我们需要安装 Logstash,但是我们需要使用 Helm logstash-values.yaml文件定制安装;见清单 14-2 。

logstashConfig:
  logstash.yml: |
    http.host: "0.0.0.0"
    xpack.monitoring.elasticsearch.hosts: [ "http://elasticsearch-master:9200" ]
    xpack.monitoring.enabled: true
  pipelines.yml: |
    - pipeline.id: custom
      path.config: "/usr/share/logstash/pipeline/logstash.conf"
logstashPipeline:
  logstash.conf: |
    input {
      gelf {
        port => 12201
        type => gelf
      }
    }

    output {
      stdout {}
      elasticsearch {
        hosts => ["http://elasticsearch-master:9200"]
        index => "logstash-%{+YYYY-MM-dd}"
      }
    }
service:
  annotations: {}
  type: ClusterIP
  ports:
    - name: filebeat
      port: 5000
      protocol: TCP
      targetPort: 5000
    - name: api
      port: 9600
      protocol: TCP
      targetPort: 9600
    - name: gelf
      port: 12201
      protocol: UDP
      targetPort: 12201

Listing 14-2logstash-values.yaml

values.yaml文件用于配置:

  • Logstash 管道。启用gelf插件,公开默认的 12201 端口,并定义 Logstash 输出模式和到 Elasticsearch 实例的流

  • Logstash 服务定义和公开的端口

现在,让我们安装 Logstash:

helm install -f ./logstash-values.yaml logstash elastic/logstash \
      --set fullnameOverride=quarkushop-logstash

要列出创建的对象,请运行以下命令:

$ k get all -l chart=logstash

NAME                        READY   STATUS    RESTARTS
pod/quarkushop-logstash-0   1/1     Running   0

NAME                                   TYPE        CLUSTER-IP      PORT(S)
service/quarkushop-logstash            ClusterIP   10.107.204.49   5000/TCP,9600/TCP,12201/UDP
service/quarkushop-logstash-headless   ClusterIP   None            5000/TCP,9600/TCP,12201/UDP

NAME                                   READY
statefulset.apps/quarkushop-logstash   1/1

太棒了!现在,ELK 堆栈已正确部署。下一步是配置微服务以登录到 ELK 堆栈。

步骤 2:配置微服务以登录 ELK 堆栈

我们将在此步骤中进行的修改适用于:

  • quarkushop-product

  • quarkushop-order

  • quarkushop-customer

  • quarkushop-user

让我们给pom.xml文件添加扩展名:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-logging-gelf</artifactId>
</dependency>

我们需要为每个微服务ConfigMap文件定义 Logstash 服务器属性,如清单 14-3 所示。

  • ①Logstash 主机是公开 log stash 的 Kubernetes 服务。

  • ②Logstash 端口在 Logstash Kubernetes 服务中定义。

apiVersion: v1
kind: ConfigMap
metadata:
  name: quarkushop-order-config
data:
  application.properties: |-
    quarkus.datasource.jdbc.url=jdbc:postgresql://postgres:5432/order
    mp.jwt.verify.publickey.location=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
    mp.jwt.verify.issuer=http://keycloak-http.keycloak/auth/realms/quarkushop-realm
    quarkus.log.handler.gelf.enabled=true
    quarkus.log.handler.gelf.host=quarkushop-logstash     ①
    quarkus.log.handler.gelf.port=12201                   ②

Listing 14-3quarkushop-order-config.yml

让我们构建、推送容器,并再次部署到我们的 Kubernetes 集群。我们需要再次导入ConfigMaps来更新它们。

步骤 3:收集日志

一旦部署和配置好一切,我们需要访问 Kibana UI 来解析收集到的日志:

kubectl port-forward service/quarkushop-kibana 5601

转到管理➤堆栈管理➤指数管理➤基巴纳➤指数模式:

img/509649_1_En_14_Fign_HTML.jpg

从这里,单击创建索引模式以创建新的索引模式。将出现一个新屏幕:

img/509649_1_En_14_Figo_HTML.jpg

logstash-*填充索引模式名称字段,并点击下一步:

img/509649_1_En_14_Figp_HTML.jpg

在下一个屏幕上,为时间字段选择@timestamp,并点击创建索引模式:

img/509649_1_En_14_Figq_HTML.jpg

将创建一个新的索引模式,并出现一个确认屏幕:

img/509649_1_En_14_Figr_HTML.jpg

现在,如果您单击 Kibana 部分中的 Discover 菜单,您将看到日志列表:img/509649_1_En_14_Figs_HTML.gif

img/509649_1_En_14_Figt_HTML.png

太棒了。现在,我们可以密切关注所有微服务中生成的所有日志。我们可以享受 ELK stack 的强大功能。例如,我们可以创建一个自定义查询来监控日志流中特定种类的错误。

实现分布式跟踪模式

分布式跟踪模式在 Quarkus 中有一个名为quarkus-smallrye-opentracing的专用扩展。img/509649_1_En_14_Figv_HTML.gif就像日志聚合模式一样,我们需要一个分布式跟踪模式的分布式跟踪系统。

分布式跟踪系统用于收集和存储微服务架构中监控通信请求所需的定时数据,以便检测延迟问题。市面上有很多分布式追踪系统,比如 Zipkin 和 Jaeger。在本书中,我们将使用 Jaeger,因为它是由quarkus-smallrye-opentracing扩展支持的默认跟踪器。

我们需要安装 Jaeger 并配置微服务来支持它,以便收集请求跟踪。

在开始安装之前,以下是 Jaeger 生态系统的组成部分:

  • Jaeger Client :包含用于分布式跟踪的 OpenTracing API 的特定语言实现。

  • Jaeger Agent :监听 UDP 上发送的 SPANS 的网络守护程序。

  • Jaeger Collector :接收跨度并将其放入队列进行处理。这允许收集器立即返回到客户端/代理,而不是等待 SPAN 进入存储。

  • 查询:从存储中检索踪迹的服务。

  • Jaeger 控制台:一个用户界面,让你可视化你的分布式追踪数据。

Jaeger 组件架构如下所示:

img/509649_1_En_14_Figw_HTML.jpg

步骤 1:将 Jaeger 一体机部署到 Kubernetes

我们首先用清单 14-4 中显示的内容创建jaeger-deployment.yml文件。

apiVersion: apps/v1
kind: Deployment
metadata:
    labels:
        app: jaeger
        app.kubernetes.io/component: all-in-one
        app.kubernetes.io/name: jaeger
    name: jaeger
spec:
    progressDeadlineSeconds: 2147483647
    replicas: 1
    revisionHistoryLimit: 2147483647
    selector:
        matchLabels:
            app: jaeger
            app.kubernetes.io/component: all-in-one
            app.kubernetes.io/name: jaeger
    strategy:
        type: Recreate
    template:
        metadata:
            annotations:
                prometheus.io/port: "16686"
                prometheus.io/scrape: "true"
            labels:
                app: jaeger
                app.kubernetes.io/component: all-in-one
                app.kubernetes.io/name: jaeger
        spec:
            containers:
                - env:
                      - name: COLLECTOR_ZIPKIN_HTTP_PORT
                        value: "9411"
                  image: jaegertracing/all-in-one
                  imagePullPolicy: Always
                  name: jaeger
                  ports:
                      - containerPort: 5775
                        protocol: UDP
                      - containerPort: 6831
                        protocol: UDP
                      - containerPort: 6832
                        protocol: UDP
                      - containerPort: 5778
                        protocol: TCP
                      - containerPort: 16686
                        protocol: TCP
                      - containerPort: 9411
                        protocol: TCP
                  readinessProbe:
                      failureThreshold: 3
                      httpGet:
                          path: /
                          port: 14269
                          scheme: HTTP
                      initialDelaySeconds: 5
                      periodSeconds: 10
                      successThreshold: 1
                      timeoutSeconds: 1
                  resources: {}
                  terminationMessagePath: /dev/termination-log
                  terminationMessagePolicy: File
            dnsPolicy: ClusterFirst
            restartPolicy: Always
            schedulerName: default-scheduler
            securityContext: {}
            terminationGracePeriodSeconds: 30

Listing 14-4jaeger/jaeger-deployment.yml

接下来,将该文件导入 Kubernetes 集群:

kubectl apply -f jaeger/jaeger-deployment.yml

这个Deployment资源将在一个容器中部署所有 Jaeger 后端组件和 UI。

我们现在需要创建一个名为jaeger-query的负载平衡的 Kubernetes 服务对象,如清单 14-5 所示。

apiVersion: v1
kind: Service
metadata:
    name: jaeger-query
    labels:
        app: jaeger
        app.kubernetes.io/name: jaeger
        app.kubernetes.io/component: query
spec:
    ports:
        - name: query-http
          port: 80
          protocol: TCP
          targetPort: 16686
    selector:
        app.kubernetes.io/name: jaeger
        app.kubernetes.io/component: all-in-one
    type: LoadBalancer

Listing 14-5jaeger/jaeger-query-service.yml

我们还需要创建另一个名为jaeger-collector的服务,如清单 14-6 所示。

apiVersion: v1
kind: Service
metadata:
    name: jaeger-collector
    labels:
        app: jaeger
        app.kubernetes.io/name: jaeger
        app.kubernetes.io/component: collector
spec:
    ports:
        - name: jaeger-collector-tchannel
          port: 14267
          protocol: TCP
          targetPort: 14267
        - name: jaeger-collector-http
          port: 14268
          protocol: TCP
          targetPort: 14268
        - name: jaeger-collector-zipkin
          port: 9411
          protocol: TCP
          targetPort: 9411
    selector:
        app.kubernetes.io/name: jaeger
        app.kubernetes.io/component: all-in-one
    type: ClusterIP

Listing 14-6jaeger/jaeger-collector-service.yml

清单 14-7 显示了我们需要创建的最后一个,名为jaeger-agent

apiVersion: v1
kind: Service
metadata:
    name: jaeger-agent
    labels:
        app: jaeger
        app.kubernetes.io/name: jaeger
        app.kubernetes.io/component: agent
spec:
    ports:
        - name: agent-zipkin-thrift
          port: 5775
          protocol: UDP
          targetPort: 5775
        - name: agent-compact
          port: 6831
          protocol: UDP
          targetPort: 6831
        - name: agent-binary
          port: 6832
          protocol: UDP
          targetPort: 6832
        - name: agent-configs
          port: 5778
          protocol: TCP
          targetPort: 5778
    clusterIP: None
    selector:
        app.kubernetes.io/name: jaeger
        app.kubernetes.io/component: all-in-one

Listing 14-7jaeger/jaeger-agent-service.yml

我们通过 TCP 和 UDP 协议为 Jaeger 代理公开了许多端口。UDP 上的 6831 消耗跨度。这是我们用来和杰格特工联系的。

接下来,让我们创建 Kubernetes 对象:

kubectl apply -f jaeger/jaeger-query-service.yml
kubectl apply -f jaeger/jaeger-collector-service.yml
kubectl apply -f jaeger/jaeger-agent-service.yml

现在很好,让我们检查一下 Jaeger 是否安装正确。我们需要对jaeger-query服务执行port-forward:

kubectl port-forward service/jaeger-query 8888:80

然后打开localhost:8888,如截图所示:

img/509649_1_En_14_Figx_HTML.jpg

太棒了。我们现在可以转到微服务配置。img/509649_1_En_14_Figy_HTML.gif

步骤 2:在我们的微服务中启用 Jaeger 支持

第一步是将quarkus-smallrye-opentracing依赖性添加到我们的微服务中:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-opentracing</artifactId>
</dependency>

然后,我们需要定义每个微服务的 Jaeger 配置:

 1 apiVersion: v1
 2 kind: ConfigMap
 3 metadata:
 4   name: quarkushop-user-config
 5 data:
 6   application.properties: |-
 7     mp.jwt.verify.publickey.location=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
 8     mp.jwt.verify.issuer=http://keycloak-http.keycloak/auth/realms/quarkushop-realm
 9     quarkus.log.handler.gelf.enabled=true
10     quarkus.log.handler.gelf.host=quarkushop-logstash
11     quarkus.log.handler.gelf.port=12201
12     quarkus.jaeger.service-name=quarkushop-user
13     quarkus.jaeger.sampler-type=const
14     quarkus.jaeger.sampler-param=1
15     quarkus.log.console.format=%d{HH:mm:ss} %-5p traceId=%X{traceId}, spanId=%X{spanId}, sampled=%X{sampled} [%c{2.}] (%t) %s%e%n
16     quarkus.jaeger.agent-host-port=jaeger-agent:6831

新的 Jaeger 特性如下:

  • quarkus.jaeger.service-name:服务名,是微服务用来向 Jaeger 服务器呈现自己的名称。

  • quarkus.jaeger.sampler-type:示例中的采样器类型为const。我们将不断发送quarkus.jaeger.sampler-param中定义的配额。

  • quarkus.jaeger.sampler-param:在 0 和 1 之间定义的样本配额,其中 1 表示 100%的请求。

  • quarkus.log.console.format:在日志消息中添加跟踪 id。

  • quarkus.jaeger.agent-host-port:通过 UDP 与 Jaeger 代理通信的主机名和端口。我们将它指向作为主机的jaeger-agent和作为端口的 6831。

很好。让我们构建、推送容器,并再次将它们部署到我们的 Kubernetes 集群。我们还需要再次导入ConfigMaps来更新它们。

步骤 3:收集痕迹

部署 Jaeger 服务器和更新微服务后,我们需要发出一些请求来生成踪迹。然后我们就能看到耶格在抓什么了。

例如,我们可以使用quarkushop-user向 Keycloak 请求一个access_token:

kubectl port-forward service/quarkushop-user 8080

然后运行一个curlquarkushop-user微服务请求access_token:

curl -X POST "http://localhost:8080/api/user/access-token?password=password&username=nebrass"

很好!让我们做一个port-forward并访问 Jaeger,看看那里发生了什么:

kubectl port-forward service/jaeger-query 8888:80

然后打开localhost:8888,如截图所示:

img/509649_1_En_14_Figz_HTML.jpg

正如您在服务部分看到的,有三个元素。只需选择quarkushop-user并点击查找痕迹:

img/509649_1_En_14_Figaa_HTML.jpg

显示了一个跨度:

img/509649_1_En_14_Figab_HTML.jpg

如果您点按它,会出现更多详细信息:

img/509649_1_En_14_Figac_HTML.jpg

您可以看到此处显示了请求的所有详细信息:

  • 网址

  • HTTP 动词

  • 持续时间

  • 诸如此类;一切都在这里img/509649_1_En_14_Figad_HTML.gif

搞定了。我们以一种非常有效和简单的方式在 Quarkus 中实现了分布式跟踪模式!我真的很开心!img/509649_1_En_14_Figae_HTML.gif

实现 API 网关模式

一个 API 网关是一个位于 API 前面的编程门面,充当一组定义的微服务的单一入口点。

为了实现 Kubernetes,一个入口管理对集群中服务的外部访问,通常是facadeHTTP。Ingress 可以提供负载平衡、SSL 终止和基于名称的虚拟主机。

img/509649_1_En_14_Figaf_HTML.jpg

一个入口是允许入站连接到达集群服务的规则集合。它可以被配置为向服务提供外部可达的 URL、负载平衡的流量、终止 SSL、基于名称的虚拟主机等等。

一个入口控制器负责实现入口,通常带有一个负载平衡器,尽管它也可以配置您的边缘路由器或附加前端,以帮助以高可用性的方式处理流量。

让我们将 API 网关模式引入 Kubernetes。img/509649_1_En_14_Figag_HTML.gif

步骤 1:在 Minikube 中启用入口支持

第一步是在 Minikube 中启用入口支持。对于那些使用真正的 Kubernetes 集群的人(幸运的人)来说,这一步是不需要的。img/509649_1_En_14_Figah_HTML.gif

要在 Minikube 中启用入口支持,只需启动您的minikube实例,然后运行以下命令:

minikube addons enable ingress

Ingress 是 Minikube 的附加产品。img/509649_1_En_14_Figaj_HTML.gif

我们需要一个入口域名;让我们使用quarkushop.io域名。我们通过键入以下命令获得 Minikube 的 IP 地址:

$ minikube ip
192.168.39.243

然后,我们需要在这个 IP 地址的/etc/hosts文件中添加一个新条目,以便将其用于我们的自定义域quarkushop.io:

192.168.39.243  quarkushop.io

该自定义内部 DNS 条目将对目标192.168.39.243进行任何调用。img/509649_1_En_14_Figak_HTML.gif

步骤 2:创建 API 网关入口

Ingress 将指向我们的四种微服务:

  • quarkushop-product

  • quarkushop-order

  • quarkushop-customer

  • quarkushop-user

img/509649_1_En_14_Figal_HTML.jpg

入口描述符如清单 14-8 所示。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-gateway
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  rules:
  - http:
      paths:
      - path: /user
        pathType: Prefix
        backend:
          service:
            name: quarkushop-user
            port:
              number: 8080
      - path: /product
        pathType: Prefix
        backend:
          service:
            name: quarkushop-product
            port:
              number: 8080
      - path: /order
        pathType: Prefix
        backend:
          service:
            name: quarkushop-order
            port:
              number: 8080
      - path: /customer
        pathType: Prefix
        backend:
          service:
            name: quarkushop-customer
            port:
              number: 8080

Listing 14-8api-gateway-ingress.yml

只需将该内容保存到api-gateway-ingress.yml并使用以下命令创建资源:

kubectl create -f api-gateway-ingress.yml

入口已成功创建!让我们检查一下:

$ kubectl get ingress

NAME          CLASS    HOSTS           ADDRESS          PORTS   AGE
api-gateway   <none>   quarkushop.io   192.168.39.243   80      7m46s

太棒了!如你所见,ADDRESS与 Minikube IP 相同。img/509649_1_En_14_Figam_HTML.gif

步骤 3:测试入口

现在我们可以享受我们的入口了。我们可以用它向quarkushop-user微服务请求一个access_token:

$ curl -X POST "http://quarkushop.io/user/api/user/access-token?password=password&username=nebrass"

eyJhbGciOiJSUzI1NiIsIn...

万岁!我们收到了access_token请求!img/509649_1_En_14_Figan_HTML.gif Ingress 非常好用!img/509649_1_En_14_Figao_HTML.gif

结论

在这一章中,我们使用不同的 Quarkus 扩展和 Kubernetes 对象实现了许多模式。这个任务非常简单,特别是因为我们将许多任务委托给了 Kubernetes。

Hakuna matata!img/509649_1_En_14_Figap_HTML.gif我们成功实施了云原生微服务。我对可用的扩展和提供的优秀文档感到非常高兴。