8.云原生之Docker容器镜像构建最佳实践浅析

转载自:https://www.bilibili.com/read/cv15220861/?from=readlist

本章目录

0x02 Docker 镜像构建最佳实践浅析

1.Dockerfile 指令最佳实践

2.Dockerfile 编写最佳实践

0x02 Docker 镜像构建最佳实践浅析

描述: Docker拥有自己的操作系统,完全基于于 Docker 的Linux发行版CoreOS。
目前常用的Linux发行版主要包括Debian/Ubuntu系列和CentOS/Fedora系列。

前者以自带软件包版本较新而出名

后者则宣称运行更稳定一些

1.Dockerfile 指令最佳实践

FROM:尽可能使用当前官方仓库作为你构建镜像的基础

LABEL: 一个镜像可以包含多个标签但建议将多个标签放入到一个 LABEL 指令中

RUN:将长的或复杂的 RUN 指令用反斜杠 \ 分割成多行 (不要使用 RUN apt-get upgrade 或 dist-upgrade,因为许多基础镜像中的「必须」包不会在一个非特权容器中升级,而且建议使用指定版本的形式)

CMD:用于执行目标镜像中包含的软件可以包含参数

EXPOSE:在执行 docker run 时使用一个标志来指示如何将指定的端口映射到所选择的端口

ENV: 为了方便新程序运行,你可以使用 ENV 来为容器中安装的程序更新 PATH 环境变量

ADD 和 COPY(优先使用它),了让镜像尽量小最好不要使用 ADD 指令从远程 URL 获取包,而是使用 curl 和 wget

ENTRYPOINT: 最佳用处是设置镜像的主命令,允许将镜像当成命令本身来运行(用 CMD 提供默认选项)。

VOLUME: 强烈建议使用它来管理镜像中的可变部分和用户可以改变的部分。

USER:如果某个服务不需要特权执行,建议使用 USER 指令切换到非 root 用户(你应该避免使用 sudo),同时也避免频繁地使用USER来回切换用户。

    使用类似 RUN groupadd -r postgres && useradd -r -g postgres postgres 的指令创建用户和用户组。

WORKDIR:为了清晰性和可靠性建议都使用结对路径;

2.Dockerfile 编写最佳实践

描述: 容器应该是短暂的,短暂意味着可以停止和销毁容器,并且创建一个新容器并部署好所需的设置和配置工作量应该是极小的;

下面列出了Dockerfile最佳实践的一些要素方法:

1.学习Docker的hub中官方仓库中镜像和对应的Dockerfile编写方法与习惯;

2.提高构建镜像的效率使用.dockerignore文件来要忽略的文件和目录与指定上下文环境, .dockerignore 文件的排除模式语法和 Git 的 .gitignore 文件相似。(Dockerfile目录尽量为空,然后将构建镜像所需要的文件添加到该目录中);

3.使用精简镜像(选择体积较小的基础镜像), 比如 alpine 或者 debian:buster-slim;
REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
debian                      buster-slim         e1af56d072b8        4 days ago          69.2MB
alpine                      latest              cc0abc535e36        8 days ago          5.59MB
#注意:Alpine的C库是musl libc 而不是正统的 glic;
4.网络环境受限的情况下,需将默认的软件源更换为国内的软件源镜像站,目前国内稳定可靠的镜像站主要有,华为云、阿里云、腾讯云、163等,其中华为云的镜像站速度最快,平均 10MB/s,峰值可达到 20MB/s,极大的能加快构建镜像的速度。
# 备注:其他基础镜像可以通过 sed 命令替换软件源配置文件中的默认域名;
# 对于 alpine 基础镜像修改软件源
echo "http://mirrors.huaweicloud.com/alpine/latest-stable/main/" > /etc/apk/repositories ;\
echo "http://mirrors.huaweicloud.com/alpine/latest-stable/community/" >> /etc/apk/repositories ;\
apk update ;\

# debian 基础镜像修改默认软件源
sed -i 's/deb.debian.org/mirrors.huaweicloud.com/g' /etc/apt/sources.list ;\
sed -i 's|security.debian.org/debian-security|mirrors.huaweicloud.com/debian-security|g' /etc/apt/sources.list ;\
4.减少镜像层数则要尽量合并指令(调整合理的指令顺序),例如多个RUN指令可以利用反斜杠符号 \合并为一条;

