每个Docker镜像是否应该包含JDK?

27

所以,我对Docker非常陌生。让我解释一下问题的背景。

  1. 我有10-20个Spring Boot微服务应用程序,在本地机器上不同的端口上运行。

  2. 但是,根据我的学习,为了迁移到Docker,每个服务必须在不同的Docker容器中,以便快速部署或创建副本。

  3. 对于每个Docker容器,我们需要创建一个新的Docker镜像。

  4. 每个Docker镜像必须包含JRE以供Spring Boot应用程序运行。最大约为200 MB。这意味着每个Docker镜像最多为350 MB。 另一方面,在我的本地PC上只有一个200MB的JRE,每个应用程序只占用几MB的空间。

  5. 基于此,我需要600 MB的本地系统空间,但需要7 GB来存储所有Docker镜像。

这种方法正确吗?应该将DockerHub的"OpenJDK"添加到每个镜像中吗?

为什么即使目标PC可能已经安装了JDK,镜像的大小也很大?


5
您似乎在谈论JDK和JRE - 理想情况下,您应该避免使用JDK构建镜像,因为您只需要在构建时使用它,并且只在生产镜像中使用JRE。请注意,在Dockerfile中可以有多个FROM,因此您可以使用JDK进行构建,然后仅使用JRE进行打包。 - mcfedr
2
确实。看一下多阶段构建。这使你可以在一个镜像中使用JDK进行构建,然后将构建好的文件复制到更轻量的运行时镜像中。 - spender
4个回答

35

你的理解不正确。

Docker 镜像是由层组成的;请看下图:

当您在镜像中安装 JRE 时,假设它的检验和在下一张图片中为 91e54dfb1179,它将实际占用您的磁盘空间。

但是,如果您所有的容器都基于同一个镜像,然后向薄的 R/W 层添加不同的东西,比如不同的微服务应用程序,所有容器都会共享 91e54dfb1179,因此它不会是 n*m 的关系。

您需要尽可能地使用相同的基础镜像来部署所有 Java 应用程序,并将不同的内容添加到薄的 R/W 层。

输入图片说明


很好的回答,但我还有一个疑问。假设Docker镜像是在不同的系统中构建的?比如每个微服务都由不同地理位置的单独团队构建?这种现有JRE与ID共享的方式就不适用了,对吧? - SamwellTarly
@SamwellTarly 在适当的情况下使用一个好的通用基础镜像,这个基础镜像应该包含常见的重要部分。 - Christian Sauer
1
@SamwellTarly,你需要将一个基础镜像与大多数常见的东西对齐,至少包括你最关心的jre,然后将其与自定义基础镜像对齐。建议使用DockerHub或私有Docker注册表进行共享。这样每个服务团队都可以在此基础镜像上添加所需的内容。 - atline
你应该考虑使用OpenJDK作为你的基础镜像。 - JimmyJames
我怀疑在多个容器中使用共同的镜像不会占用单个空间。实际上,每个容器都会安装运行该镜像所需的所有库。因此,每个容器都将拥有所有库的副本(因此每个容器都有一个独立的OpenJDK副本)。 - Jignesh M. Khatri

