容器存储

容器最初设计为「无状态」的运行环境。但现实中的应用程序总需要存储数据:数据库的文件、用户上传的内容、应用的配置文件。这些数据不能随着容器销毁而消失。

容器存储解决了这个问题——它为容器提供了持久化或临时的数据存储能力。

容器存储模型

核心概念

flowchart LR
    subgraph Container["容器"]
        C["应用进程"]
    end

    subgraph Mount["挂载层"]
        M["Mount Namespace"]
    end

    subgraph Storage["存储层"]
        V["Volume\n(数据卷)"]
    end

    subgraph FS["文件系统"]
        F["OverlayFS\nBtrfs\nDevice Mapper"]
    end

    C --> M --> V --> F

存储流:

  1. 容器内的进程写入挂载点
  2. 挂载层将请求转发到 Volume
  3. Volume 与实际存储后端交互
  4. 数据持久化到文件系统

临时存储 vs 持久存储

类型生命周期数据保留性能适用场景
tmpfs容器运行期间容器终止后丢失最快(内存)临时缓存、敏感数据
bind mount取决于宿主机路径取决于宿主机取决于宿主机配置文件、日志
named volume独立于容器持久化取决于存储类型数据持久化
tmpfs mount容器运行期间容器终止后丢失最快(内存)临时缓存

Docker Volume 命令

基本操作

创建数据卷
docker volume create my-data
docker volume ls
docker volume inspect my-data
使用数据卷启动容器
# 匿名卷
docker run -v /app/data nginx

# 命名卷
docker run -v my-data:/app/data nginx

# 只读卷
docker run -v my-data:/app/data:ro nginx

# tmpfs
docker run --tmpfs /app/tmp nginx
数据卷维护
# 查看数据卷使用情况
docker volume ls

# 删除未使用的卷
docker volume prune

# 删除指定卷
docker volume rm my-data

绑定挂载

绑定宿主机目录
# 绑定宿主机的配置目录
docker run -v /etc/app/config:/app/config:ro nginx

# 绑定单个文件
docker run -v /etc/app/secret.json:/app/secret.json nginx

# 绑定临时目录
docker run -v /tmp:/app/tmp nginx
Warning

绑定挂载的风险:

  1. 容器内的进程可以修改宿主机文件(安全风险)
  2. 宿主机路径必须存在,否则 Docker 会创建(权限可能不对)
  3. 不同平台的路径格式不同(Windows/macOS vs Linux) :::

存储驱动

工作原理

flowchart TB
    subgraph Upper["Upper 层(容器可写)"]
        U1["容器写入的新文件"]
        U2["容器修改的文件副本"]
    end

    subgraph Middle["Middle 层(不存在)"]
        M1["空"]
    end

    subgraph Lower["Lower 层(镜像层)"]
        L1["基础镜像层"]
        L2["中间镜像层"]
        L3["最底层镜像"]
    end

    subgraph Merged["Merged 视图(容器看到的)"]
        R1["合并后的文件系统"]
    end

    U1 & U2 -.->|"优先级最高"| Merged
    M1 -.->|"跳过"| Merged
    L1 & L2 & L3 --> Merged

存储驱动对比

驱动原理性能适用场景限制
overlay2叠加层高生产环境首选需要磁盘配额
aufs叠加层中旧版本 Docker不支持容器内 chown
devicemapper块设备中旧版本 CentOS/RHEL不支持共享存储
btrfsCOW 文件系统高需要 btrfs单文件大小限制
vfs直接复制低测试环境无分层
zfsZFS 文件系统高需要 ZFS内存消耗大
查看当前存储驱动
docker info | grep "Storage Driver"
# Storage Driver: overlay2

选择存储驱动

配置存储驱动(daemon.json)
{
  "storage-driver": "overlay2",
  "storage-opts": [
    "overlay2.size=50G"
  ]
}

:::info 生产环境建议:

  • Linux:使用 overlay2(默认)
  • Windows:使用 windowsfilter
  • 特殊场景:需要考虑特定驱动的功能(如 ACL、快照)

