Docker核心技术

Cgroups,Namespace,Union FS

1. 从系统架构谈起:传统分层架构 vs 微服务

在深入 Docker 之前,我们先来聊聊系统架构的演变。

传统分层架构

传统的单体应用通常采用分层架构,如下图所示:

graph LR
    A[Presentation Layer] --> B(Business Logic Layer)
    B --> C(Data Access Layer)
    C --> D{Database}

优点:

  • 对于简单的系统,易于部署、测试和横向扩展。

缺点:

  • 对于复杂的系统:
    • 难以理解整体
    • 不易快速维护
    • 启动和部署慢
    • 变更引起的回归问题多
    • 难以持续集成和持续部署

微服务架构

为了解决传统分层架构的痛点,微服务架构应运而生。它将一个庞大的系统分解成多个独立的小服务,每个服务负责特定的业务功能。

graph LR
    subgraph Microservices
        A[Service 1] -->|API| B(API Gateway)
        C[Service 2] -->|API| B
        D[Service 3] -->|API| B
    end
    B --> E{External Services}

优点:

  • 系统部署更快,更易理解和维护。
  • 不同服务可由不同团队维护,分工更细。
  • 可自主选择技术栈。
  • 易于持续集成和持续部署。
  • 每个微服务独立扩展。

缺点:

  • 增加了系统的复杂性(分布式部署)。
  • 需要处理分布式系统的复杂问题(如进程间通信、部分失败)。
  • 分布式事务更复杂(失败回滚)。
  • 测试和部署监控更复杂。

微服务改造

将单体应用拆分成微服务,可以遵循以下原则:

  • 分离业务逻辑: 审视并发现可以分离的业务逻辑。
  • 寻找隔离模块: 寻找天生隔离的代码模块(可借助静态代码分析工具)。
  • 考虑性能需求: 不同并发规模、不同内存需求的模块可分离成不同的微服务。

一些常用的可微服务化的组件:

  • 用户和账户管理
  • 授权和会话管理
  • 系统配置
  • 通知和通讯服务
  • 照片、多媒体、元数据等

分解原则: 基于 size, scope and capabilities

微服务间通讯

微服务之间通常有两种通讯方式:

  1. 点对点:

    • 多用于系统内部组件间通讯。
    • 存在大量重复模块(如认证授权)。
    • 缺少统一规范(如监控、审计)。
    • 后期维护成本高。
  2. API 网关:

    • 基于一个轻量级的 message gateway。
    • 新 API 通过注册至 Gateway 实现。
    • 整合实现 Common function。

2. 理解 Docker

Docker 是一种容器化技术,可以将应用程序及其依赖项打包到一个可移植的容器中,从而实现快速部署、一致的运行环境和高效的资源利用。

Docker 是什么?

  • Docker 基于 Linux 内核的 Cgroup、Namespace 以及 Union FS 等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。
  • Docker 容器独立于宿主机和其他容器。
  • Docker 在容器的基础上进行了进一步的封装,简化了容器的创建和维护。

为什么要用 Docker?

  • 更高效地利用系统资源: 容器共享宿主机内核,资源开销更小。
  • 更快速的启动时间: 容器启动只需几秒钟,远快于虚拟机。
  • 一致的运行环境: 容器打包了应用程序及其依赖项,保证在不同环境中的一致性。
  • 持续交付和部署: 容器易于构建、测试和部署,方便实现持续交付。
  • 更轻松地迁移: 容器可轻松迁移到不同的平台和环境。
  • 更轻松地维护和扩展: 容器易于管理和扩展,可根据需求动态调整资源。

虚拟机和容器运行态的对比

graph LR
    subgraph 虚拟机
        A[虚拟机 1] --> B(Hypervisor)
        C[虚拟机 2] --> B
        D[虚拟机 3] --> B
        B --> E(Host OS)
        E --> F{Hardware}
    end
    subgraph 容器
        G[容器 1] --> H(Docker Engine)
        I[容器 2] --> H
        J[容器 3] --> H
        H --> K(Host OS)
        K --> L{Hardware}
    end
特性 容器 虚拟机
启动 秒级 分钟级
硬盘使用 一般为 MB 一般为 GB
性能 接近原生 弱于
系统支持量 单机上千个容器 一般几十个

安装 Docker

在 Ubuntu 上安装 Docker 的步骤如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 1. 更新 apt 包索引
sudo apt-get update

# 2. 安装依赖包
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg-agent \
software-properties-common

# 3. 添加 Docker 的官方 GPG 密钥
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

# 4. 设置稳定版仓库
sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"

