Files
docker_practice/02_basic_concept/container.md

12 KiB
Raw Blame History

Docker 容器

一句话理解容器

容器是镜像的运行实例。如果把镜像比作程序,那么容器就是进程。

用面向对象编程的术语来说:镜像是类Class容器是对象Instance

  • 一个镜像可以创建多个容器
  • 每个容器相互独立,互不影响
  • 容器可以被创建、启动、停止、删除、暂停

容器的本质

💡 笔者认为,理解这一点是理解 Docker 的关键

容器的本质是一个特殊的进程。

┌─────────────────────────────────────────────────────────────┐
│                     普通进程                                 │
│  • 与其他进程共享系统资源                                     │
│  • 可以看到其他进程                                          │
│  • 共享网络和文件系统                                         │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                     容器进程                                 │
│  ┌───────────────────────────────────────────────────────┐ │
│  │  • 有自己的进程空间(看不到宿主机上的其他进程)           │ │
│  │  • 有自己的网络(独立 IP、端口                         │ │
│  │  • 有自己的文件系统(独立的 root 目录)                  │ │
│  │  • 有自己的用户(容器内的 root ≠ 宿主机的 root         │ │
│  └───────────────────────────────────────────────────────┘ │
│     但仍然运行在宿主机的内核上                                │
└─────────────────────────────────────────────────────────────┘

这种隔离是通过 Linux 内核的 Namespace 技术实现的。

容器 vs 虚拟机:核心区别

很多初学者会混淆容器和虚拟机。笔者用一张图来说明:

虚拟机                              容器
┌───────────────────────┐          ┌───────────────────────┐
│   App A    │   App B  │          │   App A    │   App B  │
├────────────┼──────────┤          ├────────────┼──────────┤
│ Guest OS   │ Guest OS │          │ Container  │ Container│
│ (完整系统) │ (完整系统)│          │  (仅应用)   │  (仅应用) │
├────────────┴──────────┤          └────────────┴──────────┤
│      Hypervisor       │          │     Docker Engine     │
├───────────────────────┤          ├───────────────────────┤
│       Host OS         │          │       Host OS         │
├───────────────────────┤          ├───────────────────────┤
│      Hardware         │          │      Hardware         │
└───────────────────────┘          └───────────────────────┘
每个 VM 运行完整 OS                  所有容器共享宿主机内核
特性 容器 虚拟机
隔离级别 进程级Namespace 硬件级Hypervisor
启动时间 秒级(甚至毫秒) 分钟级
资源占用 MB 级别 GB 级别
性能损耗 几乎为零 5-20%
内核 共享宿主机内核 各自独立内核

容器的存储层

镜像层 + 容器层

当容器运行时Docker 会在镜像的只读层之上创建一个可写层(容器存储层):

┌─────────────────────────────────────────────┐
│       容器存储层(可读写)                    │ ← 容器运行时创建
│  运行时产生的文件变化记录在这里                │
├─────────────────────────────────────────────┤
│       镜像第 N 层(只读)                     │
├─────────────────────────────────────────────┤
│       镜像第 N-1 层(只读)                   │
├─────────────────────────────────────────────┤
│           ...                               │
├─────────────────────────────────────────────┤
│       镜像第 1 层(只读)                     │ ← 基础镜像层
└─────────────────────────────────────────────┘

Copy-on-Write写时复制

当容器需要修改镜像层中的文件时:

  1. Docker 将该文件复制到容器存储层
  2. 在容器层中进行修改
  3. 原始镜像层保持不变
读取文件:直接从镜像层读取(共享,高效)
修改文件:复制到容器层,然后修改(只有这个容器能看到修改)

⚠️ 容器存储层的生命周期

笔者特别强调:这是新手最容易踩的坑!

容器存储层与容器生命周期绑定。容器删除,数据就没了!

# 创建容器,写入数据
$ docker run -it ubuntu bash
root@abc123:/# echo "important data" > /data.txt
root@abc123:/# exit

# 删除容器
$ docker rm abc123

# 数据丢了!没有任何办法恢复!

正确的数据持久化方式

按照 Docker 最佳实践,容器存储层应该保持无状态。需要持久化的数据应该使用:

方式 说明 适用场景
数据卷Volume Docker 管理的存储 数据库、应用数据
绑定挂载Bind Mount 挂载宿主机目录 开发时共享代码
# 使用数据卷(推荐)
$ docker run -v mydata:/var/lib/mysql mysql

# 使用绑定挂载
$ docker run -v /host/path:/container/path nginx

这些位置的读写会跳过容器存储层,直接写入宿主机,性能更好,也不会随容器删除而丢失。

容器的生命周期

                ┌──────────────────────────────────────────────────┐
                │                  容器生命周期                     │
                └──────────────────────────────────────────────────┘

    docker create           docker start           docker stop
         │                       │                      │
         ▼                       ▼                      ▼
    ┌─────────┐            ┌─────────┐            ┌─────────┐
    │ Created │───────────▶│ Running │───────────▶│ Stopped │
    └─────────┘            └─────────┘            └─────────┘
         │                       │                      │
         │                       │ docker pause         │
         │                       ▼                      │
         │                 ┌─────────┐                  │
         │                 │ Paused  │                  │
         │                 └─────────┘                  │
         │                       │                      │
         │        docker rm      │     docker rm        │
         └───────────────────────┴──────────────────────┘
                                 │
                                 ▼
                           ┌──────────┐
                           │ Deleted  │
                           └──────────┘

常用生命周期命令

# 创建并启动容器(最常用)
$ docker run nginx

# 分步操作
$ docker create nginx    # 创建容器(不启动)
$ docker start abc123    # 启动容器

# 停止容器
$ docker stop abc123     # 优雅停止(发送 SIGTERM等待后发送 SIGKILL
$ docker kill abc123     # 强制停止(直接发送 SIGKILL

# 暂停/恢复(不常用,但有时有用)
$ docker pause abc123    # 暂停容器内所有进程
$ docker unpause abc123  # 恢复

# 删除容器
$ docker rm abc123       # 删除已停止的容器
$ docker rm -f abc123    # 强制删除运行中的容器

容器与进程的关系

核心概念:容器的生命周期 = 主进程PID 1的生命周期

# 主进程运行,容器运行
# 主进程退出,容器停止

这就是为什么:

# 这个容器会立即退出bash 没有输入就退出了)
$ docker run ubuntu

# 这个容器会持续运行nginx 作为守护进程持续运行)
$ docker run nginx

详细解释请参考后台运行章节。

容器的隔离性

Docker 容器通过以下 Namespace 实现隔离:

Namespace 隔离内容 效果
PID 进程 ID 容器内 PID 1 是应用进程,看不到宿主机其他进程
NET 网络 独立的网络栈、IP 地址、端口
MNT 文件系统 独立的根目录和挂载点
UTS 主机名 独立的主机名和域名
IPC 进程间通信 独立的信号量、消息队列
USER 用户 独立的用户和组 ID

想深入了解?请阅读底层实现 - 命名空间

本章小结

概念 要点
容器是什么 镜像的运行实例,本质是隔离的进程
容器 vs 虚拟机 共享内核,更轻量,但隔离性较弱
存储层 可写层随容器删除而消失
数据持久化 使用 Volume 或 Bind Mount
生命周期 与主进程PID 1绑定

理解了镜像和容器,接下来让我们学习仓库——存储和分发镜像的服务。

延伸阅读