在本文中,您將了解在 Kubernetes 上運行 Java 應(yīng)用程序的最佳實踐。大多數(shù)這些建議也適用于其他語言。但是,我正在考慮 Java 特性范圍內(nèi)的所有規(guī)則,并且還展示了可用于基于 JVM 的應(yīng)用程序的解決方案和工具。當使用最流行的 Java 框架(如 Spring Boot 或 Quarkus)時,這些 Kubernetes 建議中的一些是設(shè)計強制的。我將向您展示如何有效地利用它們來簡化開發(fā)人員的生活。
1、不要將 Limit 設(shè)置得太低
我們是否應(yīng)該為 Kubernetes 上的 Java 應(yīng)用設(shè)置 limit ?答案似乎顯而易見。有許多工具可以驗證您的 Kubernetes YAML 清單,如果您沒有設(shè)置 CPU 或內(nèi)存 limit ,它們肯定會打印警告。不過,社區(qū)對此也有一些“熱議”。這是一篇有趣的文章,不建議設(shè)置任何 CPU limit 。這是另一篇文章,作為對上一篇文章的對比,他們考慮 CPU limit 。但我們也可以針對內(nèi)存 limit 開始類似的討論。特別是在 Java 應(yīng)用程序的上下文中。
然而,對于內(nèi)存管理,這個命題似乎大不相同。讓我們閱讀另一篇文章——這次是關(guān)于內(nèi)存 limit 和 request 的。簡而言之,它建議始終設(shè)置內(nèi)存 limit。此外,限制應(yīng)與 request 相同。在 Java 應(yīng)用程序的上下文中,我們可以使用 -Xmx 、 -XX:MaxMetaspaceSize 或 -XX:ReservedCodeCacheSize 等 JVM 參數(shù)限制內(nèi)存也很重要。無論如何,從 Kubernetes 的角度來看,pod 接收它 request 的資源。Limit 與它無關(guān)。
這一切讓我得出了今天的第一個建議—A—不要將你的 limit 設(shè)置得太低。即使您設(shè)置了 CPU limit ,也不應(yīng)該影響您的應(yīng)用程序。例如,您可能知道,即使您的 Java 應(yīng)用程序在正常工作中不會消耗太多 CPU,但它需要大量 CPU 才能快速啟動。對于我在 Kubernetes 上連接 MongoDB 的簡單 Spring Boot 應(yīng)用程序,無限制和甚至 0.5 核之間的差異是顯著的。通常它在 10 秒以下開始:
將 CPU limit 設(shè)置為 500 millicores ,它開始大約 30 秒:
當然,我們可以找到一些例子。但我們也會在下一節(jié)中討論它們。
2、首先考慮內(nèi)存使用
讓我們只關(guān)注內(nèi)存 limit 。如果您在 Kubernetes 上運行 Java 應(yīng)用程序,則有兩個級別的最大使用 limit :容器和 JVM。但是,如果您沒有為 JVM 指定任何設(shè)置,也有一些默認值。如果您不設(shè)置 -Xmx 參數(shù),JVM 會將其最大堆大小設(shè)置為可用 RAM 的大約 25%。該值是根據(jù)容器內(nèi)可見的內(nèi)存計算的。一旦您不在容器級別設(shè)置 limit ,JVM 將看到節(jié)點的整個內(nèi)存。
在 Kubernetes 上運行應(yīng)用程序之前,您至少應(yīng)該測量它在預(yù)期負載下消耗了多少內(nèi)存。幸運的是,有一些工具可以優(yōu)化在容器中運行的 Java 應(yīng)用程序的內(nèi)存配置。例如,Paketo Buildpacks 帶有內(nèi)置內(nèi)存計算器,它使用公式 Heap = 總?cè)萜鲀?nèi)存 - Non-Heap - Headroom 計算 JVM 的 -Xmx 參數(shù)。另一方面,非堆值是使用以下公式計算的:Non-Heap = Direct Memory + Metaspace + Reserved Code Cache + (Thread Stack * Thread Count) 。
Paketo Buildpacks 目前是構(gòu)建 Spring Boot 應(yīng)用程序的默認選項(使用 mvn spring-boot:build-image 命令)。讓我們?yōu)槲覀兊氖纠龖?yīng)用程序嘗試一下。假設(shè)我們將內(nèi)存限制設(shè)置為 512M,它將在 130M 的級別計算 -Xmx 。
我的應(yīng)用程序可以嗎?我至少應(yīng)該執(zhí)行一些負載測試來驗證我的應(yīng)用程序在高流量下的性能。但再一次 - 不要將 limit 設(shè)置得太低。例如,對于 1024M 限制, -Xmx 等于 650M。
如您所見,我們使用 JVM 參數(shù)處理內(nèi)存使用情況。它可以防止我們在第一節(jié)提到的文章中描述的 OOM kills 。因此,將 request 設(shè)置為與 limit 相同的級別并沒有太大意義。我建議將其設(shè)置為比正常使用高一點——比方說多 20%。
3、適當?shù)?liveness 和 readiness 探針
3.1 介紹
了解 Kubernetes 中的 liveness 和 readiness 探針之間的區(qū)別至關(guān)重要。如果這兩個探針都沒有仔細實施,它們可能會降低服務(wù)的整體運行,例如導(dǎo)致不必要的重啟。第三種類型的探針,啟動探針,是 Kubernetes 中一個相對較新的特性。它允許我們避免在 liveness 或 readiness 探針上設(shè)置 initialDelaySeconds ,因此如果您的應(yīng)用程序啟動需要很長時間,它特別有用。有關(guān) Kubernetes 探針的一般和最佳實踐的更多詳細信息,我可以推薦那篇非常有趣的文章。
Liveness 探針用于決定是否重啟容器。如果應(yīng)用程序因任何原因不可用,有時重啟容器是有意義的。另一方面,readiness 探針用于確定容器是否可以處理傳入流量。如果一個 pod 被識別為未就緒,它將被從負載平衡中移除。readiness 探針失敗不會導(dǎo)致 pod 重啟。Web 應(yīng)用程序最典型的 liveness 或 readiness 探針是通過 HTTP 端點實現(xiàn)的。
由于 liveness 探針的后續(xù)失敗會導(dǎo)致 pod 重新啟動,因此它不應(yīng)檢查您的應(yīng)用程序集成的可用性。這些事情應(yīng)該由 readiness 驗證。
3.2 配置詳情
好消息是,最流行的 Java 框架(如 Spring Boot 或 Quarkus)提供了兩種 Kubernetes 探針的自動配置實現(xiàn)。他們遵循最佳實踐,因此我們通常不必了解基礎(chǔ)知識。但是,在 Spring Boot 中,除了包含 Actuator 模塊之外,您還需要使用以下屬性啟用它們:
management: endpoint: health: probes: enabled:true
由于 Spring Boot Actuator 提供了多個端點(例如 metric、 trace),因此最好將其公開在與默認端口不同的端口(通常為 8080 )。當然,同樣的規(guī)則也適用于其他流行的 Java 框架。另一方面,一個好的做法是檢查您的主要應(yīng)用程序端口——尤其是在 readiness 探針中。
因為它定義了我們的應(yīng)用程序是否準備好處理傳入的請求,所以它也應(yīng)該在主端口上監(jiān)聽。它與 liveness probe 看起來正好相反。如果整個工作線程池都很忙,我不想重新啟動我的應(yīng)用程序。我只是不想在一段時間內(nèi)收到傳入流量。
我們還可以自定義 Kubernetes 探針的其他方面。假設(shè)我們的應(yīng)用程序連接到外部系統(tǒng),但我們沒有在我們的 readiness 探針中驗證該集成。它并不重要,不會對我們的運營狀態(tài)產(chǎn)生直接影響。這是一個配置,它允許我們在探針中僅包含選定的集成集 (1),并在主服務(wù)器端口上公開 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
幾乎沒有任何應(yīng)用可以不依賴外部解決方案(如數(shù)據(jù)庫、消息代理或其他應(yīng)用程序)。在配置 readiness 探針時,我們應(yīng)該仔細考慮到該系統(tǒng)的連接設(shè)置。首先你應(yīng)該考慮外部服務(wù)不可用的情況。你將如何處理?我建議將這些超時減少到較低的值,如下所示。
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
如果您已經(jīng)使用 Dockerfile 構(gòu)建了鏡像,那么您可能使用的是來自 Docker Hub 的官方 OpenJDK 基礎(chǔ)鏡像。然而,目前,鏡像網(wǎng)站上的公告稱它已被正式棄用,所有用戶都應(yīng)該找到合適的替代品。我想這可能會讓人很困惑,所以你會在這里找到對原因的詳細解釋。
好吧,讓我們考慮一下我們應(yīng)該選擇哪個備選方案。不同的供應(yīng)商提供多種替代品。如果您正在尋找它們之間的詳細比較,您應(yīng)該訪問以下站點。17版本推薦使用 Eclipse Temurin。
另一方面,Jib 或 Cloud Native Buildpacks 等最流行的鏡像構(gòu)建工具會自動為您選擇供應(yīng)商。默認情況下,Jib 使用 Eclipse Temurin,而 Paketo Buildpacks 使用 Bellsoft Liberica 實現(xiàn)。當然,您可以輕松地覆蓋這些設(shè)置。我認為,例如,如果您在與 JDK 提供程序(如 AWS 和 Amazon Corretto)匹配的環(huán)境中運行您的應(yīng)用程序,這可能是有意義的。
假設(shè)我們使用 Paketo Buildpacks 和 Skaffold 在 Kubernetes 上部署 Java 應(yīng)用程序。為了將默認的 Bellsoft Liberica buildpack 替換為另一個,我們只需要在 buildpacks 部分中逐字設(shè)置它。下面是一個利用 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 供應(yīng)商輕松測試我們的應(yīng)用程序的性能。如果您正在尋找此類比較的示例,您可以閱讀我描述此類測試和結(jié)果的文章。我使用幾個可用的 Paketo Java 構(gòu)建包測量了與 Mongo 數(shù)據(jù)庫交互的 Spring Boot 3 應(yīng)用程序的不同 JDK 性能。
5、考慮遷移到原生編譯
原生編譯是 Java 世界中真正的“游戲規(guī)則改變者”。但我敢打賭,你們中沒有多少人使用它——尤其是在生產(chǎn)中。當然,在將現(xiàn)有應(yīng)用程序遷移到本機編譯的過程中存在(現(xiàn)在仍然存在)許多挑戰(zhàn)。GraalVM 在構(gòu)建期間執(zhí)行的靜態(tài)代碼分析可能會導(dǎo)致類似 ClassNotFound 或 MethodNotFound 的錯誤。為了克服這些挑戰(zhàn),我們需要提供一些提示讓 GraalVM 了解代碼的動態(tài)元素。這些提示的數(shù)量通常取決于庫的數(shù)量和應(yīng)用程序中使用的語言功能的一般數(shù)量。
像 Quarkus 或 Micronaut 這樣的 Java 框架試圖通過設(shè)計解決與原生編譯相關(guān)的挑戰(zhàn)。例如,他們盡可能避免使用反射。Spring Boot 還通過 Spring Native 項目大大改進了原生編譯支持。因此,我在這方面的建議是,如果您要創(chuàng)建一個新的應(yīng)用程序,請按照為本機編譯做好準備的方式進行準備。例如,使用 Quarkus,您可以簡單地生成一個 Maven 配置,其中包含用于構(gòu)建原生可執(zhí)行文件的專用配置文件。
native native false native
添加后,您可以使用以下命令進行本機構(gòu)建:
$mvncleanpackage-Pnative
然后你可以分析在構(gòu)建過程中是否有任何問題。即使您現(xiàn)在不在生產(chǎn)環(huán)境中運行原生應(yīng)用程序(例如您的組織不批準它),您也應(yīng)該將 GraalVM 編譯作為您接受管道中的一個步驟。您可以使用最流行的框架輕松地為您的應(yīng)用程序構(gòu)建 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 應(yīng)用程序時,日志記錄可能不是您首先考慮的事情。然而,在全局范圍內(nèi),它變得非常重要,因為我們需要能夠收集、存儲數(shù)據(jù),并最終快速搜索和呈現(xiàn)特定條目。最佳做法是將應(yīng)用程序日志寫入標準輸出 (stdout) 和標準錯誤 (stderr) 流。Fluentd 是一種流行的開源日志聚合器,它允許您從 Kubernetes 集群收集日志、處理它們,然后將它們發(fā)送到您選擇的數(shù)據(jù)存儲后端。它與 Kubernetes 部署無縫集成。
Fluentd 嘗試將數(shù)據(jù)結(jié)構(gòu)化為 JSON 以統(tǒng)一不同來源和目的地的日志記錄。假設(shè)那樣,最好的方法可能是以這種格式準備日志。使用 JSON 格式,我們還可以輕松地包含用于標記日志的附加字段,然后使用各種條件在可視化工具中輕松搜索它們。
為了將我們的日志格式化為 Fluentd 可讀的 JSON,我們可以在 Maven 依賴項中包含 Logstash Logback 編碼器庫。
net.logstash.logback logstash-logback-encoder 7.2
然后我們只需要在文件 logback-spring.xml 中為我們的 Spring Boot 應(yīng)用程序設(shè)置一個默認的控制臺日志 Appender 。
我們是否應(yīng)該避免使用額外的日志 appenders ,而只是將日志打印到標準輸出?根據(jù)我的經(jīng)驗,答案是——不。您仍然可以使用其他機制來發(fā)送日志。特別是如果您使用不止一種工具來收集組織中的日志——例如 Kubernetes 上的內(nèi)部堆棧和外部的全局堆棧。
就個人而言,我正在使用一種工具來幫助我解決性能問題,例如消息代理作為代理。在 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、創(chuàng)建集成測試
好的,我知道——它與 Kubernetes 沒有直接關(guān)系。但是由于我們使用 Kubernetes 來管理和編排容器,我們還應(yīng)該對容器進行集成測試。幸運的是,使用 Java 框架,我們可以大大簡化該過程。
例如,Quarkus 允許我們用 @QuarkusIntegrationTest 注釋測試。結(jié)合 Quarkus 容器構(gòu)建功能,它是一個非常強大的解決方案。我們可以針對包含該應(yīng)用程序的已構(gòu)建鏡像運行測試。首先,讓我們包含 Quarkus Jib 模塊:
io.quarkus quarkus-container-image-jib
然后我們必須通過在 application.properties 文件中將 quarkus.container-image.build 屬性設(shè)置為 true 來啟用容器構(gòu)建。在測試類中,我們可以使用 @TestHTTPResource 和 @TestHTTPEndpoint 注解注入測試服務(wù)器 URL。
然后我們使用 RestClientBuilder 創(chuàng)建一個客戶端并調(diào)用在容器上啟動的服務(wù)。測試類的名字不是偶然的。為了被自動檢測為集成測試,它有 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()); } }
您可以在我之前關(guān)于使用 Quarkus 進行高級測試的文章中找到有關(guān)該過程的更多詳細信息。最終效果如下圖所示。當我們在構(gòu)建期間使用 mvn clean verify 命令運行測試時,我們的測試在構(gòu)建容器鏡像后執(zhí)行。
該 Quarkus 功能基于 Testcontainers 框架。我們還可以將 Testcontainer 與 Spring Boot 一起使用。這是 Spring REST 應(yīng)用程序及其與 PostgreSQL 數(shù)據(jù)庫集成的示例測試。
@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 應(yīng)用程序時避免一些常見的陷阱。將其視為我在類似文章中找到的其他人的建議以及我在該領(lǐng)域的個人經(jīng)驗的總結(jié)。
作者:Piotr
審核編輯:湯梓紅
-
cpu
+關(guān)注
關(guān)注
68文章
10908瀏覽量
213085 -
內(nèi)存
+關(guān)注
關(guān)注
8文章
3060瀏覽量
74353 -
JAVA
+關(guān)注
關(guān)注
19文章
2975瀏覽量
105183 -
容器
+關(guān)注
關(guān)注
0文章
499瀏覽量
22128 -
kubernetes
+關(guān)注
關(guān)注
0文章
227瀏覽量
8757
原文標題:Kubernetes 上 Java 應(yīng)用的最佳實踐
文章出處:【微信號:AndroidPush,微信公眾號:Android編程精選】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
全新java初學(xué)者實踐教程
Kubernetes之路 1 - Java應(yīng)用資源限制的迷思
Kubernetes Ingress 高可靠部署最佳實踐
虛幻引擎的紋理最佳實踐
全新java基礎(chǔ)實踐教程.chm
在Kubernetes上運行Kubernetes
![在<b class='flag-5'>Kubernetes</b><b class='flag-5'>上</b>運行<b class='flag-5'>Kubernetes</b>](https://file.elecfans.com/web2/M00/49/EE/pYYBAGKhvHiAO8-iAAAmwMj0d8g546.png)
評論