Dockerfile实现MySQL定时备份

本文利用自定制Dockerfile实现在mysql容器内自动定时备份,避免了在宿主机设置cron定时任务所带来的高耦合操作,更易于部署和搬迁.项目最新版已上传到githubDockerHub,建议直接到DockerHub查看使用.

网上其他教程大多都是利用宿主机crontab定期执行docker exec实现,很简单但每次部署mysql都需要设置,本教程则是将其封装起来,避免重复性工作.踩坑不易,请大家多多支持,未经本人允许禁止转载.

一. 文件结构

二. 文件介绍

  • cron-shell文件夹
    放置cron任务表文件crontab.bak和相关任务脚本,对应容器内文件夹为**/cron-shell**

    • backup.sh
      实现备份功能,用户可替换掉该文件以实现自己的备份逻辑
    • crontab.bak
      crontab任务表,要求结尾必须换行,且文件为Unix格式
  • start.sh
    功能:

  1. 将mysql用户的环境变量写入/etc/default/locale文件,使得这些环境变量对cron运行的脚本(如backup.sh)可见
  2. 启动cron
  3. 载入/cron-shell/crontab.bak定时任务列表文件
  • init-sql文件夹
    inti-sql文件夹下的sql脚本(.sql)或者sql的压缩文件(.sql.gz)在容器启动后由msql运行,通常用来实现数据库初始化,需要注意的是这些sql的执行顺序不固定,故如果对sql脚本执行顺序有要求的话应自己使用shell脚本实现
    • data_20180909.sql.gz
      数据库初始化sql的压缩包
  • Dockerfile
    将文件复制进镜像文件,并安装sudo,cron工具,同时授予mysql组用户免密码使用sudo的权限.

三. 实现思路

通过Dockerfile安装所需cron,利用docker-entrypoint.sh中自动执行**/docker-entrypoint-initdb.d**文件夹下shell和sql脚本的功能完成cron自启动和任务制定

四. 文件实现

1. baskup.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
#作为crontab运行的脚本,需特别注意环境变量问题,指令写成绝对路径

#读取环境变量
. /etc/profile
#如果目录不存在则新建
DIR=/var/lib/mysql/backup
if [ ! -e $DIR ]
then
/bin/mkdir -p $DIR
fi
#将所有数据库导出并按日期命名保存成sql文件并压缩
/usr/bin/mysqldump --all-databases -uroot -p"$MYSQL_ROOT_PASSWORD" | gzip > "$DIR/data_`date +%Y%m%d`.sql.gz"
#查找更改时间在7日以前的sql备份文件并删除
/usr/bin/find $DIR -mtime +7 -name "data_[1-9]*.sql.gz" -exec rm -rf {} \;

2. crontab.bak

1
*/1 * * * * /cron-shell/backup.sh

注意结尾要换行,并且文件格式要是Unix(其他所有sh脚本也是)

3. start.sh

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
#!/bin/bash

#此脚本由mysql用户执行,故需要加sudo避免权限不够,同时,新建文件一般放在/var/lib/mysql目录下,否则同样权限不够

#修改文件夹权限,否则无法在该目录下创建文件
sudo chmod 777 /etc/default
#将docker的环境变量输出到locale,使得cron定期运行的脚本可以使用这些环境变量,否则执行备份脚本时可能提示密码为空
env >> /etc/default/locale

#启动cron并将其启动结果写入文件
# 这里需要使用sudo,否则会提示cron: can't open or create /var/run/crond.pid: Permission denied
# 这个问题无法通过修改/var/run/crond或/run文件夹权限解决
# 需要注意的是,使用sudo后cron是在root用户下运行的,root用户下使用`service cron status`会出现` [ ok ] cron is running. `
# 而mysql执行`service cron status`则会出现` [FAIL] cron is not running ... failed! `
# 虽然如此,mysql用户身份制定的定时任务还是会执行的
sudo /usr/sbin/service cron start &>> /var/lib/mysql/cron-start.log

#授予权限
sudo chmod 777 -R /cron-shell

