拒绝删库跑路!上手 Docker 容器数据管理

本文由图雀社区成员 mRc 写做而成,欢迎加入图雀社区,一块儿创做精彩的免费技术教程,予力编程行业发展。html

若是您以为咱们写得还不错,记得 点赞 + 关注 + 评论 三连🥰🥰🥰,鼓励咱们写出更好的教程💪前端

数据是一切应用和服务的核心,特别是目击了一次次“删库跑路”引起的惨剧以后,咱们更能深刻体会到数据存储与备份的重要性。Docker 也为咱们提供了方便且强大的方式去处理容器的数据。在这一篇文章中,咱们将带你经过理论实战的方式掌握 Docker 的两种经常使用的数据管理方式:数据卷(Volume)和绑定挂载(Bind Mount),从而可以游刃有余地处理好数据,为你的应用提供强有力的支撑和保障。git

Docker 数据管理概览

很久不见,欢迎继续阅读“筑梦师系列” Docker 教程,前情回顾:github

  • 《一杯茶的时间,上手 Docker》中,咱们以“工做”和“作梦”来类比“应用开发”和“部署”,并经过一些小实验让你理解 Docker 是如何实现从“作梦”到“筑梦”的跨越的,而且理解了镜像容器两大关键概念,并成功地容器化了第一个应用
  • 《梦境亦相通:用 Network 实现容器互联》中,咱们了解了”梦境“是相通的,不一样的容器能够经过 Docker 网络实现相互之间的通讯

而在这一篇教程中,咱们将带你上手 Docker 数据管理,搭建起”梦境“(容器环境)与”现实“(主机环境)的桥梁。Docker 数据的管理方式主要分为三种:mongodb

  1. 数据卷(Volume),也是最为推荐的一种方式
  2. 绑定挂载(Bind Mount),Docker 早期经常使用的数据管理方式
  3. tmpfs 挂载,基于内存的数据管理,本篇教程不会涉及

注意docker

tmpfs 挂载只适用于 Linux 操做系统。数据库

咱们立刻经过几个小实验来体验一下(已经比较熟悉的同窗能够直接移步下面的”实战演练“环节)。编程

数据卷

基本命令

正如在上一篇中最后“记住几十个 Docker 命令小诀窍”所提到的,数据卷(Volume)也是常见的 Docker 对象类型的一种,所以也支持 create(建立)、inspect (查看详情)、ls (列出全部数据卷)、prune (删除无用数据卷)和 rm(删除)等操做。json

咱们来走一个流程体验一下。首先建立一个数据卷:后端

docker volume create my-volume
复制代码

查看当前全部的数据卷:

docker volume ls
复制代码

输出了刚刚建立的 my-volume 数据卷:

local               my-volume
复制代码

查看 my-volume 数据卷的详细状况:

docker volume inspect my-volume
复制代码

能够看到输出了 JSON 格式的 my-volume 信息:

[
    {
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/my-vol/_data",
        "Name": "my-volume",
        "Options": {},
        "Scope": "local"
    }
]
复制代码

提示

好奇的同窗可能会去查看 /var/lib/docker/volumes 目录下面是否是真的有数据卷,答案是:对于非 Linux 系统而言(Windows 和 Mac 系统),该目录不存在于你的文件系统中,而是存在于 Docker 虚拟机中。

最后删除 my-volume 数据卷:

docker volume rm my-volume
复制代码

单首创建一个数据卷意义不大,毕竟它原本的做用就是为容器的数据管理服务。请看下图(来源 Safari Books Online):

能够看到,数据卷在“主机环境”和“容器环境”之间架起了“一道桥梁”。一般,咱们在容器中将须要存储的数据写入数据卷所挂载的路径(位置),而后就会当即、自动地将这些数据存储到主机对应的区域。

在建立带有数据卷的容器时,一般有两种选择:1)命名卷(Named Volume);2)匿名卷(Anonymous Volume)。接下来咱们就分别详细讲解。

建立命名卷

首先咱们来演示一下如何建立带有命名卷的容器,运行如下命令:

docker run -it -v my-vol:/data --name container1 alpine
复制代码