5.避免安装不必要的包(指定软件版本号)来减少所构建镜像的大小(实际需要将各层安装的东西尽量最小),降低复杂性、减少依赖、节约构建时间 (别使用yum upgrade / apt-get upgrade / dist-upgrade 来更新依赖应用);
RUN apt update && apt-get --no-install-recommends install -y \
    package-bar \
    package-foo=1.3.* && \
6.分阶段构建在 Docker 17.05 以上版本中可以采用此种方式来减少所构建镜像的大小。
#(1)比如我们现在有一个最简单的 golang 服务,需要构建一个最小的`Docker` 镜像,源码如下:
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")
}

#(2)使用多阶段构建,你可以在一个 `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" ]

#(3)现在我们就把两个镜像的文件最终合并到一个镜像里面了。
6.增加可读性将多行参数排序, 建议在反斜杠符号 \ 之前添加一个空格 ,并且只要有可能,就将多行参数按字母顺序排序(比如要安装多个包时),帮助你避免重复包含同一个包,更新包列表时也更容易,也更容易阅读和审查;
LABEL Version="1.1" \
      Author="WeiyiGeek" \
      InnerVersion="0.1"

#来自buildpack-deps 镜像的例子
RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion
7.时区设置由于绝大多数 docker 镜像都是默认采用的 UTC 的时区,与北京时间相差 8 个小时,这将会导致容器内的时钟与北京时间不一致,因而会对一些应用造成一些影响,以及影响容器内日志和监控的数据;
#方式1.可以通过环境变量设置容器内的时区,在启动的时候可以通过设置环境变量-e TZ=Asia/Shanghai 来设定容器内的时区

#方式2.但对于 alpine 镜像无法通过环境变量的方式设定时区,需要安装 tzdata 来配置时区。
apk add --no-cache tzdata ;\
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime ;\
echo "Asia/Shanghai" > /etc/timezone ;\
apk del tzdata ;