5
其他答案已经很好地涵盖了Docker分层,所以我只想为您的问题添加细节。
这种方法正确吗?应该将DockerHub中的“OpenJDK”添加到每个镜像中吗?
是的。如果它不在镜像中,它就不会在容器中。但是,您可以通过尽可能多地重用层来节省磁盘空间。因此,请尝试从“最不可能更改”到“最可能更改”编写Dockerfile。因此,当您构建图像时,您看到“使用缓存”的次数越多,就越好。
即使目标PC可能已经有JDK,为什么镜像的大小仍然很大?
Docker希望与主机的联系越少越好。 Docker甚至不想处理主机。它要做的第一件事是创建一个VM来隐藏在其中。 Docker镜像假定主机唯一提供的是空闲的RAM、磁盘和CPU。因此,每个Docker镜像还必须包含自己的操作系统/内核。(这就是您初始的FROM所做的选择基本OS镜像)因此,您的最终镜像大小实际上是操作系统+工具+应用程序。镜像大小有点误导性,因为它是所有层的总和,这些层在镜像之间被重用。
(暗示)每个应用程序/微服务都应该在自己的容器中吗?
理想情况下,是的。通过将您的应用程序转换为独立的模块,可以更轻松地替换/负载平衡该模块。
实际上,也许不是(对于您来说)。Spring Boot并不是一个轻量级的框架。实际上,它是一个用于模块化代码的框架(在模块控制系统内运行模块控制系统)。现在你想托管10-20个应用程序?那可能无法在单个服务器上运行。 Docker将强制Spring boot在每个应用程序中加载到内存中;对象现在不能跨模块重用,因此这些对象也需要多次实例化!如果您受限于1个生产服务器,则无法进行水平扩展。(每个Spring Boot需要约1GB HEAP(RAM),具体取决于您的代码库)。有了10-20个应用程序,为了进行Docker部署而重构使应用程序变得更轻可能不可行/超出预算。更不用说,如果您无法在本地测试中运行最小设置(内存不足),开发工作将变得更加“有趣”。
Docker不是万能的工具。尝试一下,评估自己和您的团队的利弊,并决定是否值得为您和您的团队(s)带来好处。

我喜欢你的回答,但同时它也引发了思考。你会建议什么替代方案来让每个微服务作为一个Spring Boot应用运行?这样可以实现非常松散的耦合,并且不需要像旧的更大的Spring应用程序一样进行部署步骤。微服务之间可以相互通信。那么,在运行Docker镜像的机器上,它们都将使用相同的JRE,从而消除了每个容器需要1GB堆的需求,对吗? - SamwellTarly
@SamwellTarly 容器将共享(大部分)基础镜像,但它们的运行时内存(R+W层和RAM)是每个容器独立隔离的。因此,每个容器的JVM都需要将其使用的资源加载到内存中(而Spring Boot使用了大量资源)。Docker实际上是基于12 Factor App设计哲学构建的,该哲学假定您的微服务都是为单独的VMs/机器而设计的。尽管如此,一种妥协方案是首先在一个Docker容器中构建所有内容,然后在重构以实现轻量级部署时创建更多容器。 - Tezra
@SamwellTarly 最终图像越小,最终RAM占用越轻,您启动容器的速度就越快(如果您想利用Docker容器扩展/负载平衡,这将是一个重大问题)。即使您只使用1个容器,它也可以解决“在我的计算机上运行”的问题(大多数情况下)。为了获得更有针对性的答案,最好您提出另一个关于如何通过切换到Docker来解决您正在尝试解决的任何问题的问题。 - Tezra
是的,我明白容器包括RAM使用必须最小化。然而,亚马逊的云教程本身将每个微服务作为Spring Boot应用程序使用。基础JVM将要求2GB的RAM映射。但是,每个微服务在我的本地PC上只使用很少的RAM(10MB)。如果需要更多的RAM,集群管理器会处理吗?您能否指出您的来源,说明Spring Boot在云平台上很重,并且需要大量的RAM? - SamwellTarly
@SamwellTarly 如果内存不是问题,那显然这不是一个问题。如果您有有限的服务器资源限制,那么群集管理器无法分配超过群集中的资源。当然,如果您没有使用Java 11+,则使用Java +容器的第一个主要问题是Java将从群集中过度分配堆。我无法向您指出关于Spring很重的硬数字,因为任何关于它的博客都只进行表面测试,仅证明“Spring在纸上很轻”,但我已经看到Spring在实践中可能会增加巨大的启动和运行时开销。(高达X5) - Tezra
@SamwellTarly 这并不完全是Spring的问题。正如我所说,这将在很大程度上取决于你的代码库是如何设计的。如果做得“正确”,Spring Boot可以以最小的开销使用,如果做错了,它将加剧你的代码库中存在的问题。这就是为什么我说要亲自尝试它,但不要认为Docker会为你解决所有问题。它可能会让情况变得更糟。你可以调整各种东西来弥补,但这个调整过程并不总是在预算内。只有你自己可以决定。 - Tezra

2
Lagom的回答非常好,但我想补充一点,即Docker容器的大小应该尽可能小,以便于传输和存储。”
“因此,有很多基于Alpine Linux发行版的容器非常小。如果可能的话,请尝试使用它们。”
“此外,不要将每个可想象的工具都添加到您的容器中,例如,您通常可以不使用wget...”

