15.2 为生产环境配置 Spring Boot
将应用部署到生产环境需要将发布候选与其配置结合起来。现在我们已验证了发布候选已为生产环境准备就绪,是时候定制它的配置了。
我们距离把云原生应用部署到生产环境的 Kubernetes 集群越来越近了。到目前为止,我们一直使用 minikube 配合本地集群进行试验。现在我们需要一个完整的 Kubernetes 集群作为生产环境。在继续阅读本节之前,请先按照附录 B(B.1 至 B.6)的说明,在 DigitalOcean 公有云中初始化一个 Kubernetes 集群。如果你希望使用不同的云服务商,那里也有一些提示供你参考。
一旦在云中拥有了一个正在运行的 Kubernetes 集群,就可以继续阅读本节。本节将涉及那些在我们把 Spring Boot 应用部署到生产环境之前需要提供的额外配置。
在上一章中,你学习了自动化配置的 Kustomize 和 overlay 技术,用于在公共基础之上管理不同部署环境的定制化。你也尝试过为 staging 环境定制 Catalog Service 的部署。在本节我们会为生产环境做类似的事情。在第 14 章介绍的基础上扩展,我会向你展示如何为 ConfigMap 和 Secret 定制 volume 挂载。同时,你还会看到如何为 Kubernetes 中运行的容器配置 CPU 和内存,并进一步了解 Paketo Buildpacks 如何在每个容器中管理 Java 虚拟机(JVM)的资源。
15.2.1 为生产环境定义配置 overlay
首先,需要定义一个新的 overlay 来针对生产环境定制 Catalog Service 的部署。你应记得,在上一章中 Catalog Service 的 Kustomize base 存放在 catalog-service 仓库中。我们把 overlay 放在 polar-deployment 仓库中。
接下来,在 kubernetes/applications/catalog-service(位于 polar-deployment 仓库中)下创建一个新的 "production" 文件夹。我们将用它存放与生产环境相关的所有自定义配置。任何 base 或 overlay 都需要一个 kustomization.yml 文件,所以让我们为 production overlay 创建一个。请记住,在下面的清单中,将 <your_github_username> 替换为你的 GitHub 用户名(全部小写)。<release_sha> 替换为 Catalog Service 最近一个发布候选的唯一标识,你可以从 catalog-service GitHub 仓库主页的 Packages 区域获得该标识。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- github.com/<your_github_username>/catalog-service/k8s?ref=<release_sha>
清单 15.5 在远程 base 之上为生产环境定义一个 overlay(?ref=<release_sha> 指定标识你最新发布候选的 git commit hash)
注意:我假设你为 Polar Bookshop 创建的所有 GitHub 仓库都是公开的。如果情况并非如此,你可以进入 GitHub 上的具体仓库页面,访问该仓库的 Settings 部分。滚动到设置页面的底部,通过点击 Change Visibility 按钮使 package 公开。
定制环境变量
我们要做的第一项自定义,是添加一个环境变量来激活 Catalog Service 的 prod Spring profile。遵循与上一章相同的方式,在 Catalog Service 的 production overlay 中创建一个 patch-env.yml 文件(位于 catalog-service/production)。
apiVersion: apps/v1
kind: Deployment
metadata:
name: catalog-service
spec:
template:
spec:
containers:
- name: catalog-service
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
清单 15.6 用于定制容器中环境变量的 patch(定义要激活哪些 Spring profile)
接下来,我们需要指示 Kustomize 应用该 patch。在 Catalog Service 的 production overlay 的 kustomization.yml 文件中,将 patch-env.yml列入其中。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- github.com/<your_github_username>/catalog-service/k8s?ref=<release_sha>
patchesStrategicMerge:
- patch-env.yml
清单 15.7 让 Kustomize 应用环境变量的 patch(包含要应用的 patch 列表,采用 strategic merge 策略;用于定制传递给 Catalog Service 容器的环境变量的 patch)
定制 Secret 和卷
在上一章中,你学习了如何定义 ConfigMap 和 Secret,以及如何将它们以 volume 形式挂载到 Spring Boot 容器中。在基础 Kustomization 中没有配置任何 Secret,因为我们依赖开发环境使用的相同默认值。在生产环境中,我们需要传入不同的 URL 和凭据,以便 Catalog Service 能够访问 PostgreSQL 数据库和 Keycloak。
当你在 DigitalOcean 上搭建生产环境时,已经创建了一个保存 PostgreSQL 数据库访问凭据的 Secret(polar-postgres-catalog-credentials),还创建了另一个用于 Keycloak 的 Secret(keycloak-issuer-resourceserver-secret)。现在,我们可以像第 14 章挂载 ConfigMap 一样,把它们作为 volume 挂载到 Catalog Service 容器中。我们将在专门的 patch 中进行(secrets 挂载为 volume)。
在 Catalog Service 的 production overlay 文件夹(catalog-service/production)中创建一个 patch-volumes.yml 文件,并按清单 15.8 中所示进行配置。当 Kustomize 将此 patch 应用到 base deployment 清单时,它会把 base 中定义的 ConfigMap volume 与 patch 中定义的 Secret volumes 合并。
apiVersion: apps/v1
kind: Deployment
metadata:
name: catalog-service
spec:
template:
spec:
containers:
- name: catalog-service
volumeMounts:
- name: postgres-credentials
mountPath: /workspace/secrets/postgres
- name: keycloak-issuer-resourceserver-secret
mountPath: /workspace/secrets/keycloak
volumes:
- name: postgres-credentials
secret:
secretName: polar-postgres-catalog-credentials
- name: keycloak-issuer-resourceserver-secret
secret:
secretName: keycloak-issuer-resourceserver-secret
清单 15.8 将 Secret 作为 volumes 挂载到 Catalog Service 的容器(挂载包含 PostgreSQL 凭据的 Secret 卷;挂载包含 Keycloak issuer URL 的卷;从包含 PostgreSQL 凭据的 Secret 定义卷;从包含 Keycloak issuer URL 的卷定义)
然后,就像你在上一节学到的那样,需要在生产 overlay 的 kustomization.yml 文件中引用这个 patch。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- github.com/<your_github_username>/catalog-service/k8s?ref=<release_sha>
patchesStrategicMerge:
- patch-env.yml
- patch-volumes.yml
清单 15.9 让 Kustomize 应用挂载 Secret 的 patch
目前,这些 Secret 已经配置为提供给容器使用,但 Spring Boot 还不知道它们。下一节我将展示如何指示 Spring Boot 将这些 Secret 作为配置树(config tree)加载。
定制 ConfigMap
Catalog Service 的基础 Kustomization 会产生一个 catalog-config ConfigMap,基于一个 application.yml 文件。上一章你学到,可以让 Kustomize 在同一个 ConfigMap 中添加另一个文件 application-prod.yml,我们知道它会覆盖 base 的 application.yml 文件。这就是我们为生产环境定制应用配置的方式。
首先,在 Catalog Service 的生产 overlay(catalog-service/production)中创建一个 application-prod.yml 文件。我们使用这个属性文件配置自定义问候语(greeting)。为了从 Secret 挂载的 Volume 中加载配置,我们需要使用 spring.config.import 属性让 Spring Boot 加载这些 Secret 作为配置树(config tree)。关于 config tree 的更多信息,请参阅第 14 章。
polar:
greeting: Welcome to our book catalog from a production Kubernetes environment!
spring:
config:
import: configtree:/workspace/secrets/*/
清单 15.10 为 Catalog Service 提供的生产专属配置(导入挂载 Secret 的目录路径)。请注意,configtree 的路径末尾必须包含斜杠(/),否则配置导入将失败。
接下来,我们可以依赖 Kustomize 提供的 ConfigMap Generator,将定义在生产 overlay 中的 application-prod.yml 与定义在 base Kustomization 中的 application.yml 合并到同一个 catalog-config ConfigMap 中。现在来更新生产 overlay 的 kustomization.yml 文件。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- github.com/<your_github_username>/catalog-service/k8s?ref=<release_sha>
patchesStrategicMerge:
- patch-env.yml
- patch-volumes.yml
configMapGenerator:
- behavior: merge
files:
- application-prod.yml
name: catalog-config
清单 15.11 在同一个 ConfigMap 中合并属性文件(将此 ConfigMap 与 base Kustomization 中定义的合并;在此 ConfigMap 中添加到文件中的额外配置文件;被合在一处的 ConfigMap 名)
定制镜像名称和版本
下一步是更新镜像名称和版本,遵循我们在上一章使用的相同流程验证。这一次,我们能够为容器镜像(即我们的发布候选)使用正式的版本号。
首先,确保你的电脑上安装了 kustomize CLI。可以在 https://kustomize.io 看到安装说明。如果你的系统是 macOS 或 Linux,可以用以下命令安装 kustomize:brew install kustomize。
然后打开终端,进入 Catalog Service 的 production overlay 目录(catalog-service/production),用 kustomize edit set image 命令设置正确的镜像。按照 Kustomize 的用法,你会用下面的命令来定义 catalog-service 容器要使用的镜像和版本。记得把 <your_github_username> 替换为你的 GitHub 用户名(小写)。同时,把 <sha> 替换为 Catalog Service 最近一个发布候选的唯一标识。你可以在 catalog-service GitHub 仓库主页的 Packages 区域找到版本号:
$ kustomize edit set image \
catalog-service=ghcr.io/<your_github_username>/catalog-service:<sha>
这个命令会自动用新的配置更新`kustomization.yml 文件,效果如下:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- github.com/<your_github_username>/catalog-service/k8s?ref=<release_sha>
patchesStrategicMerge:
- patch-env.yml
- patch-volumes.yml
configMapGenerator:
- behavior: merge
files:
- application-prod.yml
name: catalog-config
images:
- name: catalog-service
newName: ghcr.io/<your_github_username>/catalog-service
newTag: <release_sha>
清单 15.12 为容器定制镜像名称和版本(在 Deployment manifest 中定义的容器名称;新的容器镜像名(包含小写 GitHub 用户名);容器的新标签(包含你的发布候选唯一标识))
注意:发布到 GitHub Container Registry 的镜像与对应的 GitHub 代码仓库具有相同的可见性。我假设你为 Polar Bookshop 构建的所有镜像都可以通过 GitHub Container Registry 公开访问。如果情况不是这样,请进入 GitHub 的相关仓库页面并访问 Packages 区域。然后从侧边栏菜单中选择 Package Settings,滚动到设置页面底部,点击 Change Visibility 按钮使包可以使用。
目前,我们在两个地方使用了发布候选标识:远程 base 的 URL 和镜像 tag。每当一个新的发布候选升级到生产环境,我们都需要记住同时更新这两者。更好的做法是自动化这些更新。我将在下一节实现部署流水线的生产阶段时进行描述。
定制副本数量
云原生应用应当具备高可用性,但 Catalog Service 默认只部署了一个实例。与我们在 staging 环境中所做的类似,让我们增加部署的实例数量。
打开 Catalog Service 生产 overlay 中的 kustomization.yml 文件(catalog-service/production),把 catalog-service 容器的副本数量定义为两个。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- github.com/<your_github_username>/catalog-service/k8s?ref=<release_sha>
patchesStrategicMerge:
- patch-env.yml
- patch-volumes.yml
configMapGenerator:
- behavior: merge
files:
- application-prod.yml
name: catalog-config
images:
- name: catalog-service
newName: ghcr.io/<your_github_username>/catalog-service
newTag: <release_sha>
replicas:
- name: catalog-service
count: 2
清单 15.13 为容器定义副本数量(你在定义副本数量的 Deployment 名称;副本数量)
注意:在真实场景中,你可能希望 Kubernetes 根据当前工作负载动态伸缩应用,而不是提供固定数量。动态伸缩是任何云平台的关键功能。在 Kubernetes 中,它由一个名为 Horizontal Pod Autoscaler 的专用组件实现,基于明确定义的指标,例如每个容器的 CPU 消耗等。更多信息可参阅 Kubernetes 文档(https://kubernetes.io/docs)。
下一节将介绍如何为 Kubernetes 中运行的 Spring Boot 容器配置 CPU 和内存。
15.2.2 为 Spring Boot 容器配置 CPU 和内存
处理容器化应用时,最好显式地指定资源限制。在第 2 章你已经了解到,容器是借助 Linux 特性(如 namespaces 和 cgroups)进行的隔离上下文,为进程划分并限制资源。但假如你不指定任何资源限制,那么每个容器都将可以使用宿主机的全部 CPU 和内存,由此部分容器可能会占用超过自身应得的资源份额,导致其他容器因资源不足而崩溃。
对于像 Spring Boot 这样的 JVM 应用,定义 CPU 和内存限制尤其重要,因为 JVM 会用它来正确地配置 JVM 线程池、堆内存和非堆内存等项。配置这些值一直是 Java 开发人员的挑战之一,而这个挑战之所以关键,是因为它们直接影响应用性能。幸运的是,如果你使用 Spring Boot 自带的 Paketo 实现的 Cloud Native Buildpacks,你就不必担心这些了。在第 6 章,当我们用 Paketo 打包 Catalog Service 应用时,会自动包含一个 Java Memory Calculator 组件。当你运行容器化应用时,该组件会根据分配给容器的资源限制来配置 JVM 内存。如果不指定任何限制,结果将是不可预测的,这不是你想要的。
还要考虑经济因素。如果你在公有云上运行应用,一般是根据你消耗多少资源收费的。因此,你很可能希望确切地控制你的每个容器能使用多少 CPU 和内存,以免在账单来临时受到惊吓。
在类似 Kubernetes 这样的编排器等场景中,还有另一个与资源相关的关键问题需要考虑:Kubernetes 将 Pod 调度(deploy)到集群的任何节点上,但如果一个 Pod 被分配到一个没有足够资源的节点上,会发生什么?解决办法是声明容器运行所需的最小 CPU 和内存(resource requests)。Kubernetes 会使用该信息来决定将 Pod 部署到某个特定节点,但前提是能保证容器至少可以获得所请求的资源。
资源请求(requests)和限制(limits)是按容器定义的。你可以在 Deployment manifest 中同时指定 requests 和 limits。我们目前没有在 Catalog Service 的 base 清单中定义任何限制,因为我们一直在本地环境运行,不想过度约束资源。然而,生产环境的工作负载应该始终包含资源配置。让我们看看如何在 Catalog Service 的生产部署中做到这一点。
为容器设置资源请求和限制
自然而然地,我们会使用一个 patch 来应用 CPU 和内存配置。在 Catalog Service 生产 overlay 文件夹中创建一个 patch-resources.yml 文件(catalog-service/production),并定义容器资源的 requests 和 limits。虽然我们面对的是生产场景,但为了优化集群的资源占用并避免产生额外成本,我们配置的值较低。在真实世界中,你可能需要更仔细地分析哪种 requests 和 limits 适合你的场景。
apiVersion: apps/v1
kind: Deployment
metadata:
name: catalog-service
spec:
template:
spec:
containers:
- name: catalog-service
resources:
requests:
memory: 756Mi
cpu: 0.2
limits:
cpu: 2
memory: 756Mi
清单 15.14 为容器配置资源请求和限制(容器所需的最小资源量;最小 756 MiB 内存保证;0.2 等价于 0.2 个 CPU 单位的 CPU 消耗;容器允许使用的最大资源量;最多 2 个 CPU 单位;内存最多 756 MiB)
接下来,打开 Catalog Service 生产 overlay 中的 kustomization.yml 文件,将 patch-resources.yml 添加到 patchesStrategicMerge 列表中。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- github.com/<your_github_username>/catalog-service/k8s?ref=<release_sha>
patchesStrategicMerge:
- patch-env.yml
- patch-resources.yml
- patch-volumes.yml
configMapGenerator:
- behavior: merge
files:
- application-prod.yml
name: catalog-config
images:
- name: catalog-service
newName: ghcr.io/<your_github_username>/catalog-service
newTag: <release_sha>
replicas:
- name: catalog-service
count: 2
清单 15.15 应用用于定义资源请求的 patch
在清单 15.14 中,内存请求和限制是相同的,但 CPU 却不同。以下部分会解释做出这些选择的原因。
优化 Spring Boot 应用的 CPU 和内存
容器可用的 CPU 量会直接影响 Spring Boot 等 JVM 应用的启动时间。其实 JVM 会利用所有可用的 CPU 并发执行初始化任务,以缩短启动时间。因此在启动之后,应用所需的 CPU 会要低得多。
常见的策略是将 CPU 请求(resources.requests.cpu)设置为应用在正常工作负载下会使用的量,这样应用始终可以保证得到正常运行所需的资源。随后,根据系统不同,你可以定义更高的 CPU 限制或完全省略它(resources.limits.cpu),以便在启动时让应用可以尽可能多地使用当时该节点的 CPU 资源,优化启动性能。
CPU 是一种可压缩资源。当容器命中限制时(无论是因 resources.limits.cpu 引发的,还是因为节点上 CPU 已耗尽的),操作系统会开始限制容器进程的 CPU 使用,进程仍会运行,但性能可能降低,因为它虽然可以耗用当前可用的 CPU 资源,但当它达到限制时会被节流(throttle)。因此,有时不设置 CPU 限制是有利于获得更好的性能,但整体考虑取决于你的具体场景。
与 CPU 相反,内存是一种不可压缩资源。如果容器命中内存限制(无论是 resources.limits.memory,还是节点内存耗尽),JVM 应用会抛出可怕的 OutOfMemoryError,操作系统会以 OOMKilled 状态终止容器进程。它不会节流。因此,设置正确的内存值尤为重要。至于该如何合理配置,没有捷径可走:你必须监控真实环境中的运行情况。这适用于 CPU 和内存。
一旦你为一个应用的内存设定了合适的值,我建议将它同时用作请求(resources.requests.memory)和限制(resources.limits.memory)。这样做的原因与 JVM 的工作方式密切相关,尤其是与 JVM 堆内存的行为密切相关。在容器内存上按需增减会影响应用的性能,因为堆内存的分配取决于容器可用的内存。请求和限制采用相同的值,可以保证固定的内存大小,提供更好的 JVM 性能。此外,它还能让 Paketo Buildpacks 提供的 Java Memory Calculator 以最高效的方式配置 JVM 内存。
我已经提及 Java Memory Calculator 好几次了。下一节将展开这个话题。
配置 JVM 资源
Spring Boot 插件为 Gradle/Maven 使用的 Paketo Buildpacks,在为 Java 应用构建容器镜像时会提供一个 Java Memory Calculator 组件。这个组件实现了一个经过多年打磨优化的算法,归功于 Pivotal(现 VMware Tanzu)在云中运行容器化 Java 负载的经验。
在生产环境中,默认配置对大多数应用来说是个很好的起点,但对本地开发或 demo 来说可能过于耗费资源。让 JVM 消耗更少资源的方法之一是降低默认的 250 个线程数,这个配置适用于命令式(Servlet)应用。为此,我们通过 BPL_JVM_THREAD_COUNT 环境变量来降低 Polar Bookshop 中两个 Servlet 应用(Catalog Service、Config Service)的 JVM 线程数。响应式(reactive)应用已经有了更少的线程配置,因为其资源效率远高于命令式对应应用。所以我们就不改动 Edge Service、Order Service 或 Dispatcher Service 的线程数。
注意:Paketo 团队正在扩展 Java Memory Calculator,以提供一种称为"低频模式"的配置,有助于在本地或低流量环境下工作。未来可以通过一个 flag 控制内存配置模式,而不必手调参数。关于该功能的更多信息,见 Paketo Buildpacks GitHub 项目(http://mng.bz/5Q87)。
JVM 有两个主要的内存区域:heap(堆)和 non-heap(非堆)。Java Memory Calculator 会根据特定公式计算不同 non-heap 内存区域的值,然后把剩余的内存资源分配给堆内存。如果默认配置不满意,我们可以按需定制。例如,我曾在处理 Redis session 管理的命令式应用上遇到过内存问题:它需要比默认配置更多的直接内存。于是我通过 JAVA_TOOL_OPTIONS 环境变量使用了 -XX:MaxDirectMemorySize=50M 这个标准 JVM 配置,将直接内存的最大值从 10MB 增大到 50MB。若你定制某个内存区域的大小,Calculator 会相应调整其余区域的分配。
注意:JVM 内存处理是一项迷人的技术,需要专著全面覆盖。这里我就不深入讲解如何配置了。
既然我们要为生产环境配置部署,让我们把 Catalog Service 的线程数改为更合理的数字,例如 100。真实场景中,我建议从默认值 250 作为基线开始。对于 Polar Bookshop,我试图在展示真实生产配置和尽量减少你在公有云上的资源消耗(以及可能产生的费用)之间权衡。
我们可以在早期定义的 patch-env.yml 文件中更新 Catalog Service 的环境变量。打开 Catalog Service 的 production overlay 下的 patch-env.yml(catalog-service/production),更新 JVM 线程计数如下。
apiVersion: apps/v1
kind: Deployment
metadata:
name: catalog-service
spec:
template:
spec:
containers:
- name: catalog-service
env:
- name: BPL_JVM_THREAD_COUNT
value: "100"
- name: SPRING_PROFILES_ACTIVE
value: prod
清单 15.16 Java Memory Calculator 使用的 JVM 线程数(内存计算中考虑的线程数)
这就是我们生产部署应用之前需要做的最后一项配置修改。下一步,我们将部署应用。
15.2.3 在生产环境部署 Spring Boot
我们的终极目标是自动化从代码提交到生产环境的完整流程。在深入研究部署流水线的生产阶段之前,我们先手动验证一下到目前为止定义的定制是否正确,方法是在生产环境中手动部署 Catalog Service。
正如你在上一章学到的,可以使用 Kubernetes CLI 结合 Kustomize overlay 部署 Kubernetes 应用。打开终端,进入 Catalog Service 的生产 overlay 目录(catalog-service/production),运行下面的命令。
$ kustomize build | kubectl apply -f -
你可以通过运行这条命令跟踪一次次执行进度,并看到两个应用实例何时准备就绪:
$ kubectl get pods -l app=catalog-service --watch
要获取部署的更多信息,除了 Kubernetes CLI,还可以使用 Octant,一个可以让你通过 GUI 直观查看 Kubernetes 工作负载的工具。如第 7 章所述,可以通过 octant 命令启动 Octant。同时,应用程序日志也值得关注,可以验证 Catalog Service 是否运行正常:
$ kubectl logs deployment/catalog-service
应用目前尚未对外暴露到集群之外(这需要 Edge Service 完成),但你可以使用端口转发功能,将本地的 9001 端口流量转发到集群中运行在 80 端口上的 Service 上:
$ kubectl port-forward service/catalog-service 9001:80
注意:
kubectl port-forward命令启用的进程将一直保持运行,直到你显式按下 Ctrl-C 停止它。
现在,你可以从本地机器通过 9001 端口调用 Catalog Service,请求会被转发到 Kubernetes 集群内对应的 Service 对象。打开一个新的终端窗口,调用应用暴露的根端点,验证在 ConfigMap 中定义的 polar.greeting 值(用于 prod profile)被使用而不是默认值:
$ http :9001/
Welcome to our book catalog from a production Kubernetes environment!
恭喜!你已经正式进入生产环境了!完成后,就可以用 Ctrl-C 终止端口转发。最后,从生产 overlay 目录下运行下面的命令删除部署:
$ kubectl delete -k .