让一个容器使用现有容器的OpenJDK和库

19

我正在进行与容器相关的冷启动问题实验,作为我的论文研究。我的测试应用程序是一个构建在openjdk镜像上的spring boot应用程序。我想要尝试解决冷启动问题的第一件事是:

有一个准备好的容器,容器里包含了openjdk和springboot应用程序使用的库。我启动我的另一个容器,使用已有容器的ipc和networknamespace,然后能够使用这个容器的openjdk和库来运行jar文件。

我不确定如何实现这个过程?我是否可以通过使用卷实现这个目标,还是应该寻找完全不同的方法?

另外,如果我想让x个容器运行,我会确保有x个预先存在的容器在运行。这是为了确保每个容器都有自己特定的库容器可供使用。这样做可以吗?

简而言之,任何能够通过使用通过ipc/net连接的第二个容器来加速spring boot应用程序的方法,都将对我的问题有所帮助。


你所描述的更像是传统的Java应用服务器;你可以考虑流行的开源选项Apache Tomcat。在Docker容器之间无法共享库或应用程序运行时(或正在运行的进程)。 - David Maze
有没有办法加速Spring Boot应用程序的运行速度?例如,Spring Boot使用Tomcat服务器,但是设置需要的时间有点长,我能否在现有的Docker容器中预加载该Tomcat服务器,然后使用它?我需要在Docker中完成这个任务(为了我的论文)。 - Jordy Dieltjens
1
一个简单的概念需要理解。容器意味着隔离。这就回答了你如何在容器之间使用共享库的问题。但是你可以通过阶段创建你的Docker镜像,比如一个库的镜像、一个应用程序的镜像、一个第三方应用程序的镜像,在运行时只需调用创建容器的组合即可。 - Dupinder Singh
4个回答

5

Spring Boot是一个纯粹的“运行时”框架。

如果我理解你的问题正确,你描述的情况如下:

比如说你有一个带有JDK和一些jar包的容器A。但这本身并不意味着你有一个正在运行的进程。所以更像是一个准备重复使用的文件卷(或者在docker镜像方面的一个层)。

此外,你还有另一个容器B,其中包含一个应该以某种方式启动的Spring Boot应用程序(可能使用容器A中的OpenJDK或其专用JDK)。

现在你到底想要“加速”什么?镜像大小(更小的镜像意味着在CI/CD流水线中更快的部署)?Spring Boot应用程序启动时间(从生成JVM到Spring Boot应用程序启动运行的时间间隔)?或者你试图在运行时加载更少的类?

解决这些问题的技术是不同的。但总的来说,我认为你可能需要查看Graal VM集成,其中包括创建本机映像并加速启动时间的功能。这些东西相当新,我自己还没有尝试过。我相信这还在进行中,Spring将努力推动这一进展(这只是我的猜测,所以不要过分相信)。

无论如何,您可能会对阅读这篇文章感兴趣

然而,我怀疑它与您描述的研究有关。

更新1

根据您的评论-让我给出一些额外的信息,可能有所帮助。此更新包含来自“现实生活”工作经验的信息,我发布它是因为它可能有助于在您的论文中找到方向。

所以,我们首先有一个spring boot应用程序。

默认情况下,它是一个JAR文件,并且Pivotal建议提供WAR选项(正如他们的开发者代言人Josh Long所说:“制作JAR而不是WAR”)

这个spring boot应用程序通常包括一些Web服务器-传统的Spring Web MVC应用程序默认使用Tomcat,但您可以将其切换为Jetty或undertow。如果您正在运行“反应式应用程序”(Spring WebFlux自spring boot 2以来支持),则默认选择为Netty。

需要注意的是,并非所有使用Spring Boot驱动的应用程序都必须包含某种嵌入式Web服务器,但我将忽略这个微妙的点,因为您似乎针对Web服务器的情况(您提到了Tomcat,更快地处理请求等),因此我假设您是在关注此方面。

现在,让我们尝试分析启动Spring Boot应用程序JAR时发生的情况。

首先,JVM本身开始运行 - 进程启动,堆分配,内部类加载等等。这可能需要一些时间(大约需要1秒甚至更长时间,具体取决于服务器、参数、硬盘速度等)。 这个线程讨论了JVM是否真的启动缓慢,我可能无法为此添加更多内容。

好的,现在是时候加载Tomcat的内部类了。这又可以在现代服务器上花费几秒钟。Netty似乎更快,但您可以尝试下载Tomcat的独立发行版并在计算机上启动它,或者创建一个没有Spring Boot但有嵌入式Tomcat的示例应用程序来看看我在说什么。