#修正文件格式,这里dos2unix的执行也需要sudo,否则会报错`Failed to change the owner and group of temporary output file /cron-shell/d2utmpKfjPMF: Operation not permitted`
for f in /cron-shell/*; do
sudo dos2unix "$f"
done

# 确保结尾换行,避免出现错误:`new crontab file is missing newline before EOF, can't install.`
echo "" >> /cron-shell/crontab.bak

#以/cron-shell/crontab.bak作为crontab的任务列表文件并载入
# 因为执行的定时任务一般是数据库相关的,mysql用户就可以了,如果使用root用户可能会报错:`Got error: 1045: Access denied for user 'root'@'localhost' (using password: YES) when trying to connect`
# 所以这里使用mysql用户载入定时任务表,任务脚本也将以mysql用户执行,需注意权限问题
crontab /cron-shell/crontab.bak

4. 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
FROM mysql:5.7

MAINTAINER LinShen

#修正时区
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo 'Asia/Shanghai' >/etc/timezone \
#更新源
&& apt-get update \
#安装cron
&& apt-get install -y --no-install-recommends cron \
#安装dos2unix工具
&& apt-get install -y dos2unix \
#安装sudo
&& apt-get install sudo \
#授予mysql组用户sudo免密码
&& echo '%mysql ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers \
#减小镜像的体积
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean \
#赋予脚本可执行权限
&& chmod a+x -R /docker-entrypoint-initdb.d

#将任务脚本复制进容器,需要注意不能放到/var/lib/mysql目录下,该目录随mysql初始化会被清空造成原文件丢失
COPY cron-shell/ /cron-shell/

COPY start.sh /tmp/start.sh

ENTRYPOINT ["/tmp/start.sh"]

五. 问题

(1) 在什么时候启动cron(在哪里执行service cron start)

我本来是想在Dockerfile里完成启动cron工作的,Dockerfile里执行语句无外乎RUN,CMDENTRYPOINT三种指令,但很可惜这三种指令都不行,最后只能利用docker-entrypoint.sh,下面进行分析说明:

1. RUN

RUN的特点是可以执行多条,每执行一条docker镜像就会多构建一层.
需要注意的是RUN通常只用来构建镜像,而镜像是不带有运行状态的.故RUN可完成安装软件,修改文件等操作,但使用service start是无意义的.
而CMD和ENTRYPOINT则是等到容器启动后才运行.

2. CMD

CMD每个Dockerfile文件可以有多条,但一个镜像只有最后一条CMD有效
MySQL官方镜像已经有用到CMD和ENTRYPOINT指令了,分别是CMD ["mysqld"]ENTRYPOINT ["docker-entrypoint.sh"],当ENTRYPOINT和CMD同时存在时,CMD通常是作为ENTRYPOINT的参数,其内容视ENTRYPOINT的处理而执行.
而docker-entrypoint.sh中对cmd内容的分类有如下:

  1. 开头是 - , 认为是参数的情况
    可携带参数,如**–character-set-server=utf8mb4–collation-server=utf8mb4_unicode_ci**
  2. 开头是 mysqld, 且用户 id 为0 (root 用户) 的情况
  3. 开头是 mysqld 的情况
  4. 其他情况
    由docker-entrypoint.sh最后一句的exec "$@"负责执行用户的自定义语句

特别注意:前3种情况在docker-entrypoint.sh都有对应的处理,最终都会启动mysql,而最后一种情况不会 !

在前3种情况中CMD无法携带service cron start,会被视为参数忽略或报错,而最后一种启动cron的语句如下CMD service cron start && tail -n 1 -f somefile,其中tail语句是为了让容器在前台运行而不会自动退出.但是使用docker exec 进入容器就可以发现,mysql根本没有运行.而如果想在CMD里面同时启动cron和mysql就更麻烦了,几乎不可能(因为有很多mysql的初始化工作要做,这些本来是docker-entrypoint帮你做好的),而且这样就失去了使用docker-entrypoint.sh的意义,得不偿失.

3. ENTRYPOINT

ENTRYPOINT每个Dockerfile文件可以有多条,但一个镜像只有最后一条ENTRYPOINT有效
MySQL官方镜像的ENTRYPOINT是执行docker-entrypoint.sh,所以这一句是改不了的,要改不如直接改docker-entrypoint.sh.

以上,可以看出来,想要在Dockerfile里执行service cron start是不可能的,唯一的办法在docker-entrypoint.sh文件.

4. docker-entrypoint.sh

该文件的详细解读网上有比较多,可以直接把执行语句加在这里,毕竟这个文件就是来做这种事的,这也确实是个实现思路.但这么做修改了原来的文件,很不优雅,不符合开闭原则,还好MySQL镜像的设计者考虑到了这点,留下了个/docker-entrypoint-initdb.d文件夹
2021年更新:/docker-entrypoint-initdb.d只有在数据库空白(即第一次启动且挂载目录为空)的时候才会执行初始化。如果将执行语句放这里会导致容器重启后cron执行语句失效,参考 https://github.com/linshenkx/mysql-cron/issues/1 。现已更改为使用自定义ENTRYPOINT脚本,再在脚本的最后调用docker-entrypoint.sh。
docker-entrypoint中相关语句如下

1
2
3
for f in /docker-entrypoint-initdb.d/*; do
process_init_file "$f" "${mysql[@]}"
done

如上,会遍历/docker-entrypoint-initdb.d中的每个文件作为参数调用process_init_file方法,该方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# usage: process_init_file FILENAME MYSQLCOMMAND...
# ie: process_init_file foo.sh mysql -uroot
# (process a single initializer file, based on its extension. we define this
# function here, so that initializer scripts (*.sh) can use the same logic,
# potentially recursively, or override the logic used in subsequent calls)
process_init_file() {
local f="$1"; shift
local mysql=( "$@" )

case "$f" in
*.sh) echo "$0: running $f"; . "$f" ;;
*.sql) echo "$0: running $f"; "${mysql[@]}" < "$f"; echo ;;
*.sql.gz) echo "$0: running $f"; gunzip -c "$f" | "${mysql[@]}"; echo ;;
*) echo "$0: ignoring $f" ;;
esac
echo
}

如上,需要执行的shell脚本或者sql脚本,可放到/docker-entrypoint-initdb.d/目录下,如果是sql.gz的压缩包,容器会自动解压再执行.
需要注意的是,只能保证遍历全部执行而不保证顺序,所以有顺序需求的需要利用脚本自己实现.

很明显,cron的启动脚本应该放在这个文件夹下

(2) sh脚本文件须为Unix格式(Windows下默认为dos),同时注意提供可执行权限

dos转unix格式通常有以下几种方式:

  1. windows:使用notepad++
    依次点击”编辑”->”文档格式转换” ->”转换为UNIX格式”
  2. linux:使用vim
    在命令模式输入set ff=unix回车即可,set ff可以查看格式
  3. linux:使用dos2unix
    dos2unix后直接加文件名即可

Dockerfile实现MySQL定时备份
https://linshenkx.github.io/mysql-cron-docker/
作者
John Doe
发布于
2018年9月9日
许可协议