Steve's Blog

Talk is cheap, show me the code.

0%

Docker访问MySQL

image-20220825015945304

最近工作涉及到一些Docker的学习,因此将Docker相关知识简单总结一下。

1. 目标

通过docker生成一个包含MySQL的镜像,此镜像和宿主机有端口映射,可以通过宿主机上的Java代码来访问docker上MySQL的数据,并且使用数据卷(volume)持久化docker上MySQL的数据,保证在删除容器后,数据不丢失。

拆解一下任务:

  • 生成包含MySQL的镜像
  • 根据生成的镜像来创建容器实例(container)
  • Java访问docker上MySQL的数据

接下来挨个实现

2. 生成包含MySQL的镜像

这步比想象中的要复杂一些,踩了一些坑,也因为对docker的不熟悉,走了一些弯路,在此做下记录。

Dockerfile中用到的几个文件,结构和文件内容如下:

1
2
3
4
5
- scripts/
- Dockerfile # 使用的Dockerfile
- sql/ # 存放SQL脚本的文件夹
- init.sql # 建表语句

2.1 Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 从mysql基础镜像进入
FROM mysql

# 设置terminal进入docker后的默认目录
WORKDIR /usr/bin

# Oracle linux 8 没有安装yum,但是可以使用microdnf进行安装
RUN microdnf install -y vim

# 把开始脚本放到这个文件夹里,docker容器启动后就会执行,具体参见 https://github.com/docker-library/docs/tree/master/mysql#initializing-a-fresh-instance
COPY sql/init.sql /docker-entrypoint-initdb.d/

# 声明日志数据卷
VOLUME /var/log/mysql
# 声明数据数据卷
VOLUME /var/lib/mysql
# 本地映射配置数据卷
VOLUME /etc/mysql/conf.d

# 暴露端口为3306
EXPOSE 3306

# [MYSQL_RANDOM_ROOT_PASSWORD, MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD] MySQL配置3选1

# 如果MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD没有配置则默认为true。如果为true,
# mysql会自动生成一个密码,会打印在控制台,格式是: [GENERATED ROOT PASSWORD: .....]
#ENV MYSQL_RANDOM_ROOT_PASSWORD=true
# 设置MySQL root的密码
ENV MYSQL_ROOT_PASSWORD=123456
# 为true的话,root用户的密码就是空
#ENV MYSQL_ALLOW_EMPTY_PASSWORD=true

#设置容器启动时执行的命令,这里有个坑,直接用这条会导致mysql无法正常连接,具体参见5.1分析
#CMD ["sh", "/mysql/setup_test.sh"]
CMD ["mysqld"]

此处先不对各个关键字的功能做详细记录,后续在docker学习中总结。

此处需要注意的几个点:

  • Dockerfile无法实现端口映射,只能通过EXPOSE关键字暴露端口,端口映射需要使用docker run -p 3307:3306类似的命令或者docker-compose.yml来实现。
  • Dockerfile无法指定本地数据卷和容器内目录映射,只能通过VOLUME指定匿名数据卷,指定后docker会在安装目录下的特定目录里生成一个目录来映射容器数据卷。如果需要指定数据卷映射,需要使用docker run -v /User/LocalMachine/data:/var/MySQL/data或者docker-compose.yml来实现。

2.2 Dockerfile用到的脚本

init.sql — 建表语句,放到容器/docker-entrypoint-initdb.d/文件夹下,MySQL会自动执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
-- 创建database
CREATE DATABASE myjdbcdemo DEFAULT CHARACTER SET utf8;

USE myjdbcdemo;

-- 建表
DROP TABLE IF EXISTS `user`;
DROP TABLE IF EXISTS `singer`;
DROP TABLE IF EXISTS `batch`;