到目前为止,我们的应用程序还没有出现问题。正如我在开始时所说,Spring Boot 是一个纯运行时框架。因此,必须加载 Spring/Spring Boot 本身的类,然后加载应用程序本身的类。如果应用程序使用某些库,它们也会被加载,有时甚至会在应用程序启动期间执行自定义代码:Hibernate 可以检查模式和/或扫描数据库模式定义,甚至更新底层模式,Flyway/Liquidbase 可以执行模式迁移等等,Swagger 可能会扫描控制器并生成文档等等。
现在,在“现实生活”中,这个过程甚至可能需要一分钟甚至更长时间,但这并不是因为 Spring Boot 本身,而是因为应用程序中创建的 bean 具有“构造函数”/“post-construct”中的一些代码——这是在 Spring Boot 应用程序上下文初始化期间发生的事情。另外一个副注,我不会真正深入研究 Spring Boot 应用程序启动过程的内部工作原理,Spring Boot 是一个非常强大的框架,在幕后进行了许多事情,我假设您以某种方式使用过 Spring Boot——如果没有,请随时就此提出具体问题——我/我的同事将尝试解决。
如果您访问start.spring.io,可以创建一个示例演示应用程序 - 它将加载得非常快。因此,这完全取决于您的应用程序bean。
在这种情况下,到底应该优化什么?
您在评论中提到可能会有一个tomcat运行一些JAR文件,以便它们不会在spring boot应用程序启动时加载。
好吧,正如我们的同事所提到的那样,这更像是我们这个行业“使用了大约20年”的“传统”Web Servlet容器/应用服务器模型。
这种部署方式确实有一个“始终运行”的JVM进程,它随时准备好接受你的应用程序的WAR文件 - 一种包含应用程序的归档文件。一旦检测到WAR文件被投放到某个文件夹中,它将通过创建分层类加载器并加载应用程序的JAR/类来“部署”该应用程序。在你的情况下,有趣的是可以将库“共享”给多个WAR,以便只加载一次。例如,如果你的Tomcat托管了3个应用程序(读取3个WAR),并且所有应用程序都使用Oracle数据库驱动程序,你可以将这个驱动程序的jar放到某个共享的libs文件夹中,它将仅由类加载器加载一次,这是每个“WAR”创建的类加载器的“父级”。这个类加载器层次结构非常重要,但我相信它超出了问题的范围。
我曾经使用过两种模型(基于Spring Boot驱动的嵌入式服务器模型,没有Spring Boot的嵌入式Jetty服务器和“老派”的Tomcat/JBoss部署)。
根据我的经验,并且随着时间的证明,我们许多同事都同意这一点,Spring Boot 应用在操作方面有很多更方便的原因(再次强调,这些原因超出了问题的范围,在我看来,请告诉我如果您需要更多信息),这就是为什么它是当前的“趋势”,而“传统”的部署方式仍然存在于行业中,因为有许多非纯技术原因(历史原因、系统已被定义为维护模式、您已经拥有部署基础设施、一支“系统管理员”团队“知道”如何进行部署等等,但归根结底没有纯粹的技术原因)。
现在有了所有这些信息,您可能更好地理解为什么我建议看一下 Graal VM,它将通过本地映像加快应用程序启动速度。
还有一个可能相关的问题。如果您选择能够快速启动的技术,那么您可能会进入 Amazon Lambda 或其他云提供商提供的替代方案。
这个模型允许“计算”能力(CPU)的无限扩展,他们会在幕后“启动”容器,并在检测到容器实际上没有什么事可做时立即“杀死”它们。对于这种应用来说,Spring Boot 简单地不能胜任,而基本上 Java 也是如此,因为 JVM 进程相对较慢启动,所以一旦像这样启动容器,直到它变得可操作,需要太长时间。
你可以阅读 这里 了解 Spring 生态系统在这个领域所提供的内容,但这并不与你的问题相关(我正在提供方向)。
当您需要一个可能需要一些时间来启动的应用程序,但一旦启动就可以非常快速地完成工作时,Spring Boot 发挥作用。是的,如果应用程序没有“占用”,则有可能停止应用程序(我们使用术语“扩展/缩减规模”),这种方法也是比较新的(~3-4年),最适合在像 kubernetes、amazon ECS 等“管理”的部署环境中使用。

我明天会研究这个问题,但你确实正确理解了问题。对我来说,目标是加快应用程序的启动时间,以便更快地接受请求。我考虑可以加速的事情有:将容器分成库/依赖项和实际程序;在其他机器上已经运行JVM实例;在容器上设置好可供使用的Tomcat服务器。 - Jordy Dieltjens
我快速浏览了一下这篇文章,似乎不适用于我的研究案例。理论上,我的方法应该能够支持所有Java应用程序,并对代码进行最小的更改,因此我认为Graal VM在这里无法使用;尽管明天我会更深入地研究一下。 - Jordy Dieltjens
谢谢你的更新,它们给了我一些思考的食粮! - Jordy Dieltjens