#方式3.对于 debian 基础镜像,可通过复制时区文件设定容器内时区
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime ;\
echo "Asia/shanghai" > /etc/timezone ;\
8.尽量使用 URL 添加源码,对于需要在容器内进行编译的项目,最好通过git 或者 wegt 的方式将源码打入到镜像内,而非采用 ADD 或者 COPY ,因为源码编译完成之后源码就不需要可以删掉了,而通过 ADD 或者 COPY 添加进去的源码已经用在下一层镜像中了无法被删除掉。这种方法缺点就是网络可能受限下载缓慢
RUN set -x \
    && echo "http://mirrors.aliyun.com/alpine/latest-stable/main/" > /etc/apk/repositories \
    && echo "http://mirrors.aliyun.com/alpine/latest-stable/community/" >> /etc/apk/repositories \
    && apk update \
    && apk add --no-cache --virtual .build-deps gcc libc-dev make perl-dev openssl-dev pcre-dev zlib-dev git \
    && mkdir -p /usr/local/src \
    && cd /usr/local/src \
    && git clone https://github.com/happyfish100/libfastcommon.git --depth 1 \
    && git clone https://github.com/happyfish100/fastdfs.git --depth 1    \
    && git clone https://github.com/happyfish100/fastdfs-nginx-module.git --depth 1  \
    && wget http://nginx.org/download/nginx-1.15.4.tar.gz \
    && tar -xf nginx-1.15.4.tar.gz \
    && cd /usr/local/src/libfastcommon \
    && ./make.sh \
    && ./make.sh install \
    && cd /usr/local/src/fastdfs/ \
    && ./make.sh \
    && ./make.sh install \
    && cd /usr/local/src/nginx-1.15.4/ \
    && ./configure --add-module=/usr/local/src/fastdfs-nginx-module/src/ \
    && make && make install \
    && apk del .build-deps \
    && apk add --no-cache pcre-dev bash \
    && mkdir -p /home/dfs  \
    && mv /usr/local/src/fastdfs/docker/dockerfile_network/fastdfs.sh /home \
    && mv /usr/local/src/fastdfs/docker/dockerfile_network/conf/* /etc/fdfs \
    && chmod +x /home/fastdfs.sh \
    && rm -rf /usr/local/src*
# 构建之后的对比确实比较明显
# 使用项目默认的 Dockerfile 进行构建的话,镜像大小接近 500MB ,而经过一些的优化,将所有的 RUN 指令合并为一条,最终构建出来的镜像大小为 30MB 。
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
fastdfs             alpine              e855bd197dbe        10 seconds ago      29.3MB
fastdfs             debian              e05ca1616604        20 minutes ago      103MB
fastdfs             centos              c1488537c23c        30 minutes ago      483MB
10.使用虚拟编译环境,对于只在编译过程中使用到的依赖,我们可以将这些依赖安装在虚拟环境中,编译完成之后可以一并删除这些依赖
#(1) 比如 alpine 中可以使用 `apk add --no-cache --virtual .build-deps` 后面加上需要安装的相关依赖即可;
apk add --no-cache --virtual .build-deps gcc libc-dev make perl-dev openssl-dev pcre-dev zlib-dev git

#(2) 构建完成之后可以使用 `apk del .build-deps` 命令,一并将这些编译依赖全部删除。需要注意的是 `.build-deps` 后面接的是编译时以来的软件包,不要把运行时的依赖包接在后面,最好单独 add 一下
容器应该是短暂的通过 Dockerfile 构建的镜像所启动的容器应该尽可能短暂(生命周期短),意味着可以停止和销毁容器,并且创建一个新容器并部署好所需的设置和配置工作量应该是极小的。们可以查看下[12 Factor(12要素)应用程序方法]的进程部分,可以让我们理解这种无状态方式运行容器的动机。

建立上下文,在执行docker build命令时所在工作目录被称为构建上下文,默认情况下,Dockerfile 就位于该路径下,当然您也可以使用-f参数来指定不同的位置,此时无论 Dockerfile 在什么地方,当前目录中的所有文件内容都将作为构建上下文发送到 Docker 守护进程中去。
#示例1.为构建上下文创建一个目录并 cd 放入其中。
#(1)将“hello”写入一个文本文件hello,然后并创建一个`Dockerfile`并运行`cat`。从构建上下文(.)中构建图像:
mkdir myproject && cd myproject
echo "hello" > hello
echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile
docker build -t helloapp:v1 .

#(2)现在移动 Dockerfile 和 hello 到不同的目录,并建立了图像的第二个版本(不依赖于缓存中的最后一个版本)。
#使用`-f`指向 Dockerfile 并指定构建上下文的目录:
mkdir -p dockerfiles context
mv Dockerfile dockerfiles && mv hello context
docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context

#(3)在构建的时候包含不需要的文件会导致更大的构建上下文和更大的镜像大小。这会增加构建时间,拉取和推送镜像的时间以及容器的运行时间大小。要查看您的构建环境有多大,请在构建您的系统时查找这样的消息;
Dockerfile:
Sending build context to Docker daemon  187.8MB
11.精简生成镜像的大小及时删除临时文件和缓存文件,特别是在执行apt-get指令后 /var/cache/apt 和 /var/lib/apt/lists下面会缓存一些安装包;

    删除中间文件:比如下载的压缩包

    删除临时文件:如果命令产生了临时文件,也要及时删除
#安装完软件包清楚 `/var/lib/apt/list/` 缓存
rm -rf /var/lib/apt/lists/*
rm -rf /var/cache/apt
12.构建缓存查找是否已经存在可重用的镜像,如果有就使用现存的镜像不再重复创建 , 在开启缓存的情况下,内容不变的指令尽量放在前面;当然如果你不想在构建过程中使用缓存,你可以在 docker build 命令中使用--no-cache=true选项;Docker中缓存遵循的基本规则如下:
  • 从基础镜像开始(即FROM指令指定),下一条指令将和该基础镜像的所有子镜像进行匹配,检查这些子镜像被创建时使用的指令是否和被检查的指令完全一样。IF 不是 则缓存失效;

  • 多数情况下简单地对比 Dockerfile 中的指令和子镜像,然而有些指令需要更多的检查和解释;

  • 对于 ADD 和 COPY 指令镜像中对应文件的内容也会被检查,每个文件都会计算出一个校验值;在缓存的查找过程中会将这些校验和和已存在镜像中的文件校验值进行对比,如果文件有任何改变,比如内容和元数据则缓存失效。

  • 缓存匹配过程不会查看临时容器中的文件来决定缓存是否匹配,例如当执行完 RUN apt-get -y update 指令后,容器中一些文件被更新,但 Docker 不会检查这些文件。这种情况下只有指令字符串本身被用来匹配缓存。

  • 一旦缓存失效所有后续的 Dockerfile 指令都将产生新的镜像,缓存不会被使用

    13.不要在 Dockerfile 中单独修改文件的权限, 因为 docker 镜像是分层的,任何修改都会新增一个层,修改文件或者目录权限也是如此, 如果有一个命令单独修改大文件或者目录的权限,会把这些文件复制一份这样很容易导致镜像很大。

解决方案

  • 在添加到 Dockerfile 之前就把文件的权限和用户设置好;

  • 在容器启动脚本(entrypoint)做这些修改,或者拷贝文件和修改权限放在一起做(最终也只是增加一层)

    14.保证容器的横向扩展和复用, 一个容器只运行一个进程将多个应用解耦到不同容器中(类似于微服务-K8s表现得淋漓精致)

docker build -t test:v1.1 .
Sending build context to Docker daemon   2.56kB
Step 1/6 : FROM alpine
latest: Pulling from library/alpine
df20fa9351a1: Pull complete
Digest: sha256:185518070891758909c9f839cf4ca393ee977ac378609f700f60a771a2dfe321
Status: Downloaded newer image for alpine:latest
 ---> a24bb4013296
Step 2/6 : LABEL Author="WeiyiGeek"       Description="Test Dockerfile"
 ---> Running in 88827d239060
Removing intermediate container 88827d239060
 ---> 11d6ad764d64
Step 3/6 : MAINTAINER WeiyiGeek master@WeiyiGeek.top
 ---> Running in 5c939c83df34
Removing intermediate container 5c939c83df34
 ---> 3fcdbc7bd1d8
Step 4/6 : RUN echo "http://mirrors.huaweicloud.com/alpine/latest-stable/main/" > /etc/apk/repositories ;    echo "http://mirrors.huaweicloud.com/alpine/latest-stable/community/" >> /etc/apk/repositories ;    apk update ;     apk add --no-cache tzdata ;    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime ;    echo "Asia/Shanghai" > /etc/timezone ;    apk del tzdata ;
 ---> Running in 2879a08f5f04
fetch http://mirrors.huaweicloud.com/alpine/latest-stable/main/x86_64/APKINDEX.tar.gz
fetch http://mirrors.huaweicloud.com/alpine/latest-stable/community/x86_64/APKINDEX.tar.gz
v3.12.0-74-gb8f92cf12a [http://mirrors.huaweicloud.com/alpine/latest-stable/main/]
v3.12.0-77-ge186c138b6 [http://mirrors.huaweicloud.com/alpine/latest-stable/community/]
OK: 12734 distinct packages available
fetch http://mirrors.huaweicloud.com/alpine/latest-stable/main/x86_64/APKINDEX.tar.gz
fetch http://mirrors.huaweicloud.com/alpine/latest-stable/community/x86_64/APKINDEX.tar.gz
(1/1) Installing tzdata (2020a-r0)
Executing busybox-1.31.1-r16.trigger
OK: 9 MiB in 15 packages
(1/1) Purging tzdata (2020a-r0)
Executing busybox-1.31.1-r16.trigger
OK: 6 MiB in 14 packages
Removing intermediate container 2879a08f5f04
 ---> afed82abd763
Step 5/6 : ENTRYPOINT ["top","-b"]
 ---> Running in ce8bc0f98fca
Removing intermediate container ce8bc0f98fca
 ---> c5fe50376063
Step 6/6 : CMD ["d2"]
 ---> Running in d6bbe5535a87
Removing intermediate container d6bbe5535a87
 ---> fe57118d688c
Successfully built fe57118d688c
Successfully tagged test:v1.1
posted @ 2022-07-04 17:41  哈喽哈喽111111  阅读(324)  评论(0编辑  收藏  举报