使用 Eclipse OpenJ9 运行 Eclipse Vert.x 应用程序

本操作指南提供了一些使用 OpenJ9 运行 Vert.x 应用程序的技巧。OpenJ9 是一个基于 OpenJDK 构建的替代 Java 虚拟机,内存使用率低。

Vert.x 是一种资源高效的工具包,用于构建各种现代分布式应用程序;而 OpenJ9 是一种资源高效的运行时,非常适合虚拟化和容器化部署。

你将构建和运行什么

  • 你将构建一个简单的微服务,通过 HTTP JSON 端点计算两个数字的和。

  • 我们将探讨使用 OpenJ9 改进启动时间的选项。

  • 我们将衡量 OpenJ9 在工作负载下的驻留集大小内存占用。

  • 你将为该微服务和 OpenJ9 构建一个 Docker 镜像。

  • 我们将讨论如何改进 Docker 容器的启动时间,以及如何在该环境中调优 OpenJ9。

您需要什么

  • 文本编辑器或 IDE

  • Java 21

  • OpenJ9

  • Maven 或 Gradle

  • Docker

  • Locust 用于生成工作负载

注意

Eclipse 基金会项目不允许分发、营销或推广 JDK 二进制文件,除非它们已通过从 Oracle 获得许可的 Java SE 技术兼容性工具包,而 Eclipse OpenJ9 项目目前无法访问该工具包。你可以构建自己的 Eclipse OpenJ9 二进制文件,或下载一个 IBM Semeru runtime

创建项目

此项目的代码包含功能等效的 Maven 和 Gradle 构建文件。

使用 Gradle

这是你应该使用的 build.gradle.kts 文件的内容

使用 Maven

编写服务

该服务公开了一个 HTTP 服务器,并包含在一个单独的 Java 类中

package io.vertx.howtos.openj9;

import io.vertx.core.Future;
import io.vertx.core.VerticleBase;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;

public class Main extends VerticleBase {

  @Override
  public Future<?> start() {
    Router router = Router.router(vertx);
    router.post().handler(BodyHandler.create());
    router.post("/sum").handler(this::sum);

    return vertx.createHttpServer()
      .requestHandler(router)
      .listen(8080);
  }

  private void sum(RoutingContext context) {
    JsonObject input = context.body().asJsonObject();

    Integer a = input.getInteger("a", 0);
    Integer b = input.getInteger("b", 0);

    JsonObject response = new JsonObject().put("sum", a + b);

    context.response()
      .putHeader("Content-Type", "application/json")
      .end(response.encode());
  }

  public static void main(String[] args) {
    long startTime = System.nanoTime();
    Vertx vertx = Vertx.vertx();
    vertx.deployVerticle(new Main()).await();
    long duration = MILLISECONDS.convert(System.nanoTime() - startTime, NANOSECONDS);
    System.out.println("Started in " + duration + "ms");
  }
}

我们可以运行该服务

$ ./gradlew run

然后使用 HTTPie 进行测试

$ http :8080/sum a:=1 b:=2
HTTP/1.1 200 OK
Content-Type: application/json
content-length: 9

{
    "sum": 3
}

$

我们还可以构建一个包含所有依赖项的 JAR 存档,然后执行它

$ ./gradlew shadowJar
$ java -jar build/libs/openj9-howto-all.jar

改进启动时间

微服务通过测量 main 方法入口与 HTTP 服务器启动时的回调通知之间的时间来报告启动时间。

我们可以运行几次 java -jar build/libs/openj9-howto-all.jar 并选择最佳时间。在我的机器上,我得到的最好时间是 311ms

OpenJ9 提供了预编译(ahead-of-time compiler)和类数据共享缓存,以改善启动时间并减少内存消耗。首次运行通常开销较大,但随后的所有运行都将受益于缓存,并且缓存也会定期更新。

相关的 OpenJ9 标志如下

  • -Xshareclasses:启用类共享

  • -Xshareclasses:name=NAME:缓存的名称,通常每个应用程序一个

  • -Xshareclasses:cacheDir=DIR:用于存储缓存文件的文件夹

让我们运行几次

$ java -Xshareclasses -Xshareclasses:name=sum -Xshareclasses:cacheDir=_cache -jar build/libs/openj9-howto-all.jar

