Kubernetes 上的Spring

时间:2022-12-27 20:05:42

Kubernetes 上的Spring

在构建在云中运行的 Java 应用程序时,弹簧和弹簧靴显然是最受欢迎的.同样越来越明显的是,Docker和Kubernetes等技术在春季社区中发挥重要作用.

Kubernetes 上的Spring

将 Spring Boot 应用程序打包在 Docker 容器中将该应用程序部署到 Kubernetes 已经有一段时间了,而且花费很少的努力。由于“让 jar 而不是战争”的座右铭,容器化 Spring Boot 应用程序所需要的只是一个带有 JRE 的容器来运行 jar。一旦你有一个Docker容器,在Kubernetes中运行容器化的Spring Boot应用程序只是运行容器的问题。

也就是说,随着越来越多的人将Spring Boot应用程序部署到Kubernetes,很明显我们可以做得更好。为此,我们有在 Spring Boot 2.3 中进行了多项增强并且是在即将发布的 Spring Boot 2.4 版本中提供更多内容使在 Kubernetes 上运行 Spring Boot 成为更好的体验。

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

入门:start.spring.io

那么,在 Kubernetes 上运行 Spring Boot 应用程序需要什么呢?

无非是快速前往“互联网上每个人最喜欢的地方”:start.spring.io".

为应用程序创建一个目录。然后运行以下 cURL 命令以从 start.spring.io 生成应用程序:

$ curl https://start.spring.io/starter.tgz -d dependencies=webflux,actuator | tar -xzvf -

或者点击这里以使用正确的配置打开 start.spring.io,然后单击生成以下载项目。

使用基本的 Spring Boot Web 应用程序,我们现在需要创建一个 Docker 容器。在 Spring Boot 2.3 中,我们可以使用 Spring Boot Maven 或 Gradle 插件为我们执行此操作,而无需修改应用程序。为了使构建映像插件正常工作,您需要具有本地安装的 Docker.

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

构建完成后,我们现在应该为我们的应用程序提供一个 Docker 映像,我们可以使用以下命令进行检查:

$ docker images spring-k8s/gs-spring-boot-k8s

REPOSITORY TAG IMAGE ID CREATED SIZE
spring-k8s/gs-spring-boot-k8s latest 21f21558c005 40 years ago 257MB

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

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

我们可以通过向执行器/运行状况终结点发出 HTTP 请求来测试一切是否正常工作:

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

{"status":"UP"}

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

$ docker stop gs-spring-boot-k8s

关于 Kubernetes

有了我们应用程序的容器映像(只需访问 start.spring.io!),我们就准备好让我们的应用程序在 Kubernetes 上运行了。为此,我们需要两件事:

  1. The Kubernetes CLI (kubectl)
  2. 要将我们的应用程序部署到的 Kubernetes 集群

关注这些指示来安装 Kubernetes CLI。

任何 Kubernetes 集群都可以工作,但是,出于本文的目的,我们在本地启动一个集群以使其尽可能简单。在本地运行 Kubernetes 集群的最简单方法是使用一个名为类.关注这些指示以安装种类。安装 Kind 后,我们现在可以创建一个集群。

$ kind create cluster

Kind 创建集群后,会自动将 Kubernetes CLI 配置为指向该集群。要确保一切设置正确,请运行:

$ kubectl cluster-info

如果您没有看到任何错误,则可以将应用程序部署到 Kubernetes。

部署到 Kubernetes

要将我们的应用程序部署到 Kubernetes,我们需要生成一些 YAML,Kubernetes 可以使用这些 YAML 来部署、运行和管理我们的应用程序,并将该应用程序公开给集群的其余部分。

首先为我们的 YAML 创建一个目录:

$ mkdir k8s

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

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

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

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

在将这些 YAML 文件应用于我们的 Kubernetes 集群之前,我们需要将 Docker 镜像加载到 Kind 集群中。如果我们不这样做,Kubernetes 会尝试在 Docker Hub 中找到我们映像的容器,当然,该容器不存在。

