最小化 Java 镜像的常用技巧

背景

随着容器技术的普及,越来越多的应用被容器化。人们使用容器的频率越来越高,但常常忽略一个基本但又非常重要的问题 - 容器镜像的体积。本文将介绍精简容器镜像的必要性并以基于 spring boot 的 java 应用为例描述最小化容器镜像的常用技巧。

精简容器镜像的必要性

精简容器镜像是非常必要的,下面分别从安全性和敏捷性两个角度进行阐释。

安全性

基于安全方面的考虑,将不必要的组件从镜像中移除可以减少攻击面、降低安全风险。虽然 docker 支持用户通过 Seccomp 限制容器内可以执行操作或者使用 AppArmor 为容器配置安全策略,但它们的使用门槛较高,要求用户具备安全领域的专业素养。

敏捷性

精简的容器镜像能提高容器的部署速度。假设某一时刻访问流量激增,您需要通过增加容器副本数以应对突发压力。如果某些宿主机不包含目标镜像,需要先拉取镜像,然后启动容器,这时使用体积较小的镜像能加速这一过程、缩短扩容时间。另外,镜像体积越小,其构建速度也越快,同时还能减少存储和传输的成本。

常用技巧

将一个 java 应用容器化所需的步骤可归纳如下:

编译 java 源码并生成 jar 包。

将应用 jar 包和依赖的第三方 jar 包移动到合适的位置。

本章所用的样例是一个基于 spring boot 的 java 应用 spring-boot-docker,所用的未经优化的 dockerfile 如下:

FROM maven:3.5-jdk-8

COPY src /usr/src/app/src

COPY pom.xml /usr/src/app

RUN mvn -f /usr/src/app/pom.xml clean package

ENTRYPOINT ["java","-jar","/usr/src/app/target/spring-boot-docker-1.0.0.jar"]

由于应用使用 maven 构建,dockerfile 中指定maven:3.5-jdk-8作为基础镜像,该镜像的大小为 635MB。通过这种方式最终构建出的镜像非常大,达到了 719MB,这是因为一方面基础镜像本身就很大,另一方面 maven 在构建过程中会下载许多用于执行构建任务的 jar 包。

多阶段构建

Java 程序的运行只依赖 JRE,并不需要 maven 或者 JDK 中众多用于编译、调试、运行的工具,因此一个明显的优化方法是将用于编译构建 java 源码的镜像和用于运行 java 应用的镜像分开。为了达到这一目的,在 docker 17.05 版本之前需要用户维护 2 个 dockerfile 文件,这无疑增加了构建的复杂性。好在自 17.05 开始,docker 引入了多阶段构建的概念,它允许用户在一个 dockerfile 中使用多个 From 语句。每个 From 语句可以指定不同的基础镜像并将开启一个全新的构建流程。您可以选择性地将前一阶段的构建产物复制到另一个阶段,从而只将必要的内容保留在最终的镜像里。优化后的 dockerfile 如下:

FROM maven:3.5-jdk-8 AS build

COPY src /usr/src/app/src

COPY pom.xml /usr/src/app

RUN mvn -f /usr/src/app/pom.xml clean package

FROM openjdk:8-jre

ARG DEPENDENCY=/usr/src/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"]

该 dockerfile 选用maven:3.5-jdk-8作为第一阶段的构建镜像,选用openjdk:8-jre作为运行 java 应用的基础镜像并且只拷贝了第一阶段编译好的.claass文件和依赖的第三方 jar 包到最终的镜像里。通过这种方式优化后的镜像大小为 459MB。

使用 distroless 作为基础镜像

虽然通过多阶段构建能减小最终生成的镜像的大小,但 459MB 的体积仍相对过大。经调查发现,这是因为使用的基础镜像openjdk:8-jre体积过大,到达了 443MB,因此下一步的优化方向是减小基础镜像的体积。

Google 开源的项目 distroless 正是为了解决基础镜像体积过大这一问题。Distroless 镜像只包含应用程序及其运行时依赖项,不包含包管理器、shell 以及在标准 Linux 发行版中可以找到的任何其他程序。目前,distroless 为依赖 javapythonnodejsdotnet 等环境的应用提供了基础镜像。

使用 distroless 的 dockerfile 如下:

FROM maven:3.5-jdk-8 AS build

COPY src /usr/src/app/src

COPY pom.xml /usr/src/app

RUN mvn -f /usr/src/app/pom.xml clean package

FROM gcr.io/distroless/java

ARG DEPENDENCY=/usr/src/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"]

该 dockerfile 和上一版的唯一区别在于将运行阶段依赖的基础镜像由openjdk:8-jre(443 MB)替换成了gcr.io/distroless/java(119 MB)。经过这一优化,最终镜像的大小为 135MB。

使用 distroless 的唯一不便是您无法 attach 到一个正在运行的容器上排查问题,因为镜像中不包含 shell。虽然 distroless 的 debug 镜像提供 busybox shell,但需要用户重新打包镜像、部署容器,对于那些已经基于非 debug 镜像部署的容器无济于事。 但从安全角度来看,无法 attach 容器并不完全是坏事,因为攻击者无法通过 shell 进行攻击。

使用 alpine 作为基础镜像

如果您确实有 attach 容器的需求,又希望最小化镜像的大小,可以选用 alpine 作为基础镜像。Alpine 镜像的特点是体积非常下,基础款镜像的体积仅 4 MB 左右。

使用 alpine 后的 dockerfile 如下:

FROM maven:3.5-jdk-8 AS build

COPY src /usr/src/app/src

COPY pom.xml /usr/src/app

RUN mvn -f /usr/src/app/pom.xml clean package

FROM openjdk:8-jre-alpine

ARG DEPENDENCY=/usr/src/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"]

这里并未直接继承基础款 alpine,而是选用从 alpine 构建出的包含 java 运行时的openjdk:8-jre-alpine(83MB)作为基础镜像。使用该 dockerfile 构建出的镜像体积为 99.2MB,比基于 distroless 的还要小。

执行命令docker exec -ti <container_id> sh可以成功 attach 到运行的容器中。

distroless vs alpine

既然 distroless 和 alpine 都能提供非常小的基础镜像,那么在生产环境中到底应该选择哪一种呢?如果安全性是您的首要考虑因素,建议选用 distroless,因为它唯一可运行的二进制文件就是您打包的应用;如果您更关注镜像的体积,可以选用 alpine。

其他技巧

除了可以通过上述技巧精简镜像外,还有以下方式:

将 dockerfile 中的多条指令合并成一条,通过减少镜像层数的方式达到精简镜像体积的目的。

将稳定且体积较大的内容置于镜像下层,将变动频繁且体积较小的内容置于镜像上层。虽然该方式无法直接精简镜像体积,但充分利用了镜像的缓存机制,同样可以达到加快镜像构建和容器部署的目的。

想了解更多优化 dockerfile 的小窍门可参考教程 Best practices for writing Dockerfiles

总结

本文通过一系列的优化,将 java 应用的镜像体积由最初的 719MB 缩小到 100MB 左右。如果您的应用依赖其他环境,也可以用类似的原则进行优化。

针对 java 镜像,google 提供的另一款工具 jib 能为您屏蔽镜像构建过程中的复杂细节,自动构建出精简的 java 镜像。使用它您无须编写 dockerfile,甚至不需要安装 docker。

对于类似 distroless 这样无法 attach 或者不方便 attach 的容器,建议您将它们的日志中心化存储,以便问题的追踪和排查。具体方法可参考文章面向容器日志的技术实践


转载:https://zhuanlan.zhihu.com/p/54640767

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容