Spring Boot 与 Docker
观察 GraphQL 的实际运行

你将构建什么

Spring 环境中的 Kubernetes 正在逐渐成熟。根据 2024 年 Spring 调查,65% 的受访者在其 Spring 环境中使用了 Kubernetes。

在 Kubernetes 上运行 Spring Boot 应用程序之前,您首先需要生成一个容器镜像。Spring Boot 支持使用 Cloud Native Buildpacks 通过 Maven 或 Gradle 插件轻松生成 Docker 镜像。

本指南的目标是向您展示如何在 Kubernetes 上运行 Spring Boot 应用程序,并利用平台的多种功能来构建云原生应用程序。

在本指南中,您将构建两个 Spring Boot Web 应用程序。您将使用 Cloud Native Buildpacks 将每个 Web 应用程序打包到 Docker 镜像中,基于该镜像创建一个 Kubernetes 部署,并创建一个服务以访问该部署。

所需条件

  • 一个喜欢的文本编辑器或 IDE

  • Java 17 或更高版本

  • 一个 Docker 环境

  • 一个 Kubernetes 环境

Docker Desktop 提供了本指南所需的 Docker 和 Kubernetes 环境。

如何完成本指南

本指南重点介绍创建在 Kubernetes 上运行 Spring Boot 应用程序所需的工件。因此,最佳的方法是使用此仓库中提供的代码。

该仓库提供了我们将要使用的两个服务:

  • hello-spring-k8s 是一个基础的 Spring Boot REST 应用程序,它会返回一个 "Hello World" 的消息。

  • hello-caller 会调用 Spring Boot REST 应用程序 hello-spring-k8shello-caller 服务用于演示在 Kubernetes 环境中服务发现的工作原理。

这两个应用程序都是 Spring Boot REST 应用程序,并且可以使用本指南从零开始创建。随着课程的进行,本指南的具体代码将在下面列出。

本指南分为不同的部分。

在解决方案仓库中,您会发现 Kubernetes 的构件已经创建好了。本指南将逐步引导您创建这些对象,但您可以随时参考解决方案中的工作示例。

生成 Docker 镜像

首先,使用Cloud Native Buildpacks生成hello-spring-k8s项目的 Docker 镜像。在hello-spring-k8s目录下,运行以下命令:

$ ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=spring-k8s/hello-spring-k8s

这将生成一个名为 spring-k8s/hello-spring-k8s 的 Docker 镜像。构建完成后,我们现在应该已经有了应用程序的 Docker 镜像,可以通过以下命令进行确认:

$ docker images spring-k8s/hello-spring-k8s

REPOSITORY                    TAG       IMAGE ID       CREATED        SIZE
spring-k8s/hello-spring-k8s   latest    <ID>        44 years ago   325MB

现在我们可以启动容器镜像并确保它正常工作:

$ docker run -p 8080:8080 --name hello-spring-k8s -t spring-k8s/hello-spring-k8s

我们可以通过向 actuator/health 端点发起 HTTP 请求来测试一切是否正常:

$ curl http://localhost:8080/actuator/health

{"status":"UP"}

在继续之前,请确保停止正在运行的容器。

$ docker stop hello-spring-k8s

Kubernetes 要求

有了我们的应用程序的容器镜像(仅需访问 start.spring.io!),我们就可以准备将应用程序运行在 Kubernetes 上了。为此,我们需要两样东西:

  1. Kubernetes CLI (kubectl)
  2. 用于部署我们应用程序的 Kubernetes 集群

按照这些说明来安装 Kubernetes CLI。

任何 Kubernetes 集群都可以工作,但为了本文的目的,我们在本地启动一个集群,以使其尽可能简单。在本地运行 Kubernetes 集群的最简单方法是使用 Docker Desktop

在本教程中使用了一些常见的 Kubernetes 标志,值得注意。--dry-run=client 标志告诉 Kubernetes 仅打印将要发送的对象,而不实际发送它。-o yaml 标志指定命令的输出应为 yaml 格式。这两个标志与输出重定向 > 结合使用,以便将 Kubernetes 命令捕获到文件中。这对于在创建之前编辑对象以及创建可重复的过程非常有用。

