使用官方容器、按照官方文档部署一个中间件就意味着最佳实践吗,可能并不是这样,docker的世界也有很多无序的情况。
概述
Docker镜像打包了制品和运行的环境,使用分层的方式存储,似乎是一个完美的软件发布方式。但是你如果真正使用它去部署一批各式各样的中间件就会发现并没有那么简单,原因可能会被归结到官方没有提供清晰、简单和正确的文档,确实是这样,但观察后会发现不同中间件的容器发行版本的构建方式可能存在比较大的差异,这对于使用者来说就是不断适应的过程。
找到文档
这里以MySQL和Nacos的容器部署做个对比,来看下存在哪些差异。我们可以到DockerHub(国内不能访问)上找找看,有没有官方发布的镜像,如果有,你会收获一份官方维护的文档。当然,还有另一个选择,就是从各自的官方网站进入,看能否找到使用Docker部署的条目。
从DockerHub
现在我们分别来搜索:
可以看到MySQL的搜索结果中存在“Docker Official Image”标记,这个标记意味着至少是Docker社区维护的,比如MySQL就会是“Maintained by: the Docker Community and the MySQL Team”,而Tomcat是“Maintained by: the Docker Community”,对于Nacos,从维护用户的身份看,仅仅是“Community User”,其实是由Nacos官方维护的,但此时我们无法确认维护者的身份,获取一个机构的身份会比较难吗?(TODO:如何在DockerHub中获取机构身份),我们当然希望得到靠谱的镜像和文档。
从官方网站
另外一个途径就是从官方文档中找到权威的说明,对于MySQL,我么可以进入它的官方网站尝试找找如何使用Docker部署,实际的过程并不简单,在“Installation”相关的索引中只能看到YUM、APT之类的传统发布方式,根本就没有Docker的踪影:
这时候随便进入一个条目,才能在索引中找到“Deploying MySQL on Linux with Docker Containers”的条目:
看起来至少有一个不错的文档结构,事实也的确如此,除了不太容易找到之外。在More Topics on Deploying MySQL Server with Docker小节中分为几个主题介绍Docker下的部署,这些主题实际上对于任何使用Docker容器运行的软件来说是通用的,有必要在这里简略介绍下:
- 专门为Docker容器安装优化,比如减小体积
- 配置MySQL服务器,一般来说使用容器方式运行的软件都能够支持
docker run
命令参数或者传入环境变量的方式配置,但并不是所有的软件都能做到像MySQL一样的命令行参数支持(执行mysqld --verbose --help
看看),因此就不得不将配置文件挂载至外部,以获得相对更好一些的体验 - 持久化数据和配置,首先说数据,MySQL以文件的方式持久化数据,这些文件不能被随便丢在Docker维护的临时目录里面,因此我们可以将指定的目录挂载至容器中,主动去管理数据目录,而不是托管给Docker,配置文件同样可以挂载至容器内,这样就可以通过配置文件配置程序了,这部分文档还贴心的给出了自建配置文件的最小配置,非常周到
- 运行附加的初始化脚本,可以将执行初始化的
.sh
和.sql
文件以挂载的方式传入容器内部,以在容器启动后立即执行 - 其他以容器方式运行的程序如何连接到容器方式运行的MySQL,容器必须实现网络隔离,所以每个容器拥有独立的网络栈,每个容器相当于通过网线(Veth Pair)连接到了Docker Daemon管理的虚拟交换机(Bridge,比如docker0),而容器的IP地址在启动时被重新分配,因此你无法在容器间通过指定IP地址的方式做稳定的通信,此时你需要创建容器网络,位于相同网络的容器可以通过容器名字通信
- 错误日志,这里说的是程序日志输出到哪里的问题,我们在前面说数据持久化时提到,Docker的持久化是暂时的,我们必须主动管理数据的生命周期,对于日志则不然,如果它写入到容器内部而非挂载至外部的文件实际上是非常糟糕的,你不能方便地查看它,它还会在容器生命周期内不停增长,如果输出到stdout和stderr则不然,Docker Daemon会替你管理文件(切割、滚动之类,可配置),你还可以很方便的使用
docker logs
命令查看日志,当然,写日志的方式不仅仅考虑方便查看,还要考虑Docker Daemon的承压能力以及分类日志,标准输出和标准错误是混杂的日志流 - 备份数据库,其实就是启动一个覆盖entrypoint的临时容器,使用容器内的mysqldump工具
可以看到,文档中非常全面的说明了部署方式,对于容器部署,看完这篇文档应该能够解答你的很多疑惑,这是一个很好的范例,尽管可能也会存在一些问题,比如我在使用见环境变量MYSQL_ROOT_PASSWORD
和MYSQL_ONETIME_PASSWORD
的时候好像遇到了一些与文档描述不符的问题,不过这是另外一个问题。
接下来看看Nacos的官方网站,进入官方文档后一眼就能看到“Nacos Docker”,但是再看就会发现太过简单了,甚至称得上简陋,再回头看看DockerHub上的文档甚至还会觉得更好一点,起码它告诉你如何通过挂载配置文件进行配置,如果你真的相信了文档就会发现你按照文档挂载的配置文件custom.properties
根本不生效,原因是新版本中不支持这个配置文件了(后面会提到不支持的佐证,但是在Nacos仓库的Issues里面也能发现端倪,Issues是个好地方,Google搜索返回的Issues相关结果往往很有用),还有一些需要特别注意的事项也没有在文档中出现,比如你必须设置容器的hostname:--hostname nacos-1
,还必须设置环境变量-e PREFER_HOST_MODE=hostname
指定通过主机名发现节点,否则你的Nacos容器集群就不要想建立成功,不是集群的情况下Nacos的功能是受限的,比如编辑服务的Metadata会失败,还有一个值得控诉的点是,它把日志文件输出到了容器内部的文件,而没有告诉你这方面的最佳实践是什么,也许官方仅针对已经非常熟悉Nacos的人给出容器化方案,这很不原生,以下是进入容器可以看到的日志文件:
不得不说,Nacos的文档体验太糟糕了,很难想象它是知名的开源项目。这其实是一种常态,也是我写这篇博客的原因,使用Docker部署也有很让人难受的地方。但是对于Nacos来说,还有一个可以参考的地方,就是官方文档中提到的Nacos Docker项目,虽然什么说明也没有,但是你能看到能够Run的命令是怎样的,还有,它终于告诉你custom.properties
文件已经不生效了,剩下的还是要靠你自己,或者是那些杂乱的博客。
部署
这里把具体的部署命令写下来,首先创建网络:
sudo docker network create my-net
部署MySQL:
# 拉取镜像
docker pull container-registry.oracle.com/mysql/community-server:8.0.27
# 创建目录
sudo mkdir /data && sudo chmod 777 $_
mkdir -p /data/mysql-1/config
mkdir -p /data/mysql-1/data
# 创建配置文件,必须指定运行用户为mysql
cat > /data/mysql-1/config/my.cnf << "EOF"
[mysqld]
user=mysql
EOF
# 启动数据库
sudo docker run -d --name=mysql-1 \
--mount type=bind,src=/data/mysql-1/config/my.cnf,dst=/etc/my.cnf \
--mount type=bind,src=/data/mysql-1/data,dst=/var/lib/mysql \
--network my-net \
--restart unless-stopped \
-e MYSQL_ROOT_PASSWORD=123456 \
-p 3306:3306 \
container-registry.oracle.com/mysql/community-server:8.0.27
还需要解决以下两个问题,你的数据库客户端才能连接:
使用之前设置的临时密码连接数据库:
sudo docker exec -it mysql-1 mysql -uroot -p
执行以下SQL:
-- 此时,heidisql连接,显示:...not allowed to connect...
-- 允许任何主机连接
use mysql;
update user set host='%' where user='root';
flush privileges;
-- 此时,heidisql连接,显示:Access denied for user...
-- root用户具备足够的权限,需要修改密码
alter user 'root'@'%' identified by 'root@123';
flush privileges;
-- 此时,heidisql连接成功
部署Nacos,首先给出一个配置文件的例子:
# 注意修改不同节点的端口
server.port=8848
spring.sql.init.platform=mysql
db.num=1
# 默认连接串的数据库名称为nacos,这里创建的是nacos_config
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
db.user.0=root
db.password.0=root@123
# 启用认证
nacos.core.auth.enabled=true
nacos.core.auth.server.identity.key=name
nacos.core.auth.server.identity.value=nacos-1
nacos.core.auth.plugin.nacos.token.secret.key=b3BlOXpFbUtrWnJDajMyT0ZId1RiUEpOWlZLRG1KZEE=
然后部署:
# 拉取镜像
docker pull nacos/nacos-server:v2.2.3
# 创建目录
sudo mkdir /data && sudo chmod 777 $_
mkdir -p /data/nacos-1/config
# 创建配置文件 /data/nacos-1/config/application.properties
# 见[2.2.3-application.properties](https://github.com/alibaba/nacos/blob/2.2.3/distribution/conf/application.properties)
# 初始化mysql
# 1. 创建schema `CREATE SCHEMA nacos_config;`
# 2. 建表,见[数据库脚本](https://github.com/alibaba/nacos/blob/2.2.3/distribution/conf/mysql-schema.sql)
# 启动,以nacos-1为例
# 启动nacos-2时注意替换nacos-1关键字与端口
sudo docker run -d --name nacos-1 \
--hostname nacos-1 \
--mount type=bind,src=/data/nacos-1/config/application.properties,dst=/home/nacos/conf/application.properties \
--mount type=bind,src=/data/nacos-1/log,dst=/home/nacos/logs \
--network my-net \
--restart unless-stopped \
-e PREFER_HOST_MODE=hostname \
-e NACOS_SERVERS="nacos-1:8848 nacos-2:8948" \
-e JVM_XMS=128M \
-e JVM_XMX=128M \
-e JVM_XMN=48M \
-p 8848:8848 \
nacos/nacos-server:v2.2.3
# 测试
# 因为已配置认证,可以获取token
curl -X POST '127.0.0.1:8848/nacos/v1/auth/login' -d 'username=nacos&password=nacos'
# 访问控制台 http://192.168.56.58:8848/nacos
有一点要注意,在使用MySQL8.x的情况下,Nacos配置文件MySQL连接串中要包含allowPublicKeyRetrieval=true
参数,否则非SSL连接的情况下,Nacos使用root用户全局首次连接MySQL会失败,因为此时MySQL无法缓存公钥,此时只有其他客户端为root用户缓存了公钥后Nacos才能连接成功。
排查容器启动时的问题
这里说几个关于容器调试的方法
如果容器启动后不停的崩溃重启,该如何排查呢?
使用nsenter,基本原理是暂停容器后进入容器的namespace中,此时你将可以在容器中使用宿主机的工具:
# 暂停容器
docker pause <container-name>
# 调试
# 查看容器PID
sudo docker inspect <container-name> | grep Pid
# 可以查看nsenter的帮助信息
# 示例为进入所有容器的所有namespace
sudo nsenter -t <container-pid> -a
怎样导出一个镜像的目录结构
# 创建非启动状态下的容器
docker create --name datalink-container <image>
# 输出文件树到文件
docker export datalink-container | tar t > datalink-container.txt
或者也可以试试覆盖entrypoint 为/bin/bash
,然后进入容器中观察。
怎样抓包分析
# 查看容器网络,找到目标NETWORK ID
sudo docker network ls
# 查看本地网络信息,找到与NETWORK ID匹配的网卡
ip addr | grep <NETWORK ID>
# 启动抓包并写入文件
tcpdump -i br-2c7a43811545 -w xxx.cap
# 导出文件使用Wireshark分析
总结
本篇博客主要是通过两个比较典型的例子讨论了一些比较浅显的问题,更加深入的话题可能是如何设计和构建一个镜像,需要深入到CMD当中去,构建镜像是否存在一些难以解决的问题对于我来说目前无法给出答案,可能后续会做一些学习总结。但是通过这两个例子也可以看出浅显的问题也还是存在的,使用容器打包软件打包是一回事,打包得好是另一回事,在这方面还是要多做一些考量。
前段时间看到了一篇InfoQ推送的翻译文章,也是吐槽镜像问题的:Docker 的诅咒:曾以为它是终极解法,最后却是“罪大恶极”?