# 5. 再次更新 apt 包索引
sudo apt-get update

# 6. 安装 Docker Engine
sudo apt-get install docker-ce docker-ce-cli containerd.io

容器操作

  • 启动容器:
    • docker run:创建并启动一个新的容器。
      • -it:交互式运行。
      • -d:后台运行。
      • -p:端口映射。
      • -v:磁盘挂载。
    • docker start:启动已终止的容器。
  • 停止容器:
    • docker stop:停止正在运行的容器。
  • 查看容器进程:
    • docker ps:列出正在运行的容器。
    • docker ps -a:列出所有容器(包括已停止的)。
  • 查看容器细节
* `docker inspect <containerid>`
  • 进入容器
    • docker attach
    • nsenter
    1
    2
    3
    PID=$(docker inspect --format "{{ .State.Pid }}"
    <container>)
    $ nsenter --target $PID --mount --uts --ipc --net --pid
  • 拷贝文件至容器内
    • docker cp file1 <containerid>:/file-to-path

初识容器

让我们通过一个简单的例子来体验 Docker 的魅力。

  1. 编写 Dockerfile:

    1
    2
    3
    4
    FROM ubuntu
    ENV MY_SERVICE_PORT=80
    ADD bin/amd64/httpserver /httpserver
    ENTRYPOINT /httpserver
    • FROM:指定基础镜像(这里使用 Ubuntu)。
    • ENV:设置环境变量。
    • ADD:将本地文件复制到容器中。
    • ENTRYPOINT:指定容器启动时执行的命令。
  2. 构建镜像:

    1
    2
    docker build -t cncamp/httpserver:${tag} .
    docker push cncamp/httpserver:v1.0
    • -t:指定镜像的名称和标签。
    • .:表示 Dockerfile 所在的当前目录。
  3. 运行容器:

    1
    docker run -d cncamp/httpserver:v1.0

容器标准

为了规范容器技术的发展,业界制定了 OCI(Open Container Initiative)标准。

  • OCI 是什么?
    • 一个轻量级开放式管理组织。
    • 主要定义两个规范:
      • Runtime Specification: 定义如何解压文件系统包并在运行时运行。
      • Image Specification: 定义如何打包镜像、生成清单、文件系统序列化文件和镜像配置。

容器主要特性

  • 安全性: 容器提供了隔离的运行环境,增强了安全性。
  • 便携性: 容器可在不同平台和环境中轻松迁移。
  • 隔离性: 容器之间相互隔离,互不影响。
  • 可配额: 可对容器的资源使用进行限制。

3. Docker 核心技术:Namespace、Cgroups、UnionFS

Docker 的核心技术主要包括 Namespace、Cgroups 和 UnionFS。

Namespace

Namespace 是 Linux 内核提供的一种资源隔离方案。它可以为进程分配不同的 Namespace,保证不同 Namespace 下的资源独立分配,进程彼此隔离。

  • Linux 内核代码中 Namespace 的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 进程数据结构
    struct task_struct {
    ...
    /* namespaces */
    struct nsproxy *nsproxy;
    ...
    }

    // Namespace 数据结构
    struct nsproxy {
    atomic_t count;
    struct uts_namespace *uts_ns;
    struct ipc_namespace *ipc_ns;
    struct mnt_namespace *mnt_ns;
    struct pid_namespace *pid_ns_for_children;
    struct net *net_ns;
    }
  • Linux 对 Namespace 操作方法:

    • clone:创建新进程时,可通过 flags 参数指定需要新建的 Namespace 类型。
    • setns:将调用进程加入某个已存在的 Namespace。
    • unshare:将调用进程移动到新的 Namespace。
  • Namespace 类型:

    Namespace 类型 隔离资源 Kernel 版本
    IPC System V IPC 和 POSIX 消息队列 2.6.19
    Network 网络设备、网络协议栈、网络端口等 2.6.29
    PID 进程 2.6.14
    Mount 挂载点 2.4.19
    UTS 主机名和域名 2.6.19
    User 用户和用户组 3.8
  • Namespace 常用操作:

    • 查看当前系统的 namespace:lsns -t <type>
    • 查看某进程的 namespace:ls -la /proc/<pid>/ns/
    • 进入某 namespace 运行命令:nsenter -t <pid> -n ip addr
  • Namespace 练习:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 在新 network namespace 执行 sleep 指令
    unshare -fn sleep 60

    # 查看进程信息
    ps -ef | grep sleep

    # 查看网络 Namespace
    lsns -t net

    # 进入该进程所在 Namespace 查看网络配置
    nsenter -t <pid> -n ip a

