- 1、不要将 Limit 设置得太低
- 2、首先考虑内存使用
- 3、适当的 liveness 和 readiness 探针
- 4、选择合适的 JDK
- 5、考虑迁移到原生编译
- 6、正确配置日志记录
- 7、创建集成测试
- 8、最后的想法
在本文中,您将了解在 Kubernetes 上运行 Java 应用程序的最佳实践。大多数这些建议也适用于其他语言。但是,我正在考虑 Java 特性范围内的所有规则,并且还展示了可用于基于 JVM 的应用程序的解决方案和工具。当使用最流行的 Java 框架(如 Spring Boot 或 Quarkus)时,这些 Kubernetes 建议中的一些是设计强制的。我将向您展示如何有效地利用它们来简化开发人员的生活。
1、不要将 Limit 设置得太低
我们是否应该为 Kubernetes 上的 Java 应用设置 limit ?答案似乎显而易见。有许多工具可以验证您的 Kubernetes YAML 清单,如果您没有设置 CPU 或内存 limit ,它们肯定会打印警告。不过,社区对此也有一些“热议”。这是一篇有趣的文章,不建议设置任何 CPU limit 。这是另一篇文章,作为对上一篇文章的对比,他们考虑 CPU limit 。但我们也可以针对内存 limit 开始类似的讨论。特别是在 Java 应用程序的上下文中。
然而,对于内存管理,这个命题似乎大不相同。让我们阅读另一篇文章——这次是关于内存 limit 和 request 的。简而言之,它建议始终设置内存 limit。此外,限制应与 request 相同。在 Java 应用程序的上下文中,我们可以使用 -Xmx 、 -XX:MaxMetaspaceSize 或 -XX:ReservedCodeCacheSize 等 JVM 参数限制内存也很重要。无论如何,从 Kubernetes 的角度来看,pod 接收它 request 的资源。Limit 与它无关。
这一切让我得出了今天的第一个建议—A—不要将你的 limit 设置得太低。即使您设置了 CPU limit ,也不应该影响您的应用程序。例如,您可能知道,即使您的 Java 应用程序在正常工作中不会消耗太多 CPU,但它需要大量 CPU 才能快速启动。对于我在 Kubernetes 上连接 MongoDB 的简单 Spring Boot 应用程序,无限制和甚至 0.5 核之间的差异是显着的。通常它在 10 秒以下开始:
将 CPU limit 设置为 500 millicores ,它开始大约 30 秒:
当然,我们可以找到一些例子。但我们也会在下一节中讨论它们。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
2、首先考虑内存使用
让我们只关注内存 limit 。如果您在 Kubernetes 上运行 Java 应用程序,则有两个级别的最大使用 limit :容器和 JVM。但是,如果您没有为 JVM 指定任何设置,也有一些默认值。如果您不设置 -Xmx 参数,JVM 会将其最大堆大小设置为可用 RAM 的大约 25%。该值是根据容器内可见的内存计算的。一旦您不在容器级别设置 limit ,JVM 将看到节点的整个内存。
在 Kubernetes 上运行应用程序之前,您至少应该测量它在预期负载下消耗了多少内存。幸运的是,有一些工具可以优化在容器中运行的 Java 应用程序的内存配置。例如,Paketo Buildpacks 带有内置内存计算器,它使用公式 Heap = 总容器内存 - Non-Heap - Headroom 计算 JVM 的 -Xmx 参数。另一方面,非堆值是使用以下公式计算的:Non-Heap = Direct Memory + Metaspace + Reserved Code Cache + (Thread Stack * Thread Count) 。
Paketo Buildpacks 目前是构建 Spring Boot 应用程序的默认选项(使用 mvn spring-boot:build-image 命令)。让我们为我们的示例应用程序尝试一下。假设我们将内存限制设置为 512M,它将在 130M 的级别计算 -Xmx 。
我的应用程序可以吗?我至少应该执行一些负载测试来验证我的应用程序在高流量下的性能。但再一次 - 不要将 limit 设置得太低。例如,对于 1024M 限制, -Xmx 等于 650M。
如您所见,我们使用 JVM 参数处理内存使用情况。它可以防止我们在第一节提到的文章中描述的 OOM kills 。因此,将 request 设置为与 limit 相同的级别并没有太大意义。我建议将其设置为比正常使用高一点——比方说多 20%。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
3、适当的 liveness 和 readiness 探针
3.1 介绍
了解 Kubernetes 中的 liveness 和 readiness 探针之间的区别至关重要。如果这两个探针都没有仔细实施,它们可能会降低服务的整体运行,例如导致不必要的重启。第三种类型的探针,启动探针,是 Kubernetes 中一个相对较新的特性。它允许我们避免在 liveness 或 readiness 探针上设置 initialDelaySeconds ,因此如果您的应用程序启动需要很长时间,它特别有用。有关 Kubernetes 探针的一般和最佳实践的更多详细信息,我可以推荐那篇非常有趣的文章。
Liveness 探针用于决定是否重启容器。如果应用程序因任何原因不可用,有时重启容器是有意义的。另一方面,readiness 探针用于确定容器是否可以处理传入流量。如果一个 pod 被识别为未就绪,它将被从负载平衡中移除。readiness 探针失败不会导致 pod 重启。Web 应用程序最典型的 liveness 或 readiness 探针是通过 HTTP 端点实现的。
由于 liveness 探针的后续失败会导致 pod 重新启动,因此它不应检查您的应用程序集成的可用性。这些事情应该由 readiness 验证。
3.2 配置详情
好消息是,最流行的 Java 框架(如 Spring Boot 或 Quarkus)提供了两种 Kubernetes 探针的自动配置实现。他们遵循最佳实践,因此我们通常不必了解基础知识。但是,在 Spring Boot 中,除了包含 Actuator 模块之外,您还需要使用以下属性启用它们:
management: endpoint: health: probes: enabled: true
由于 Spring Boot Actuator 提供了多个端点(例如 metric、 trace),因此最好将其公开在与默认端口不同的端口(通常为 8080 )。当然,同样的规则也适用于其他流行的 Java 框架。另一方面,一个好的做法是检查您的主要应用程序端口——尤其是在 readiness 探针中。因为它定义了我们的应用程序是否准备好处理传入的请求,所以它也应该在主端口上监听。它与 liveness probe 看起来正好相反。如果整个工作线程池都很忙,我不想重新启动我的应用程序。我只是不想在一段时间内收到传入流量。
我们还可以自定义 Kubernetes 探针的其他方面。假设我们的应用程序连接到外部系统,但我们没有在我们的 readiness 探针中验证该集成。它并不重要,不会对我们的运营状态产生直接影响。这是一个配置,它允许我们在探针中仅包含选定的集成集 (1),并在主服务器端口上公开 readiness 情况 (2) 。
spring: application: name: sample-spring-boot-on-kubernetes data: mongodb: host: ${MONGO_URL} port: 27017 username: ${MONGO_USERNAME} password: ${MONGO_PASSWORD} database: ${MONGO_DATABASE} authentication-database: admin management: endpoint.health: show-details: always group: readiness: include: mongo # (1) additional-path: server:/readiness # (2) probes: enabled: true server: port: 8081
几乎没有任何应用可以不依赖外部解决方案(如数据库、消息代理或其他应用程序)。在配置 readiness 探针时,我们应该仔细考虑到该系统的连接设置。首先你应该考虑外部服务不可用的情况。你将如何处理?我建议将这些超时减少到较低的值,如下所示。
spring: application: name: sample-spring-kotlin-microservice datasource: url: jdbc:postgresql://postgres:5432/postgres username: postgres password: postgres123 hikari: connection-timeout: 2000 initialization-fail-timeout: 0 jpa: database-platform: org.hibernate.dialect.PostgreSQLDialect rabbitmq: host: rabbitmq port: 5672 connection-timeout: 2000
4、选择合适的 JDK
如果您已经使用 Dockerfile 构建了镜像,那么您可能使用的是来自 Docker Hub 的官方 OpenJDK 基础镜像。然而,目前,镜像网站上的公告称它已被正式弃用,所有用户都应该找到合适的替代品。我想这可能会让人很困惑,所以你会在这里找到对原因的详细解释。
好吧,让我们考虑一下我们应该选择哪个备选方案。不同的供应商提供多种替代品。如果您正在寻找它们之间的详细比较,您应该访问以下站点。17版本推荐使用 Eclipse Temurin。
另一方面,Jib 或 Cloud Native Buildpacks 等最流行的镜像构建工具会自动为您选择供应商。默认情况下,Jib 使用 Eclipse Temurin,而 Paketo Buildpacks 使用 Bellsoft Liberica 实现。当然,您可以轻松地覆盖这些设置。我认为,例如,如果您在与 JDK 提供程序(如 AWS 和 Amazon Corretto)匹配的环境中运行您的应用程序,这可能是有意义的。
假设我们使用 Paketo Buildpacks 和 Skaffold 在 Kubernetes 上部署 Java 应用程序。为了将默认的 Bellsoft Liberica buildpack 替换为另一个,我们只需要在 buildpacks 部分中逐字设置它。下面是一个利用 Amazon Corretto buildpack 的示例。
apiVersion: skaffold/v2beta22 kind: Config metadata: name: sample-spring-boot-on-kubernetes build: artifacts: - image: piomin/sample-spring-boot-on-kubernetes buildpacks: builder: paketobuildpacks/builder:base buildpacks: - paketo-buildpacks/amazon-corretto - paketo-buildpacks/java env: - BP_JVM_VERSION=17
我们还可以使用不同的 JDK 供应商轻松测试我们的应用程序的性能。如果您正在寻找此类比较的示例,您可以阅读我描述此类测试和结果的文章。我使用几个可用的 Paketo Java 构建包测量了与 Mongo 数据库交互的 Spring Boot 3 应用程序的不同 JDK 性能。