典型的 Linux 文件系统
bootfs(bootfilesystem)
- Bootloader - 引导加载 kernel
- Kernel - 当 kernel 被加载到内存中后 umount bootfs
rootfs(rootfilesystem)
- /dev,/proc,/bin,/etc 等标准目录和文件
对于不同的 Linux 发行版,bootfs 基本是一致的,但 rootfs 会有差别
Docker 的文件系统如何启动
Linux
- 在启动后,首先将 rootfs 置为 readonly,进行一系列检查,然后将其切换为“readwrite”共用户使用
Docker
也是将 rootfs 以 readonly 方式加载并检查,然而接下来利用 union mount 将一个 readwrite 文件系统挂载在 readonly 的 rootfs 之上
并且允许再次将下层的 file system 设定为 readonly 并且向上叠加
这样一组 readonly 和一个 writeable 的结构构成一个 container 的运行目录,每一个被称作一个 Layer
AUFS
AUFS 是一种 Union File System,所谓 UnionFS 就是把不同物理位置的目录合并 mount 到同一个目录中。UnionFS 的一个最主要的应用是,把一张CD/DVD 和一个硬盘目录给联合 mount 在一起,然后,你就可以对这个只读的 CD/DVD 上的文件进行修改(当然,修改的文件存于硬盘上的目录里)
AUFS 是将多个目录合并成一个虚拟文件系统,成员目录称为虚拟文件系统的一个分支(branch),每个 branch 可以指定 readwrite/whiteout-able/readonly 权限,只读(ro),读写(rw),写隐藏(wo)。一般情况下,aufs 只有最上层的 branch 具有读写权限,其余 branch 均为只读权限。只读 branch 只能逻辑上修改,AUFS 每层branch 可以动态的增加删除,每增加一层,下层默认置为 ro,最上一层为 rw。删除 branch 是在 aufs 挂载点移除,并未删除挂载目录
AUFS 的文件读写与删除
当需要修改一个文件,而该文件位于低层 branch时,顶层 branch 会直接复制低层 branch 的文件至顶层再进行修改,而低层的文件不变,这种方式即是 CoW 技术(写复制),AUFS 默认支持 Cow 技术,当容器删除一个低层 branch 文件时,只是在顶层 branch 对该文件进行重命名并隐藏,实际并未删除文件,只是不可见,这种方式即 AUFS 的whiteout(写隐藏)
- 添加文件:创建文件时,新文件被添加到branch中
- 读取文件 :读取某个文件时,会从上往下依次在各镜像层中查找此文件。一旦找到,打开并读入内存。
- 修改文件 :修改已存在的文件时,会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改之。
- 删除文件 :删除文件时,也是从上往下依次在镜像层中查找此文件。找到后,会在branch中记录下此删除操作。
Docker 中的 AUFS
Docker 镜像(Image)是由一个或多个 AUFS branch 组成,并且所有的 branch 均为只读权限。简单来说,AUFS 所有 robranch 按照一定顺序堆积构成 Docker Image 镜像
在运行容器的时候,创建一个 AUFS branch 位于image 层之上,具有 rw 权限,并把这些 branch 联合挂载到一个挂载点下。这就是 Docker 能够一个镜像运行多个容器的原理所在
写时复制(Copy-on-Write)
- 容器启动时,一个新的可写层被加载到镜像的顶部,这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”
- 所有对容器的改动,无论添加、删除、还是修改文件都只会发生在容器层中
- 只有容器层是可写的,容器层下面的所有镜像层都是只读的
- 只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。可见,容器层保存的是镜像变化的部分, 不会对镜像本身进行任何修改
AUFS 的好处
- 节省存储空间:多个容器可以共享基础镜像(Base Image)存储
- 快速部署:如果要部署多个容器,基础镜像(Base Image)可以避免多次拷贝
- 内存更省:因为多个容器共享 Base Image,以及OS 的 Disk 缓存机制,多个容器中的进程命中缓存内容的几率大大增加
- 允许在不更改基础镜像的同时修改其目录中的文件:所有写操作都发生在最上层的 writeable 层中,这样可以大大增加 Base Image 能共享的文件内容
镜像的定义
镜像(Image)就是一堆只读层(read-only layer)的统一视角
从左边我们看到了多个只读层,它们重叠在一起。除了最下面一层,其它层都会有一个指针指向下一层。这些层是 Docker 内部的实现细节,并且能够在主机(译者注:运行 Docker 的机器)的文件系统上访问到。统一文件系统(union file system)技术能够将不同的层整合成一个文件系统,为这些层提供了一个统一的视角
镜像与分层
- Docker 镜像是多个的,堆叠的分层的只读文件系统
- 每一层的变化是基于下一层的
- 相邻层之间的改动是有延续性的
- 从 docker 1.10 之后,每个镜像层的由一致性的 hash 生成 id,取代了旧版使用随机生成的 UUID
FROM ubuntu:15.10
COPY . /app
RUN make /app
CMD python /app/app.py
镜像命名规则
registry.abc.net/orgname/imagename:tag
- registry.abc.net:Remote registry 的地址
-(docker.io,gcr.io) - orgname:(optional)组织区分的镜像集合
- imagename:镜像名称
- tag:标签
docker.io/oracle/mysql:v5.7.1
k8s.gcr.io/kube-apiserver-amd64:v1.11.0
小心 latest,千万别被 latest tag 给误导了。latest 其实并没有什么特殊的含义。当没指明镜像
tag 时,Docker 会使用默认值 latest,仅此而已,所以我们在使用镜像时最好还是避免使用 latest,明确指定某个 tag,比如 httpd:2.3
容器的定义
- 容器(container)的定义和镜像(image)几乎一模一样,也是一堆层的统一视角,
唯一区别在于容器的最上面那一层是可读可写的 - 要点:容器 = 镜像 + 读写层,并且容器的定义并没有提及是否要运行容器
Dockerfile
dockerfile
FROM debian
RUN apt-get install emacs
RUN apt-get install apache2
CMD ["/bin/bash"]
- 新镜像不再是从 scratch 开始,而是直接在 Debian base 镜像上构建
- 安装 emacs 编辑器
- 安装 apache2
- 容器启动时运行 bash
构建过程如下图所示:
Dockerfile 是 docker 构建镜像的基础,也是 docker区别于其他容器的重要特征,正是有了 Dockerfile,docker 的自动化和可移植性才成为可能
编写Dockerfile命令:
FROM:从一个基础镜像构建新的镜像
- FROM ubuntu
MAINTAINER:维护者信息
- MAINTAINER William <wlj@nicescale.com>
RUN:非交互式运行shell命令
- RUN apt-get -y update
- RUN apt-get -y install nginx
ENV:设置环境变量
- ENV MYSQL 5.7
WORKDIR /path/to/workdir:设置工作目录
- WORKDIR /var/www
USER:设置用户ID
- USER nginx
VOLUME <#dir>:设置volume
- VOLUME [‘/data’]
EXPOSE:暴露哪些端口
- EXPOSE 80 443
ENTRYPOINT [‘executable’, ‘param1’,’param2’]:执行命令
- ENTRYPOINT ["/usr/sbin/nginx"]
CMD [“param1”,”param2”]
- CMD ["start"]
注意:
- ENTRYPOINT 指令和 CMD 指令虽然是在 Dockerfile 中定义,但是在构建镜像的时候并不会被执行,只有在执行 docker run 命令启动容器时才会起作用
- 在 Dockerfile 中,只能有一个 ENTRYPOINT 指令,如果有多个 ENTRYPOINT 指令则以最后一个为准
- 在 Dockerfile 中,只能有一个 CMD 指令,如果有多个 CMD 指令则以最后一个为准
- 在 Dockerfile 中,ENTRYPOINT 指令或 CMD指令,至少必有其一,如果设置了 ENTRYPOINT,则 CMD 将作为参数
Dockerfile 例子
Dockerfile Best Practices
- 使用统一的base镜像 - Centos, Ubuntu
- 使用小型基础镜像 - 生成镜像也较小
- 动静分离 - 经常变化的内容和基本不会变化的内容要分开,把不怎么变化的内容放在下层, 创建出来不同基础镜像供上层使用。
- 最小原则:只安装必需的东西
- 一个原则:每个镜像只有一个功能 - 不要在容器里运行多个不同功能的进程,每个镜像中只安装一个应用的软件包和文件,需要交互的程序通过 pod 或者容器之间的网络进行交流。
- 使用更少的层 - 尽量把相关的内容放到同一个层,使用换行符进行分割
- 切勿映射公有端口 - 镜像应该可以多次运行在任何主机上
- 不要在构建中升级软件版本 - 应在基础镜像中更新,类似于类的继承,在基类里实现
- 利用 cache 来加快构建速度 - docker build --cache-from 参数可以手动指定一个镜像来使用它的缓存。
- 版本控制和自动构建 - 最好把Dockerfile和对应的应用代码一起放到版本控制中,然后能够自动构建镜像。