现代 Docker 默认使用 overlay2,不需要额外配置。

数据持久化机制

命名卷

创建和管理命名卷
# 创建卷
docker volume create my-db

# 在容器中使用
docker run -v my-db:/var/lib/postgresql/data postgres:15

# 查看卷详情
docker volume inspect my-db
卷详情示例
{
    "CreatedAt": "2024-01-15T10:00:00Z",
    "Driver": "local",
    "Labels": {},
    "Mountpoint": "/var/lib/docker/volumes/my-db/_data",
    "Name": "my-db",
    "Options": {},
    "Scope": "local"
}

只读挂载

配置只读挂载
# 配置文件只读挂载
docker run -v config:/app/config:ro nginx

# 系统文件只读挂载
docker run --read-only -v /tmp:/tmp nginx

# 组合使用
docker run \
  --read-only \
  -v my-data:/app/data:ro \
  -v /tmp:/tmp \
  nginx

tmpfs 内存存储

使用
docker run --tmpfs /app/sessions:rw,size=100M,mode=1700 nginx

# 或者使用 docker volume
docker volume create --driver=tmpfs session-data
docker run -v session-data:/app/sessions nginx
tmpfs
# tmpfs 的优势:
# - 存储在内存中,速度极快
# - 容器终止后自动清空(适合存储敏感临时数据)
# - 不会写入磁盘(适合安全敏感场景)

# tmpfs 的限制:
# - 受内存大小限制
# - 重启容器后数据丢失

容器存储最佳实践

分离应用层和数据层

Dockerfile
# 避免的做法:
# 将数据存储在镜像层
FROM nginx
COPY ./uploads /usr/share/nginx/html/uploads  # 数据在镜像层

# 正确的做法:
# 数据通过卷挂载
FROM nginx
COPY ./static /usr/share/nginx/html/static    # 静态资源在镜像层
# 动态内容(用户上传)通过 -v 挂载

使用卷插件扩展存储

RexRay
# 安装 RexRay
docker plugin install rexray/ebs \
  EBS_ACCESSKEY=xxx \
  EBS_SECRETKEY=yyy

# 使用 EBS 卷
docker volume create --driver rexray/ebs --name my-ebs-volume
docker run -v my-ebs-volume:/data nginx

数据库存储配置

PostgreSQL
# 创建专用卷
docker volume create pg-data

# 使用命名卷持久化数据
docker run -d \
  --name postgres \
  -v pg-data:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=secret \
  postgres:15

# 确认数据持久化
docker stop postgres
docker rm postgres
docker run -d \
  --name postgres2 \
  -v pg-data:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=secret \
  postgres:15

# 数据完整保留

存储权限

用户权限控制

指定容器用户
# 指定 UID 运行
docker run -u 1000 nginx

# 指定用户组
docker run -u 1000:2000 nginx

# 使用用户名
docker run -u nginx nginx

卷权限问题

常见的权限问题
# 问题:卷的 UID/GID 与容器内不匹配
# 表现:容器无法写入数据

# 解决方案 1:容器内创建用户
docker run -u 0 nginx  # 先用 root 创建目录
mkdir -p /app/data
chown 1000:1000 /app/data

# 解决方案 2:挂载时指定 uid:gid
docker run -v $(pwd)/data:/app/data:rw,U nginx

# 解决方案 3:使用 ACL
setfacl -m u:1000:rw /host/path
Dockerfile
FROM nginx

# 创建应用用户
RUN groupadd -g 1000 app && \
    useradd -u 1000 -g app -s /bin/bash appuser

# 应用用户拥有工作目录
RUN mkdir -p /app/data && \
    chown -R appuser:app /app

USER appuser

常见问题与反模式

问题一:匿名卷堆积

问题场景
# 每次创建容器都会产生匿名卷
docker run -v /data nginx
docker run -v /data nginx
docker run -v /data nginx

# 检查卷
docker volume ls
# DRIVER    VOLUME NAME
# local     a123456...
# local     b789012...
# local     c345678...

