在本文中,您將了解在 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 秒:
當然,我們可以找到一些例子。但我們也會在下一節中討論它們。
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%。
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//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 性能。
5、考慮遷移到原生編譯
原生編譯是 Java 世界中真正的“游戲規則改變者”。但我敢打賭,你們中沒有多少人使用它——尤其是在生產中。當然,在將現有應用程序遷移到本機編譯的過程中存在(現在仍然存在)許多挑戰。GraalVM 在構建期間執行的靜態代碼分析可能會導致類似 ClassNotFound 或 MethodNotFound 的錯誤。為了克服這些挑戰,我們需要提供一些提示讓 GraalVM 了解代碼的動態元素。這些提示的數量通常取決于庫的數量和應用程序中使用的語言功能的一般數量。
像 Quarkus 或 Micronaut 這樣的 Java 框架試圖通過設計解決與原生編譯相關的挑戰。例如,他們盡可能避免使用反射。Spring Boot 還通過 Spring Native 項目大大改進了原生編譯支持。因此,我在這方面的建議是,如果您要創建一個新的應用程序,請按照為本機編譯做好準備的方式進行準備。例如,使用 Quarkus,您可以簡單地生成一個 Maven 配置,其中包含用于構建原生可執行文件的專用配置文件。
native native false native
添加后,您可以使用以下命令進行本機構建:
$mvncleanpackage-Pnative
然后你可以分析在構建過程中是否有任何問題。即使您現在不在生產環境中運行原生應用程序(例如您的組織不批準它),您也應該將 GraalVM 編譯作為您接受管道中的一個步驟。您可以使用最流行的框架輕松地為您的應用程序構建 Java 原生鏡像。例如,使用 Spring Boot,您只需在 Maven pom.xml 中提供以下配置,如下所示:
org.springframework.boot spring-boot-maven-plugin build-info build-image paketobuildpacks/builder:tiny true --allow-incomplete-classpath
6、正確配置日志記錄
在編寫 Java 應用程序時,日志記錄可能不是您首先考慮的事情。然而,在全局范圍內,它變得非常重要,因為我們需要能夠收集、存儲數據,并最終快速搜索和呈現特定條目。最佳做法是將應用程序日志寫入標準輸出 (stdout) 和標準錯誤 (stderr) 流。Fluentd 是一種流行的開源日志聚合器,它允許您從 Kubernetes 集群收集日志、處理它們,然后將它們發送到您選擇的數據存儲后端。它與 Kubernetes 部署無縫集成。
Fluentd 嘗試將數據結構化為 JSON 以統一不同來源和目的地的日志記錄。假設那樣,最好的方法可能是以這種格式準備日志。使用 JSON 格式,我們還可以輕松地包含用于標記日志的附加字段,然后使用各種條件在可視化工具中輕松搜索它們。
為了將我們的日志格式化為 Fluentd 可讀的 JSON,我們可以在 Maven 依賴項中包含 Logstash Logback 編碼器庫。
net.logstash.logback logstash-logback-encoder 7.2
然后我們只需要在文件 logback-spring.xml 中為我們的 Spring Boot 應用程序設置一個默認的控制臺日志 Appender 。
我們是否應該避免使用額外的日志 appenders ,而只是將日志打印到標準輸出?根據我的經驗,答案是——不。您仍然可以使用其他機制來發送日志。特別是如果您使用不止一種工具來收集組織中的日志——例如 Kubernetes 上的內部堆棧和外部的全局堆棧。
就個人而言,我正在使用一種工具來幫助我解決性能問題,例如消息代理作為代理。在 Spring Boot 中,我們可以輕松地使用 RabbitMQ。只需包括以下 starter:
org.springframework.boot spring-boot-starter-amqp
然后你需要在 logback-spring.xml 中提供一個類似的 appender 配置:
{ "time":"%date{ISO8601}", "thread":"%thread", "level":"%level", "class":"%logger{36}", "message":"%message" } ${destination} api-service logs true ex_logstash
7、創建集成測試
好的,我知道——它與 Kubernetes 沒有直接關系。但是由于我們使用 Kubernetes 來管理和編排容器,我們還應該對容器進行集成測試。幸運的是,使用 Java 框架,我們可以大大簡化該過程。
例如,Quarkus 允許我們用 @QuarkusIntegrationTest 注釋測試。結合 Quarkus 容器構建功能,它是一個非常強大的解決方案。我們可以針對包含該應用程序的已構建鏡像運行測試。首先,讓我們包含 Quarkus Jib 模塊:
io.quarkus quarkus-container-image-jib
然后我們必須通過在 application.properties 文件中將 quarkus.container-image.build 屬性設置為 true 來啟用容器構建。在測試類中,我們可以使用 @TestHTTPResource 和 @TestHTTPEndpoint 注解注入測試服務器 URL。
然后我們使用 RestClientBuilder 創建一個客戶端并調用在容器上啟動的服務。測試類的名字不是偶然的。為了被自動檢測為集成測試,它有 IT 后綴。
@QuarkusIntegrationTest publicclassEmployeeControllerIT{ @TestHTTPEndpoint(EmployeeController.class) @TestHTTPResource URLurl; @Test voidadd(){ EmployeeServiceservice=RestClientBuilder.newBuilder() .baseUrl(url) .build(EmployeeService.class); Employeeemployee=newEmployee(1L,1L,"JoshStevens", 23,"Developer"); employee=service.add(employee); assertNotNull(employee.getId()); } @Test publicvoidfindAll(){ EmployeeServiceservice=RestClientBuilder.newBuilder() .baseUrl(url) .build(EmployeeService.class); Setemployees=service.findAll(); assertTrue(employees.size()>=3); } @Test publicvoidfindById(){ EmployeeServiceservice=RestClientBuilder.newBuilder() .baseUrl(url) .build(EmployeeService.class); Employeeemployee=service.findById(1L); assertNotNull(employee.getId()); } }
您可以在我之前關于使用 Quarkus 進行高級測試的文章中找到有關該過程的更多詳細信息。最終效果如下圖所示。當我們在構建期間使用 mvn clean verify 命令運行測試時,我們的測試在構建容器鏡像后執行。
該 Quarkus 功能基于 Testcontainers 框架。我們還可以將 Testcontainer 與 Spring Boot 一起使用。這是 Spring REST 應用程序及其與 PostgreSQL 數據庫集成的示例測試。
@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT) @Testcontainers @TestMethodOrder(MethodOrderer.OrderAnnotation.class) publicclassPersonControllerTests{ @Autowired TestRestTemplaterestTemplate; @Container staticPostgreSQLContainer>postgres= newPostgreSQLContainer<>("postgres:15.1") .withExposedPorts(5432); @DynamicPropertySource staticvoidregisterMySQLProperties(DynamicPropertyRegistryregistry){ registry.add("spring.datasource.url",postgres::getJdbcUrl); registry.add("spring.datasource.username",postgres::getUsername); registry.add("spring.datasource.password",postgres::getPassword); } @Test @Order(1) voidadd(){ Personperson=Instancio.of(Person.class) .ignore(Select.field("id")) .create(); person=restTemplate.postForObject("/persons",person,Person.class); Assertions.assertNotNull(person); Assertions.assertNotNull(person.getId()); } @Test @Order(2) voidupdateAndGet(){ finalIntegerid=1; Personperson=Instancio.of(Person.class) .set(Select.field("id"),id) .create(); restTemplate.put("/persons",person); Personupdated=restTemplate.getForObject("/persons/{id}",Person.class,id); Assertions.assertNotNull(updated); Assertions.assertNotNull(updated.getId()); Assertions.assertEquals(id,updated.getId()); } }
8、最后的想法
我希望這篇文章能幫助您在 Kubernetes 上運行 Java 應用程序時避免一些常見的陷阱。將其視為我在類似文章中找到的其他人的建議以及我在該領域的個人經驗的總結。
作者:Piotr
審核編輯:湯梓紅
-
cpu
+關注
關注
68文章
10824瀏覽量
211133 -
內存
+關注
關注
8文章
2998瀏覽量
73881 -
JAVA
+關注
關注
19文章
2957瀏覽量
104544 -
容器
+關注
關注
0文章
494瀏覽量
22044 -
kubernetes
+關注
關注
0文章
223瀏覽量
8695
原文標題:Kubernetes 上 Java 應用的最佳實踐
文章出處:【微信號:AndroidPush,微信公眾號:Android編程精選】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論