$ docker tag spring-k8s/gs-spring-boot-k8s spring-k8s/gs-spring-boot-k8s:snapshot

$ kind load docker-image spring-k8s/gs-spring-boot-k8s:snapshot

我们为映像创建一个新标记,因为使用最新标记的映像的默认 Kubernetes 拉取策略是 。由于镜像在 Docker 仓库中不存在,因此我们希望使用 or 的镜像拉取策略。当使用非最新标签时,默认的 Kubernetes 拉取策略为 。​​Always​​​​Never​​​​IfNotPresent​​​​IfNotPresent​

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

$ kubectl apply -f ./k8s

然后,您可以运行:

$ 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/actuator; echo
{
"_links":{
"self":{
"href":"http://localhost:9090/actuator",
"templated":false
},
"health-path":{
"href":"http://localhost:9090/actuator/health/{*path}",
"templated":true
},
"health":{
"href":"http://localhost:9090/actuator/health",
"templated":false
},
"info":{
"href":"http://localhost:9090/actuator/info",
"templated":false
}
}
}

在继续之前,请务必停止上面的命令。​​port-forward​

最佳实践

我们的应用程序在 Kubernetes 上运行,但是,为了使我们的应用程序以最佳方式运行,我们建议实现几个最佳实践:

  1. 添加就绪和活动探测器
  2. 等待容器生命周期流程完成
  3. 启用正常关机

在文本编辑器中打开,并将就绪性、活动性和生命周期属性添加到文件中:​​k8s/deployment.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/gs-spring-boot-k8s:snapshot
name: gs-spring-boot-k8s
resources: {}
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
status: {}

这负责处理最佳做法 1 和 2。

为了解决第三个最佳实践,我们需要向应用程序配置添加一个属性。由于我们在 Kubernetes 上运行我们的应用程序,我们可以利用Kubernetes ConfigMaps将此属性外部化,就像一个好的云开发人员应该做的那样。我们现在来看看如何做到这一点。

使用配置映射将配置外部化

要在 Spring 引导应用程序中启用正常关闭,我们需要设置 .​​server.shutdown=graceful​

我们可以创建一个属性文件,该文件支持正常关闭,并公开所有执行器端点。我们可以使用执行器端点来验证我们的应用程序是否正在将属性文件从我们的配置映射添加到属性源列表中。

Create a new file called in the directory. In that file add the following properties.​​application.properties​​​​k8s​

应用程序属性

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

或者,您可以通过运行以下命令从命令行通过一个简单的步骤执行此操作。

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

创建属性文件后,我们现在可以创建配置映射与库贝克特尔。

$ kubectl create configmap gs-spring-boot-k8s --from-file=./k8s/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

最后一步是将此配置映射挂载为卷在容器中。

为此,我们需要修改部署 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/gs-spring-boot-k8s:snapshot
name: gs-spring-boot-k8s
resources: {}
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
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 ./k8s

如果激活和就绪探测器配置正确,Pod 将成功启动并转换为就绪状态。如果 Pod 从未达到就绪状态,请返回并检查就绪探测配置。如果你的 Pod 达到就绪状态,但 Kubernetes 不断重启 Pod,那么你的 liveness 探测配置不正确。如果 Pod 启动并保持运行状态,则一切正常。

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

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

现在,如果您访问​​http://localhost:9090/actuator/env​​您将看到从我们装载的卷贡献的属性源。