问题:忘记使用命名卷,导致匿名卷堆积,难以清理。

正确做法:始终使用命名卷。

正确示例
docker volume create my-data
docker run -v my-data:/data nginx

问题二:挂载覆盖关键目录

问题场景
docker run -v /etc/nginx/nginx.conf:/etc/nginx/nginx.conf nginx

# 问题:如果 /etc/nginx/nginx.conf 不存在
# Docker 会创建一个目录,容器无法启动

正确做法:

  1. 先检查宿主机路径是否存在
  2. 使用 ro 模式挂载只读配置
  3. 考虑使用 docker cp 或进入容器修改
替代方案:使用
# 先启动容器
docker run -d --name nginx nginx

# 复制配置文件
docker cp nginx.conf nginx:/etc/nginx/nginx.conf

# 提交修改
docker commit nginx my-nginx

问题三:混合使用多种存储方式

混乱的配置示例
docker run \
  -v /data:/app/data \           # 绑定挂载
  -v /logs:/var/log \            # 绑定挂载
  -v db-data:/var/lib/mysql \    # 命名卷
  --tmpfs /session \             # tmpfs
  mysql:8

问题:维护困难,难以追踪数据位置。

正确做法:统一存储策略,清晰区分数据用途。

清晰配置示例
# 使用 docker-compose.yml 管理
version: '3.8'
services:
  mysql:
    image: mysql:8
    volumes:
      - mysql-data:/var/lib/mysql
      - ./config:/etc/mysql/conf.d:ro
      - ./logs:/var/log/mysql

volumes:
  mysql-data:

问题四:生产环境使用绑定挂载

生产环境错误配置
# 将宿主机的 /var/data 绑定到容器
docker run -v /var/data:/app/data -p 80:80 myapp

问题:紧耦合宿主机,难以迁移和扩展。

正确做法:使用命名卷或云存储。

生产环境正确配置
# 使用云存储插件
docker plugin install cloudstor:aws

# 创建云卷
docker volume create --driver cloudstor:aws my-cloud-volume

# 使用云卷
docker run -v my-cloud-volume:/app/data myapp

存储监控

查看存储使用

检查卷使用情况
docker system df

# 输出
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          10        3        2.5GB     1.2GB (48%)
Containers      3         2        0B        0B (0%)
Local Volumes   5         3        500MB     200MB (40%)
清理未使用的资源
docker system prune

# 完整清理
docker system prune -a

# 清理卷
docker volume prune

# 清理构建缓存
docker builder prune

权衡矩阵

存储类型性能持久性共享性推荐场景
匿名卷中容器期间无临时测试
命名卷高持久无生产应用数据
绑定挂载高取决于宿主机困难配置文件
tmpfs最高容器期间无敏感临时数据
云存储卷中持久是跨主机共享

延伸思考

容器存储看似简单,但有几个值得深入思考的问题:

  1. 数据备份策略:命名卷不会自动备份,需要外部机制
  2. 存储迁移:跨主机迁移容器时,数据是否可用?
  3. 性能优化:存储是否成为瓶颈?
数据备份策略示例
#!/bin/bash
# 备份命名卷

VOLUME_NAME=$1
BACKUP_DIR=${2:-./backups}
DATE=$(date +%Y%m%d_%H%M%S)

# 创建临时容器用于备份
docker run --rm \
  -v ${VOLUME_NAME}:/data \
  -v ${BACKUP_DIR}:/backup \
  alpine tar czf /backup/${VOLUME_NAME}_${DATE}.tar.gz /data

echo "Backup saved to ${BACKUP_DIR}/${VOLUME_NAME}_${DATE}.tar.gz"

对于需要高性能、大容量或跨主机共享的存储,可以考虑:

  • GlusterFS:分布式文件系统
  • Ceph:统一的软件定义���储
  • NFS:简单共享存储
  • 云厂商存储服务:AWS EBS、GCP PD、Azure Disk

理解容器存储的原理和限制,才能设计出既高效又可靠的应用架构。