能够看到,咱们经过 -v (或者 --volume )参数指定了数据卷的配置为 my-vol:/data ,其中(你应该猜到了)my-vol 就是数据卷的名称,/data 就是容器中数据卷的路径。

在进入容器中后,咱们向 /data 目录中添加一个文件后退出:

/ # touch /data/file.txt
/ # exit
复制代码

注意

/ # 是 alpine 镜像默认的命令提示符,后面的 touch /data/file.txt 才是真正要执行的命令哦。

为了验证 /data 中的数据是否真的保存下来,咱们删除 container1 容器,而后再建立一个新的容器 container2 ,查看其中的 /data 目录内容:

docker rm container1
docker run -it -v my-vol:/data --name container2 alpine
/ # ls /data
file.txt
/ # exit
复制代码

能够看到刚刚在 container1 中建立的 file.txt 文件!事实上,这种在容器之间共享数据卷的模式很是常见,Docker 提供了一个方便的参数 --volumes-from 来轻松实现数据卷共享:

docker run -it --volumes-from container2 --name container3 alpine
/ # ls /data
file.txt
复制代码

一样,container3 中也能访问到数据卷中的内容。

建立匿名卷

建立匿名卷的方式就很简单了,以前咱们经过 my-vol:/data 做为 -v 的参数,而建立匿名卷只需省略数据卷名称(my-vol 便可):

docker run -v /data --name container4 alpine
复制代码

咱们经过 inspect 命令来查看一下 container4 的状况:

docker inspect container4
复制代码

咱们能够在其中的 Mounts 字段中看到以下数据:

"Mounts": [
    {
        "Type": "volume",
        "Name": "dfee1d707956e427cc1818a6ee6060699514102e145cde314d4d938ceb12dfd3",
        "Source": "/var/lib/docker/volumes/dfee1d707956e427cc1818a6ee6060699514102e145cde314d4d938ceb12dfd3/_data",
        "Destination": "/data",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": ""
    }
]
复制代码

咱们来分析一下重要的字段:

  • Name 即数据卷的名称,因为是匿名卷,因此 Name 字段就是一串长长的随机数,命名卷则为指定的名称
  • Source 为数据卷在主机文件系统中的存储路径(以前说了,Windows 和 Mac 在 Docker 虚拟机中)
  • Destination 为数据卷在容器中的挂载点
  • RW 指可读写(Read-Write),若是为 false ,则为只读数据卷

在 Dockerfile 中使用数据卷

在 Dockerfile 中使用数据卷很是简单,只需经过 VOLUME 关键词指定数据卷就能够了:

VOLUME /data 
# 或者经过 JSON 数组的方式指定多个数据卷
VOLUME ["/data1", "/data2", "/data3"] 复制代码

有两点须要注意:

  • 只能建立匿名卷
  • 当经过 docker run -v 指定数据卷时,Dockerfile 中的配置会被覆盖

绑定挂载

绑定挂载(Bind Mount)是出现最先的 Docker 数据管理和存储解决方案,它的大体思路和数据卷是一致的,只不过是直接创建本机文件系统容器文件系统之间的映射关系,很是适合简单、灵活地在本机和容器之间传递数据。

咱们能够试着把本身机器的桌面(或者其余路径)挂载到容器中:

docker run -it --rm -v ~/Desktop:/desktop alpine
复制代码

咱们仍是经过 -v 参数来进行配置,~/Desktop 是本机文件系统路径,/desktop 则是容器中的路径,~/Desktop:/desktop 则是将本机路径和容器路径进行绑定,仿佛架起了一道桥梁。这里的 --rm 选项是指在容器中止以后自动删除(关于容器生命周期的更多细节,请参考第一篇文章)。

进入到容器以后,能够试试看 /desktop 下面有没有本身桌面上的东西,而后再在容器中建立一个文件,看看桌面上有没有收到这个文件:

/# ls /desktop
# 我本身桌面上的不少东西 :D
/# touch /desktop/from-container.txt
复制代码

你应该能看到本身的桌面上多了容器中建立的 from-container.txt 文件!

小结

咱们贴出官方文档这张示意图:

