1

使用 Jib 构建 Docker 镜像是否有助于优化远程 Docker 存储库存储?

我们在 Docker 中使用 Spring Boot 和 Gradle。目前,我们正在创建标准的胖 Boot jar,其中包含所有依赖项,然后我们用它创建一个图像,如下所示:

FROM gcr.io/distroless/java:11
COPY ./build/libs/*.jar app.jar
CMD ["app.jar"]

这导致我们每次构建时都会生成一个大 (250 MB) 的新映像,即使实际上更改的代码很少。这是因为 fat jar 包含共享依赖项(不经常更改)和我们的代码。这是我们私有存储库中存储空间的低效使用,我们想改变它。

为此,想法如下:

  • 我们创建一个只包含 /opt/libs 中的依赖项的基础镜像,让我们调用它spring-base:1.0.0并推送到我们的私有 Docker 注册表。

  • 我们将该图像用作仅包含我们的代码的应用程序图像的父/基。Dockerfile 看起来与此类似(未经测试,只是为了呈现概念):

    FROM our-registry/spring-base:1.0.0
    COPY ./build/classes/kotlin/main/* /opt/classes
    COPY ./build/resources/main/* /opt/resources
    ENTRYPOINT ["java", "-cp", "/opt/libs/*:/opt/resources:/opt/classes", "com.example.MainKt"]
    

期望这些镜像要小得多,具有依赖关系的大基础镜像只存储一次,节省大量存储空间。

我们的一位同事研究了 Jib 并坚持认为它确实做到了这一点,但是在阅读了整个文档和常见问题解答并玩了一下之后,我不太确定。我们集成并使用./gradlew jibDockerBuild它,它似乎确实为依赖项、资源和类创建了层,但仍然只有一个大图像。Jib 似乎专注于加快构建时间(通过利用 Docker 层缓存)和可重现的构建,但我认为当我们将该图像上传到我们的存储库时,相对于我们当前的解决方案没有任何改变 - 我们仍将存储“静态”依赖项多次,但现在我们将有多个图层,而不是每个新图像中只有一个。

有更多 Docker 和 Jib 经验的人能否解释一下 Jib 是否为我们提供了我们正在寻找的存储空间优化?

编辑:在等待答案时,我玩弄了所有这些并使用了https://github.com/wagoodman/divedocker system dfdocker images检查尺寸并查看图像和图层,似乎 Jib 确实做了什么我们需要。

4

1 回答 1

3

使用 Jib 构建 Docker 镜像是否有助于优化远程 Docker 存储库存储?

是的。事实上,它在很大程度上有助于实现这一点,因为图像层的再现性很强。仅使用 时Dockerfile,您通常会完全失去大多数层的可重复性,因为文件时间戳被考虑到检查层是否相同。例如,即使您的字节.class根本没有改变,如果再次生成文件,您将失去再现性。这对 jar 来说更糟;不仅它的时间戳可以改变,而且 jar 元数据(例如,META-INF/MANIFEST.MF)包含编译时信息,包括时间戳、构建工具信息、JVM 版本等。构建在不同机器上的 jar 在 Docker 世界中将被认为是不同的。

这导致我们每次构建时都会生成一个大 (250 MB) 的新映像,即使实际上更改的代码很少。这是因为 fat jar 包含共享依赖项(不经常更改)和我们的代码。

部分正确的是大小很大(250MB),但不是因为胖罐子。构建的镜像的大小将始终为 250MB,即使它不是一个胖 jar,即使您为共享库指定了不同的层。最终映像 (250MB) 的大小将始终包括基础映像 ( gcr.io/distroless/java:11) 的大小和共享库的大小,无论该映像是由哪个工具构建的。

但是,Docker 引擎不会复制他们在存储中已经知道的层。同样,远程注册表也不会复制存储库中已经存在的层。此外,注册中心通常甚至在不同的存储库中仅存储一个层的副本。因此,当您只更新您的代码(因此是您的 jar)时,只有包含该 jar 的层才会占用新的存储空间。Docker 和 Jib 只会通过网络将新层发送到远程注册中心。也就是说,gcr.io/distroless/java:11不会发送基础图像层。

我们创建一个只包含 /opt/libs 中的依赖项的基础镜像,让我们调用它spring-base:1.0.0并推送到我们的私有 Docker 注册表。

创建一个单独的图像只包含共享库并不是闻所未闻的,我见过一些人尝试这样做。但是,我认为您并不打算在概念上将此特殊基础映像视为一个独立的、独立的映像,以便在您的组织中的不同类型的映像之间共享。所以我认为在这种情况下这样做是非常规的,如果只是想到节省存储空间(和网络带宽)的想法,这个技巧很可能是不必要的。请继续阅读。

期望这些图像要小得多

不,正如我所解释的,无论如何,您都将创建一个大小相同的 250MB 图像。它包括基本映像的大小,其中包括您的共享库。运行时docker images,您的本地 Docker 引擎将显示图像大小为 250MB。但正如我所说,这并不意味着每当您构建新映像时,您的 Docker 引擎都会占用额外的 250MB 空间。

具有依赖关系的大基础映像仅存储一次

是的,但是当您开始时也是如此FROM gcr.io/distroless/java:11. 将您的共享库推入另一个“基础映像”是没有意义的,只要您可以为共享库创建一个自己的单独层并保持该层稳定(即可重现)。Jib 非常擅长可重复地构建这样的层。保存在注册表中的位粒度是层而不是图像,因此实际上没有必要“标记”库层位于某个“基本图像”中(只要您为库创建自己的层)。注册表只能看到层,“图像”的概念是通过声明“该图像由 A 层、B 层和 C 层以及此元数据组成”而形成的。图像甚至没有基本图像的概念。它并没有说“这个图像是通过将 A 层放在这个基础图像之上的”。

节省大量存储空间。

因此,这是不正确的。毕竟,Docker 引擎和注册表不会无缘无故地多次存储同一层。

我们集成并使用./gradlew jibDockerBuild它,它似乎确实为依赖项、资源和类创建了层,但仍然只有一个大图像。

是的。图像大小为 250MB。当您使用Dockerfile或任何其他图像构建工具时,这仍然是正确的。但是,在使用 Jib 时,如果您只更改应用程序.java文件,则 Jib 在重建时只会通过网络将小型应用程序层(不包含共享库或资源)发送到远程注册表;它不会发送整个 250MB 的层,因为 Jib 具有很强的可重复性。同样,如果您只更新共享库,Jib 将只发送库层,从而节省时间、带宽和存储空间。

但是请注意,由于 Docker 引擎 API 的功能有限,Jib 无法检查某些层是否已存储在 Docker 引擎中,因此 Jib 在使用时必须加载整个 250MB 层jibDockerBuild. 这通常不是问题,因为加载是在本地完成的,无需通过网络。但是由于这个 API 限制,令人惊讶的是,Jib 直接将图像推送到远程注册表通常比本地 Docker 引擎更快;Jib 只需要发送已经改变的层。然而,正如我多次强调的那样,即使 Jib(或任何其他图像构建工具)将整个 250MB 的层加载到 Docker 引擎中,引擎也只会保存必要的(即,它从未见过的新层——或者它相信如此)。它不会复制基础镜像或共享库层;只有新的、不同的层才会占用存储空间。使用Dockerfile,您通常最终会生成“新层”,即使由于可重复性差,它们实际上并不是新的。

于 2020-03-18T15:45:37.930 回答