当然不仅仅是wget - 我曾经看到生产Docker镜像中包含各种愚蠢的东西,甚至包括完整的GCC发行版(在PHP应用程序中)。 - Sebastian Lenartowicz
@SebastianLenartowicz 很有趣!为什么?我看到的大多数东西都是用于测试或构建Python包的。大多数人不倾向于使用多层图像,这将防止出现这种特定的问题。 - Christian Sauer
明白了。因此需要具有最大继承性的强大设计。 - SamwellTarly
@ChristianSauer 因为Docker镜像是由对其目的有不完全理解的人构建的。他们想象需要在其中放置整个类Unix系统,以便在运行时可以进行修改和管理(我知道,我知道)。 - Sebastian Lenartowicz
2
@SamwellTarly 警告!这取决于情况!过多的继承会使整个项目难以处理。例如,如果您部署了多个微服务,则可能有益于拥有各种Java版本 - 例如,因为一个软件包存在缺陷,导致它无法在您首选的版本上运行所有其他服务。要找到平衡点!开发时间也是一个考虑因素 - 如果需要安装deps,那么让alpine映像正常工作可能会很麻烦。 - Christian Sauer

0
基于此,我的本地系统需要600 MB,而所有Docker镜像则需要7 GB。
这种方法是否正确?应该在每个图像中添加来自DockerHub的“OpenJDK”吗?
没错。虽然你可能会想知道是否仅使用JRE就足够了。
即使目标计算机可能已经安装了JDK,为什么映像的大小仍然很大?
你比较了不可比较的事物:本地环境(除了生产机器之外)与集成/生产环境。
在集成/生产环境中,您的应用程序负载可能很高,并且通常建议在应用程序之间进行隔离。因此,在此处,您希望通过每台计算机(裸机、VM或容器)托管最少数量的应用程序(UI/服务)以防止应用程序之间出现副作用:共享库不兼容性、软件升级副作用、资源耗尽、应用程序之间的链式故障…

在本地环境中,应用程序的负载相当低,并且应用程序之间的隔离通常不是一个严重的问题。因此,在本地机器上可以托管多个应用程序(ui/services),并且还可以共享操作系统提供的一些公共库/依赖项。 虽然您可以这样做,但将所有内容混合和共享在本地真的是一个好习惯吗? 我认为不是因为:
1)本地机器不是垃圾箱:您整天都在使用它。更加干净,您的开发效率就越高。例如:在本地托管的应用程序之间可能存在JDK/JRE的差异,某些应用程序使用的文件夹可能具有相同的位置,数据库版本可能不同,应用程序可能安装了不同的Java服务器(Tomcat、Netty、Weblogic)和/或不同版本...
通过容器,这不是问题:根据您的要求安装和删除所有内容。

2)环境(从本地到生产)应尽可能接近,以便简化整个集成-部署链,并及早检测问题,而不仅仅是在生产中检测。

顺便说一句,在本地实现这一点需要为开发人员提供真正的机器。


所有事物都有代价,但实际上并不昂贵

除了隔离(硬件和软件资源),容器还带来其他优点,如快速部署/取消部署、可扩展性和故障转移友好(例如:Kubernetes 依赖于容器)。 隔离、快速性、可扩展性和鲁棒性友好都有一个代价:不能在容器之间共享任何资源(操作系统、库、JVM 等)。

这意味着,即使您在应用程序中使用完全相同的操作系统、库、JVM,每个应用程序也必须将它们包含在其镜像中。
这很昂贵吗? 实际上并不是:官方镜像通常依赖于 Alpine(轻量级 Linux 操作系统,具有限制但如果需要可以自定义),那么一个 350 MB 的镜像(您引用的值实际上就是这个)在成本上代表什么?
事实上,这非常便宜。 在集成/生产中,您的所有服务很可能不会托管在同一台机器上,因此将容器的 350 MB 与传统虚拟机中用于集成/生产的资源进行比较,后者包含完整的操作系统以及安装的多个其他程序。您会明白容器的资源消耗并不是问题。这甚至被认为是超越本地环境的优势。


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