Cgroups

Cgroups(Control Groups)是 Linux 下用于对一个或一组进程进行资源控制和监控的机制。它可以对 CPU 使用时间、内存、磁盘 I/O 等进程所需的资源进行限制。

  • Linux 内核代码中 Cgroups 的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct task_struct {
    #ifdef CONFIG_CGROUPS
    struct css_set __rcu *cgroups;
    struct list_head cg_list;
    #endif
    }

    struct css_set {
    /*
    * Set of subsystem states, one for each subsystem. This array is
    * immutable after creation apart from the init_css_set during
    * subsystem registration (at boot time).
    */
    struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
    };
  • Cgroups 子系统:

    子系统 作用
    blkio 限制每个块设备的输入输出
    cpu 使用调度程序为 cgroup 任务提供 CPU 的访问
    cpuacct 产生 cgroup 任务的 CPU 资源报告
    cpuset 为 cgroup 任务分配单独的 CPU 和内存
    devices 允许或拒绝 cgroup 任务对设备的访问
    freezer 暂停和恢复 cgroup 任务
    memory 设置每个 cgroup 的内存限制以及产生内存资源报告
    net_cls 标记每个网络包以供 cgroup 方便使用
    ns 名称空间子系统
    pid 进程标识子系统
  • CPU 子系统:

    • cpu.shares:可出让的能获得 CPU 使用时间的相对值。
    • cpu.cfs_period_us:时间周期长度(微秒)。
    • cpu.cfs_quota_us:当前 Cgroup 在 cfs_period_us 时间内最多能使用的 CPU 时间数(微秒)。
    • cpu.stat:Cgroup 内的进程使用的 CPU 时间统计。
      • nr_periods:经过 cpu.cfs_period_us 的时间周期数量。
      • nr_throttled:进程受限次数。
      • throttled_time:进程被限制使用 CPU 的总用时(纳秒)。
  • Linux 调度器:

    • Stop 调度器(stop_sched_class): 优先级最高,可抢占其他所有进程。
    • Deadline 调度器(dl_sched_class): 使用红黑树,按绝对截止期限排序进程。
    • RT 调度器(rt_sched_class): 实时调度器,为每个优先级维护一个队列。
    • CFS 调度器(cfs_sched_class): 完全公平调度器,采用完全公平调度算法,引入虚拟运行时间概念。
    • IDLE-Task 调度器(idle_sched_class): 空闲调度器,每个 CPU 都有一个 idle 线程。
  • CFS 调度器:

    • 维护任务提供处理器时间方面的平衡。
    • 通过虚拟运行时间(vruntime)来实现平衡。
    • vruntime = 实际运行时间 * 1024 / 进程权重
    • 进程按各自不同的速率在物理时钟节拍内前进,优先级高则权重大,其虚拟时钟比真实时钟跑得慢,但获得比较多的运行时间。
graph LR
    subgraph 红黑树
    A[vruntime 最小] --> B
    B --> C
    C --> D
    D --> E[vruntime 最大]
    end
  • CFS 进程调度:

    1. 时钟周期开始时,调用 __schedule() 函数。
    2. __schedule() 调用 pick_next_task() 选择最合适的进程(红黑树最左边的节点)。
    3. 通过 context_switch() 切换到新的地址空间,保证进程运行。
    4. 时钟周期结束时,调用 entity_tick() 函数更新进程负载、状态和 vruntime。
    5. 将该进程的虚拟时间与就绪队列红黑树中最左边的调度实体的虚拟时间做比较,决定是否触发调度。
  • CPU 子系统练习:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # 在 cgroup cpu 子系统目录中创建目录结构
    cd /sys/fs/cgroup/cpu
    mkdir cpudemo
    cd cpudemo

    # 运行 busyloop

    # 查看 CPU 使用情况
    top

    # 通过 cgroup 限制 cpu
    cd /sys/fs/cgroup/cpu/cpudemo

    # 把进程添加到 cgroup 进程配置组
    echo $(ps -ef | grep busyloop | grep -v grep | awk '{print $2}') > cgroup.procs

    # 设置 cpuquota
    echo 10000 > cpu.cfs_quota_us

    # 再次查看 CPU 使用情况
    top
  • cpuacct 子系统:

    • cpuacct.usage:Cgroup 及其子 Cgroup 下进程使用 CPU 的时间(纳秒)。
    • cpuacct.stat:Cgroup 及其子 Cgroup 下进程使用的 CPU 时间(用户态和内核态)。
  • Memory 子系统:

    • memory.usage_in_bytes:Cgroup 下进程使用的内存。
    • memory.max_usage_in_bytes:Cgroup 下进程使用内存的最大值。
    • memory.limit_in_bytes:设置 Cgroup 下进程最多能使用的内存。
    • memory.soft_limit_in_bytes:软限制,系统内存足够时优先回收超过限额的内存。
    • memory.oom_control:设置是否在 Cgroup 中使用 OOM Killer。
  • Cgroup driver:

    • systemd: 当操作系统使用 systemd 作为 init system 时,初始化进程生成一个根 cgroup 目录结构并作为 cgroup 管理器。
    • cgroupfs: Docker 默认使用 cgroupfs 作为 cgroup 驱动。
      • 存在问题: 在 systemd 作为 init system 的系统中,可能存在两套 group driver,导致管理混乱。
      • 建议: 将 kubelet 的 --cgroup-driver 设置为 systemd
  • 课后练习 3.1:Memory 子系统练习

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # 在 cgroup memory 子系统目录中创建目录结构
    cd /sys/fs/cgroup/memory
    mkdir memorydemo
    cd memorydemo

    # 运行 malloc(在 linux 机器 make build)

    # 查看内存使用情况
    watch 'ps -aux|grep malloc|grep -v grep'

    # 通过 cgroup 限制 memory
    # 把进程添加到 cgroup 进程配置组
    echo $(ps -ef | grep malloc | grep -v grep | awk '{print $2}') > cgroup.procs

    # 设置 memory.limit_in_bytes
    echo 104960000 > memory.limit_in_bytes

    # 等待进程被 oom kill

