原生编译的 Java 微服务实践
1. 配置文件与验证脚本更新
在微服务开发中,对于产品微服务,
src/test/resources/access-filter.json
文件内容如下:
{ "rules":
[
{"excludeClasses": "org.apache.maven.surefire.**"},
{"excludeClasses": "net.bytebuddy.**"},
{"excludeClasses": "org.apiguardian.**"},
{"excludeClasses": "org.junit.**"},
{"excludeClasses": "org.gradle.**"},
{"excludeClasses": "org.mockito.**"},
{"excludeClasses": "org.springframework.test.**"},
{"excludeClasses": "org.springframework.boot.test.**"},
{"excludeClasses": "org.testcontainers.**"},
{"excludeClasses": "se.magnus.microservices.core.product.MapperTests"},
{"excludeClasses": "se.magnus.microservices.core.product.MongoDbTestBase"},
{"excludeClasses": "se.magnus.microservices.core.product.PersistenceTests"},
{"excludeClasses": "se.magnus.microservices.core.product.ProductServiceApplicationTests"}
]
}
config-output-dir
参数指定的文件夹将包含生成的配置文件,而
src/main/resources/META-INF/Native Image
文件夹是 GraalVM 原生编译器查找可达性元数据的位置。
在验证脚本方面,之前使用
eclipse - temurin
作为 Docker 镜像的基础镜像,
test - em - all.bash
验证脚本使用该基础镜像自带的
curl
命令在
product - composite
容器内运行断路器测试。但对于原生编译的微服务,Docker 镜像不再包含
curl
等实用工具。为解决此问题,
curl
命令将从
auth - server
容器执行,因为其 Docker 镜像仍基于
eclipse - temurin
,包含所需的
curl
命令。由于用于验证断路器功能的端点未在 Docker 内部网络外部暴露,所以验证脚本在
product - composite
容器内运行
curl
命令。同时,由于断路器测试从
auth - server
执行,主机名
localhost
需替换为
product - composite
。
2. 测试和编译原生镜像
要进行原生镜像的测试和编译,需使用以下工具:
- 运行跟踪代理
- 执行原生测试
- 为当前操作系统创建原生镜像
- 将原生镜像创建为 Docker 镜像
由于前三个工具要求本地安装 GraalVM 及其原生镜像编译器,所以首先要进行安装。
2.1 安装 GraalVM 及其原生镜像编译器
安装步骤如下:
1. 使用 SDKman(https://sdkman.io)安装 GraalVM。若未安装 SDKman,可使用以下命令安装:
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
- 使用以下命令验证 SDKman 是否正确安装:
sdk version
期望输出类似:
SDKMAN 5.18.1
- 在 Linux 上,GraalVM 的原生镜像编译器需要安装 GCC。若在 Windows 的 WSL 2 下运行 Ubuntu 实例,可使用以下命令安装 GCC 及其依赖项:
sudo apt install -y build-essential libz-dev zlib1g-dev
- 安装 GraalVM 22.3.1 版本(适用于 Java 17),并将其设置为默认 Java 版本:
sdk install java 22.3.1.r17-grl
sdk default java 22.3.1.r17-grl
- 安装原生镜像编译器:
gu install Native Image
- 验证安装:
java -version
gu list
java -version
命令期望输出类似:
openjdk version "17.0.6" 2023-01-17
OpenJDK Runtime Environment GraalVM CE 22.3.1 (build 17.0.6+10-jvmci-22.3-b13)
OpenJDK 64-Bit Server VM GraalVM CE 22.3.1 (build 17.0.6+10-jvmci-22.3-b13, mixed mode, sharing)
gu list
命令期望输出:
| ComponentId | Version | Component name |
| — | — | — |
| graalvm | 22.3.1 | GraalVM Core |
| Native Image | 22.3.1 | Native Image |
2.2 运行跟踪代理
对于某些情况,可能需要使用跟踪代理生成所需的可达性元数据。若要尝试使用跟踪代理,可按以下步骤操作:
1. 移除
build.gradle
文件中所选微服务部分
jvmArgs
参数前的注释字符
//
以激活该参数。
2. 运行 Gradle 测试命令,以产品服务为例:
cd $BOOK_HOME/Chapter23
./gradlew :microservices:product-service:test --no-daemon
此为正常的 Gradle 测试命令,但为避免内存不足,禁用了 Gradle 守护进程。默认情况下,守护进程的堆内存限制为 512 MB,在大多数情况下对跟踪代理而言不足。
3. 测试完成后,应在
microservices/product - service/src/main/resources/META - INF/Native Image
文件夹中找到以下文件:
-
jni - config.json
-
predefined - classes - config.json
-
proxy - config.json
-
reflect - config.json
-
resource - config.json
-
serialization - config.json
浏览生成的文件后,在构建文件的
jvmArgs
参数前添加注释以禁用跟踪代理,并删除创建的文件。
2.3 执行原生测试
执行原生测试有助于自动发现创建原生镜像时的问题,但目前存在一些问题导致无法对部分微服务使用原生测试:
- 使用 Testcontainers 的测试不能用于原生测试,详情见 https://github.com/spring - projects/spring - boot/issues/35663。
- 使用 Mockito 的测试也不能用于原生测试,详情见 https://github.com/spring - projects/spring - boot/issues/32195 和 https://github.com/mockito/mockito/issues/2435。
因此,所有测试类都使用
@DisabledInNativeImage
注解在类级别禁用了原生测试。这意味着仍可运行原生测试命令,所有原生镜像将被创建,但目前原生镜像中不会执行任何测试。随着这些问题的解决,可逐步移除
@DisabledInNativeImage
注解,使更多测试能由原生测试命令运行。
要对所有四个微服务运行原生测试,可使用以下命令:
./gradlew nativeTest
要测试特定微服务,可使用类似命令:
./gradlew :microservices:product-service:nativeTest
每次对微服务进行测试后,原生测试工具会生成如下测试报告:
JUnit Platform on Native Image - report
...
[ 13 tests found ]
[ 13 tests skipped ]
[ 0 tests started ]
[ 0 tests aborted ]
[ 0 tests successful ]
[ 0 tests failed ]
从报告中可看出,目前所有测试均被跳过。
2.4 为当前操作系统创建原生镜像
可使用 Gradle 的
nativeImage
命令为当前操作系统和硬件架构创建可执行文件。以
product - composite
微服务为例,操作步骤如下:
1. 使用以下命令创建原生镜像:
./gradlew microservices:product-composite-service:nativeCompile
可执行文件将在
build/native/nativeCompile
文件夹中创建,文件名为
product - composite - service
。
2. 使用
file
命令检查可执行文件:
file microservices/product-composite-service/build/native/nativeCompile/product-composite-service
输出可能类似:
…product-composite-service: Mach-O 64-bit executable arm64
其中,
Mach - O
表示文件是为 macOS 编译的,
arm64
表示是为 Apple 硅芯片编译的。
3. 手动启动所需资源。在此例中,仅需启动 RabbitMQ 即可成功启动:
docker-compose up -d rabbitmq
-
在终端中通过指定与
docker - compose文件中相同的环境变量来启动原生镜像:
SPRING_RABBITMQ_USERNAME=rabbit-user-prod \
SPRING_RABBITMQ_PASSWORD=rabbit-pwd-prod \
SPRING_CONFIG_LOCATION=file:config-repo/application.yml,file:config-repo/product-composite.yml \
microservices/product-composite-service/build/native/nativeCompile/product-composite-service
它应能快速启动,并在日志输出中打印类似内容:
Started ProductCompositeServiceApplication in 0.543 seconds
- 调用其存活探针进行测试:
curl localhost:4004/actuator/health/liveness
期望输出:
{"status":"UP"}
-
按
Ctrl + C停止执行,并使用以下命令停止 RabbitMQ 容器:
docker-compose down
虽然这是创建原生镜像最快的方法,但对于后续测试场景不太实用,更需要为 Linux 构建原生镜像并将其放入 Docker 容器中。
2.5 将原生镜像创建为 Docker 镜像
创建步骤如下:
1. 此过程非常消耗资源,首先确保 Docker Desktop 允许至少消耗 10 GB 内存,以避免内存不足错误。
2. 若计算机内存小于 32 GB,此时可停止 minikube 实例以避免计算机内存不足,使用以下命令:
minikube stop
- 确保 Docker 客户端与 Docker Desktop 通信,而非与 minikube 实例通信:
eval $(minikube docker-env -u)
- 运行以下命令编译产品服务:
./gradlew :microservices:product-service:bootBuildImage --no-daemon
若构建失败并出现类似
<container - name> exited with code 137
的错误消息,表明 Docker 内存不足。编译过程中会有大量输出,包括各种警告和错误消息。成功编译的日志输出类似:
Successfully built image 'docker.io/hands-on/native-product-service:latest'
- 使用以下命令对其余三个微服务进行原生编译:
./gradlew :microservices:product-composite-service:bootBuildImage --no-daemon
./gradlew :microservices:recommendation-service:bootBuildImage --no-daemon
./gradlew :microservices:review-service:bootBuildImage --no-daemon
- 使用以下命令验证 Docker 镜像是否成功构建:
docker images | grep "hands-on/native"
3. 使用 Docker Compose 进行测试
为使用包含原生编译微服务的 Docker 镜像,创建了两个新的 Docker Compose 文件
docker - compose - native.yml
和
docker - compose - partitions - native.yml
。它们是
docker - compose.yml
和
docker - compose - partitions.yml
的副本,移除了微服务定义中的
build
选项,并更改了要使用的 Docker 镜像名称,使用之前创建的以
native -
开头的镜像。
为比较启动时间和初始内存消耗,将进行以下测试:
- 使用禁用 AOT 模式的基于 Java VM 的微服务。
- 使用启用 AOT 模式的基于 Java VM 的微服务。
- 使用包含原生编译微服务的 Docker 镜像。
首先,使用以下命令停止 minikube 实例以避免端口冲突:
minikube stop
3.1 测试禁用 AOT 模式的基于 Java VM 的微服务
操作步骤如下:
1. 在 Docker Desktop 中编译源代码并构建基于 Java VM 的 Docker 镜像:
cd $BOOK_HOME/Chapter23
eval $(minikube docker-env -u)
./gradlew build
docker-compose build
- 使用基于 Java VM 的微服务的默认 Docker Compose 文件:
unset COMPOSE_FILE
- 启动除微服务容器外的所有容器:
docker-compose up -d mysql mongodb rabbitmq auth-server gateway
等待容器启动,直到 CPU 负载下降。
4. 使用 Java VM 启动微服务:
docker-compose up -d
等待微服务启动,再次监控 CPU 负载。
5. 使用以下命令查找微服务启动所需的时间:
docker-compose logs product-composite product review recommendation | grep ": Started"
输出中可看到启动时间在 5.5 到 7 秒之间。由于四个微服务实例同时启动,与逐个启动相比,启动时间更长。
6. 运行测试以验证系统环境是否按预期工作:
USE_K8S=false HOST=localhost PORT=8443 HEALTH_URL=https://localhost:8443 ./test-em-all.bash
- 期望测试输出与之前看到的相同。
- 使用以下命令查看启动并运行测试后使用的内存:
docker stats --no-stream
可看到微服务消耗约 240 - 310 MB 内存。
9. 关闭系统环境:
docker compose down
3.2 测试启用 AOT 模式的基于 Java VM 的微服务
操作步骤如下:
1. 启动除微服务容器外的所有容器:
docker-compose up -d mysql mongodb rabbitmq auth-server gateway
-
编辑每个微服务的 Dockerfile,在
ENVIRONMENT命令中设置-Dspring.aot.enabled=true,使其如下所示:
ENTRYPOINT ["java", "-Dspring.aot.enabled=true", "org.springframework.boot.loader.JarLauncher"]
- 重建微服务:
docker-compose build
- 启动微服务:
docker-compose up -d
- 检查 AOT 模式:
docker-compose logs product-composite product review recommendation | grep "Starting AOT-processed"
期望输出包含四行
Starting AOT - processed
。
6. 检查启动时间:
docker-compose logs product-composite product review recommendation | grep ": Started"
期望输出与禁用 AOT 模式时类似,但启动时间稍短。例如,启动时间可能在 4.5 到 5.5 秒之间,比正常 Java VM 启动时间快 1 到 1.5 秒。
7. 运行
test - em - all.bash
:
USE_K8S=false HOST=localhost PORT=8443 HEALTH_URL=https://localhost:8443 ./test-em-all.bash
期望输出与禁用 AOT 模式时相同。
8. 恢复 Dockerfile 中的更改并重建 Docker 镜像以禁用 AOT 模式。
9. 关闭系统环境:
docker compose down
3.3 测试原生编译的微服务
操作步骤如下:
1. 切换到新的 Docker Compose 文件:
export COMPOSE_FILE=docker-compose-native.yml
- 启动除微服务容器外的所有容器:
docker-compose up -d mysql mongodb rabbitmq auth-server gateway
等待容器启动,直到 CPU 负载下降。
3. 使用 Java VM 启动微服务:
docker-compose up -d
等待微服务启动,再次监控 CPU 负载。
4. 使用以下命令查找原生编译微服务的启动时间:
docker-compose logs product-composite product review recommendation | grep ": Started"
可看到启动时间在 0.2 - 0.5 秒之间。考虑到所有微服务实例同时启动,与基于 Java VM 的测试所需的 5.5 到 7 秒相比,这个时间非常可观。
5. 运行测试以验证系统环境是否按预期工作:
USE_K8S=false HOST=localhost PORT=8443 HEALTH_URL=https://localhost:8443 ./test-em-all.bash
期望输出与使用基于 Java VM 的 Docker 镜像的测试相同。
6. 使用以下命令查看启动并运行测试后使用的内存:
docker stats --no-stream
可看到微服务消耗约 80 - 130 MB 内存,与 Java VM 容器使用的 240 - 310 MB 相比,有明显减少。
7. 关闭系统环境:
docker compose down
总结
通过上述步骤,我们完成了原生编译 Java 微服务的一系列操作,包括配置文件的设置、验证脚本的更新、GraalVM 及其原生镜像编译器的安装、原生镜像的测试和编译,以及使用 Docker Compose 进行不同模式下的测试。从测试结果可以看出,原生编译的微服务在启动时间和内存消耗方面具有明显优势。
以下是整体流程的 mermaid 流程图:
graph LR
A[安装 GraalVM 及其原生镜像编译器] --> B[运行跟踪代理]
B --> C[执行原生测试]
C --> D[为当前操作系统创建原生镜像]
D --> E[将原生镜像创建为 Docker 镜像]
E --> F[使用 Docker Compose 进行测试]
F --> F1[测试禁用 AOT 模式的基于 Java VM 的微服务]
F --> F2[测试启用 AOT 模式的基于 Java VM 的微服务]
F --> F3[测试原生编译的微服务]
通过这些操作和测试,我们可以更好地了解原生编译 Java 微服务的性能和优势,为实际项目的应用提供参考。
4. 性能对比分析
为了更直观地展示不同模式下微服务的性能差异,我们将上述测试结果进行汇总对比,如下表所示:
| 测试模式 | 启动时间范围 | 内存消耗范围 |
| — | — | — |
| 禁用 AOT 模式的基于 Java VM 的微服务 | 5.5 - 7 秒 | 240 - 310 MB |
| 启用 AOT 模式的基于 Java VM 的微服务 | 4.5 - 5.5 秒 | 240 - 310 MB |
| 原生编译的微服务 | 0.2 - 0.5 秒 | 80 - 130 MB |
从表格中可以清晰地看出,原生编译的微服务在启动时间和内存消耗方面都有显著的优化。启动时间大幅缩短,这对于需要快速响应和部署的场景非常有利,例如在云原生环境中进行弹性伸缩时,能够更快地启动新的服务实例。而内存消耗的减少,意味着在相同的硬件资源下,可以运行更多的微服务实例,提高资源利用率,降低成本。
5. 常见问题及解决方法
在进行原生编译 Java 微服务的过程中,可能会遇到一些常见问题,以下是一些问题及对应的解决方法:
5.1 内存不足问题
-
问题描述
:在运行跟踪代理或编译原生镜像时,可能会出现内存不足的错误,如构建 Docker 镜像时出现
<container - name> exited with code 137错误。 -
解决方法
:
- 确保 Docker Desktop 允许至少消耗 10 GB 内存。
- 若计算机内存小于 32 GB,停止 minikube 实例以释放内存。
-
在运行 Gradle 测试命令时,禁用 Gradle 守护进程,如
./gradlew :microservices:product-service:test --no-daemon。
5.2 原生测试无法使用问题
- 问题描述 :使用 Testcontainers 或 Mockito 的测试不能用于原生测试。
-
解决方法
:目前所有测试类使用
@DisabledInNativeImage注解在类级别禁用原生测试。随着相关问题的解决,逐步移除该注解,使更多测试能由原生测试命令运行。
6. 实际应用建议
根据上述测试和分析,以下是一些在实际项目中应用原生编译 Java 微服务的建议:
6.1 选择合适的场景
原生编译的微服务在启动时间和内存消耗方面有优势,适合对启动速度要求高、资源受限的场景,如云原生环境、边缘计算等。而对于对启动时间要求不高、开发和测试阶段,基于 Java VM 的微服务可能更合适,因为其开发和调试更加方便。
6.2 逐步引入
在实际项目中,可以先选择部分对性能要求较高的微服务进行原生编译测试,验证其性能和稳定性。在确保没有问题后,再逐步将更多的微服务迁移到原生编译模式。
6.3 持续监控和优化
引入原生编译微服务后,需要持续监控其性能指标,如启动时间、内存消耗、CPU 使用率等。根据监控结果进行优化,如调整配置参数、优化代码等。
7. 未来展望
随着技术的不断发展,原生编译 Java 微服务的性能和稳定性将不断提高。未来可能会有更多的工具和框架支持原生编译,使开发和部署更加便捷。同时,随着硬件技术的进步,原生编译微服务在更多的场景中将会得到更广泛的应用。
以下是一个关于原生编译 Java 微服务应用流程的 mermaid 流程图:
graph LR
A[需求分析] --> B{是否适合原生编译}
B -- 是 --> C[选择部分微服务进行原生编译测试]
B -- 否 --> D[使用基于 Java VM 的微服务]
C --> E[验证性能和稳定性]
E -- 通过 --> F[逐步迁移更多微服务]
E -- 未通过 --> G[优化和调整]
G --> C
F --> H[持续监控和优化]
综上所述,原生编译 Java 微服务为我们提供了一种更高效、更节省资源的微服务部署方式。通过合理的应用和优化,能够显著提升系统的性能和竞争力。在实际项目中,我们可以根据具体需求和场景,灵活运用这种技术,为业务的发展提供有力支持。
超级会员免费看
49

被折叠的 条评论
为什么被折叠?