4

所以,如果加速应用程序启动是您的目标,我认为您需要采取不同的方法。以下是我认为如此的原因:

  • Docker: 容器是镜像的运行实例,可以将镜像看作文件系统(实际上不仅是这样,但我们正在讨论库)。在容器中,您有JDK(我猜您的镜像基于Tomcat)。Docker引擎具有非常良好设计的缓存系统,因此容器启动非常快,如果对容器没有进行更改,则Docker只需从缓存中检索一些信息。这些容器被隔离开来,出于好的原因(安全性,模块化以及关于库的隔离,使您可以在不同的容器中拥有更多版本的库)。卷并不是您想象的那样,它们不是设计用于共享库,它们允许您打破隔离以进行某些操作。例如,您可以为代码库创建一个卷,以便在编程阶段进行更改时无需为每个更改重新构建图像,但通常您在生产环境中看不到它们(也许是某些配置文件)。

  • Java/Spring:Spring是基于Java的框架,Java基于JDK,Java代码在虚拟机上运行。因此,要运行Java程序,您必须启动该虚拟机(没有其他方法),当然您无法缩短此启动时间。Java环境非常强大,但这就是为什么很多人更喜欢Node.js,特别是对于小型服务,Java代码的启动速度很慢(分钟级别相对于秒级别)。Spring如前所述基于Java,Servelets和Context。 Spring应用程序存在于该上下文中,因此要运行Spring应用程序,您必须初始化该上下文。

您正在运行一个容器,在其上运行一个虚拟机,然后初始化一个Spring上下文,并最终初始化应用程序的bean。出于依赖关系的原因,这些步骤是顺序执行的。您不能在初始化Docker、VM和Spring上下文后在其他地方运行应用程序,例如,如果您在Spring应用程序中添加了ChainFilter,则需要重新启动应用程序,因为您需要向系统添加Servlet。如果要加速启动过程,则需要更改Java VM或对Spring初始化进行一些更改。总之,您正在尝试以高层次而不是低层次来处理此问题。


你能给我一些低级技巧的例子,让我可以研究解决我的问题吗?另外,库共享也是我觉得很有趣并希望能在我的系统中实现的东西,但我还不太理解这个概念。 - Jordy Dieltjens
我编辑了我的回答,我错过了一个重要的“不”,卷并不是为了在容器之间共享库而设计的,但它们是为了在主机文件系统和客户机之间共享文件而设计的。唯一可以加速启动的低级操作是:更改Java语言(减少机器指令,提高速度),轻量级VM(专门为Spring设计)以及更改Spring上下文使其更模块化,并从远程初始化,这样您实际上可以将上下文与应用程序或部分分离,并减少需要重新启动应用程序的次数。 - GJCode
嗯,好的,所以Docker不支持任何共享库的方式,我应该更深入地研究问题的根源? - Jordy Dieltjens
是的,Docker 无法解决这个问题,而且你的问题与库无关,库中的代码可能会进行惰性加载或静态链接,因此不会增加启动时间。无论如何,您需要查看 Spring Boot(和 Tomcat)架构,因为使启动变慢的原因与必须创建多少个 Servlet、这些 Servlet 之间的网络配置以及如何创建 Sping 上下文(Spring Boot 需要的所有东西的配置)有关。 - GJCode

4

回答你的第一个问题:

我不确定如何实现这个?我可以通过使用卷来实现吗,还是应该寻找完全不同的方法?

这必须与您基础设施的实际能力保持平衡。

  1. 可以拥有完整的光纤电缆网络,在此网络中具有Docker存储库,因此具有可允许“大”图像的带宽
  2. 可能连接到其Docker存储库的连接较差,因此需要额外小的图像,但与库存储库之间的快速连接
  3. 可以使用蓝/绿部署技术,并且不关心图像和层的大小或启动时间

一件事情是,如果您关心图像和层的大小,那么这很好,这绝对是Docker建议的良好实践,但所有这些都取决于您的需求。关于保持图像和层的小建议是为您要分发的图像。如果这是您自己的应用程序的自己的图像,则应根据您的需求采取行动。

以下是我的一点经验:在我曾工作的一家公司中,我们需要将数据库从生产环境同步回用户验收测试和开发环境。
由于生产环境的规模,将数据从容器的入口点中的SQL文件导入大约需要20分钟。这对于UAT环境可能还好,但对于开发人员的环境来说不行。