Union FS

Union FS 是一种将不同目录挂载到同一个虚拟文件系统下的文件系统。

  • 特点:

    • 支持为每一个成员目录设定 readonly、readwrite 和 whiteout-able 权限。
    • 文件系统分层,对 readonly 权限的 branch 可以逻辑上进行修改(增量地,不影响 readonly 部分)。
  • 用途:

    • 将多个 disk 挂到同一个目录下。
    • 将一个 readonly 的 branch 和一个 writeable 的 branch 联合在一起。
graph LR
    subgraph Union FS
        A[Read-Only Layer 1] --> C(Merged View)
        B[Read-Write Layer 2] --> C
    end

容器镜像

Dockerfile 示例:

1
2
3
4
5
6
7
8
9
10
11
12
# Dockerfile-java:
FROM ubuntu
RUN apt install -y default-jre
COPY ./app.jar /app.jar
EntryPoint ["Java", "app1.jar"]

# Dockerfile-elasticsearch:
FROM ubuntu
RUN apt install -y default-jre
RUN wget elasticseach
RUN untar elasticseach
EntryPoint: ["./elasticseach"]
graph LR
    subgraph "myapp:v1.0"
        A["BaseImage(ubuntu)"] --> B("install default-jre")
        B --> C("copy app1.jar")
        C --> D("EntryPoint: [Java, app1.jar]")
    end
    subgraph "elasticsearch:v7.14"
        E["BaseImage(ubuntu)"] --> F("install default-jre")
        F --> G("wget elasticseach")
        G --> H("untar elasticseach")
        H --> I("EntryPoint: [./elasticseach]")
    end
    subgraph "通用层"
    A --> B
    E --> F
    end

Docker 的文件系统

典型的 Linux 文件系统组成:

  • Bootfs(boot file system):
    • Bootloader:引导加载 kernel。
    • Kernel:当 kernel 被加载到内存中后 umount bootfs。
  • rootfs(root file system):
    • /dev/proc/bin/etc 等标准目录和文件。
    • 不同的 linux 发行版,bootfs 基本一致,但 rootfs 会有差别。

Docker 启动:

  • Linux:启动后,首先将 rootfs 设置为 readonly,进行一系列检查,然后将其切换为 “readwrite” 供用户使用。
  • Docker:初始化时也是将 rootfs 以 readonly 方式加载并检查,然后利用 union mount 的方式将一个 readwrite 文件系统挂载在 readonly 的 rootfs 之上。
graph LR
    subgraph Docker File System
        A["Read-Only Layer 1 (Base Image)"] --> C(Container Layer - Read-Write)
        B[Read-Only Layer 2] --> C
        C --> D{Running Container}
    end

写操作:

  • 写时复制(Copy-on-Write):
    • 一个镜像可以被多个容器使用,不需要在内存和磁盘上做多个拷贝。
    • 需要对镜像提供的文件进行修改时,该文件会从镜像的文件系统被复制到容器的可写层的文件系统进行修改。
    • 不同容器对文件的修改相互独立。
  • 用时分配:
    • 按需分配空间,而非提前分配。