在我的机器上,第一次运行需要 457ms,这比 311ms “多得多”!然而,随后的运行都在 130ms 附近,最好成绩是 112ms,这对于 JVM 应用程序的启动时间来说非常好。

内存使用

现在让我们测量微服务在使用 OpenJ9 时的内存使用情况,并与 OpenJDK 进行比较。

警告

这不是一个严格的基准测试。你已被警告 😉

生成一些工作负载

我们正在使用 Locust 生成一些工作负载。locustfile.py 文件包含模拟用户执行随机数求和的代码

from locust import *
import random
import json

class Client(HttpUser):
    wait_time = between(0.5, 1)
    host = "https://:8080"

    @task
    def sum(self):
        data = json.dumps({"a": random.randint(1, 100), "b": random.randint(1, 100)})
        self.client.post("/sum", data=data, name="Sum", headers={"content-type": "application/json"})

然后我们可以运行 locust,并连接到 https://:8089 来开始测试。让我们模拟 100 个用户,孵化率为每秒 10 个新用户。这使我们每秒大约有 130 个请求。

测量 RSS

Quarkus 团队有一篇关于测量 RSS 的好指南。在 Linux 上,你可以使用 pspmap 来测量 RSS,而在 macOS 上,ps 即可。我正在使用 macOS,所以一旦我获得正在运行的应用程序的进程 ID,我就可以如下获取其 RSS

$ ps x -o pid,rss,command -p 66820
    PID   RSS COMMAND
  66820 124032 java -jar build/libs/openj9-howto-all.jar

对于所有测量,我们都启动 Locust 并让它预热微服务。一分钟后,我们重置统计数据并重新开始测试,然后查看 RSS 和 99% 的延迟。我们将尝试在不进行调优的情况下运行应用程序,然后通过限制最大堆大小(参见 -Xmx 标志)来运行。

使用 OpenJDK 21 且未调优

  • RSS: 约 143 MB

  • 99% 延迟: 1ms

使用 Semeru 21 且未调优

  • RSS: 约 90 MB

  • 99% 延迟: 1ms

OpenJ9 在内存消耗方面显然非常高效,同时不影响延迟。

提示

像往常一样,请对这些数据持保留态度,并根据你的用途,在自己的服务上使用适当的工作负载进行自己的测量。

构建和运行 Docker 镜像

好的,我们已经看到 OpenJ9 在内存方面是多么温和,即使没有进行调优。现在让我们将微服务打包成一个 Docker 镜像。

这是你可以使用的 Dockerfile

FROM ibm-semeru-runtimes:open-21-jdk
RUN mkdir -p /app/_cache
COPY build/libs/openj9-howto-all.jar /app/app.jar
VOLUME /app/_cache
EXPOSE 8080
CMD ["java", "-Xvirtualized", "-Xshareclasses", "-Xshareclasses:name=sum", "-Xshareclasses:cacheDir=/app/_cache", "-jar", "/app/app.jar"]

你可以注意

  • -Xvirtualized 是一个用于虚拟化/容器环境的标志,以便 OpenJ9 在空闲时减少 CPU 消耗

  • /app/_cache 是一个卷,必须挂载它才能让容器共享 OpenJ9 类缓存。

镜像可以像这样构建

$ docker build . -t openj9-app

然后我们可以从该镜像创建容器

$ docker run -it --rm -v /tmp/_cache:/app/_cache -p 8080:8080 openj9-app

同样,第一个容器启动较慢,而随后的容器则受益于缓存。

提示

在某些平台上,cgroups 可能不会授予访问类数据缓存目录的权限。你可以使用 z 标志来解决此类问题,例如

docker (…​) -v /tmp/_cache:/app/_cache:z (…​)

总结

  • 我们使用 Vert.x 编写了一个微服务。

  • 我们在 OpenJ9 上运行了这个微服务。

  • 我们通过使用类数据共享改进了启动时间。

  • 我们让微服务承受了一定负载,然后检查了与使用 HotSpot 的 OpenJDK 相比,OpenJ9 的内存占用仍然很低。

  • 我们构建了一个包含 OpenJ9 的 Docker 镜像,通过类数据共享实现了快速的容器启动时间,并在空闲时减少了 CPU 使用。


最后发布时间:2025-02-08 00:37:51 +0000。