精简docker镜像的建议 有更新!

2019-07-17

精简docker镜像的建议

作者: 张首富
时间: 2019-05-03
个人博客: www.zhangshoufu.com
QQ群: 895291458

前提

因为公司业务需求,需要到客户现场部署我们代码的离线环境,因为各大银行和运营商所提供的底层系统各不相同,代码不一定能运行的起来,所以我们就采用了docker版的离线部署方式,报我们所有的应用全打成docker包,然后再到客户现场部署.
但是这又引发了另外一个问题,因为我们的客户一般都是银行和运营商,所以我们要拷贝个东西到他们的系统里面是很费劲的,因为全是docker包,因为我们打包没有精简,导致打出来的docker非常庞大,传输文件到客户服务器里面往往需要大半天时间或者更久.
为了提高工作效率,缩短传输包的时间,我们决定对docker镜像进行精简

精简docker镜像的必要性

1,我们大家都知道docker镜像是分层存储的,镜像层依赖于一系列底层技术(FileSystem,copy-on-wirte,union mounts联合挂载),而docker镜像最多有127层,当超过127层的时候docker镜像打包就会失败.
2,精简docker镜像大小能减少我们的构建时间,只装必须使用的包,不需要的就不装
3,减少磁盘使用量
4,因为包含的文件少,所以漏洞如果就少
5,传输速度,部署速度加快

精简docker镜像的建议

a, 选择基础镜像

当我们编写Dockerfile FROM的时候选择最合适的最小的基础镜像,我们在这个基础上安装我们必须的安装包.
常用的 Linux 系统镜像一般有 Ubuntu、CentOs、Alpine,其中 Alpine 更推荐使用。大小对比如下:


Alpine 是一个高度精简又包含了基本工具的轻量级 Linux 发行版,基础镜像只有 4.41M,各开发语言和框架都有基于 Alpine 制作的基础镜像,强烈推荐使用它。

查看上面的镜像尺寸对比结果,你会发现最小的镜像也有 4.41M,那么有办法构建更小的镜像吗?答案是肯定的,例如 gcr.io/google_containers/pause-amd64:3.1 镜像仅有 742KB。为什么这个镜像能这么小?在为大家解密之前,再推荐两个基础镜像:

1) scratch 镜像

scratch 是一个空镜像,只能用于构建其他镜像,比如你要运行一个包含所有依赖的二进制文件,如centos 7.5镜像,他就是基于scratch来构建的,他的Dockerfile文件来构建的

FROM scratch
ADD centos-7-docker.tar.xz /

LABEL org.label-schema.schema-version = "1.0" \
    org.label-schema.name="CentOS Base Image" \
    org.label-schema.vendor="CentOS" \
    org.label-schema.license="GPLv2" \
    org.label-schema.build-date="20180531"

CMD ["/bin/bash"]

centos镜像使用了scratch 作为基础镜像,这个镜像本身是不占空间的,使用它构建的镜像大小几乎和二进制文件本身一样大,所以镜像非常小。

2) busybox 镜像

scratch 是个空镜像,如果希望镜像里可以包含一些常用的 Linux 工具,busybox 镜像是个不错选择,镜像本身只有 1.16M,非常便于构建小镜像。

b, 串联 DOckerfile 指令

大家在定义 Dockerfile 时,如果太多的使用 RUN 指令,经常会导致镜像有特别多的层,镜像很臃肿,而且甚至会碰到超出最大层数(127层)限制的问题,遵循 Dockerfile 最佳实践,我们应该把多个命令串联合并为一个 RUN(通过运算符&&和/ 来实现),每一个 RUN 要精心设计,确保安装构建最后进行清理,这样才可以降低镜像体积,以及最大化的利用构建缓存。
下面是一个优化前 Dockerfile:
下面的例子只是做个测试,正常来讲没有人会这样构建docker镜像
docker镜像打包比对