容器存储驱动

存储驱动 优点 缺点 应用场景
AUFS Docker 最早支持的驱动类型,稳定性高 未进入主线内核,只能在有限场合下使用。实现上具有多层结构,在层比较多的场景下,做写时复制有时会需要比较长的时间 少 I/O 的场景
OverlayFS 并入主线内核,可在几乎所有发行版本上使用。实现上只有两层,性能比 AUFS 高 写时复制机制需要复制整个文件,不能只针对修改部分进行复制,对大文件操作会需要比较长的时间。其中 Overlay 在 Docker 的后续版本中被移除 少 I/O 的场景
Device Mapper 并入主线内核,针对块操作,性能比较高。修改文件时只需复制需要修改的块,效率高 不同容器之间不能共享缓存。在 Docker 的后续版本中会被移除 I/O 密集场景
BtrFS 并入主线内核,虽然是文件级操作系统,但是可以对块进行操作。 需要消耗比较多的内存,稳定性相对比较差 需要支持 Snapshot 等比较特
ZFS 不同的容器之间可以共享缓存,多个容器访问相同的文件能够共享一个单一的 Page Cache。 在频繁写操作的场景下,会产生比较严重的磁盘碎片。需要消耗比较多的内存,另外稳定性相对比较差 容器高密度部署的场景

以 OverlayFS 为例:

  • OverlayFS 是一种与 AUFS 类似的联合文件系统,属于文件级的存储驱动。
  • 包含最初的 Overlay 和更新更稳定的 overlay2。
  • Overlay 只有两层:upper 层和 lower 层。
    • Lower 层:镜像层。
    • Upper 层:容器可写层。

OverlayFS 文件系统练习:

1
2
3
4
5
6
7
8
9
10
mkdir upper lower merged work
echo "from lower" > lower/in_lower.txt
echo "from upper" > upper/in_upper.txt
echo "from lower" > lower/in_both.txt
echo "from upper" > upper/in_both.txt
sudo mount -t overlay overlay -o lowerdir=`pwd`/lower,upperdir=`pwd`/upper,workdir=`pwd`/work `pwd`/merged
cat merged/in_both.txt
rm merged/in_both.txt
rm merged/in_lower.txt
rm merged/in_upper.txt

OCI 容器标准

  • Open Container Initiative:
    • OCI 组织于 2015 年创建,是一个致力于定义容器镜像标准和运行时标准的开放式组织。
    • OCI 定义了镜像标准(Runtime Specification)、运行时标准(Image Specification)和分发标准(Distribution Specification)。
      • 镜像标准:定义应用如何打包。
      • 运行时标准:定义如何解压应用包并运行。
      • 分发标准:定义如何分发容器镜像。

Docker 引擎架构

graph LR
    A[Docker Client] --> B(Docker Daemon)
    B --> C{Images}
    B --> D{Containers}
    B --> E{Networks}
    B --> F{Volumes}

网络

  • Null(--net=None):

    • 把容器放入独立的网络空间但不做任何网络配置。
    • 用户需要通过运行 docker network 命令来完成网络配置。
  • Host:

    • 使用主机网络名空间,复用主机网络。
  • Container:

    • 重用其他容器的网络。
  • Bridge(--net=bridge):

    • 使用 Linux 网桥和 iptables 提供容器互联。
    • Docker 在每台主机上创建一个名叫 docker0 的网桥,通过 veth pair 来连接该主机的每一个 EndPoint。