能够看到:

  • 数据卷(Volume)是 Docker 在本地文件系统中专门维护了一个区域用于存储容器数据
  • 绑定挂载(Bind Mount)则是创建容器文件系统和本地文件系统的映射
  • tmpfs 则是直接在内存中管理容器数据

在指定数据卷或绑定挂载时,-v 参数的格式为 <first_field>:<second_field>:<rw_options> (注意经过冒号分隔),包括三个字段,分别是:

  • 数据卷名称或者本机路径,可省略(省略的话就是匿名卷)
  • 数据卷在容器内的挂载点(路径),必填
  • 读写选项,默认是可读写,若是指定 ro (Read-only),则为只读

提示

Docker 在 17.06 版本以后引入了 --mount 参数,功能与 -v / --volume 参数几乎一致,经过键值对的方式指定数据卷的配置,更为冗长但也更清晰。这篇文章将详细讲解更为常见和广泛的 -v 参数,--mount 参数的更多使用可参考文档

实战演练

准备工做和目标

好的,终于到了实战演练环节——继续部署咱们以前一直在作的全栈待办事项项目(React 前端 + Express 后端 + MongoDB 数据库)。若是你没有阅读以前的教程,想直接从这一步开始作起,请运行如下命令:

git clone -b volume-start https://github.com/tuture-dev/docker-dream.git
cd docker-dream
复制代码

在以前项目的基础上,咱们打算

  • 存储和备份 Express 服务器输出的日志数据,而不是存储在”朝生暮死“的容器中
  • MongoDB 镜像已经作了数据卷配置,因此咱们只需实践一波怎么备份和恢复数据

为 Express 服务器挂载数据卷

OK,咱们在 server/Dockerfile 中添加 VOLUME 配置,而且指定 LOG_PATH (日志输出路径环境变量,可参考 server/index.js 的源码)为 /var/log/server/access.log,代码以下:

# ...

# 指定工做目录为 /usr/src/app,接下来的命令所有在这个目录下操做
WORKDIR /usr/src/app 
VOLUME /var/log/server 
# ...

# 设置环境变量(服务器的主机 IP 和端口)
ENV MONGO_URI=mongodb://dream-db:27017/todos
ENV HOST=0.0.0.0
ENV PORT=4000
ENV LOG_PATH=/var/log/server/access.log

# ...
复制代码

而后 build 服务器镜像:

docker build -t dream-server server/
复制代码

稍等片刻后,咱们把整个项目开起来:

# 建立网络,便于容器互联
docker network create dream-net

# 启动 MongoDB 容器(dream-db)
docker run --name dream-db --network dream-net -d mongo

# 启动 Express API 容器(dream-api)
docker run -p 4000:4000 --name dream-api --network dream-net -d dream-server

# 构建提供 React 前端页面的 Nginx 服务器
docker build -t dream-client client

# 启动 Nginx 服务器容器(client)
docker run -p 8080:80 --name client -d dream-client
复制代码

经过 docker ps 确保三个容器都已经开启:

访问 localhost:8080,进入到待办事项页面,建立几个事项:

日志数据的备份

以前咱们把日志数据存储到了匿名卷中,因为直接获取数据卷中的数据是比较麻烦的,推荐的作法是经过建立一个新的临时容器,经过共享数据卷的方式来备份数据。听着有点晕?请看下图:

按照如下步骤进行:

第一步,实现 dream-api 容器和数据卷之间的数据共享(已实现)。

第二步,建立临时容器,获取 dream-api 的数据卷。运行如下命令:

docker run -it --rm --volumes-from dream-api -v $(pwd):/backup alpine
复制代码

上面这句命令同时用到了上面讲解的数据卷和绑定挂载:

  • --volumes-from dream-api 用于容器之间共享数据卷,这里咱们获取 dream-api 的数据卷
  • -v $(pwd):/backup 用于创建当前本机文件路径(pwd 命令获取)和临时容器内 /backup 路径的绑定挂载

第三步,进入临时容器以后,咱们把日志数据压缩成 tar 包放到 /backup 目录下,而后退出:

/ # tar cvf /backup/backup.tar /var/log/server/
tar: removing leading '/' from member names
var/log/server/
var/log/server/access.log
/ # exit
复制代码

退出以后,是否是在当前目录看到了日志的备份 backup.tar ?事实上,咱们能够经过一条命令搞定:

docker run -it --rm --volumes-from dream-api -v $(pwd):/backup alpine tar cvf /backup/backup.tar /var/log/server
复制代码

若是你以为上面这条命令难以理解的话,答应我,必定要去仔细看看上一篇文章中的”回忆与升华“-”理解命令:梦境的主旋律“这一部分!

数据库备份与恢复

接下里就是这篇文章的重头戏,各位打起十二分的精神!咱们的应用会不会遭遇删库跑路的危机全看你有没有学会这一节的操做技巧了!

提示

咱们这里使用 MongoDB 自带的备份与恢复命令(mongodumpmongorestore ),其余数据库(例如 MySQL)也有相似的命令,均可以借鉴本文的方式。

备份思路一:临时容器+容器互联

按照以前共享数据卷的思路,咱们也尝试经过一个临时 Mongo 容器来备份数据。示意图以下:

首先,咱们的临时容器得链接上 dream-db 容器,并配置好绑定挂载,命令以下:

docker run -it --rm -v $(pwd):/backup --network dream-net mongo sh
复制代码

和以前备份日志数据相比,咱们要把这个临时容器链接到 dream-net 网络中,它才能访问到 dream-db 的数据进行备份(不熟悉 Docker 网络的同窗可复习前一篇文章)。

第二步,进入到这个临时容器后,运行 mongodump 命令:

/ # mongodump -v --host dream-db:27017 --archive --gzip > /backup/mongo-backup.gz
复制代码

此时,因为绑定挂载,输出到 /backup 的文件将保存到当前目录(pwd)中。退出后,就能够在当前目录下看到 mongo-backup.gz 文件了。

备份思路二:提早作好绑定挂载

前一篇教程的”回忆与升华“部分,咱们轻描淡写地讲解了经过 docker exec 执行 mongodump 命令来作备份,可是当时输出的备份文件仍是停留在容器中,只要容器被删除,备份文件也就消失了。因而一个很天然的想法就出现了:咱们能不能在建立数据库容器的时候就作好绑定挂载,而后经过 mongodump 把数据备份到挂载区域?

事实上,以前在建立数据库容器的时候,运行如下命令:

docker run --name dream-db --network dream-net -v $(pwd):/backup -d mongo
复制代码

而后再经过 docker exec 执行 mongodump 命令:

docker exec dream-db sh -c 'mongodump -v --archive --gzip > /backup/mongo-backup.gz'
复制代码

就能够轻松实现。这里咱们用 sh -c 来执行一整条 Shell 命令(字符串形式),这样避免了重定向符 > 引起的歧义(不理解的话能够把 sh -c 'xxx' 替换成 xxx)。能够看到,mongodump 的命令简单了许多,咱们不再须要指定 --host 参数,由于数据库就在本容器内。

可是有个问题:若是已经建立了数据库,而且没有提早作绑定挂载,这种方法就行不通了!

注意,这不是演习!

有了数据库备份文件,咱们就能够肆无忌惮地来作一波”演习“了。经过如下命令,直接端了目前的数据库和 API 服务器:

docker rm -f --volumes dream-db
docker rm -f dream-api
复制代码

没错,经过 --volumes 开关,咱们不只把 dream-db 容器删了,还顺带把挂载的数据卷所有删除!演习就是要足够逼真才行。这时候再访问 localhost:8080 ,以前的待办数据所有丢失!

开始灾后重建,让咱们再次建立新的 dream-db 容器:

docker run --name dream-db --network dream-net -v $(pwd):/backup -d mongo
复制代码

注意到,咱们经过绑定挂载的方式把当前目录映射到容器的 /backup 目录,这意味着能够在这个新的容器中经过 /backup/mongo-backup.gz 来恢复数据,运行如下命令:

docker exec dream-db sh -c 'mongorestore --archive --gzip < /backup/mongo-backup.gz'
复制代码