部署到 Kubernetes

本部分的解决方案定义在 k8s-artifacts/basic/* 中。

要将我们的 hello-spring-k8s 应用程序部署到 Kubernetes,我们需要生成一些 YAML 文件,Kubernetes 可以使用这些文件来部署、运行和管理我们的应用程序,并将该应用程序暴露给集群中的其他部分。

如果您选择自己构建 YAML 文件而不是运行提供的解决方案,首先为您的 YAML 文件创建一个目录。这个文件夹的位置无关紧要,因为我们生成的 YAML 文件不会依赖于路径。

$ mkdir k8s
$ cd k8s

现在我们可以使用 kubectl 生成所需的基本 YAML:

$ kubectl create deployment gs-spring-boot-k8s --image spring-k8s/spring-k8s/hello-spring-k8s:latest -o yaml --dry-run=client > deployment.yaml

由于我们使用的镜像是本地的,因此需要更改部署中容器的 imagePullPolicy。现在 yaml 的 containers: 配置应如下所示:

    spec:
      containers:
      * image: spring-k8s/hello-spring-k8s
        imagePullPolicy: Never
        name: hello-spring-k8s
        resources: {}

如果您尝试在不修改 imagePullPolicy 的情况下运行部署,您的 pod 将显示 ErrImagePull 状态。

deployment.yaml 文件告诉 Kubernetes 如何部署和管理我们的应用程序,但它并没有让我们的应用程序成为其他应用程序的网络服务。为此,我们需要一个服务资源。Kubectl 可以帮助我们生成服务资源的 YAML 文件:

$ kubectl create service clusterip gs-spring-boot-k8s --tcp 80:8080 -o yaml --dry-run=client > service.yaml

现在我们已经准备好将 YAML 文件应用到 Kubernetes 上了:

$ kubectl apply -f deployment.yaml
$ kubectl apply -f service.yaml

然后您可以运行:

$ kubectl get all

您应该会看到我们新创建的部署、服务和 Pod 正在运行:

NAME                                      READY   STATUS    RESTARTS   AGE
pod/gs-spring-boot-k8s-779d4fcb4d-xlt9g   1/1     Running   0          3m40s

NAME                         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/gs-spring-boot-k8s   ClusterIP   10.96.142.74   <none>        80/TCP    3m40s
service/kubernetes           ClusterIP   10.96.0.1      <none>        443/TCP   4h55m

NAME                                 READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/gs-spring-boot-k8s   1/1     1            1           3m40s

NAME                                            DESIRED   CURRENT   READY   AGE
replicaset.apps/gs-spring-boot-k8s-779d4fcb4d   1         1         1       3m40s

不幸的是,我们无法直接向 Kubernetes 中的服务发起 HTTP 请求,因为它并未暴露在集群网络之外。借助 kubectl,我们可以将本地机器的 HTTP 流量转发到集群中运行的服务:

$ kubectl port-forward svc/gs-spring-boot-k8s 9090:80

通过运行 port-forward 命令,我们现在可以向 localhost:9090 发起 HTTP 请求,该请求会被转发到在 Kubernetes 中运行的服务:

$ curl http://localhost:9090/helloWorld
Hello World!!

在继续之前,请确保停止上面提到的port-forward命令。

最佳实践

本部分的解决方案定义在 k8s-artifacts/best_practice/* 中。

我们的应用程序运行在 Kubernetes 上,但为了使应用程序能够以最佳状态运行,我们建议实施以下最佳实践:

  1. 添加就绪和存活探针

  2. 启用优雅关机

在文本编辑器中打开 deployment.yaml 文件,并添加 readiness 和 liveness 属性到您的文件中:

k8s/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: gs-spring-boot-k8s
  name: gs-spring-boot-k8s
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gs-spring-boot-k8s
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: gs-spring-boot-k8s
    spec:
      containers:
      * image: spring-k8s/hello-spring-k8s
        imagePullPolicy: Never
        name: hello-spring-k8s
        resources: {}
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
status: {}

这将解决第一个最佳实践。此外,我们需要在应用配置中添加一个属性。由于我们在 Kubernetes 上运行我们的应用程序,我们可以利用 Kubernetes ConfigMaps 来外部化这个属性,作为一个优秀的云开发者应该做的。现在我们来看看如何做到这一点。

使用 ConfigMaps 来外部化配置

本节中的解决方案在 k8s-artifacts/config_map/* 中定义。

要在 Spring Boot 应用程序中启用优雅关闭,我们可以在 application.properties 中设置 server.shutdown=graceful。与其直接将这行代码添加到我们的代码中,不如使用一个 ConfigMap。我们可以使用 Actuator 端点来验证我们的应用程序是否将来自 ConfigMap 的属性文件添加到 PropertySources 列表中。

我们可以创建一个属性文件,启用优雅关闭并暴露所有的 Actuator 端点。我们可以使用 Actuator 端点来验证我们的应用程序是否将来自 ConfigMap 的属性文件添加到 PropertySources 列表中。

在存放 yaml 文件的目录中创建一个名为 application.properties 的新文件。在该文件中添加以下属性。

application.properties

server.shutdown=graceful
management.endpoints.web.exposure.include=*

或者,您可以通过运行以下命令在命令行中轻松一步完成此操作。

$ cat <<EOF >./application.properties
server.shutdown=graceful
management.endpoints.web.exposure.include=*
EOF

创建了我们的属性文件后,我们现在可以使用 kubectl 创建一个 ConfigMap

$ kubectl create configmap gs-spring-boot-k8s --from-file=./application.properties

创建好我们的 ConfigMap 后,我们可以看看它的样子:

$ kubectl get configmap gs-spring-boot-k8s -o yaml
apiVersion: v1
data:
  application.properties: |
    server.shutdown=graceful
    management.endpoints.web.exposure.include=*
kind: ConfigMap
metadata:
  creationTimestamp: "2020-09-10T21:09:34Z"
  name: gs-spring-boot-k8s
  namespace: default
  resourceVersion: "178779"
  selfLink: /api/v1/namespaces/default/configmaps/gs-spring-boot-k8s
  uid: 9be36768-5fbd-460d-93d3-4ad8bc6d4dd9

最后一步是将这个 ConfigMap 作为卷挂载到容器中。

为此,我们需要修改部署 YAML 文件,首先创建卷,然后将该卷挂载到容器中:

k8s/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: gs-spring-boot-k8s
  name: gs-spring-boot-k8s
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gs-spring-boot-k8s
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: gs-spring-boot-k8s
    spec:
      containers:
      * image: spring-k8s/hello-spring-k8s
        imagePullPolicy: Never
        name: hello-spring-k8s
        resources: {}
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
        volumeMounts:
          * name: config-volume
            mountPath: /workspace/config
      volumes:
        * name: config-volume
          configMap:
            name: gs-spring-boot-k8s
status: {}

在我们实施了所有最佳实践之后,我们可以将新的部署应用到 Kubernetes 上。这将部署一个新的 Pod 并停止旧的 Pod(只要新的 Pod 成功启动)。

$ kubectl apply -f deployment.yaml

如果您的存活探针和就绪探针配置正确,Pod 会成功启动并进入就绪状态。如果 Pod 从未达到就绪状态,请返回并检查您的就绪探针配置。如果 Pod 达到就绪状态但 Kubernetes 不断重启 Pod,则您的存活探针配置不当。如果 Pod 启动并保持运行,则一切正常。

您可以通过访问 /actuator/env 端点来验证 ConfigMap 卷是否已挂载,以及应用程序是否正在使用属性文件。

$ kubectl port-forward svc/gs-spring-boot-k8s 9090:80

现在,如果您访问 http://localhost:9090/actuator/env,您将会看到从我们挂载的卷中提供的属性源。

curl http://localhost:9090/actuator/env | jq
{
   "name":"applicationConfig: [file:./config/application.properties]",
   "properties":{
      "server.shutdown":{
         "value":"graceful",
         "origin":"URL [file:./config/application.properties]:1:17"
      },
      "management.endpoints.web.exposure.include":{
         "value":"*",
         "origin":"URL [file:./config/application.properties]:2:43"
      }
   }
}

继续之前,请确保停止 port-forward 命令。

服务发现与负载均衡

本指南的这部分内容添加了 hello-caller 应用程序。该部分的解决方案定义在 k8s-artifacts/service_discovery/* 中。

为了演示负载均衡,首先让我们将现有的 hello-spring-k8s 服务扩展到 3 个副本。这可以通过在您的部署中添加 replicas 配置来实现。

...
metadata:
  creationTimestamp: null
  labels:
    app: gs-spring-boot-k8s
  name: gs-spring-boot-k8s
spec:
  replicas: 3
  selector:
...

通过运行以下命令更新部署:

kubectl apply -f deployment.yaml

现在我们应该看到有 3 个 pod 在运行:

$ kubectl get pod --selector=app=gs-spring-boot-k8s

NAME                                  READY   STATUS    RESTARTS   AGE
gs-spring-boot-k8s-76477c6c99-2psl4   1/1     Running   0          15m
gs-spring-boot-k8s-76477c6c99-ss6jt   1/1     Running   0          3m28s
gs-spring-boot-k8s-76477c6c99-wjbhr   1/1     Running   0          3m28s

我们需要为这一部分运行第二个服务,所以让我们把注意力转向hello-caller。这个应用程序有一个单一的端点,它会调用hello-spring-k8s。请注意,URL与 Kubernetes 中的服务名称相同。

    @GetMapping
    public Mono<String> index() {
        return webClient.get().uri("http://gs-spring-boot-k8s/name")
                .retrieve()
                .toEntity(String.class)
                .map(entity -> {
                    String host = entity.getHeaders().get("k8s-host").get(0);
                    return "Hello " + entity.getBody() + " from " + host;
                });

    }

Kubernetes 设置了 DNS 条目,这样我们可以使用 hello-spring-k8s 的服务 ID 向服务发起 HTTP 请求,而无需知道 Pod 的 IP 地址。Kubernetes 服务还会在这些请求之间进行负载均衡。

现在我们需要将 hello-caller 应用程序打包为 Docker 镜像,并将其作为 Kubernetes 资源运行。为了生成 Docker 镜像,我们将再次使用 Cloud Native Buildpacks。在 hello-caller 文件夹中,运行以下命令:

./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=spring-k8s/hello-caller

当 Docker 镜像创建完成后,您可以创建一个与我们已经见过的类似的部署。caller_deployment.yaml 文件中提供了一个完整的配置。运行这个文件:

kubectl apply -f caller_deployment.yaml

我们可以使用以下命令来验证应用程序是否正在运行:

$ kubectl get pod --selector=app=gs-hello-caller

NAME                               READY   STATUS    RESTARTS   AGE
gs-hello-caller-774469758b-qdtsx   1/1     Running   0          2m34s

我们还需要按照提供的文件 caller_service.yaml 中的定义创建一个服务。该文件可以通过以下命令运行:

kubectl apply -f caller_service.yaml

现在您已经运行了两个部署和两个服务,可以准备测试应用程序了。

$ kubectl port-forward svc/gs-hello-caller 9090:80

$ curl http://localhost:9090 -i; echo

HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 4
Date: Mon, 14 Sep 2020 15:37:51 GMT

Hello Paul from gs-spring-boot-k8s-76477c6c99-5xdq8

如果您发出多个请求,您应该会看到返回不同的名称。Pod 名称也会在请求中列出。如果您提交多个请求,这个值也会发生变化。在等待 Kubernetes 负载均衡器选择不同的 Pod 时,您可以通过删除最近返回请求的 Pod 来加快这一过程。

$ kubectl delete pod gs-spring-boot-k8s-76477c6c99-5xdq8

总结

在 Kubernetes 上运行 Spring Boot 应用程序只需要访问 start.spring.io。Spring Boot 的目标始终是使构建和运行 Java 应用程序尽可能简单,我们努力实现这一点,无论您选择如何运行您的应用程序。使用 Kubernetes 构建云原生应用程序只需要创建一个使用 Spring Boot 内置镜像构建器的镜像,并充分利用 Kubernetes 平台的功能。

本页目录