graph LR
    subgraph Host
        A[Container 1] -->|veth pair| B(docker0 bridge)
        C[Container 2] -->|veth pair| B
        B --> D{External Network}
    end
  • Overlay(libnetwork, libkv):

    • 通过网络封包实现。
  • Remote (work with remote drivers)

    • Underlay
      • 使用现有底层网络,为每一个容器配置可路由的网络 IP。
    • Overlay
      • 通过网络封包实现。
  • Null 模式

    • Null 模式是一个空实现;
    • 可以通过 Null 模式启动容器并在宿主机上通过命令为容器配置网络。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    mkdir -p /var/run/netns
    find -L /var/run/netns -type l -delete
    ln -s /proc/$pid/ns/net /var/run/netns/$pid
    ip link add A type veth peer name B
    brctl addif br0 A
    ip link set A up
    ip link set B netns $pid
    ip netns exec $pid ip link set dev B name eth0
    ip netns exec $pid ip link set eth0 up
    ip netns exec $pid ip addr add
    $SETIP/$SETMASK dev eth0
    ip netns exec $pid ip route add default via
    $GATEWAY
  • 默认模式– 网桥和 NAT

  •     graph LR
        subgraph Host A
            A[Container 1] -->|eth0 172.17.0.2| B(veth)
            B --> C(docker0 172.17.0.1/16)
            D[Container 2] -->|eth0 172.17.0.3| E(veth)
            E --> C
            C -->|NAT| F(eth0 192.168.0.101)
        end
    • 为主机 eth0 分配 IP 192.168.0.101。
    • 启动 docker daemon,查看主机 iptables。
      • POSTROUTING -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
    • 在主机启动容器:
      • docker run -d --name ssh -p 2333:22 centos-ssh
      • Docker 会以标准模式配置网络:
        • 创建 veth pair。
        • 将 veth pair 的一端连接到 docker0 网桥。
        • veth pair 的另外一端设置为容器名空间的 eth0。
        • 为容器名空间的 eth0 分配 ip。
        • 主机上的 Iptables 规则:PREROUTING -A DOCKER ! -i docker0 -p tcp -m tcp --dport 2333 -j DNAT --to-destination 172.17.0.2:22
  • Underlay

      graph LR
        subgraph Host A
            A[Container 1] -->|eth0 10.249.67.31| B(veth)
            B --> C(mydr0 10.249.67.30)
            D[Container 2] -->|eth0 10.249.67.32| E(veth)
            E --> C
            C -->|eth0| F{External Network}
        end
    • 采用 Linux 网桥设备(brctl),通过物理网络连通容器。
    • 创建新的网桥设备 mydr0。
    • 将主机网卡加入网桥。
    • 把主机网卡的地址配置到网桥,并把默认路由规则转移到网桥 mydr0。
    • 启动容器。
    • 创建 veth 对,并且把一个 peer 添加到网桥 mydr0。
    • 配置容器把 veth 的另一个 peer 分配给容器网卡。
  • Docker Libnetwork Overlay

    • Docker overlay 网络驱动原生支持多主机网络。
    • Libnetwork 是一个内置的基于 VXLAN 的网络驱动。

VXLAN

graph LR
    subgraph Host 1
        A[VM 1] --> B(VXLAN Tunnel Endpoint)
        B -->|VXLAN Encapsulation| C(Underlay Network)
    end
    subgraph Host 2
        C --> D(VXLAN Tunnel Endpoint)
        D --> E[VM 2]
    end

Overlay network sample – Flannel

graph LR
    subgraph Host 1
        A[Pod 1] -->|Bridge| B(Flannel)
        B -->|UDP Encapsulation| C(Underlay Network)
    end
    subgraph Host 2
        C --> D(Flannel)
        D -->|Bridge| E[Pod 2]
    end
  • 同一主机内的 Pod 可以使用网桥进行通信。
  • 不同主机上的 Pod 将通过 flanneld 将其流量封装在 UDP 数据包中。

Flannel packet sample

1
2
3
4
5
6
7
[Outer Ethernet Header]
[Outer IP Header]
[UDP Header]
[VXLAN Header]
[Inner Ethernet Header]
[Inner IP Header]
[Payload]

创建 docker 镜像

  • 定义 dockerfile
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    FROM ubuntu
    # so apt - get doesn't complain
    ENV DEBIAN_FRONTEND=noninteractive
    RUN sed -i 's/^exit 101/exit 0/' /usr/sbin/policy-rc.d
    RUN \
    apt-get update && \
    apt-get install -y ca-certificates && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*
    ADD ./bin/eic eic
    ENTRYPOINT ["/eic"]
  • docker build

4. Dockerfile 的最佳实践

回顾 12 Factor 之进程

  • 运行环境中,应用程序通常是以一个和多个进程运行的。
  • 12-Factor 应用的进程必须无状态(Stateless)且无共享(Share nothing)。
  • 任何需要持久化的数据都要存储在后端服务内,比如数据库。
  • 应在构建阶段将源代码编译成待执行应用。
  • Session Sticky 是 12-Factor 极力反对的。
  • Session 中的数据应该保存在诸如 Memcached 或 Redis 这样的带有过期时间的缓存中。

Docker 遵循以上原则管理和构建应用。

理解构建上下文(Build Context)

  • 当运行 docker build 命令时,当前工作目录被称为构建上下文。
  • docker build 默认查找当前目录的 Dockerfile 作为构建输入,也可以通过 -f 指定 Dockerfile。
    • docker build -f ./Dockerfile
  • docker build 运行时,首先会把构建上下文传输给 docker daemon。
  • 可以通过 .dockerignore 文件从编译上下文排除某些文件。

