总结下docker中数据的存储管理机制
最近有小伙伴问到docker中如何持久化数据,故通过一篇文章总结下docker中数据的存储管理机制。
1 docker 数据的存储管理机制概述
按照 docker 容器运行时读写的数据是否支持持久化,docker 存储和管理的数据可以分为两大类:
- 非持久化数据 non-persistence data, 所谓的非持久化数据,其生命周期和容器的生命周期相同,需要通过 storage driver 存储驱动器来管理,具体又包含了容器镜像的多个只读层(image r/o layers),以及容器的可读写数据层(container r/w layer);docker 通过可插拔机制支持多种 storage driver 存储驱动器,常见的包括overlay2/fuse-overlayfs/vfs等多种类型;
- 持久化数据 persistence data:所谓的持久化数据,其生命周期独立于容器的生命周期,通过 mount 将宿主机上可见的文件系统(如local本地文件系统,nfs网络文件系统等等),挂载到容器的文件系统中,具体来讲又包括 volume mounts 卷挂载,bind mounts 绑定挂载 和 tmpfs mounts 临时挂载三种类型;
2 docker的非持久化数据管理机制-storage driver 存储驱动
启动一个docker容器后,容器底层包含一个 union filesystem 联合文件系统,该文件系统底层又包含多个只读的镜像层(image r/o layers)和最上层的一个可读写数据层(container r/w layer),Docker使用storage driver 存储驱动器来管理只读镜像层和容器可写层中的数据:
- 所有不需要修改的文件都只存在于下层的镜像只读层中;
- 只有需要修改的文件,在修改时才会被拷贝到上层的容器读写层中,并在容器读写层中进行修改操作(也就是copy-on-write,写时复制机制);
- 容器停止后,所有只读镜像层和容器读写层的数据都会丢失(当然镜像只读层中的数据在镜像仓库的镜像文件中还有备份),所以容器可写层只适合存储在运行时生成的临时数据;
- 如果需要保存这些 union filesystem 中容器读写层的数据,只能通过 docker commit 生成一个新的镜像,并将镜像报错到镜像仓库中;
- storage driver 在存储空间的利用效率方面进行了优化(多个容器可以公用相同的镜像只读层),但是由于修改数据时需要拷贝到上层的容器读写层,所以对于write-heavy的写入密集型应用程序,性能会受到影响,(比如数据库存储,只读层中预先存在大量数据,运行时又需要更新大量数据); 对 docker storage driver 概括如下:
- docker storage driver 负责不同 union filesystem中layer之间的交互,它允许在容器的读写层创建数据,单读写层数据不会被持久化,且读写效率较低;
- 容器镜像的layer是只读的,当创建一个容器时,会新增一个读写层,称为”container layer“,对容器的所有修改都在该layer上进行;当容器删除后,该读写层也会被删除;
- 不同的storage driver实现不同,但所有的storage driver都使用了CoW(copy-on-write)策略:CoW技术可以让所有的容器共享image的文件系统,所有数据都从image中读取,只有当要对文件进行写操作时,才从image里把要写的文件复制到自己的文件系统进行修改。所以无论有多少个容器共享同一个image,所做的写操作都是对从image中复制到自己的文件系统中的复本上进行,并不会修改image的源文件,且多个容器操作同一个文件,会在每个容器的文件系统里生成一个复本,每个容器修改的都是自己的复本,相互隔离,相互不影响。使用CoW可以有效的提高磁盘的利用率;
可以使用命令 docker ps -s 查看容器大小,其中:
- size 字段表示容器读写层的大小:the amount of data (on disk) that is used for the writable layer of each container.
- virtual size表示镜像只读层和容器读写层的总大小: the amount of data used for the read-only image data used by the container plus the container’s writable layer size.
- 多个容器可以共享镜像只读层:Multiple containers may share some or all read-only image data. Two containers started from the same image share 100% of the read-only data, while two containers with different images which have layers in common share those common layers. Therefore, you can’t just total the virtual sizes. This over-estimates the total disk usage by a potentially non-trivial amount.
最后概括下,默认情况下,容器内部创建的所有文件都存储在容器的可读写数据层上(container r/w layer),当然这也意味着:
- 数据是非持久化的:容器读写层(container r/w layer)与容器的生命周期相同,当容器不再存在时,这些数据会丢失;
- 数据与容器宿主机绑定:由于容器读写层(container r/w layer)与运行容器的主机紧密耦合,所以无法轻松地将数据移动到其他主机其它位置;
- 需要 storage driver 存储驱动的支持:向容器的可读写数据层(container r/w layer)写入数据需要storage driver 存储驱动程序的支持,storage driver 通过Linux内核提供了union filesystem联合文件系统,与直接写入主机文件系统的数据卷相比,这种额外的抽象会降低性能;
- 所以如果需要持久化保存数据,需要使用 volume/bind mounts 等持久化机制。
3 docker的持久化数据管理机制概述
- Docker 有两种数据持久化机制,可以将文件存储在宿主机上,以独立于容器的生命周期,即卷(volumes)和绑定挂载(bind mounts);
- 另外,Docker还支持通过 tmpfs 将文件存储在宿主机内存中,其本质并没有持久化数据;
- 无论选择使用哪种挂载类型,从容器内部来看是相同的:数据都是以容器文件系统中的目录或单个文件的形式暴露出来的。
4. docker的持久化数据管理机制-bind mounts
Bind mounts 是 docker 从早期就开始支持的持久化机制,其功能比 VOLUMES 数据持久化机制要少:
- bind mounts 绑定挂载是一种灵活的存储方式,其将主机文件系统中的文件或目录直接挂载到容器中,且主机和容器之间的更改是实时同步的,这意味着容器可以访问主机上的文件,并且对文件的更改会立即反映在容器中;
- bind mounts 绑定挂载的优势,概括起来是对主机文件系统的直接访问,和对容器和主机之间数据更改的实时同步;
- bind mounts 绑定挂载可以存储在主机系统的任何位置,甚至可以是重要的系统文件或目录,且Docker宿主机或Docker容器上的非Docker进程可以随时修改它们;
- Bind mounts 在宿主机上的文件或目录事先可以不存在,此时在创建容器执行绑定操作时会自动在宿主机上创建对应的文件或目录;
- bind mounts 挂载时可以通过参数 ro 指定为以只读方式进行挂载;
- 使用 bind mounts 时,通过绝对路径引用宿主即中的文件或目录,通过绝对路径引用容器中的文件或目录;
- Bind mounts 性能很好,但其依赖宿主机的文件系统拥有对应的目录结构,且不能通过 docker cli 命令直接管理;
- 目前docker官方推荐使用 volume 替代 bind mounts;
- 一些开源组件借助于docker容器进行编译打包或作业运行时,常常使用 bind mounts挂载宿主机的指定路径,以进行数据的持久化,比如 mpp 数据库 apache doris:
- bind mouts 使用实例如下:
## docker run -v 简单示例: docker run -v /host/path:/container/path myimage ## 官网 docker run -v 且指定只读挂载示例: docker run -d \ -it \ --name devtest \ -v "$(pwd)"/target:/app:ro \ nginx:latest ## 官网 docker run --mount 且指定只读挂载示例: docker run -d \ -it \ --name devtest \ --mount type=bind,source="$(pwd)"/target,target=/app,readonly \ nginx:latest ## 官网 docker compose 挂载示例: version: "3.9" services: frontend: image: node:lts volumes: - type: bind source: ./static target: /opt/app/static volumes: myapp:
5 docker的持久化数据管理机制-volume mounts
Volumes 是 docker 社区推荐的数据持久化机制,它由 docker 容器实施全面管理,而不依赖宿主机文件系统的目录结构:
- Volumes是 Docker中一种持久化数据的机制,卷独立于容器的生命周期(通过使用卷,可以将数据从容器中分离出来,以便容器的更换或销毁不会导致数据丢失),提供了高效的IO,且不会增大容器读写层的大小,借助 volumes 很容易在不同容器之间共享数据,所以可以用来存储写入密集型作业的数据,以及需要在容器之间共享的数据;
- 卷可以在主机的文件系统上创建,也可以在网络存储服务NAS,或云对象存储如S3上创建(需要依赖volume driver);
- 概括起来,使用卷的优点是:数据的持久化存储,容器之间数据共享,卷的生命周期独立于容器;
- volumes 存储在Docker宿主机文件系统的特定目录下,在Linux上该目录是 /var/lib/docker/volumes/,该目录由 Docker实施全面管理的,宿主机上其它非Docker进程不应修改这些目录(named volumes 和 anonymous volumes 在宿主机上对应的存储目录分别以volume名字和随机名字命名);
- 不同于 bind mount, 我们可以在容器之外提前创建和管理 volumes;如果没有提前创建和管理volume,而在启动容器时引用了该事前不存在的volume,则 Docker 会自动创建该 volume(在linux上是存储在/var/lib/docker/volumes/目录下);
- volumes 可以被多个容器同时挂载,其中有些容器可以以 read-write 方式挂载,有些容器可以以 read-only 方式挂载(readonly/ro);
- 可以通过如下方式将特定数据提前灌入到某volue中(pre-Populate a volume using a container): 首先启动一个容器,该容器创建一个volume 并将该volume挂载到容器的特定目录比如 /app/, 而该容器镜像中该目录下有事先准备好的数据,此后挂载启动其它容器并挂载该volume时,其它容器就可以访问这些提前灌入的数据;
- 最后说下volume driver:可以借助 volume driver,基于 nfs或amazon s3协议,实现在不同宿主机的docker容器中共享访问相同的数据。Volume drivers 在应用程序和底层存储系统之间提供了一层抽象,可以在不修改应用代码的情况下,通过切换 volume driver,轻松实现底层存储的切换。
相比 bind mounts,volume 优势如下:
- Volumes 比 bind mounts 更容易备份和迁移;
- 可以使用 docker cli 命令或api 管理 volumes;
- Volumes 适用与所有 Linux 和 Windows 容器;
- Volumes 可以更安全地在多个容器之间共享;
- Volume drivers:通过 Volume drivers,可以在docker容器宿主机之外的远程主机或云存储中存储volume数据,且可以加密volumes数据;
- volumes 的数据内容可以由其它容器事先生成;
- 在 mac 和 windows系统上的 Docker Desktop 中,Volumes 比 bind mounts 性能高很多;
- 另外强调下,k8s也有volume卷的概念:Docker的卷是容器级别的持久存储,由Docker引擎管理;而Kubernetes的卷是Pod级别的持久存储,由Kubernetes集群管理器管理,Kubernetes提供了更丰富的卷类型(Kubernetes支持各种类型的卷,包括持久卷Persistent Volume、空白卷EmptyDir、主机路径卷HostPath等)和存储插件选项(Kubernetes通过持久卷Persistent Volume和持久卷声明Persistent Volume Claim的机制,支持使用外部存储系统如NFS、iSCSI、Ceph等来提供持久化存储),适用于更复杂的容器编排和存储需求;
## 不同于 bind mount,我们可以在容器生命周期之外管理volumes: ## 创建/查看卷 docker volume create myvolume docker volume create my-vol docker volume ls docker volume inspect my-vol ## 将卷挂载到容器中 docker run -v myvolume:/path/in/container myimage ## 也可以在启动容器时直接指定一个不存在的卷,此时会自动创建对应的卷-If you start a container with a volume that doesn’t yet exist, Docker creates the volume for you: ##使用-v/--volume指定只读卷 docker run -d \ --name=nginxtest \ -v nginx-vol:/usr/share/nginx/html:ro \ nginx:latest ##使用--mount指定只读卷 docker run -d \ --name=nginxtest \ --mount source=nginx-vol,destination=/usr/share/nginx/html,readonly \ nginx:latest ## docker servie中直接使用卷: docker service create -d \ --replicas=4 \ --name devtest-service \ --mount source=myvol2,target=/app \ nginx:latest ## docker compose中使用一个不存在的卷,首次执行会自动创建对应的卷,后续会复用该卷- Running docker compose up for the first time creates a volume. Docker reuses the same volume when you run the command subsequently. services: frontend: image: node:lts volumes: - myapp:/home/node/app volumes: myapp: ## docker compose中使用一个已经创建好的卷-You can create a volume directly outside of Compose using docker volume create and then reference it inside docker-compose.yml as follows: services: frontend: image: node:lts volumes: - myapp:/home/node/app volumes: myapp: external: true
6. docker的持久化数据-tmpfs mounts
Volumes 和 bind mounts 在宿主机和容器之间基于宿主机的文件系统共享数据,从而提供了独立于容器生命周期的数据持久化的机制;而 tmpfs 在宿主机和容器之间基于宿主机的内存共享数据,由于内存的非持久性,所以虽然tmpfs使用的不是容器读写层,其本质仍是非持久性存储:
- Tmpfs是一种基于宿主机内存的文件系统,可以将其挂载到容器的指定路径上,当容器停止时,Tmpfs挂载的数据将丢失;
- Tmpfs 适用于快速的临时存储,或者临时存储敏感信息(敏感信息不适合存储到宿主机上或容器读写层);
- Tmpfs的优点是基于内存的快速临时存储,但不能用来在容器之间共享数据,且只适用于linux上的容器;
- 在Docker命令通过--tmpfs 或 --mount type=tmpfs 来创建和使用临时挂载;
## docker run 使用 --tmpfs: docker run --tmpfs /path/in/container myimage docker run -d \ -it \ --name tmptest \ --tmpfs /app \ nginx:latest ## docker run 使用 --mount type=tmpfs: docker run -d \ -it \ --name tmptest \ --mount type=tmpfs,destination=/app,tmpfs-mode=1770 \ nginx:latest
7 Docker各种数据管理方案的对比和总结
这些方案可以根据实际需求选择合适的数据管理方式:
- Volumes适用于跨容器共享和持久化数据;
- Bind Mounts适用于直接访问宿主机文件系统和灵活性要求较高的场景;
- Tmpfs Mounts适用于临时数据存储和读写速度要求较高的场景;
- Named Pipes适用于容器内部进程间通信的需求;
- 在选择Storage Drivers时,需要考虑性能和功能的差异,并根据具体情况做出选择。
方案 | 描述 | 优点 | 缺点 |
Volumes | Docker创建的持久化存储卷,用于在容器之间共享和持久化数据 | 支持跨主机访问数据,管理方便,性能较好 | 数据存储在宿主机上,需要额外空间 |
Bind Mounts | 将宿主机文件或目录直接挂载到容器中 | 可以直接访问宿主机文件系统,灵活性高 | 需要手动管理权限和文件路径 |
Tmpfs Mounts | 将tmpfs挂载到容器中,用于存储临时数据 | 数据存储在内存中,读写速度快,安全性高 | 数据不持久,容器重启后数据丢失 |
Named Pipes | 用于容器内部和容器外部进程之间的通信 | 实时性好,可以实现进程间通信 | 只适用于容器内部进程间通信 |
Storage Drivers | Docker使用的底层存储引擎,用于管理镜像和容器的数据 | 提供了不同的存储选项,可根据需求选择 | 不同存储驱动的性能和功能各不相同 |