{
"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​

服务发现和负载平衡

对于本指南的这一部分,您应该安装库斯托米兹.Kustomize 在使用 Kubernetes 并针对不同的环境(开发、测试、暂存、生产)时是一个有用的工具。我们使用它来生成 YAML,以将另一个应用程序部署到 Kubernetes,然后我们将能够使用服务发现调用该应用程序。

运行以下命令以部署名称-服务:

$ kustomize build "github.com/ryanjbaxter/k8s-spring-workshop/name-service/kustomize/multi-replica/" | kubectl apply -f -

这应该将 部署到您的 Kubernetes 集群。部署应为 创建两个副本:​​name-service​​​​name-service​

$ kubectl get pods --selector app=k8s-workshop-name-service

NAME READY STATUS RESTARTS AGE
k8s-workshop-name-service-56b986b664-6qt59 1/1 Running 0 7m26s
k8s-workshop-name-service-56b986b664-wjcr9 1/1 Running 0 7m26s

为了演示此服务的功能,我们可以向其发出请求:

$ kubectl port-forward svc/k8s-workshop-name-service 9090:80

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

HTTP/1.1 200
k8s-host: k8s-workshop-name-service-56b986b664-6qt59
Content-Type: text/plain;charset=UTF-8
Content-Length: 4
Date: Mon, 14 Sep 2020 15:37:51 GMT

Paul

如果发出多个请求,应会看到返回不同的名称。另请注意标题:。这应与为请求提供服务的 Pod 的 ID 一致。​​k8s-host​

使用端口转发命令时,它只会向单个 Pod 发出请求,因此您将在响应中只看到一个主机。

请务必在继续之前停止该命令。​​port-forward​

在服务运行的情况下,我们可以修改应用程序以向 .​​name-service​

Kubernetes 设置 DNS 条目,以便我们可以使用服务 ID 向服务发出 HTTP 请求,而无需知道 Pod 的 IP 地址。Kubernetes 服务还会在所有 Pod 之间对这些请求进行负载均衡。​​name-service​

在应用程序中,在 中打开。修改代码,如下所示:​​DemoApplication.java​​​​src/main/java/com/example/demo​

package com.example.demo;

import reactor.core.publisher.Mono;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;

@SpringBootApplication
@RestController
public class DemoApplication {

private WebClient webClient = WebClient.create();

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

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

}
}

请注意,请求中的 URL 是 。这是我们在 Kubernetes 中的服务的 ID。​​WebClient​​​​k8s-workshop-name-service​

由于我们更新了应用程序代码,我们需要构建一个新映像并将其部署到 Kubernetes:

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

$ docker tag spring-k8s/gs-spring-boot-k8s:latest spring-k8s/gs-spring-boot-k8s:snapshot

$ kind load docker-image spring-k8s/gs-spring-boot-k8s:snapshot

部署新映像的一种简单方法是删除应用程序 Pod。Kubernetes 会自动使用我们刚刚加载到集群中的新映像创建另一个 Pod。

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

新 Pod 启动并运行后,您可以将请求转发到服务:

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

现在,如果您向服务发出请求,您应该看到请求发送到名称服务的哪个 pod:

$ curl http://localhost:9090; echo

Hello Paul from k8s-workshop-name-service-56b986b664-wjcr9

验证负载平衡可能更具挑战性。您可以持续发出相同的 cURL 请求,并观察容器 ID 是否更改。像手表这样的工具可能非常有用:

$ watch -n 1 curl http://localhost:9090

监视命令每秒发出 cURL 请求。缺点是您必须监视终端并等待。不过,最终,您应该会注意到 pod ID 的变化。

查看事物切换的更快方法是运行 watch 命令,然后删除当前正在处理请求的 pod:

$ kubectl delete pod k8s-workshop-name-service-56b986b664-wjcr9

执行此操作时,应立即注意到监视命令中的 pod ID 更改。

在 Kubernetes 上运行 Spring Boot 应用程序只需要访问start.spring.io.Spring Boot 的目标一直是使构建和运行 Java 应用程序尽可能简单,无论您选择如何运行应用程序,我们都会尝试实现这一点。使用 Kubernetes 构建云原生应用程序只涉及创建一个使用 Spring Boot 内置映像生成器的映像并利用 Kubernetes 平台的功能。在 Spring Boot 和 Kubernetes 的第二部分中,我们将了解 Spring Cloud 如何融入这个故事。