镜像构建日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
docker build $GOPATH/src/github.com/cncamp/golang/httpserver/
Sending build context to Docker daemon 14.57MB
Step 1/4 : FROM ubuntu
---> cf0f3ca922e0
Step 2/4 : ENV MY_SERVICE_PORT=80
---> Using cache
---> a7d824f74410
Step 3/4 : ADD bin/amd64/httpserver /httpserver
---> Using cache
---> 00bb47fce704
Step 4/4 : ENTRYPOINT /httpserver
---> Using cache
---> f77ee3366d08
Successfully built f77ee3366d08

Build Cache

  • Docker 读取指令后,会先判断缓存中是否有可用的已存镜像,只有已存镜像不存在时才会重新构建。

  • 通常 Docker 简单判断 Dockerfile 中的指令与镜像。

  • 针对 ADDCOPY 指令,Docker 会比较文件的 checksum。

  • 其他指令(如 RUN apt-get -y update),Docker 简单比较指令字串。

  • 当某一层 cache 失效,所有后续层级的 cache 均一并失效。

多段构建(Multi-stage build)

多段构建是减少镜像层级、缩小镜像体积的有效方式。它允许您在一个 Dockerfile 中使用多个 FROM 指令,每个 FROM 指令都可以使用不同的基础镜像,并且可以从之前的构建阶段复制文件到当前阶段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 第一阶段:构建阶段
FROM golang:1.16-alpine AS build
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
RUN dep ensure -vendor-only
COPY . /go/src/project/
RUN go build -o /bin/project # 只有这个二进制文件是最终需要的

# 第二阶段:运行阶段
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]

解释:

  1. 第一阶段 (build):
    • 使用 golang:1.16-alpine 作为基础镜像。
    • 安装依赖、复制代码、编译项目。
    • 最终生成可执行文件 /bin/project
  2. 第二阶段 (无基础镜像):
    • 使用 scratch 作为基础镜像(一个空的镜像)。
    • 从第一阶段(build)复制 /bin/project 到当前阶段。
    • 设置 ENTRYPOINTCMD

优点:

  • 最终镜像只包含运行所需的二进制文件,极大地减小了镜像体积。
  • 避免了将编译工具、依赖库等不必要的文件包含在最终镜像中。

Dockerfile 常用指令

  • FROM 选择基础镜像,推荐使用 Alpine Linux(体积小)。

    1
    FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
  • LABEL 按标签组织项目。

    1
    LABEL multi.label1="value1" multi.label2="value2" other="value3"

    配合 label filter 可过滤镜像查询结果:

    1
    docker images -f label=multi.label1="value1"
  • RUN 执行命令。

    1
    RUN apt-get update && apt-get install -y <package>
    • 最常见的用法是 RUN apt-get update && apt-get install,这两条命令应该永远用 && 连接,以避免缓存导致的问题。
  • CMD 容器镜像中包含应用的运行命令(需要带参数)。

    1
    CMD ["executable", "param1", "param2"...]
  • EXPOSE 发布端口。

    1
    EXPOSE <port> [<port>/<protocol>...]
    • 是镜像创建者和使用者的约定。
    • docker run -P 时,Docker 会自动映射 EXPOSE 的端口到主机大端口。
  • ENV 设置环境变量。

    1
    ENV <key>=<value> ...
  • ADD 从源地址(文件、目录或者 URL)复制文件到目标路径。

    1
    2
    ADD [--chown=<user>:<group>] <src>... <dest>
    ADD [--chown=<user>:<group>] ["<src>",... "<dest>"] # 路径中有空格时使用
    • ADD 支持 Go 风格的通配符,如 ADD check* /testdir/
    • src 如果是文件,则必须包含在编译上下文中。
    • src 如果是 URL:
      • 如果 dest 结尾没有 /,则 dest 是目标文件名。
      • 如果 dest 结尾有 /,则 dest 是目标目录名。
    • 如果 src 是一个目录,则所有文件都会被复制至 dest
    • 如果 src 是一个本地压缩文件,则在 ADD 的同时完整解压操作。
    • 如果 dest 不存在,则 ADD 指令会创建目标目录。
    • 应尽量减少通过 ADD URL 添加 remote 文件,建议使用 curl 或者 wget && untar
  • COPY 从源地址(文件、目录)复制文件到目标路径。

    1
    2
    COPY [--chown=<user>:<group>] <src>... <dest>
    COPY [--chown=<user>:<group>] ["<src>",... "<dest>"] # 路径中有空格时使用
    • COPY 的使用与 ADD 类似,但有如下区别:
      • COPY 只支持本地文件的复制,不支持 URL。
      • COPY 不解压文件。
      • COPY 可以用于多阶段编译场景。
    • COPY 语义上更直白,复制本地文件时,优先使用 COPY
  • ENTRYPOINT 定义可以执行的容器镜像入口命令。

    1
    2
    ENTRYPOINT ["executable", "param1", "param2"]  # docker run 参数追加模式
    ENTRYPOINT command param1 param2 # docker run 参数替换模式
    • docker run --entrypoint 可替换 Dockerfile 中定义的 ENTRYPOINT
    • 最佳实践:用 ENTRYPOINT 定义镜像主命令,并通过 CMD 定义主要参数。
      1
      2
      ENTRYPOINT ["s3cmd"]
      CMD ["--help"]
  • VOLUME 将指定目录定义为外挂存储卷,Dockerfile 中在该指令之后所有对同一目录的修改都无效。

    1
    VOLUME ["/data"]

    等价于 docker run -v /data,可通过 docker inspect 查看主机的 mount point。

  • USER 切换运行镜像的用户和用户组。

    1
    USER <user>[:<group>]
  • WORKDIR 等价于 cd,切换工作目录。

    1
    WORKDIR /path/to/workdir
  • 其他非常用指令:

    • ARG
    • ONBUILD
    • STOPSIGNAL
    • HEALTHCHECK
    • SHELL

