docker瘦身技巧

一、简介
docker镜像太大,带来以下几个问题:
1. 存储开销
这块影响其实不算大,因为对服务器磁盘来说,15GB的存储空间不算大,除非用户磁盘空间很紧张。
2. 部署事件
这块影响真的很大,交付件zip包太大,导致用户部署该产品时,花费的事件变长,客户现场中反馈部署事件超过1.5个小时,这严重影响用户体验,减低满意度。
3. 性能不稳定
如果客户得到服务器规格不够(特别是磁盘读写性能不够),会增大部署失败的概率。
二、瘦身思路
以下思路是我在该任务中尝试使用用于镜像瘦身的方法,均可以不同程度的降低docker镜像的尺寸。

  • 清理docker镜像中无用的安装包
    在dockerfile构建Docker镜像过程中,有可能引入临时文件,比如:安装包、文件压缩包。这些临时文件忘记清理,导致占据了一定的尺寸,有必要对其进行清理。
    如下Dockerfile:
FROM rasa/rasa:2.8.17
MAINTAINER <docker@linfeng.cn>
USER root
COPY zh_core_web_trf-3.2.0-py3-none-any.whl /app/
RUN  pip config set global.index-url  https://pypi.doubanio.com/simple/ && apt-get update && apt install make gcc g++ -y libgl1-mesa-glx libglib2.0-dev
RUN  pip install  --upgrade pip  flask==2.0.2 zxing==0.14 jieba==0.42.1 zh_core_web_trf-3.2.0-py3-none-any.whl  jionlp==1.3.34 chinese2digits==6.0.1 \
opencv-contrib-python opencv-python && apt-get clean && rm -rf /var/lib/apt/lists/*  zh_core_web_trf-3.2.0-py3-none-any.whl
WORKDIR /app
CMD ["/bin/bash"]

Dockerfile里面经常安装很多工具,安装完成后,需要及时删除安装缓存包:

  • (alpine) apk del openssh vim :删除包及其依赖包
  • (Ubuntu) apt-get clean:删除所有已下载的包文件
  • (centos) yum clean all:yum 会把下载的软件包和header存储在cache中,而不自动删除。
    如果觉得占用磁盘空间,可以使用yum clean指令进行清除,更精确的用法时yum clean headers清楚header,yum clean packages清除下载的rpm包,yum clean all全部清除。上面的dockerfile中在安装工具后应该执行下:
    && apt-get clean && rm -rf /var/lib/apt/lists/*
/var/lib/apt/lists/*是什么包?
/var/lib/apt 是与apt软件包管理器相关的文件和数据存储目录。/var/lib/apt/lists是用于保存系统source.list中指定的每个软件
包资源信息的目录。简单点来说,/var/lib/apt/lists 保存软件包信息缓存。当你要安装或更新程序时,系统会在此目录中检查该软件包中
的信息。如果找到了该包的详细信息,那么它将远程仓库并实际下载程序或更新。当运行sudo apt update时,它将构建缓存。这就是为什么
即使删除/var/lib/apt/lists目录中的所有内容,运行更新也会建立新的缓存得到原因。这就是处理文件无法解析问题的方式。你的系统
某个软件包或仓库信息以某种方式损坏了。删除该文件(或所有文件)并重建缓存即可解决此问题。
  • 避免不必要的工具安装
    有的Dockerfile中安装了很多工具,这个工具加载一起尺寸比较大,这块需要挨个排查:客户环境下,需不需要安装该工具?很多工具其实是面向开始使用的,而用户根本不会使用。那么就没必要再客户环境上使用安装这么工具镜像,应该仔细排查工具的必要性。比如:dockfile中安装了JDK,这个情况下,完全没必要,实际上可能jre就能搞定。
  • 多阶段构建
    Docker多阶段构建是17.05以后引入的新特性,旨在解决编译、构建复杂和镜像大小的问题。对于多阶段构建,额可以在Dockerfile中使用多个FROM语句。每个FROM指令可以使用不同的基础,并且每个指令都开始一个新的构建。可以选择性地将构建从一个阶段复制到另一个阶段,从而在最终images中留下你想要的内容。
    如下是多阶段构建示例:
FROM golang:alpine as builder
WORKDIR /go/src
COPY httpserver.go .
RUN go build -o myhttpserver ./httpserver.go
FROM apline:latest
WORKDIR /root/
COPY --from=builder /go/src/myhttpserver
RUN chmod + x /root/myhttpserver
ENTRYPOINT ["/root/myhttpserver"]

把多个Dockerfile合并在一起,每个dockerfile单独作为一个“阶段”,“阶段”之间可以相互联系,让后一个阶段构建可以使用前一个阶段构建的产物,形成一条构建阶段的chain,最终结果仅产生一个image,避免产生冗余的多个临时image或临时容器对象。
(1)多阶段构建使用之前
针对多阶段构建的特点,分析我们产品里面的dockerfile,如下面所示,该操作的目的是将tar包拷贝到容器内的路径下,并解压,执行后续操作。然而COPY层具有一定的大小,只是起到临时层的作用。

FROM xxxx:${project.version}COPY karaf-${ccsdk.opendaylight.version}.tar.gz /tmp/
RUN mkdir /opt/opendaylight \
      && tar zxvf /tmp/karaf-${ccsdk.opendaylight.version}.tar.gz --directory /opt/opendaylight \&& rm -rf /tmp/karaf-${ccsdk.opendaylight.version}.tar.gz \ 
      && mv /opt/opendaylight/karaf-${ccsdk.opendaylight.version} /opt/opendaylight/current && mkdir -p  /opt/opendaylight/current  && ln -s  /opt/opendaylight/current /opt/opendaylight/karaf-${ccsdk.opendaylight.version}
RUN mkdir -p /opt/opendaylight/current/system/org/mariadb/jdbc/mariadb-java-client/${ccsdk.mariadb-connector-java.version}
COPY mariadb-java-client-${ccsdk.mariadb-connector-java.version}.jar /opt/opendaylight/current/system/org/mariadb/jdbc/mariadb-java-client/${ccsdk.mariadb-connector-java.version}
EXPOSE 8181

(2)使用多阶段构建
使用多阶段构建,修改后的dockerfile,修改实现将第一阶段拷贝并解压好的文件复制过来,少了拷贝tar包的环节,这样使得最终形成的镜像中镜像层数得到有效的降低。

FROM xxxx:${project.version} as baseFirst
COPY karaf-${ccsdk.opendaylight.version}.tar.gz /tmp/
RUN mkdir /opt/opendaylight \
      && tar zxvf /tmp/karaf-${ccsdk.opendaylight.version}.tar.gz --directory /opt/opendaylight \&& rm -rf /tmp/karaf-${ccsdk.opendaylight.version}.tar.gz \ 
      && mv /opt/opendaylight/karaf-${ccsdk.opendaylight.version} /opt/opendaylight/current 

FROM xxxxxe:${project.version} as baseSecondRUN mkdir -p  /opt/opendaylight/current  && ln -s  /opt/opendaylight/current /opt/opendaylight/karaf-${ccsdk.opendaylight.version}
COPY --from=baseFirst /opt/opendaylight/current  /opt/opendaylight/current
RUN mkdir -p /opt/opendaylight/current/system/org/mariadb/jdbc/mariadb-java-client/${ccsdk.mariadb-connector-java.version}
COPY mariadb-java-client-${ccsdk.mariadb-connector-java.version}.jar /opt/opendaylight/current/system/org/mariadb/jdbc/mariadb-java-client/${ccsdk.mariadb-connector-java.version}
EXPOSE 8181
  • Copy和赋权同时执行
FROM ubuntu:16.04
# Copy APIKeys
COPY ./messageservice/ /tmp/zookeeper/gerrit  ------A 
EXPOSE 2181 2888 3888
B------> RUN useradd $ZK_USER && [ `id -u $ZK_USER` -eq 1000 ] && [ `id -g $ZK_USER` -eq 1000 ] && chown -R $ZK_USER:$ZK_USER /opt/$ZK_DIST/ /opt/zookeeper/ /var/lib/ /var/log/ /tmp/zookeeper/    
USER $ZK_USER

问题排查如下:A处copy的文件700MB大小,很多文件没有用到
B处给/tmp/zookeeper添加属组和属主,该层也很大。
修改:使用COPY -chown=1000:1000 ./messageservice/ /tmp/zookeeper/gerrit,镜像大小从1.4Gb下降到700Mb。

  • 镜像层的复用
    这一块修改得当的话,得到的收益是最大的!!
    我们知道docker镜像层具有层级结构,如果很多镜像具有相同的层,则这些相同的层就能得到服用(把多个镜像生成一个tar),docker不会保存两份相同放入层文件,通过提高层得到复用显著降低整体的镜像尺寸。比如常见的方法有:替换统一的基础镜像、创建出统一的基础镜像,调整层的顺序结构等等。