咱们应该会看到输出了一些日志,提示咱们数据恢复成功。最后从新开启 API 服务器:

docker run -p 4000:4000 --name dream-api --network dream-net -d dream-server
复制代码

回头访问咱们的待办应用,数据是否是都回来了!?

回忆与升华

另外一种共享数据的方式:docker cp

以前,咱们经过共享数据卷或者绑定挂载的方式来把容器的数据传送到容器以外。事实上,在容器和本机之间还能够经过另外一种方式传递和共享数据:docker cp 命令。没错,若是你用过 cp 命令拷贝文件,它的用法必定不会陌生。例如,咱们将 dream-api 容器内的日志文件拷贝到当前目录下:

docker cp dream-api:/var/log/server/access.log .
复制代码

看!access.log 就有了!固然,咱们还能够”反向操做“一波,把本地的文件拷贝到容器里面去:

docker cp /path/to/some/file dream-api:/dest/path
复制代码

能够看到,docker cp 用起来很是方便,很适合一次性的操做。缺陷也很明显:

  1. 彻底手动的数据管理
  2. 须要知道数据在容器中的具体路径,这对于反复迭代的应用来讲很麻烦
  3. 实现多个容器之间的数据共享比较繁琐

另外一种备份恢复的方式:docker import/export

在备份和恢复数据库时,有一个更加简单粗暴的思路:为何咱们不能直接备份整个容器呢?事实上,Docker 确实为咱们提供了两个命令来搞定整个容器的打包和装载:exportimport

例如,经过如下命令将整个容器的文件系统导出为 tar 包:

docker export my-container > my-container.tar
复制代码

注意

export 命令不会导出容器相关数据卷的内容。

而后能够经过 import 命令建立拥有彻底相同内容的镜像

docker import my-container.tar
复制代码

import 命令会输出一个 SHA256 字符串,就是镜像的 UUID。接着能够用 docker run 命令启动这个镜像(能够指定 SHA256 串,也能够先经过 docker tag 打个标签)。

若是你刚刚尝试了 exportimport 命令,必定会发现一个至关严重的问题:容器打包以后的 tar 包有好几百兆。很显然,简单粗暴地打包容器也包括了不少根本无用的数据(例如操做系统中的其余文件),对硬盘的压力陡然增长。

追本溯源:探寻镜像和容器的本质(UFS)

在学习和实践了数据卷的知识后,咱们还接触了一下 docker cpdocker export/import 命令。至此,咱们不由追问,镜像和容器的本质究竟是什么,其中的数据是怎样存储的?

或者咱们提一个更具体的问题:为何镜像中的数据(例如操做系统中的各类文件)每次建立容器时都会存在,而在建立容器后写入的数据会在容器删除后却丢失?

这背后的一切就是 Docker 赖以生存的 Union File System(UFS)机制。咱们经过一张图(来源:The Docker Ecosystem)来大体感觉一下:

咱们来一点点分析上面这张 UFS 示意图的要点:

  • 整个 UFS 都是由一层层的内容组成的,从底层的操做系统内核(Kernel),到上层的软件(例如 Apache 服务器)
  • UFS 中的每一层可分为只读层(read-only,也就是图中的不透明盒子)和可写层(writable,也就是图中的透明盒子
  • 镜像(例如图中的 add Apache 和 Busybox)由一系列只读层构成
  • 当咱们根据镜像建立容器时,就是在该镜像全部只读层之上加一层可写层,在容器中进行的任何数据的修改都会记录在这个可写层中,而不会影响到底下的只读层
  • 当容器销毁后,在可写层中修改的全部内容将丢失

而咱们这一篇文章所讲解的数据管理技巧(数据卷、绑定挂载),则是彻底绕开了 UFS,让重要的业务数据独立存储,而且可备份、可恢复,而不是陷入在容器的可写层中让整个容器变得臃肿不堪。

再回过头看上面的问题,是否是有思路了?

想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。

本文所涉及的源代码都放在了 Github 上,若是您以为咱们写得还不错,但愿您能给❤️这篇文章点赞+Github仓库加星❤️哦

相关文章
相关标签/搜索