5、考虑迁移到原生编译
原生编译是 Java 世界中真正的“游戏规则改变者”。但我敢打赌,你们中没有多少人使用它——尤其是在生产中。当然,在将现有应用程序迁移到本机编译的过程中存在(现在仍然存在)许多挑战。GraalVM 在构建期间执行的静态代码分析可能会导致类似 ClassNotFound 或 MethodNotFound 的错误。为了克服这些挑战,我们需要提供一些提示让 GraalVM 了解代码的动态元素。这些提示的数量通常取决于库的数量和应用程序中使用的语言功能的一般数量。
像 Quarkus 或 Micronaut 这样的 Java 框架试图通过设计解决与原生编译相关的挑战。例如,他们尽可能避免使用反射。Spring Boot 还通过 Spring Native 项目大大改进了原生编译支持。因此,我在这方面的建议是,如果您要创建一个新的应用程序,请按照为本机编译做好准备的方式进行准备。例如,使用 Quarkus,您可以简单地生成一个 Maven 配置,其中包含用于构建原生可执行文件的专用配置文件。
<profiles> <profile> <id>native</id> <activation> <property> <name>native</name> </property> </activation> <properties> <skipITs>false</skipITs> <quarkus.package.type>native</quarkus.package.type> </properties> </profile> </profiles>
添加后,您可以使用以下命令进行本机构建:
$ mvn clean package -Pnative
然后你可以分析在构建过程中是否有任何问题。即使您现在不在生产环境中运行原生应用程序(例如您的组织不批准它),您也应该将 GraalVM 编译作为您接受管道中的一个步骤。您可以使用最流行的框架轻松地为您的应用程序构建 Java 原生镜像。例如,使用 Spring Boot,您只需在 Maven pom.xml 中提供以下配置,如下所示:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>build-info</goal> <goal>build-image</goal> </goals> </execution> </executions> <configuration> <image> <builder>paketobuildpacks/builder:tiny</builder> <env> <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE> <BP_NATIVE_IMAGE_BUILD_ARGUMENTS> --allow-incomplete-classpath </BP_NATIVE_IMAGE_BUILD_ARGUMENTS> </env> </image> </configuration> </plugin>
6、正确配置日志记录
在编写 Java 应用程序时,日志记录可能不是您首先考虑的事情。然而,在全局范围内,它变得非常重要,因为我们需要能够收集、存储数据,并最终快速搜索和呈现特定条目。最佳做法是将应用程序日志写入标准输出 (stdout) 和标准错误 (stderr) 流。
Fluentd 是一种流行的开源日志聚合器,它允许您从 Kubernetes 集群收集日志、处理它们,然后将它们发送到您选择的数据存储后端。它与 Kubernetes 部署无缝集成。Fluentd 尝试将数据结构化为 JSON 以统一不同来源和目的地的日志记录。假设那样,最好的方法可能是以这种格式准备日志。使用 JSON 格式,我们还可以轻松地包含用于标记日志的附加字段,然后使用各种条件在可视化工具中轻松搜索它们。
为了将我们的日志格式化为 Fluentd 可读的 JSON,我们可以在 Maven 依赖项中包含 Logstash Logback 编码器库。
<dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>7.2</version> </dependency>
然后我们只需要在文件 logback-spring.xml 中为我们的 Spring Boot 应用程序设置一个默认的控制台日志 Appender 。
<configuration> <appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LogstashEncoder"/> </appender> <logger name="jsonLogger" additivity="false" level="DEBUG"> <appender-ref ref="consoleAppender"/> </logger> <root level="INFO"> <appender-ref ref="consoleAppender"/> </root> </configuration>
我们是否应该避免使用额外的日志 appenders ,而只是将日志打印到标准输出?根据我的经验,答案是——不。您仍然可以使用其他机制来发送日志。特别是如果您使用不止一种工具来收集组织中的日志——例如 Kubernetes 上的内部堆栈和外部的全局堆栈。就个人而言,我正在使用一种工具来帮助我解决性能问题,例如消息代理作为代理。在 Spring Boot 中,我们可以轻松地使用 RabbitMQ。只需包括以下 starter:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
然后你需要在 logback-spring.xml 中提供一个类似的 appender 配置:
<?xml version="1.0" encoding="UTF-8"?> <configuration> <springProperty name="destination" source="app.amqp.url" /> <appender name="AMQP" class="org.springframework.amqp.rabbit.logback.AmqpAppender"> <layout> <pattern> { "time": "%date{ISO8601}", "thread": "%thread", "level": "%level", "class": "%logger{36}", "message": "%message" } </pattern> </layout> <addresses>${destination}</addresses> <applicationId>api-service</applicationId> <routingKeyPattern>logs</routingKeyPattern> <declareExchange>true</declareExchange> <exchangeName>ex_logstash</exchangeName> </appender> <root level="INFO"> <appender-ref ref="AMQP" /> </root> </configuration>
7、创建集成测试
好的,我知道——它与 Kubernetes 没有直接关系。但是由于我们使用 Kubernetes 来管理和编排容器,我们还应该对容器进行集成测试。幸运的是,使用 Java 框架,我们可以大大简化该过程。例如,Quarkus 允许我们用 @QuarkusIntegrationTest 注释测试。结合 Quarkus 容器构建功能,它是一个非常强大的解决方案。我们可以针对包含该应用程序的已构建镜像运行测试。首先,让我们包含 Quarkus Jib 模块:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-container-image-jib</artifactId> </dependency>
然后我们必须通过在 application.properties 文件中将 quarkus.container-image.build 属性设置为 true 来启用容器构建。在测试类中,我们可以使用 @TestHTTPResource 和 @TestHTTPEndpoint 注解注入测试服务器 URL。然后我们使用 RestClientBuilder 创建一个客户端并调用在容器上启动的服务。测试类的名字不是偶然的。为了被自动检测为集成测试,它有 IT 后缀。
@QuarkusIntegrationTest public class EmployeeControllerIT { @TestHTTPEndpoint(EmployeeController.class) @TestHTTPResource URL url; @Test void add() { EmployeeService service = RestClientBuilder.newBuilder() .baseUrl(url) .build(EmployeeService.class); Employee employee = new Employee(1L, 1L, "Josh Stevens", 23, "Developer"); employee = service.add(employee); assertNotNull(employee.getId()); } @Test public void findAll() { EmployeeService service = RestClientBuilder.newBuilder() .baseUrl(url) .build(EmployeeService.class); Set<Employee> employees = service.findAll(); assertTrue(employees.size() >= 3); } @Test public void findById() { EmployeeService service = RestClientBuilder.newBuilder() .baseUrl(url) .build(EmployeeService.class); Employee employee = 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) public class PersonControllerTests { @Autowired TestRestTemplate restTemplate; @Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15.1") .withExposedPorts(5432); @DynamicPropertySource static void registerMySQLProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", postgres::getJdbcUrl); registry.add("spring.datasource.username", postgres::getUsername); registry.add("spring.datasource.password", postgres::getPassword); } @Test @Order(1) void add() { Person person = 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) void updateAndGet() { final Integer id = 1; Person person = Instancio.of(Person.class) .set(Select.field("id"), id) .create(); restTemplate.put("/persons", person); Person updated = restTemplate.getForObject("/persons/{id}", Person.class, id); Assertions.assertNotNull(updated); Assertions.assertNotNull(updated.getId()); Assertions.assertEquals(id, updated.getId()); } }
8、最后的想法
我希望这篇文章能帮助您在 Kubernetes 上运行 Java 应用程序时避免一些常见的陷阱。将其视为我在类似文章中找到的其他人的建议以及我在该领域的个人经验的总结。