一、前言
默认情况下,在Docker容器内创建的所有文件都只能在容器内部使用。容器删除后,数据也跟着删除,虽然通常我们不会删除容器,但是一旦宿主机发生故障,我们重新创建容器恢复服务,那么之前容器创建的文件就会丢失,这会为我们带来不必要的麻烦。另外,由于在容器中的文件对于Docker来说是卸载了“可写层”,性能也会下降,所以我们需要把数据写到宿主机,方便数据的存储、转移,以及容器间的数据共享,提高数据读写性能等等
1、本文主要内容
- 使用Golang提供HTTP服务,将日志写入磁盘,并制作镜像
- 数据卷绑定、文件挂载、tmpfs缓存挂载优点与特性介绍
- 将数据卷(Volume)绑定到容器指定目录,实现容器数据的持久化存储与共享
- 将宿主机文件/目录挂载(bind mounts)到容器指定目录,实现容器数据的持久化存储与共享
- 将宿主机tmpfs缓存挂载到容器指定目录
2、本文环境
环境 | 说明 |
---|---|
Docker | Docker CE 20.10.21 |
Docker Desktop | 4.14 |
Docker Golang镜像 | 1.19.4 |
Golang | 1.19.4 |
CentOS | CentOS 7.X |
Windows | Windows 11 |
curl for Windows | 7.87 |
本文中使用命令如非专门说明,均在Windows cmd中执行,如使用macOS,可以替换为对应命令操作
二、数据卷与挂载
1、数据卷(Volume)
数据卷(Volume)是Docker官方推荐的数据持久化存储方式,也是目前最成熟的Docker持久化存储方案,它具备以下优点/特性
- 数据卷比绑定挂载(Bind mounts)更容易备份或迁移
- 数据卷可以通过 Docker CLI 命令或 Docker API 进行管理
- 数据卷适用于 Linux 和 Windows 容器
- 数据卷支持存储在远端主机上,并支持加密存储
- 数据卷的数据不支持在宿主机上直接查看或管理
- 数据卷可以在宿主机上预先占用空间,以免磁盘被占用导致容器无法正常运行
- 在 Mac 和 Windows 开发环境下,数据卷相比绑定挂载(Bind mounts)有更好的性能
- 数据卷可以用于容器之间共享数据
2、绑定挂载(Bind mounts)
绑定挂载(Bind mounts)是Docker早期提供的数据持久化存储方式,我们可以将宿主机的目录/文件挂载到容器中, 并绑定在容器指定的目录/文件上,它具备以下优点/特性
- 挂载目录/文件非常方便,但文件的备份跟迁移相对麻烦
- 挂载的目录/文件无法通过Docker本身进行管理
- 挂载的目录/文件使用的磁盘空间可能会受其他程序影响
- 挂载的目录/文件可以便捷的在宿主机上进行查看及管理
- 挂载的目录/文件可以用于容器之间共享数据 绑定挂载为直译,我更愿称之为文件挂载
3、缓存挂载(tmpfs mounts)
Docker在Linux上提供了tmpfs(一种基于内存的文件系统)挂载,可以让容器把内容放在宿主机内存中进行读写,它具备以下优点/特性
- 存储在内存中,有较好的读写性能
- 存储在内存中,适合存储一些敏感信息,或者随着容器关闭就丢弃的数据
- 只能被Linux上运行的Docker容器使用(?),且不能用于容器间的数据共享 虽然官方文档上说只能在Linux环境下的Docker上使用,但我基于Docker Desktop 4.14 on Windows测试下来是可以,猜测是Docker官方在某个Docker Desktop版本之后支持了该特性的测试,但并没有更新文档
4、关系说明图
这个图可以帮助我们理解这三种方式,后面我就简称为数据卷绑定、文件挂载、缓存挂载
三、镜像制作
创建镜像制作根目录,例如:d:dockersharedata(Windows),~/docker/sharedata(macOS),后续所有文件都放在该目录中
1、准备测试代码
用golang写的一个简单http server,并将运行日志写入到文件中,新建main.go文件保存以下代码
代码语言:javascript复制package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
)
// 初始化日志配置
func initLog() {
// 创建文件夹
err := os.MkdirAll("/app/logs/sharedata", 0755)
if err != nil {
log.Fatalf("Failed to open log file: %v", err)
return
}
// 打开或者创建一个日志文件
logFile, err := os.OpenFile("/app/logs/sharedata/default.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("Failed to open log file: %v", err)
return
}
// 设置日志输出到文件&终端
multiLog := io.MultiWriter(logFile, os.Stdout)
log.SetOutput(multiLog)
// 设置hostname作为日志前缀
log.SetPrefix(getHostName() " - ")
}
// 获取主机名
func getHostName() string {
hostname, err := os.Hostname()
if err != nil {
log.Fatalf("Failed to open log file: %v", err)
}
return hostname
}
// 接收HTTP请求
func handler(w http.ResponseWriter, r *http.Request) {
log.Println("request:", r.Host, r.URL)
var welcome = r.URL.Path[1:]
if len(welcome) == 0 {
welcome = "World"
}
fmt.Fprintf(w, "Hello, %s! ---ken.io", welcome)
}
func main() {
initLog()
http.HandleFunc("/", handler)
log.Println("starting server on port 8000")
log.Fatal(http.ListenAndServe(":8000", nil))
}
2、编写Dockerfile文件
新建Dockerfile文件,保存以下指令
代码语言:javascript复制# 使用官方提供的 Go 镜像作为基础镜像
FROM golang:1.19.4
# 将工作目录设置为 /app
WORKDIR /app
# 将当前目录下的所有内容复制到 /app 下
COPY . /app
# 创建日志文件夹
RUN mkdir -m 777 logs
# 允许宿主机访问容器的 8000 端口
EXPOSE 8000
# 设置容器进程为:go run main.go
CMD go run main.go
3、编译镜像文件
代码语言:javascript复制#进入目录
d: && cd d:dockersharedata
#编译指定版本镜像(注意结尾一定要加.)
docker build -t sharedata:1.0 .
#查看本地镜像
docker images
#镜像列表
REPOSITORY TAG IMAGE ID CREATED SIZE
sharedata 1.0 edbb8e808d63 19 seconds ago 992MB
4、启动容器
代码语言:javascript复制docker run -d --name sdtest -p 8000:8000 sharedata:1.0
容器启动成功后,我们curl命令或者浏览器访问下http://localhost:8000,看到以下内容表示运行成功
代码语言:javascript复制Hello, World! ---ken.io
5、查看日志
进入已启动的容器sdtest,查看我们在代码中写入的日志情况
代码语言:javascript复制# 进入已启动的容器sdtest
docker exec -it sdtest /bin/bash
# 查看目录中的文件
ls
# 这里可以看到在Dockerfile中创建的logs文件夹
# Dockerfile logs main.go
# 进入logs/sharedata目录,然后通过cat命令查看日志信息
cd logs/sharedata/
cat default.log
# 输出内容示例
6b3e4d12e2b6 - 2022/12/22 22:19:58 starting server on port 8000
6b3e4d12e2b6 - 2022/12/22 22:20:31 request: localhost:8000 /
四、数据卷绑定
1、创建数据卷
代码语言:javascript复制# 创建数据卷
docker volume create voltest
# 查看所有数据卷
docker volume ls
# 查看指定数据卷属性
docker volume inspect voltest
# 示例
[
{
"CreatedAt": "2022-12-22T12:11:54Z",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/voltest/_data",
"Name": "voltest",
"Options": {},
"Scope": "local"
}
]
2、创建容器并绑定数据卷
这里我们基于sharedata镜像创建2-4个容器,将/app/logs目录映射到数据卷voltest,这样在容器中对该目录的数据读写,实际上都是读写的数据卷:voltest
代码语言:javascript复制# 使用-v参数(简洁)
docker run -d --name sd01 -v voltest:/app/logs -p 8001:8000 sharedata:1.0
docker run -d --name sd02 -v voltest:/app/logs -p 8002:8000 sharedata:1.0
# 使用--mount参数(可指定更详细参数)
docker run -d --name sd03 --mount source=voltest,target=/app/logs -p 8003:8000 sharedata:1.0
docker run -d --name sd04 --mount source=voltest,target=/app/logs -p 8004:8000 sharedata:1.0
3、查看日志
进入已启动的容器sd01(01-04任选一个即可),查看我们在代码中写入的日志情况
代码语言:javascript复制# 进入已启动的容器sd01
docker exec -it sd01 /bin/bash
# 查看日志
root@b7f5df537c57:/app cat /app/logs/sharedata/default.log
# 输出内容示例
b7f5df537c57 - 2022/12/22 22:55:11 starting server on port 8000
42d90decf7a9 - 2022/12/22 22:55:17 starting server on port 8000
0a85e6bd4e32 - 2022/12/22 22:55:24 starting server on port 8000
bd99a6c2fcd7 - 2022/12/22 22:55:30 starting server on port 8000
从日志中可以看出,在其中1个容器中,看到了4个容器中mian.go的启动日志
此时我们可以分别访问下4个容器中HTTP Server
代码语言:javascript复制# 新开一个终端窗口,访问以下日志
# 如果没有curl,也可以通过浏览器访问
curl http://localhost:8001
curl http://localhost:8002
curl http://localhost:8003
curl http://localhost:8004
然后我们再看下日志
代码语言:javascript复制# 查看日志
root@b7f5df537c57:/app cat /app/logs/sharedata/default.log
# 输出内容示例
b7f5df537c57 - 2022/12/22 22:55:11 starting server on port 8000
42d90decf7a9 - 2022/12/22 22:55:17 starting server on port 8000
0a85e6bd4e32 - 2022/12/22 22:55:24 starting server on port 8000
bd99a6c2fcd7 - 2022/12/22 22:55:30 starting server on port 8000
b7f5df537c57 - 2022/12/22 22:08:01 request: localhost:8001 /
42d90decf7a9 - 2022/12/22 22:08:01 request: localhost:8002 /
0a85e6bd4e32 - 2022/12/22 22:08:01 request: localhost:8003 /
bd99a6c2fcd7 - 2022/12/22 22:08:01 request: localhost:8004 /
五、文件挂载
1、创建容器并挂载目录
首先要在宿主机上创建目录 d:dockerlogs(Windows),~/docker/logs(macOS)
这里我们基于sharedata镜像创建2-4个容器,并将宿主机目录挂载到容器的/app/logs目录,这样在容器中对/app/logs目录的数据读写,实际上都是读写宿主机目录的读写
另外,我们是在Windows上,我们可以指定Windows上的路径格式,也可以保持风格统一,把目录转换为:/d/docker/logs
代码语言:javascript复制# 使用-v参数(简洁)
docker run -d --name sd05 -v /d/docker/logs:/app/logs -p 8005:8000 sharedata:1.0
docker run -d --name sd06 -v /d/docker/logs:/app/logs -p 8006:8000 sharedata:1.0
# 使用--mount参数(可指定更详细参数)
docker run -d --name sd07 --mount type=bind,source=/d/docker/logs,target=/app/logs -p 8007:8000 sharedata:1.0
docker run -d --name sd08 --mount type=bind,source=/d/docker/logs,target=/app/logs -p 8008:8000 sharedata:1.0
如果执行命令时弹窗提示需要授权,记得确认下即可
另外,如果你不想挂载目录,你也可以选择挂载文件,例如:
代码语言:javascript复制docker run -d --name sd09 -v /d/docker/logs/log.txt:/app/logs/sharedata/default.log -p 8009:8000 sharedata:1.0
2、查看日志
查看宿主机上日志的写入情况
代码语言:javascript复制# 进入日志目录并查看所有文件
d: && cd d:dockerlogs && dir
# 查看日志
type sharedatadefault.log
# 输出内容示例
669133c17e2d - 2022/12/22 23:13:39 starting server on port 8000
b54d1857a6f5 - 2022/12/22 23:13:45 starting server on port 8000
4045ec36d051 - 2022/12/22 23:13:52 starting server on port 8000
9e1b80edb8fa - 2022/12/22 23:13:58 starting server on port 8000
从日志中可以看出,在其中1个容器中,看到了4个容器中mian.go的启动日志
访问HTTP Server之后的日志,可以参考上一章节
六、缓存挂载
tmpfs是Linux上的缓存系统,为了更接近生产环境的使用,这里使用CentOS虚拟机进行测试,镜像制作等准备工作略去。不过,如果你电脑没有额外资源来运行虚拟机进行测试了,那么你基于Docker Desktop环境测试也可以
1、创建容器并挂载缓存
代码语言:javascript复制# 使用--tmpfs参数(简洁)
docker run -d --name sd10 --tmpfs /app/logs -p 8010:8000 sharedata:1.0
# 使用--mount参数(可指定更详细参数)
docker run -d --name sd11 --mount target=/app/logs -p 8011:8000 sharedata:1.0
2、查看日志
这里我们先访问下2个容器中HTTP Server
代码语言:javascript复制# 新开一个终端窗口,访问以下日志
# 如果没有curl,也可以通过浏览器访问
curl http://localhost:8010
curl http://localhost:8011
curl http://localhost:8010/ken
curl http://localhost:8011/ken
然后选择sd10进行查看日志
代码语言:javascript复制# 进入已启动的容器sd10
docker exec -it sd10 /bin/bash
# 查看日志
root@345bfa4c4cbe:/app cat /app/logs/sharedata/default.log
# 输出内容示例
345bfa4c4cbe - 2022/12/23 21:03:40 starting server on port 8000
345bfa4c4cbe - 2022/12/23 21:03:55 request: localhost:8010 /
345bfa4c4cbe - 2022/12/23 21:03:56 request: localhost:8010 /ken
通过日志也可以看出,两个容器的日志内容并没有互通,我们把容器重启一下,再进入容器查看,会发现只有新的一条启动日志了
代码语言:javascript复制# 重启容器
docker restart sd10
# 进入已启动的容器sd10
docker exec -it sd10 /bin/bash
# 查看日志
root@345bfa4c4cbe:/app cat /app/logs/sharedata/default.log
# 输出内容示例
345bfa4c4cbe - 2022/12/23 21:08:11 starting server on port 8000
七、备注
一、参数说明
Docker提供-v(—volume)、—mount、—tmpfs 三个参数,可以在创建容器的时候绑定数据卷/挂载文件/挂载缓存,其中—tmpfs只用于挂载缓存,非常简单,只有上述一种用法,这里只介绍-v和—mount
-v参数说明
-v命令相对简单,是通过:连接的三段式参数来指定源以及目标
参数格式 | 示例 | 说明 |
---|---|---|
{source}:{target} | -v voltest:/app/logs | {source}可以是数据卷也可以是文件/目录 |
—mount参数说明
—mount命令要比-v命令更强大,它主要有三个参数:type、source、target用来指定类型、源、目标,但也可以指定其他参数来调整对源的读取权限等等
参数 | 说明 |
---|---|
type | 挂载类型:bind、volume、 tmpfs,分别对应目录、数据卷、tmpfs缓存 |
source | 挂载源,可以是:宿主机目录/文件绝对路径、数据卷名称,type=tmpfs时无需指定source |
target | 挂载目标:容器目录/文件的绝对路径 |
destination | 作用相当于target |
readonly | 只读模式,不可修改源 |
bind-propagation | 传播模式,挂载文件/目录时使用,默认是rprivate,也可指定为:private, rshared, shared, rslave, slave,只支持在服务器环境下使用,不支持Docker Desktop |
tmpfs-size | tmpfs缓存大小,单位:字节,默认无限制;type=tmpfs时生效 |
tmpfs-mode | tmpfs缓存权限,例如:700或0770,默认为1777;type=tmpfs时生效 |
二、本文参考
- https://docs.docker.com/storage/volumes/
- https://docs.docker.com/storage/bind-mounts/
- https://docs.docker.com/storage/tmpfs/
- 系列名称:Docker入门教程
- 上一篇:Docker入门:使用Dockerfile构建Docker镜像
- 下一篇:Docker入门:端口映射与容器互联