CREATE TABLE user
(
id BIGINT,
name VARCHAR(30),
password VARCHAR(30)
);
CREATE TABLE singer
(
id BIGINT,
name VARCHAR(30),
bestsong VARCHAR(30),
image MEDIUMBLOB
);
CREATE TABLE batch
(
id BIGINT,
name VARCHAR(30),
password VARCHAR(30)
);
-- 插入测试数据
INSERT INTO singer (id, name, bestsong, image)
VALUES (1, 'JayChou', '七里香', NULL);
INSERT INTO singer (id, name, bestsong, image)
VALUES (2, '林俊杰', '可惜没如果', NULL);
INSERT INTO singer (id, name, bestsong, image)
VALUES (2, 'Jolin Cai', '布拉格广场', NULL);

2.3 生成镜像

完成Dockerfile的debug后,我们在本地使用docker build语句生成一个镜像:

1
2
3
# -t 是为镜像设定tag和名称,可以设置多个
# .是Dockerfile路径,docker build命令会自动寻找目录下的Dockfile文件
docker build -t jdbcdemoimg:0.1 -t jdbcdemoimg:latest .

创建成功后通过docker images命令查看现有镜像:

image-20220821161102688

3. 根据生成的镜像来创建容器实例

此处需要考虑2个点:

  • 镜像和宿主机的端口映射
  • 使用数据卷来对需要持久化的数据进行本地映射

使用docker run命令指定端口映射数据卷本地映射后生成容器实例:

1
2
3
# 本地3307端口映射容器的3306,因为本地的3306被本地装的MySQL占用了
# 映射3个数据卷到本地,防止MySQL数据丢失
docker run -d -p 3307:3306 -v ~/Dev/docker_volumn/mysql/log:/var/log/mysql -v ~/Dev/docker_volumn/mysql/data:/var/lib/mysql -v ~/Dev/docker_volumn/mysql/conf:/etc/mysql/conf.d --name jdbc-1 jdbcdemoimg:latest

先通过docker ps查看容器是否启动成功:

image-20220825003202026

可以看到已经成功启动,容器name为jdbc-1。

使用docker port命令查看容器和宿主机的端口映射,可以看到3306端口映射到了本地的3307端口。

image-20220825003243469

最后通过docker exec命令进入容器 CLI界面:

image-20220825003439081

出现bash标志表示此处已经成功进入docker 容器的cli。

通过mysql -uroot -p进入docker内部的MySQL,输入密码后成功进入。

输入show databases;查看当前的库,可以看到myjdbcdemo在其中:

image-20220821162734752

输入命令use myjdbcdemo;使用此库,可以使用select语句查看表中数据。

image-20220821162851682

3.1 MySQL root密码问题

docker给了3个环境变量来控制MySQL的密码问题,分别是MYSQL_RANDOM_ROOT_PASSWORD, MYSQL_ROOT_PASSWORDMYSQL_ALLOW_EMPTY_PASSWORD,三个优先级有先后,挨个介绍下:

1
2
3
4
5
6
7
# 如果MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD没有配置则默认为true。如果为true,
# mysql会自动生成一个密码,会打印在控制台,格式是: [GENERATED ROOT PASSWORD: .....]
#ENV MYSQL_RANDOM_ROOT_PASSWORD=true
# 设置MySQL root的密码
ENV MYSQL_ROOT_PASSWORD=123456
# 为true的话,root用户的密码就是空
#ENV MYSQL_ALLOW_EMPTY_PASSWORD=true
  • MYSQL_RANDOM_ROOT_PASSWORD。如果MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD没有配置则默认为true。如果为true,mysql会自动生成一个密码,会打印在控制台,格式是: [GENERATED ROOT PASSWORD: .....]

  • MYSQL_ROOT_PASSWORD。设置MySQL root的密码。这个没什么可说的,就是正常的密码设置,例如ENV MYSQL_ROOT_PASSWORD=123456

  • MYSQL_ALLOW_EMPTY_PASSWORD。为true的话,root用户的密码就是空。

    此处使用debug时可以进入docker容器,通过以下命令查看MySQL用户信息。

    1
    2
    # 在docker容器内进行MySQL
    bash> mysql -uroot -p123456;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 进入MySQL内部后,选择mysql库
    mysql> use mysql;
    # 查看用户信息
    mysql> select user, host, authentication_string from user;
    +------------------+-----------+------------------------------------------------------------------------+
    | user | host | authentication_string |
    +------------------+-----------+------------------------------------------------------------------------+
    | root | % | $A$005$|[lde}}}^RXsbBUMKjoSEwZkvPMuCFiecEQqa8UIN9o6b1dKRtcyTQv0C |
    | mysql.infoschema | localhost | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED |
    | mysql.session | localhost | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED |
    | mysql.sys | localhost | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED |
    | root | localhost | $A$005$MEKL81XEj[C.>rB2luQaqISkkixQjSffptkkhsWs2xm3y7D/qYgPMg970/ |
    +------------------+-----------+------------------------------------------------------------------------+
    5 rows in set (0.00 sec)

3.2 远程登录失败问题

使用的时候一直显示会连接失败:

image-20220823010324056

查了很多结果,有说和防火墙有关的,有的需要修改my.cnf文件中bind-address=0.0.0.0的。但是最后发现。。。是因为容器没有完全启动起来,所以连接失败了,等待其启动结束后,再连接就好了。搞了一个大乌龙 T_T。。。

不过查问题的过程中也看到了不少知识,还是打算记录下来。

修改my.cnf文件中bind-address=0.0.0.0后,MySQL会监听所有网络端口的连接,具体参见这个回答

Note: if you use bind-address = 0.0.0.0 your MySQL server will listen for connections on all network interfaces. That means your MySQL server could be reached from the Internet ; make sure to setup firewall rules accordingly.

如果不做bind-address的配置,MySQL的bind-address默认是**也表示所有IP都可以连接,这里可以参见官方文档

1
2
# 查看docker ip 
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' jdbc-1

4. Java访问docker上MySQL的数据

此处除了端口改为3307,其它和访问本机MySQL实例流程完全一致,不再赘述。

5. 开发过程中遇到的问题

由于Dockerfile是在网上找的,debug过程中遇到了一系列问题。

当时思路是:

  • 自己写脚本来启动和初始化MySQL(尽管后面发现这是不必要的)
  • 初始密码由脚本来设置,而非MySQL 镜像提供的MYSQL_ROOT_PASSWORD字段设置(这是谬误2,如果有官方提供的入口,不使用而打算自己处理,那么就要做好面对各种异常的准备。)

5.1 使用shell脚本插入数据问题

先来列举一下文件内容,[D]表示最新代码中已经被删掉。

1
2
3
4
5
6
7
- scripts/
- Dockerfile # 使用的Dockerfile
- [D] setup.js # 使用此脚本进行初始化
- sql/ # 存放SQL脚本的文件夹
- init.sql # 建表语句
- [D] privilege.sh # 原来的新增密码脚本

当时Dockerfile是打算使用setup.js进行MySQL初始化的:

1
2
# 把setup.js放到docker-entrypoint-initdb.d文件夹下,这样容器第一次启动时,setup.js就会执行。
COPY setup.js /docker-entrypoint-initdb.d/

原始的setup.sh — 启动脚本,执行一些建表、给root用户新增密码等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash

# 若指令传回值不等于0,则立即退出shell
set -e

# 设置mysql密码
# mysql < /mysql/init.sql
# 此处必须指定user为root,为什么?看下面分析
mysql -uroot < /mysql/privileges.sql

# 导入数据
mysql -uroot -p123456 < /mysql/init.sql

tail -f /dev/null

如果没有设置MySQL的root密码,在容器的bash命令行里是直接可以输入mysql < some_sql.sql来执行sql文件的。

但是在shell脚本中使用此命令插入sql文件时,遇到了问题。命令如下:

1
2
# setup.js文件中的语句
mysql < /mysql/init.sql

image-20220821032929849

可以看到,报错中user为'mysql'@'localhost',但我们使用的明显不是这个user,而是'root'@'localhost'。尝试指定了一下user,问题解决:

1
2
3
# setup.js文件中的语句
# 新加一个-uroot指定用户为root就不报错了
mysql -uroot < /mysql/init.sql

这里为什么用户是mysql呢??https://stackoverflow.com/a/11216911/8706905 这个可以作为参考,但明显不是答案。

后面在官方文档里找到,如果不通过-u或者--user选项来覆盖MySQL用户名,类Unix系统(如MacOS或者Linux)中会使用当前OS的用户名来作为MySQL的用户名,这在用户名没指定的情况下只是为了方便。

On Unix, most MySQL clients by default try to log in using the current Unix user name as the MySQL user name, but that is for convenience only.

image-20220827003233755

我们在报错的bash语句前加一句打印当前用户的命令:

1
2
# 打印当前用户
id;

可以看到,在docker镜像启动之时,使用的用户是mysql,推测是MySQL初始化时创建了专门的OS用户来操作初始化,所以才会报上面提到的错误。

image-20220827003802863

5.2 容器启动失败

还是和被删掉的setup.sh文件有关。

MySQL镜像如果拉取下来后,如果Dockerfile最后一句为

1
2
3
CMD ["sh", "setup.sh"]
# 应该执行这一句对MySQL进行初始化
# CMD ["mysqld"]

会报ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)的错误。

