镜像瘦身10斤:Rust优化攻略

我们团队使用K8S来编排Docker,而小的镜像的体积的好处不言而喻,它可以有更快的构建和部署速度、更少的存储空间、更快的镜像传输和下载速度以及更好的可移植性。

但在实际工作中,团队伊始使用的Rust运行时镜像体积非常大(114M),令我感觉不可思议(我之前做过一个Go项目,总共也就30来M),于是下决心优化:

运行时镜像

在编写Dockerfile时,选择合适的基础镜像非常重要。常用的Linux系统镜像包括Ubuntu、CentOS和Alpine。其中,Alpine是一个高度精简的轻量级Linux发行版,包含了基本工具,基础镜像只有4.41M。此外,各开发语言和框架都有基于Alpine制作的基础镜像。因此,强烈推荐使用Alpine作为基础镜像。

当然,还有更小的镜像,例如scratch和busybox,读者如果想做极致的优化,可以考虑使用它们。

制作镜像本身并不复杂,对于Rust项目而言,基本上只需要安装libgcc就可以了,镜像体积缩减到5.96M:

后来我为了修改时区,我又额外安装了tzdata,一个时区数据库包,体积稍微大了点,为9.46M:

镜像代码

以下是最终的Dockerfile代码:

dockerfile 复制代码
FROM alpine:3.17.3

ENV TZ=Asia/Shanghai