Dockerfile 最佳实践总结

  • 不要安装无效软件包。
  • 简化镜像中同时运行的进程数: 理想状况下,每个镜像应该只有一个进程。
  • 最小化层级数:
    • 最新的 Docker 只有 RUNCOPYADD 创建新层。
    • 多条 RUN 命令可通过连接符连接成一条指令集。
    • 通过多段构建减少镜像层数。
  • 把多行参数按字母排序。
  • 编写 dockerfile 的时候,应该把变更频率低的编译指令优先构建。
  • 复制文件时,每个文件应独立复制。

目标: 易管理、少漏洞、镜像小、层级少、利用缓存。

多进程的容器镜像

  • 选择适当的 init 进程:
    • 需要捕获 SIGTERM 信号并完成子进程的优雅终止。
    • 负责清理退出的子进程以避免僵尸进程。
  • 开源项目: https://github.com/krallin/tini

5. Docker 镜像管理

  • docker save/load:保存和加载镜像。
  • docker tag:为镜像打标签。
  • docker push/pull:推送和拉取镜像。

基于 Docker 镜像的版本管理

  • docker tag

    1
    docker tag <image_id> <repository>/<image_name>:<tag>
    • hub.docker.com:镜像仓库地址(如果不填,默认为 hub.docker.com)。
    • cncamp:repository。
    • httpserver:镜像名。
    • v1.0:tag(常用来记录版本信息)。
  • Docker tag 与 GitHub 的版本管理合力:

    以 Kubernetes 为例:

    1. 开发分支: git checkout master
    2. Release 分支: git checkout -b release-1.21
    3. 版本发布:
      • 以 release branch 为基础构建镜像,并为镜像标记版本信息:docker tag <image_id> k8s.io/kubernetes/apiserver:v1.21
      • 在 GitHub 中保存 release 代码快照:git tag v1.21

镜像仓库

6. Docker 优势与劣势

Docker 优势

  • 封装性:
    • 应用扩缩容时可以秒速启动。
    • 资源利用率高。
    • 方便的 CPU、内存资源调整。
    • 能实现秒级快速回滚。
    • 一键启动所有依赖服务。
    • 镜像一次编译,随处使用。
    • 测试、生产环境高度一致。
  • 隔离性:
    • 应用的运行环境和宿主机环境无关,完全由镜像控制。
    • 多个应用版本可以并存在机器上。
  • 社区活跃:
    • Docker 命令简单、易用,社区十分活跃,且周边组件丰富。

Docker 劣势

  • 隔离性不如虚拟机: 容器共享宿主机内核,隔离性相对较弱。
  • 安全性: 如果容器内的应用存在漏洞,可能会影响到宿主机或其他容器。
  • 网络配置复杂: 跨主机容器网络配置相对复杂。
  • 资源限制: 虽然可以使用 Cgroups 进行资源限制,但配置和管理相对复杂。
  • 持久化存储: 容器的持久化存储需要额外的配置和管理。

Docker核心技术
https://mfzzf.github.io/2025/03/16/docker/
作者
Mzzf
发布于
2025年3月16日
许可协议