image-20220825001344885

查了许多地方,这个回答中提到:数据库还未初始化。

解决方式是最后一句由CMD ["sh", "setup.sh"]改为执行CMD ["mysqld"]同时通过Docker官方MySQL镜像文档,把setup.js文件copy到/docker-entrypoint-initdb.d/文件夹下(文件已经删除,所以也没有这一说,但是学到如何使用这个文件夹 ^_^)。文档中是这么说的:

image-20220826232813652

容器在第一次启动时,会执行在/docker-entrypoint-initdb.d/文件夹下的.sh, .sql.sql.gz文件。

另外在MySQL官网mysqld的页面有明确的解释,其实mysqld就是MySQL server,需要启动它对MySQL服务端进行初始化。

mysqld, also known as MySQL Server, is a single multithreaded program that does most of the work in a MySQL installation.

另一个佐证是MySQL官方docker镜像的image layers描述中,最后一句也是 CMD ["mysqld"]

截屏2022-08-28 00.34.40

在使用这个解决方法后,之前报错找不到的/var/run/mysqld/mysqld.sock文件出现了。

修改后:

image-20220821030721690

5.3 使用SQL脚本修改MySQL的密码

privilege.sh — 给root用户添加密码的SQL脚本,这里有些需要注意的点。

1
2
3
4
5
6
7
8
USE mysql;
-- 给root配置密码,两行都不可以缺少,因为'root'@'%'和'root'@'localhost'是分开的
-- 如果是此行,那么root只有远程连接有密码
ALTER USER 'root'@'%' IDENTIFIED BY '123456';
-- 如果是此行,那么root只有本地有密码
ALTER USER 'root'@'localhost' IDENTIFIED BY '123456';
-- 这一条命令一定要有:
FLUSH PRIVILEGES;

image-20220823005709843

后面发现其实只需要设置MYSQL_ROOT_PASSWORD不为空即可,这段就删掉了,放在这里权做记录。

6. 结论

尽量还是寻找比较正式的文档进行二次开发,遇到的坑比较少,例如这个文档.,虽然不是Dockerfile生成MySQL镜像的,但整体相当顺利。。

官方成本可以降低学习成本,不过如果时间空闲较多的话,倒也可以深入探究一下,在查问题的过程中,的确收获了不少新的知识。

源码地址:https://github.com/StephenHuge/blog-code/tree/master/MyJdbcDemo/scripts