FROM centos:7.5.1804
COPY ./*.repo /etc/yum.repos.d/
RUN yum -y install nginx 
RUN yum -y install mariadb*
CMD nginx
FROM centos:7.5.1804
COPY ./*.repo /etc/yum.repos.d/
RUN yum -y install nginx  mariadb*
CMD nginx
FROM centos:7.5.1804
COPY ./*.repo /etc/yum.repos.d/
RUN yum -y install nginx  mariadb* && \
    yum clean all
CMD nginx
REPOSITORY                                           TAG                 IMAGE ID            CREATED             SIZE
test                                                 V3.0                4ce16d6e1859        6 seconds ago       586MB
test                                                 V2.0                6cb5da7d6afc        26 minutes ago      685MB
test                                                 V1.0                49738c7042d6        31 minutes ago      805MB

通过上面的实验,我们可以看出,将yum命令分成两个RUN来写远远比卸载一个RUN里面节省空间大小的多。
两个镜像所安装的软件全是一模一样,但是一个RUN和两个RUN有本质的区别,因为每多一个RUN镜像就会多一层

c,使用多阶段构建

Dockerfile 中每个指令都会为镜像增加一个镜像层,并且你需要在移动到下一个镜像层之前清理不需要的组件,实际上,有一个Dockerfile用于开发,(其中
包含构建应用程序所需的所有内容)以及一个用于生产的瘦客户端,它只包含您的应用程序以及运行它所需的内容,“这被称为建造者模式”。Dockerfile 17.05.0-ce 版本
以后支持多阶段构建,使用多阶段构建,你可以在Dockerfile中使用多个FROM 语句,每条 FROM 指令可以使用不同的基础镜像,这样可以选择性的将服务组件从一个阶段
COPY到另一个阶段,最终只保留镜像中需要的内容。

d, 构建业务镜像的技巧

1,把依赖的库和代码分成COPY,因为docker镜像打包的时候回去中心仓库(server端)对比有没有相同的层,有相同的层直接拿来使用,所以我们尽量把不会改变成东西
放在一层,这样我们打包速度就会很快了
2,使用代码本身的启动,不要安装一些无所谓的东西来辅助启动,因为我发现有好多开发会把代码使用Supervisor的方式去启动他的代码,这种方式是违法了docker本身
的理念的,因为这种可能造成docker启动没问题,但是里面的服务不正常

e, 编写 .dockerignore文件

构建镜像时,Docker 需要先准备context ,将所有需要的文件收集到进程中。默认的context包含 Dockerfile 目录中的所有文件,但是实际上,有些目录我们是不需要的,所以我们可以采用 编写一个 .dockerignore 文件来加快镜像构建,同时减少docker镜像的大小

f,其他优化方法

1,使用yum 或者apt安装完软件之后删除缓存的安装数据
2, apt-get install 可以添加 --no-install-recommends参数来不安装非必须的依赖
3,能用一条命令RUN完的就不要写两条

采用Multi-stage builds(多阶构建)精简docker镜像

在公司的CI/CD流程中发现打包出来的docker镜像有时候往往都很大,当时就在思考为什么镜像打出来会那么大呢? 在了解情况之后发现原来是因为他们的代码需要编译打包之后才能使用,在构建处理来的docker包里面存在编译构建的时候运行的环境和源码包,但是我们实际使用的时候只需要吧最终的二进制包构建到docker里面运行即可,我们并未满足上面的最小化构建镜像,把运行中不需要的命令也打包进去了。
于是我们采用如下方式改进:

第一个版本的改建介绍

这里我们简单的举个例子:使用go语言编写一个最简单的web服务

package main
import (
    "github.com/gin-gonic/gin"
    "net/http"
)
func main() {
    router := gin.Default()
    router.GET("/ping", func(c *gin.Context) {
        c.String(http.StatusOK, "PONG")
    })
    router.Run(":8080")
}

这个时候我们采用如下方式去构建我们可运行的docker包,首先我们写一个基础的编译go代码的容器,构建完成之后我们把代码拷贝到宿主机上,然后在构建第二个镜像,然后copy到镜像中
第一个基础镜像dockerfile.one

FROM golang
WORKDIR /go/src/app
ADD . /go/src/app
RUN go get -u -v github.com/kardianos/govendor
RUN govendor sync
RUN GOOS=linux GOARCH=386 go build -v -o /go/src/app/app-server

定义程序运行的dockerfile,采用最小镜像老构建Dockerfile.two

FROM alpine:latest
RUN apk add -U tzdata
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai  /etc/localtime
WORKDIR /root/
COPY app-server .
CMD ["./app-server"]

更具我们执行的步骤,去编写一个脚本

#!/bin/bash
echo Building Compile:go
docker build -t Compile:go -f Dockerfile.one .
docker create --name test Compile:go
docker cp /go/src/app/app-server  ./app-server
docker rm -f test

echo Building run:go
docker build --no-cache -t run:go -f Dockerfile.two .
rm -rf ./app-server

当我们构建完上面的脚本就完成啦这个镜像的构建,这个时候我们通过jenkins来构建就需要触发这个脚本来完成,是能实现我们的需求但是太low了。有没有一种更加简单的方式来实现上面的镜像构建过程呢?

采用Multi-stage builds(多阶构建)来完成构建

Docker 17.05版本以后,官方就提供了一个新的特性:Multi-stage builds(多阶段构建)。 使用多阶段构建,你可以在一个 Dockerfile 中使用多个 FROM 语句。每个 FROM 指令都可以使用不同的基础镜像,并表示开始一个新的构建阶段。你可以很方便的将一个阶段的文件复制到另外一个阶段,在最终的镜像中保留下你需要的内容即可。

FROM golang AS build-env
ADD . /go/src/app
WORKDIR /go/src/app
RUN go get -u -v github.com/kardianos/govendor
RUN govendor sync
RUN GOOS=linux GOARCH=386 go build -v -o /go/src/app/app-server

FROM alpine
RUN apk add -U tzdata
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai  /etc/localtime
COPY --from=build-env /go/src/app/app-server /usr/local/bin/app-server
EXPOSE 8080
CMD [ "app-server" ]

现在我们只需要一个Dockerfile文件即可,也不需要拆分构建脚本了,只需要执行 build 命令即可:

docker build -t cnych/docker-multi-stage-demo:latest .

默认情况下,构建阶段是没有命令的,我们可以通过它们的索引来引用它们,第一个 FROM 指令从0开始,我们也可以用AS指令为阶段命令,比如我们这里的将第一阶段命名为build-env,然后在其他阶段需要引用的时候使用--from=build-env参数即可。从那个镜像中copy什么内容

最后我们简单的运行下该容器测试:

docker run --rm -p 8080:8080 cnych/docker-multi-stage-demo:latest

最后请求http://localhost:8080/ 即可得到我们想返回的结果,这样是不是就瞬间高大上许多了呢?


标题:精简docker镜像的建议
作者:shoufuzhang
地址:https://www.zhangshoufu.com/articles/2019/07/17/1563369907854.html
名言:The master has failed more times than the beginner has tried.
评论
发表评论