RUN set -eux && sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories \
    && apk update \
    && apk add --no-cache libgcc tzdata \
    && echo "${TZ}" > /etc/timezone \
    && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \
    && rm /var/cache/apk/*

镜像使用

这是一个构建Rust镜像的Dockerfile,它分成2部分,上半部分构建出产物二进制文件,下半部分将之前的文件copy到运行时镜像里。

dockerfile 复制代码
FROM rust:alpine3.16
# This is important, see https://github.com/rust-lang/docker-rust/issues/85
ENV RUSTFLAGS="-C target-feature=-crt-static"
RUN set -eux && sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
# if needed, add additional dependencies here
RUN apk add --no-cache musl-dev pkgconfig openssl-dev
# set the workdir and copy the source into it
WORKDIR /app
COPY ./ /app
# do a release build
RUN cargo build --release
# RUN strip target/release/search

# use a plain alpine image, the alpine version needs to match the builder
FROM alpine
# if needed, install additional dependencies here
RUN apk add --no-cache libgcc
# copy the binary into the final image
WORKDIR /app
COPY --from=0 /app/target/release/search .
# set the binary as entrypoint
ENTRYPOINT ["./search"]

我们有了上面的镜像以后,后面部分修改为:

dockerfile 复制代码
FROM dk.uino.cn/runner/rust-runtime-alpine:0.0.3

COPY --from=0 /app/target/release/search .

# set the binary as entrypoint
ENTRYPOINT ["./search"]

但我们不建议在GitLab流水线中这样使用,为什么呢?

上篇文章《提高GitLab CICD效率:Rust编译速度飙升秘籍》提到,Rust编译速度非常缓慢,为解决这个问题,我们在GitLab流水线中加入sccache、mold以及调整CPU来加速。而在流水线中构建镜像(我们用的Buildah,详见这篇探索Buildah:简化Docker镜像构建),并不能充分利用这个优势。

所以,最佳做法是将流水线任务拆分成两步,一步是构建产物,一步是构建镜像,后面这步只将上一步产物复制。比如.gitlab-ci.yml文件:

yaml 复制代码
build:
	stage: build
  script:
    - cargo build --release
  artifacts:
    paths:
      - target/release/$RUST_BIN
    expire_in: 1 day  

docker:
  stage: docker
	variables:
    CI_DOCKERFILE: Dockerfile
    CI_DOCKER_PROJECT: xx # 你有权限推送的方件夹
    CI_DOCKER_REPO: xx-xx # 仓库名
  script:
    - docker_build --build-arg GIT_REVISION=${CI_COMMIT_SHA}

Rust减少二进制文件体积

在 Cargo.toml 文件中添加以下配置:

toml 复制代码
[profile.release]
codegen-units = 1
strip = true
lto = true
opt-level = "z"

这些配置是用于Rust的发布模式(release mode)的:

  1. codegen-units = 1 :这个配置指定在编译期间生成代码的单元数量。它的值为1,表示只生成一个代码单元。通过减少代码单元的数量,可以提高编译速度和减小最终生成的可执行文件的大小。然而,这可能会导致一些性能损失。
  2. strip = true :这个配置用于指定是否在编译完成后剥离(strip)可执行文件中的调试符号和其他不必要的信息。剥离可执行文件可以减小其大小,并且可以防止他人通过分析可执行文件来获取敏感信息。设置它,效果与在cargo build之后再显式执行strip -s是一样的。
  3. lto = true :这个配置用于启用链接时优化(Link-Time Optimization,简称LTO)。链接时优化是在链接阶段对整个程序进行优化,而不仅仅是单个源文件。通过LTO,编译器可以更好地优化代码,提高最终可执行文件的性能。然而,它会显著增加编译时间和内存占用,有时候对程序性能没有正面影响,所以默认是没有激活的。
  4. opt-level = "z":这个配置用于指定优化级别,通常有0、1、2、3、s、z几种。在这里,"z"表示最小化优化,这意味着编译器将尽可能地减小体积,但可能会降低性能。Debug模式,缺省使用0,Release模式缺省是3。

这些配置可以根据你的需求进行调整,以平衡编译速度、可执行文件大小和性能。

以下是我验证过的一个项目的结果:

大小 耗时
原始 15_581_576 3m
strip 8_796_320 2m53s
strip+codegen 7_506_032 2m57s
strip+codegen+lto 6_797_320 3m52s
strip+codegen+lto+opt-level 5_548_040 2m59s

使用strip后,体积减少的最多。而后面几个虽然也有效果,但各有缺点,codegen可能增加编译时间(本例中并没有,应该与机器配置有关,Mac上某项目正常编译是1分30秒,加了codegen后是2分钟),lto明显地增加了编译时间,对性能的影响不确定。奇怪的是最后4个一起时间居然又减少了。这个样本可能不太正常。

综上,我觉得开启strip的最简单有效,对性能没有影响,又减少了相当的体积,再往下优化的几M意义不大。 由于我们老的项目基本都没有开启这几项优化,所以我在.gitlab-ci.yml文件里build这一步添加strip:

yaml 复制代码
build
  stage: build
  script:
    - cargo build --release
    - strip -s target/release/$RUST_BIN

总结

我们通过使用alpine镜像作为基础镜像,减少了Rust的运行时镜像体积,又使用strip命令,减少了Rust构建产物的体积,将最终镜像体积从130M减少到18M左右,基本满足了我们的需求。

相关推荐
techdashen1 小时前
清理Go/Rust编译时产生的缓存
缓存·golang·rust
大田斗小木子2 小时前
Docker学习
学习·docker·容器
铁板鱿鱼1404 小时前
docker基本(仅供自己参考)
运维·docker·容器
江池俊5 小时前
本地快速部署一个简洁美观的个人Halo博客网站并发布公网远程访问
docker·个人博客
admin_2336 小时前
docker入门总结(附错误处理,持续更新)
运维·docker·容器
linux修理工6 小时前
docker desktop windows stop
docker
Ceder1c6 小时前
【已解决】Linux ubuntu 20.04 docker 不需要sudo权限
linux·ubuntu·docker
hybaym6 小时前
Docker修改默认的存储路径
docker
EricWang13589 小时前
【无标题】
docker
x-cmd9 小时前
x-cmd pkg | bat: cat 命令现代化替代品,终端用户必备工具
运维·python·rust·终端·命令行·bat·cat