因此,在尝试了各种SQL文件的小改进(如禁用外键检查等)后,我想出了一个全新的方法:我创建了一个包含数据库的大型镜像,在每晚的构建中生成。这确实违反了Docker的所有最佳实践,但办公室的带宽允许容器在最坏的情况下在5分钟内启动,而之前需要20分钟。

因此,我的Docker SQL镜像的构建时间确实很长,但考虑到可用带宽,下载时间可接受,并且运行时间最大程度地缩短了。

这是利用一个事实的优势,即图像的构建只会发生一次,而从该图像派生的所有容器的启动时间将会发生。
为了回答您的第二个问题:
引申一下,如果我想运行x个容器,我会确保有x个预先存在的容器在运行。这是为了确保每个容器都有自己特定的librarycontainer可供使用。这样做可以吗?
我会说答案是否定的。
即使在微服务架构中,每个服务也应该能够完成某些操作。据我了解,您的实际not-library-container无法执行任何操作,因为它们与另一个容器的预先存在紧密耦合。
话虽如此,以下两点可能对您有用: 首先:请记住,您始终可以从另一个预先存在的镜像(即使是您自己的镜像)构建。
鉴于这将是您的library-container Dockerfile
FROM: openjdk:8-jdk-alpine
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

来源: https://spring.io/guides/topicals/spring-boot-docker/#_a_basic_dockerfile

然后您可以通过以下方式构建它

docker build -t my/spring-boot .

然后,您可以在该图像的基础上构建另一个容器:

FROM: my/spring-boot
COPY some-specific-lib lib.jar

其次:Docker 中有一种很好的技术来处理库,称为多阶段构建,可以在您的情况下使用。

FROM openjdk:8-jdk-alpine as build
WORKDIR /workspace/app

COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src

RUN ./mvnw install -DskipTests
RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=/workspace/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

来源:https://spring.io/guides/topicals/spring-boot-docker/#_multi_stage_build

正如在这个多阶段构建的来源中所述,Spring官网的指南中甚至提到了这种技术。


对于您的第一种方法,我的问题是它不能解决我目前想要解决的启动时间问题。对于您的第二种方法,这只能帮助处理图像大小?不过,我可以引用它们使用的共享卷中的库/依存项/类,并以这种方式将其用于我的jar包吗?之所以坚持我的方法,是因为这是我选择的论文主题,并且必须通过库共享来解决它。 - Jordy Dieltjens
好的,这就是我的Spring Boot知识不足以完全回答的地方。话虽如此,这取决于是什么让你的启动时间如此之长。如果这是因为你在entrypoint安装了第三方库,那么答案肯定是肯定的;如果是其他原因,那么你就必须理解它是什么,并且是否可以构建到你的容器中。最终,在纯Docker的角度来看,这就是你应该问自己的问题:“我有什么,我需要什么,我的所谓启动时间中的一些时间可以转移到构建时间吗?” - β.εηοιτ.βε
我会在答案中加入一些我的个人经验,但请记住,我首先不是一个Spring Boot开发者。 - β.εηοιτ.βε

1
您试图达到目标的方式违背了容器化的整个要点。
我们可以回到专注于目标 -- 您旨在“解决冷启动问题”和“加速Spring Boot应用程序”的目标。
您是否考虑过将Java应用程序实际编译为本地二进制代码?
JVM的本质是支持Java在相应的主机环境中的互操作性特性。由于容器本质上解决了互操作性,因此JVM的另一层解决方案是完全无关紧要的。
应用程序的本地编译将从应用程序运行时中分离出JVM,因此最终解决了冷启动问题。 GraalVM是一个工具,您可以使用它来进行Java应用程序的本地编译。有GraalVM容器映像(GraalVM Container Images)以支持您开发应用程序容器。
下面是一个示例Dockerfile,演示如何构建本地编译的Java应用程序的Docker映像。
# Dockerfile

FROM oracle/graalvm-ce AS builder

LABEL maintainer="Igwe Kalu <igwe.kalu@live.com>"

COPY HelloWorld.java /app/HelloWorld.java

RUN \
    set -euxo pipefail \
    && gu install native-image \
    && cd /app \
    && javac HelloWorld.java \
    && native-image HelloWorld

FROM debian:10.4-slim

COPY --from=builder /app/helloworld /app/helloworld

CMD [ "/app/helloworld" ]

# .dockerignore

**/*

!HelloWorld.java

// HelloWorld.java

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, Native Java World!");
    }
}

构建镜像并运行容器:

# Building...
docker build -t graalvm-demo-debian-v0 .

# Running...
docker run graalvm-demo-debian-v0:latest

## Prints
## Hello, Native Java World!

Spring Tips: The GraalVM Native Image Builder Feature 是一篇演示如何使用GraalVM构建Spring Boot应用程序的文章。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接