我们在上一章节成功的从官网中下载并正确启动了一个Tomcat容器。假设现在Tomcat容器中正在运行这一些服务,那么产生的数据该何去何从呢?在这一张,我们将探讨如何将Docker容器内产生的数据持久化。
cue:计算机的内存也不会存放数据。当计算机关闭电源时,内存中的内容将消失。因此如果要将某个计算结果长久地保存下来,就应当将它写入到文件中,并将这个文件保存到硬盘上。
我们之前掌握了如何将容器的文件拷贝到宿主机*上:
$ docker cp ${containerID}:${sourcePath} ${localPath}
这里的宿主机指带Docker所依赖的虚拟机环境(后文同。实际操作的机器笔者使用物理机来称呼),因为笔者是在虚拟机中运行的CentOS环境。在实际环境中,这个宿主机可能还指代你租赁或者搭建的云服务器。
的确!使用crond定时脚本或许可以解决这个问题,但它一定不是最佳方法。
Docker的理念
尽管Docker强调每个容器之间的运行环境是封闭且独立的,但是Docker希望容器之间的数据是可以被共享的,或者说,容器内的数据不会丢失。
一般来说,Docker容器产生的数据,如果不通过Docker commit生成新的镜像,让数据伴随着镜像保存下来,假使我们删除了这个容器,那么数据就一同消失了。而避免这个现象的办法只有一个,就是不断地通过commit把数据也封装到新的images上,或者写一个crond脚本不断地从容器内部拷贝数据。
因此我们使用数据卷来解决这个问题。数据卷允许宿主机和虚拟容器共享某个文件夹。虚拟容器所产生的数据放到这个共享文件夹当中,当数据需要迁移时,我们只需要直接将这个共享文件夹拷贝到远端即可。
类比Redis的数据持久化
Redis是一个运行在内存的NoSQL数据库,我们通常拿它用作消息队列或者消息缓冲用。那为什么Redis既然运行在内存中,它怎么做到每次电脑重新启动并加载时都能找到之前的数据呢?
它会将这些信息写入到rdb和aof文件当中。(在这里不用关心它们是做什么的,总之Redis是通过文件保存的数据)Docker容器卷的作用与它们类似,其作用主要有二:
1、 容器的持久化
2、 容器间继承+共享数据
Docker容器卷的特点
1、 数据卷可以在容器间共享。
2、 卷中的更改直接作用到数据卷上。
3、 数据卷中的更改不会包含到镜像的更新中。
4、 数据卷的声明周期持续到没有容器使用它为止。
添加数据卷的三种方式
v命令添加(可读写权限)
在运行一个镜像的之后,通过-v(Volume)参数添加数据卷,这个数据实质上是保存在宿主机当中。
$ docker run -it -v ${localPath}:${volumePath} ${imageID}
如果宿主机中没有${localPath}
文件夹,则Docker会自动根据该路径创建一个文件夹,${volumePath}
同理。
我们再次根据centos
的镜像创建一个新的容器,并且希望添加一个容器卷,使得容器内的/dataVolume
和宿主机根目录下的/dktemp
文件夹相连通:
$ docker run -it -v /dktemp:/dataVolume --centos7inst centos
我们使用inspect
命令,以json形式输出该容器的内部信息,可以发现有如下内容:
"Binds": ["/dktemp:/dataVolume"]
从这段描述当中我们就知道,这两个容器已经被绑定在一起了。我们另起一个终端,在主机的/dktemp
下新建任意一个文件:
$ cd /dktemp
$ touch hello.txt
然后进入到容器内的对应文件夹下使用查看命令,就可以发现hello.txt
的存在了:
$ cd /dataVolume
$ ll
另外,即便是容器被停止的期间,如果宿主机向/dktemp
存放了新的文件,在这个容器重新启动的时候能够查看到它。
v命令添加(只读)
有时我们数据卷内的数据会被保护起来,只希望宿主机可以对此自由读写,而容器内只有读权限。
$ docker run -it -v ${localPath}:${volumePath}:ro ${imageID}
我们需要在容器路径的后面添加:ro
表示容器内是read-only
的。注意,这不是创建了一个../ro
文件夹。当以这种形式启动容器时,容器只能通过读方式获取数据卷内的信息,但无法做出更改。
我们使用inspect
命令检查容器配置,可以看到有如下一项:
"Mounts": [
{
"Type": "bind",
"Source": "/dktemp",
"Destination": "/dataVolume",
"Mode": "ro",
"RW": false,
"Propagation": "rprivate"
}
]
注意到"RW"
一项已经被标注了false
。
DockerFile添加
DockerFile是Docker中一个重要的组成部分。我们在时候去专门介绍DockerFile的具体内容。
在此之间简单了解DockerFile。我们都知道所有的容器都依赖image
创建生成。那谁来负责对image
文件本身进行描述的呢?正是DockerFile。
在这一小节,我们通过编写DockerFile来实现为一个镜像绑定一个或多个数据卷。它和v命令添加的方式有什么区别呢?
我们刚才说过,通过v命令想要为添加数据卷,要同时绑定${localPath}
还有{imagePath}
。但是,我们一般不这么做。原因是出于可移植和可分享的考虑,并不保证所有宿主机中的${localPath}
路径都是一致的。
我们自定义一个工作目录,并touch一个DockerFile
文件。
#use local image 'centos'
FROM centos
#set volume
VOLUME ["/dataVolumeContainer1","/dataVolumeContainer2"]
CMD echo "finished"
CMD /bin/bash
我们在VOLUME
行当中,为centos
镜像绑定了2个新的数据卷(这是指定了容器内部的路径。那对应宿主机的目录哪去了?我们等会再去讨论)。保存退出。随后通过build
命令让Docker根据这个DockerFile
文件生成新的image
镜像:
$ docker build -f ./dockerFile -t ljh/centos .
其中./dockerFile
为笔者DockerFile的文件名,ljh/centos
为笔者自定义的镜像名称,注意这个命令后面有个.
。
当运行这个命令时,屏幕打印出了如下信息(imageID会有区别):
Step 1/4 : FROM centos
---> 470671670cac
Step 2/4 : VOLUME ["/dataVolumeContainer1","/dataVolumeContainer2"]
---> Running in 48817b55708b
Removing intermediate container 48817b55708b
---> 975e445781db
Step 3/4 : CMD echo "finished"
---> Running in ee08adab0f3d
Removing intermediate container ee08adab0f3d
---> 133c078c0b71
Step 4/4 : CMD /bin/bash
---> Running in 18183f9d3ea3
Removing intermediate container 18183f9d3ea3
---> ff471674859b
由此我们也可以推断Docker构建image
文件的方式:基于联合文件系统UnionFS一层一层铺开。
我们使用docker images
命令,就可以看到基于centos
镜像生成的ljh/centos
新镜像。我们不妨直接基于这个镜像创建一个新的容器,然后观察内部是否有刚才的DockerFile所配置的两个文件夹:
#我们刚才通过DockerFile配置过数据卷,再这里不需要再使用-v参数了。
$ docker run -it ljh/centos
但是,我们知道了容器内的容器卷对应目录,那宿主机下对应的目录在哪里呢?实际上,Docker会自动帮我们生成一个默认的文件夹,并避免了和宿主机原来的文件夹重名的情况。
具体可以使用inspect
来检查这个被创建出来的容器:
"Mounts": [
{
"Type": "volume",
"Name": "c87509fb79c5ed0c3c484f5b146f01dfcb96228063190c9ab58aca7efd396db5",
"Source": "/var/lib/docker/volumes/c87509fb79c5ed0c3c484f5b146f01dfcb96228063190c9ab58aca7efd396db5/_data",
"Destination": "/dataVolumeContainer1",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
},
{
"Type": "volume",
"Name": "1a5c4cc8e94167f5279b6c5b48e2d3073b417e9499fc5ff1dba470eeccb8b929",
"Source": "/var/lib/docker/volumes/1a5c4cc8e94167f5279b6c5b48e2d3073b417e9499fc5ff1dba470eeccb8b929/_data",
"Destination": "/dataVolumeContainer2",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
]
可以看到,Source
文件夹的命名相当之长。
Docker数据卷容器
注意,数据卷和数据卷容器是两个不同的概念。数据卷容器表示:这个容器正挂在数据卷,来为其它的子容器提供数据共享渠道。
我们还是使用ljh/centos
镜像来创建一个新的容器,并命名为doc1
,我们将它称之为数据卷容器。
$ dokcer run -it --name doc1 ljh/centos
我们再创建容器doc2
,doc3
,并希望它使用doc1
容器内的数据卷,通过--volumes-from
来实现这种容器和容器间的数据共享。
$ docker run -it --name doc2 --volumes-from doc1 ljh/centos
$ docker run -it --name doc3 --volumes-from doc1 ljh/centos
我们实际上是建立了这样的一个关系:
容器
doc2
, doc3
正在共享 doc1
内的数据,三者的数据是互通的。我们可以在 doc2
的 dataVolumeContainer1
当中创建一个新文件,然后去 doc3
容器的相同目录下检查是否存在该文件。
假如说将doc1
容器删除掉,会不会造成很严重的后果?这样的数据共享关系还会存在吗?
$ docker rm -f doc1
我们将doc1
删除掉,随后再attach
到doc2
的对应目录底下,发现,文件仍然是被保留的。并且,doc2
下创建新文件,我们仍然也可以从doc3
文件中再获取到它,这说明doc2
和doc3
之间的数据共享关系仍然没有被破坏。
那这两个数据卷的生命周期到底是什么样的呢? 当没有任何一个子容器再挂载该数据卷时,这个剩余的数据卷才会被彻底删除。换句话说,当
doc2
和 doc3
也被一并删除的时候,原来 doc1
中的数据卷才会被真正删除。
常用数据库安装:安装MySQL
这一节我们将学习如何在Docker中安装MySQL数据库。
$ docker search mysql
$ docker pull mysql
启动MySQL数据库,设置数据卷以及初始化的ROOT密码。
$ docker run -p 10000:3306 \
--name mysql -v /root/mysqlTmp/conf:/etc/mysql/conf.d \
-v /root/mysqlTmp/logs:/logs \
-v /root/mysqlTmp/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 -d mysql
(可选)更改数据库加密规则
注意,MySQL 8版本之前的加密规则是mysql_native_password。而MySQL之后加密规则是caching_sha2_password。如果Navicat无法连接此数据库(错误代码:1251),则主要通过两种渠道解决,这里我们选择第二种方式:
1、 升级Navicat驱动。
2、 更改MySQL登录的加密规则。
进入到容器内,然后打开mysql命令行:
$ docker exec -it ${containerID | containerName} /bin/bash
$ mysql -uroot -p
其中,容器ID为mysql容器的ID号。通过密码登录mysql终端,密码为初始设定的MYSQL_ROOT_PASSWORD
。
注意,每行mysql命令以;
符号结尾。
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'
提示:root
可以替换成你指定的用户名称。
localhost
表示只允许本机访问,这样除了本机测试以外,外部是无法连接的。因此替换成'*.*.*.*'
的IP。通常情况下仅开放有限的IP地址(出于安全状况考虑)。如果允许任何IP连接进来,则设置为'%'
。
password
为你指定设置的密码。
在设置完毕后,刷新配置。
FLUSH PRIVILEGES;
这样,在物理机端,我们打开Navicat,就可以便捷地操作来自于容器的mysql表了。注意端口号的设置,是在run命令时所指定的Docker容器开放的端口,未必是3306(取决于你的设置)。
如果您是在阿里云,腾讯云等平台进行的操作,要记得设置入站规则,允许外部访问Docker对应的端口。
(建议,如果您是Java程序员)设置时区
对于MySQL 8.0+的版本,我们在使用基于JDBC的数据库驱动来编写持久层组件时,可能都遇到过由于MySQL的默认时区与系统时区不同导致无法连接的问题。
依赖JDBC驱动连接MySQL的工具可能也会存在这个问题,比如DataGrip。
第一种方式是,在每次连接时MySQL都带上中国时区。
private static final String url =
"jdbc:mysql://127.0.0.1/dataBase?serverTimezone=Asia/Shanghai"
第二种方式是,直接进入MySQL数据库内更改配置。
$ docker exec -it ${containerID | containerName} /bin/bash
$ mysql -uroot -p
#设置东8区。
mysql> set global time_zone = '+08:00';
#检查是否更改成功。
#默认情况下time_zone的值是SYSTEM。
mysql> show variable like "%time_zone%";