Web站点和MySQL数据库的自动与远程备份:auto-backup w/ mysqldump and rsync on Ubuntu

起先给这篇日志起的名是《WordPress站点和数据库的自动、异地备份:……》,虽然前后也修改了几次,但还是读起来不够理想。再想了一下,实际上这篇日志内容不止适用于WordPress站点,甚至不止适用于Web站点,也可以当做其他重要资源的备份参考。还有呢,这篇日志是在《网络服务配置笔记:……》的配置基础上完成的,很多备份方式的理念和脚本也是根据那篇日志里所述内容来决定的。哦,对了,文末也曾说大概应该会写的内容还包括“SSL证书定期自动更新”,后来查到如果Certbot是采用官方的软件包系统安装的话,在一些系统上会默认设置定期自动更新SSL证书的计划任务,当然这也都是可以自己添加cron job来实现的。

注意:这篇内容,是为我方便所记录,有很多地方不涉及严格的考究和选择,代码部分亦可能存在纰漏,酌情参考,小心使用!

那么,结合在《网络服务配置笔记》中已经完成的工作,先讲讲这篇日志里所记录的备份工作的背景:

  • Web服务和MySQL服务、数据库存储都在同一台VPS上
  • 以TLD站点*.site.com为例,所属用户组grpSiteCom的用户负责该TLD站点所有子站点文件的维护,该站点文件存储在/var/www/site_com/下:
    • Web站点文件位于:/var/www/site_com/htdocs/
    • 各类log文件位于:/var/www/site_com/logs/
  • site.com及其子站点所使用MySQL数据库的用户的权限一般是根据数据库使用情况来确定的,可能不适合用作数据库备份

其次,需要讲明的是配置备份工作的目标:

  • 备份该TLD对应的Web站点文件,其他log等日志文件不备份
  • 备份该TLD所使用的MySQL数据库
  • 自动备份,定期备份,如按天、按周、按月等备份
  • 维持最近X天、周、月的备份,以免占用过多存储空间
  • 远程备份
  • 以尽可能安全的方式完成

无论是备份Web站点文件还是MySQL数据库,都可以从(至少)两个层面来完成:一是在操作系统层面完成备份工作,二是以专用的Web应用程序来完成。对于第二种方法来说,不仅需要部署额外的Web站点为这些Web应用程序提供访问界面,还需要Web服务器拥有所需要备份文件的读权限。前者会引入更多的不安全因素,如在phpMyAdmin的简易安装中还做了额外的加固,以WordPress站点为例,若采用备份类插件也会有潜在的漏洞[],此外,使用Web应用还不方便实现自动备份;而对后者来说,可能存在无法满足的情形。

选择在操作系统层面完成这里所想要的备份是最方便的了。那么,在Ubuntu系统上,考虑以这几点原则来部署备份:

  • 使用tar对Web站点文件进行存档,再加以压缩,如gzipxz
  • 使用MySQL Server自带的mysqldump对所需备份的MySQL数据库导出SQL文件,必要时对其进行压缩
  • 建立相应的cron条目,实现自动备份和定期备份的功能
  • 利用bash脚本实现对过期备份文件的清理功能
  • rsync从这台Web服务器与其他机器实现远程备份
  • 建立专用的用户,作为备份文件的所有者和用于rsync的SSH连接之用

结合上面讲明的备份工作背景、配置目标以及原则,下面记录配置备份的过程。

设置备份专用用户及备份文件夹

如果只做本地备份,那么用root账户完成文件打包、数据库导出等工作是完全可行和安全的。但考虑到远程备份,备份数据、甚至服务器内数据的安全性,就从这单一台Web服务器的安全性来决定,扩大到由Web服务器和远程备份服务器的安全性来决定了。而远程备份涉及到的两台服务器之间的通信,需要由账户验证来完成传输通道的建立。这里的“账户”并非必需是操作系统中的账户,如可以通过专用的FTP或FTPS服务器来实现远程备份,但如使用SFTP完成远程文件传输,则需要使用系统用户账户进行验证。因此,应当选择一个不具有超级权限的用户,甚至是可以满足工作要求的最低权限用户。

今次决定用rsync实现远程备份,无论在哪一端pull(拉取)或者push(推送)备份文件,都需要另一端的用户具有shell权限运行相应的rsync命令。此外,无论哪一端,也需要有用户的主目录(home directory),用于存储该用户的SSH密钥对(key pairs)和/或已验证主机的公钥(public key)。为什么不用密码登录呢?原因有两个:不安全,无法自动化。因此,在Ubuntu中正常建立一个用户就可以了:

# 建立专用于备份的用户
# 用户主目录/home/webbackup,默认bash shell
sudo adduser webbackup
# 锁定其密码登录(无法在控制台使用密码登录),不影响使用SSH密钥对验证登录
sudo passwd -l webbackup

其实Ubuntu系统中内置了一个名为“backup”的用户,是上源系统Debian中存在很久了的默认配置,给予的解释是用来做本地备份/恢复的。我们可以直接用这个已有的用户,但需要为其配置一个可用的shell。不过,这些内置用户有可能会在以后的发行版中有所变动(可是看起来又不怎么会变了),就有可能影响下面的备份工作流程,因此还是新建一个用户来的舒服。

接着建立一个专用来存放备份文件的目录。跟着上一篇日志,Web站点的根目录是/var/www/,既然是Web站点的备份,也就一齐放在这个目录下吧:

# 为TLD站点site.com建立备份文件夹
sudo mkdir -p /var/www/.webbackup/site_com

# 设置站点site.com备份文件夹的所有权、文件夹权限及setgid权限
sudo chown -R webbackup:grpSiteCom /var/www/.webbackup/site_com
sudo chmod 2750 /var/www/.webbackup/site_com

对Web站点文件存档并压缩

做这一点很简单,另外再加用打包文件时的日期作为站点备份文件名的一部分,并用xz进行压缩:

# 参数开关“-C {PATH}”指在路径“{PATH}”下执行,
# 这样得到的打包不会将从根“/”开始的“/var/www/site_com”包含在内
tar --xz -cf /var/www/.webbackup/site_com/site_com-$(date +%Y%m%d).tar.xz -C /var/www/site_com ./htdocs

需要注意的是,一般以root权限执行上面的命令,之后再更改所生成文件的所有者和读写权限。

对MySQL数据库导出SQL文件

对MySQL数据库的备份方法有很多种。使用Web应用自带的数据库备份工具,如果有提供的话,大概应该是最人性化、与Web应用兼容性最好的方法了。但可能不是很适合配置自动备份。MySQL官方提出了几种备份方法

  • 使用MySQL企业备份工具实现热备份:一听就不是免费的
  • 用MySQL Server所带的mysqldump备份:命令行程序,提供多个参数、选项控制备份行为
  • 直接备份数据库所在文件夹:适合采用MyISAM引擎的数据表,采用InnoDB引擎的数据表可能不合适
  • ……

mysqldump是备份MySQL数据库的常用的工具,而且方便控制,容易集成到脚本中,也提供了参数选项来读入MySQL配置文件加载/覆写命令行参数。很多VPS提供商的教程也提供了如何用mysqldump备份数据库,比如可以参考Linode这篇。而mysqldump的解释、用法、局限性,应当咨询MySQL官方的mysqldump说明文档及其用于备份的参考指南。至于这篇日志里,使用的命令和Linode教程中的基本相同:

# {MYSQL_DATABASE_NAME} 是数据库名
mysqldump --defaults-extra-file=/var/www/.webbackup/.script/my.cnf --single-transaction {MYSQL_DATABASE_NAME} > /var/www/.webbackup/site_com/{MYSQL_DATABASE_NAME}-$(date +%Y%m%d).sql

其中/var/www/.webbackup/.script/my.cnf文件中写入了具有所需权限的MySQL用户及其密码:

[client]
user = resu
password = ~drowssap~

这里所说的权限,是指mysqldump所使用的MySQL用户需要具有对数据库“{MYSQL_DATABASE_NAME}”中数据表进行相应备份操作的权限,应当根据数据库中数据表的引擎和备份策略来确定。另外,如果需要额外的数据库导出行为,也需要用户具有导出行为所需要的SQL执行权限。最后,设置该MySQL配置文件的所有者和权限,以保障安全:

sudo chown root:root /var/www/.webbackup/.script/my.cnf
sudo chmod 600 /var/www/.webbackup/.script/my.cnf

设立cron条目以实现自动按期备份

/etc/cron.d/目录下建立一个用于备份工作的cron job文件:

sudo vim /etc/cron.d/backup

并编辑其内容:

# 在每天凌晨4点备份MySQL数据库“TEST”
0 4 * * * root mysqldump --defaults-extra-file=/var/www/.webbackup/.script/my.cnf --single-transaction TEST > /var/www/.webbackup/site_com/TEST-$(date +%Y%m%d).sql

# 在每周第3天凌晨3点备份Web站点文件
0 3 * * 3 root tar --xz -cf /var/www/.webbackup/site_com/site_com-$(date +%Y%m%d).tar.xz -C /var/www/site_com ./htdocs

之后刷新或重启cron服务:

sudo service cron reload
# 或
sudo service cron restart

上面的定期自动任务是由root用户执行的,生成的备份文件也所属root用户,有必要更改备份文件的所有者和权限。以MySQL数据库备份为例,将备份、压缩与权限设置写在同一个bash脚本文件中/var/www/.webbackup/.script/mysql_backup_TEST.sh

#!/bin/bash

MYSQL_DBNAME="TEST"
MYSQL_CNF_FILE="/var/www/.webbackup/.script/my.cnf"
BAK_DOMAIN="site_com"
BAK_USER="webbackup"
BAK_GROUP="grpSiteCom"
BAK_PERM=660
BAK_DIR_BASE="/var/www/.webbackup/site_com"

MYSQLDUMP_EXEC="$(which mysqldump)"
BAK_FN_PART="${MYSQL_DBNAME}-$(date +%Y%m%d)"
BAK_PATH_SQL="${BAK_DIR_BASE}/${BAK_FN_PART}.sql"
BAK_PATH_TAR="${BAK_DIR_BASE}/${BAK_FN_PART}.tar.xz"

# mysqldump导出数据库为SQL文件
${MYSQLDUMP_EXEC} --defaults-extra-file=${MYSQL_CNF_FILE} --single-transaction ${MYSQL_DBNAME} > ${BAK_PATH_SQL}

# 以xz压缩SQL文件
tar --xz -cf ${BAK_PATH_TAR} -C ${BAK_DIR_BASE} ${BAK_PATH_SQL}
# 删除SQL文件
rm ${BAK_PATH_SQL}

# 设置所有者和权限
chown ${BAK_USER}:${BAK_GROUP} ${BAK_PATH_TAR}
chmod ${BAK_PERM} ${BAK_PATH_TAR}

编辑好之后,为脚本文件增加执行权限,也应当建议设置该脚本文件的所有者为root,权限不高于740。最后,更改之前设置的cron job条目为:

# 在每天凌晨4点备份MySQL数据库“TEST”、压缩并处理文件权限
0 4 * * * root /var/www/.webbackup/.script/mysql_backup_TEST.sh

利用脚本实现过期备份文件清理

由于备份文件名已经包含了生成文件的日期部分,那来筛选过期文件就变得很简单,直接对文件名排序就可以了。我不知道有什么好的小工具可以来用,就自己写了一个直接、简单且“丑陋”的脚本。直接加在上一节的shell文件后面就可以:

# 如果单另一个脚本,应顶头写“#!/bin/bash”,并复制所需变量在文件开头

# 只保留最新的3个备份文件
BAK_FILE_RECENT=3

BAK_FILE_LIST=$(ls "${BAK_DIR_BASE}/${MYSQL_DBNAME}-*.tar.xz | sort -V -r)
BAK_FILE_NUM=0
for BAK_FILE_PER in ${BAK_FILE_LIST}
do
	BAK_FILE_NUM=$(($BAK_FILE_NUM+1))
	if [ $BAK_FILE_NUM -le $BAK_FILE_RECENT ]; then
		continue
	fi
	rm ${BAK_FILE_PER}
done

配置远程备份服务器以实现远程备份

远程备份,和异地备份应该是不同的概念。不过,是不是远程备份的服务器和源服务器不在同一地,就可以称作是异地备份了?(划掉上一句,有空的时候查一查学习一下。)

上面的几部分中,在Web服务器上已经做好了本地备份的配置,也设置了专门用于备份的用户。这一小节来配置远程备份。简单的远程备份,(我想)也就是将备份文件由Web服务器传输到另一台远程服务器上(吧),一时间想得到的方法有:

  • FTP over TLS:或者称FTPS,是增加了TLS验证的FTP,和SFTP是不同的。
  • SCP:Secure copy protocol,利用SSH协议加密。
  • SFTP:SSH File Transfer Protocol,虽然也以FTP结尾,但与FTP/FTPS是不同的协议。

因为我也就用过这几个,还有很多其他的文件传输方法,可以咨询Wikipedia上关于多种文件传输方法的比较。上面这3种也都是安全的传输方法,但缺少一些类似“同步”的直接使用方式。虽然可以通过在Web服务器和远程服务器两端加以脚本实现“同步”也好、“增量备份”也好,但增加了很多的配置压力。因此,不得不提的是rsync恰恰可以满足我的需要:将Web服务器上的备份文件同步到远程服务器上,也就是说,保持两端的备份文件是一致的(,当然,也可以不同步文件的删除)。

为实现Web服务器和远程备份服务器两端的备份文件同步,也就是说,需要自动、定期的执行rsync任务,这可以通过设置cron任务来实现。此外,在同步这一过程中,很重要的一点是两端服务器的连接。在第一节中,已经在Web服务器上建立了备份专用的用户。考虑到密码登录因其不安全、无法自动验证登录,接着先配置由远程备份服务器到Web服务器的SSH密钥对验证。

还有,应当提前说的一点是,在远程备份服务器上,除了设置了日常管理的管理员用户之外,并无其他用户,服务器也无其他用途。因此,在这台服务器上以root用户完成备份文件同步,而同步过来的文件也属root用户所有,权限也暂时无需更改了。

差点有一点忘记讲,就是pull还是push的选择。对于从Web服务器发起文件传输到远程备份服务器的方式,也就是push(推送)的方式。而从远程备份服务器提起由Web服务器传输文件的方式,也就是pull(拉取)的方式。这两种方式都是可行的,对rsync来说本质上可能也没什么太大差异。不过,结合SSH的密钥验证过程,考虑下面的内容:

  • 如果采用push的方式,则需要由Web服务器发起SSH连接到远程备份服务器,需要在远程备份服务器上存储Web服务器中某一用于备份同步的用户的SSH公钥。也就是说,Web服务器将具有远程备份服务器部分资源的访问权限
  • 如果采用pull的方式,则发起SSH连接的方向相反,由远程备份服务器发起SSH连接到Web服务器,因此需要在Web服务器上存储远程备份服务器中用于备份同步的用户的SSH公钥。也就是说,远程服务器将具有Web服务器部分资源的访问权限

这样说起来可能还不是很清楚,不过看过这篇文章(竟然是……算了)之后,应该就可以给这两台服务器贴个标签了:相比我这个远程备份服务器,Web服务器是高危的。高危的意思也就是说,因为运行着不少服务器端软件、各种Web应用脚本,Web服务器更倾向于存在漏洞、容易被打开后门,而远程备份服务器没有其他的用途,相比之下安全的多。因此,在这个时候,对于最不安全的服务器,应给与其最少的可访问的机密资源。用最简单的白话说,就是如果rsync采用push方式实现备份同步,Web服务器则可以访问到远程备份服务器的部分资源,若是Web服务器被侵入,则远程备份服务器的那部分资源也一并暴露在外了。

通过这一段分析,应选择以pull方式用rsync完成备份文件的同步。即使Web服务器被攻陷,存储之上的远程备份服务器的用于备份同步的用户的SSH公钥,也并没有什么直接的使用价值。在之前已在Web服务器上建立了备份专用的用户webbackup的基础上,rsync远程备份同步的过程也即以下几步:

  • 由远程备份服务器以root身份打开SSH验证连接到Web服务器,登录用户为webbackup
  • 在Web服务器上webbackup用户的已认证密钥中,通过其中的远程服务器的root用户的公钥,与远程备份服务器(拥有该公钥对应的私钥)完成验证,为webbackup用户生成bash shell
  • 从该shell发送rsync命令和参数,在Web服务器端运行对应rsync完成同步

服务器域名及IP配置(前提)

与上一篇的域名解析配置原则保持一致,为两台服务器分配不作为Web服务使用的域名,但没必要再加CNAME解析了:

  • A记录:web1.servers.com => a.b.c.d
  • AAAA记录:web1.servers.com => [a:b:c:d::1]
  • A记录:bak1.servers.com => a.b.c.e
  • AAAA记录:bak1.servers.com => [a:b:c:e::1]

以上,Web服务器的域名为web1.servers.com,远程备份服务器的域名为bak1.servers.com

SSH密钥对验证配置

与密码方式登录相比,SSH密钥对是相对安全很多的方式,是很适合在Internet环境中使用的验证方式。同时,采用SSH密钥对验证登录,可以实现自动化的登录,也就可以实现从一台服务器到另一台服务器的自动任务了。文章[12]对SSH及其密钥对登录方式讲的很白话,实际上也是这一小节配置的实现基础。还有,文章[3]基本也就是本节内容了,除了有那么一点点不同。

远程备份服务器生成SSH密钥对

前面已经提到在远程备份服务器上除了sudoer之外没有其他用户,而且备份同步都由root完成(,但用一个专用的、低权限的用户似乎更好一些),因此,之后在远程备份服务器上的操作都以root运行。首先在远程备份服务器上为root生成SSH密钥对,如果在安装服务器时没有生成:

# 运行下面的命令,根据提示,要求输入passphrase时按Enter跳过,
# 即不使用passphrase对私钥(private key,或identification)加密
# 生成密钥对的路径,按照默认设置即可
ssh-keygen

完成之后,将会在root的主目录(一般是/root)下生成.ssh目录,存储SSH密钥对:

/root/.ssh/
        |- id_rsa           # SSH私钥(private key)或身份(identification)
        |- id_rsa.pub       # SSH公钥(public key),可根据情况自由分发

检查这个文件夹和两个文件的权限,可以看到已经是设置好的够安全的状态:

drwx------ root root .ssh/

-rw------- root root id_rsa
-rw-r--r-- root root id_rsa.pub

接下来就是将SSH公钥(文件id_rsa.pub中的内容)放置到Web服务器中webbackup用户的已验证密钥列表中了。

在Web服务器上添加SSH公钥

要在Web服务器上添加远程备份服务器root用户的SSH公钥有几种方法。除了在远程备份服务器上运行ssh-copy-id工具之外,其他方法一般都需要直接或间接编辑Web服务器上所需连接用户的已验证密钥列表,可以参考Digital Ocean上的这个教程

作为一个懒人,我还是用避免编辑文件的方法,也就是用ssh-copy-id工具来完成。这里有点不同的是,之前在Web服务器上建立webbackup用户之后,就直接禁止了其密码登录。因此,需要在使用ssh-copy-id工具之前打开该用户的密码登录:

# 在Web服务器端,解除密码登录限制
sudo passwd -u webbackup
# 为其设置密码,如果有必要的话
sudo passwd webbackup

之后就可以在远程服务器上操作,分发root用户的SSH公钥:

# 在远程备份服务器端,以root身份运行下面的命令,
# 根据提示接受到Web服务器(web1.servers.com)的连接,
# 再根据提示输入webbackup用户的密码,则完成了SSH公钥分发
ssh-copy-id webbackup@web1.servers.com

完成之后,从远程备份服务器以root身份,测试到Web服务器的SSH连接,预期是不需要输入密码即可建立SSH连接:

ssh webbackup@web1.servers.com

查看webbackup用户主目录的.ssh目录之下的authorized_keys文件,则应当看到其中某行是远程备份服务器的公钥(可能应以root@bak1结尾)。确定成功之后,在Web服务器上禁止用户webbackup的密码登录:

sudo passwd -l webbackup

至此,基础工作都做完了。

测试rsync运行效果并添加cron任务

远程备份服务器到Web服务器的SSH连接的自动验证做好之后,只需要在远程备份服务器上添加rsync的cron任务项,就可以实现备份文件的同步了。rsync的官方手册官方FAQUbuntu发行版(16.04 LTS)手册Ubuntu帮助Wiki,这些都是很好的帮助材料。

一条简单易行的同步命令,可以写作:

# 远程备份服务器上备份文件的存放目录:/var/webbackup
# -a, --archive   存档模式,相当于 -rlptgoD
# -r, --recursive 递归子目录
# -l, --links     复制符号链接
# -p, --perms     保持权限
# -t, --times     保持修改时间
# -g, --group     保持用户组所有者
# -o, --owner     保持用户所有者
# -D              相当于 --devices --specials
#     --devices   保持设备文件
#     --specials  保持特殊文件
# --delete        清除目标目录中,不存在于源中的文件/目录
# --exclude=PATT  同步时,从源中排除符合PATT规则的项目
rsync -a --delete --exclude=/.scripts/ webbackup@web1.servers.com:/var/www/.webbackup/ /var/webbackup

在写入cron job之前,先做好测试:

# 添加 --dry-run 参数,测试运行,不执行实际的同步
# 添加 -vv 参数,增加详细输出
rsync --dry-run -vv -a --delete --exclude=/.scripts/ webbackup@web1.servers.com:/var/www/.webbackup/ /var/webbackup

# 添加 --progress 参数,实际执行同步时,显示进度
# 添加 --stats 参数,提供文件传输统计
rsync --progress --stats -vv -a --delete --exclude=/.scripts/ webbackup@web1.servers.com:/var/www/.webbackup/ /var/webbackup

测试好之后,添加cron job

vim /etc/cron.d/rsync_webbackup

编辑这个文件:

# 在每3天的凌晨3点30分和13点30分,以root身份运行rsync远程pull同步
30 3,13 */3 * * root rsync -a --delete --exclude=/.scripts/ webbackup@web1.servers.com:/var/www/.webbackup/ /var/webbackup

完成编辑之后,刷新或重启cron服务。至此,远程备份就配置完成了,自动、定期、(应该是比较)安全,这些要求都满足了。


喔,终于把这一篇日志写完了。前两天写一会儿,读一会儿,就发现自己写下的文字真是……真是……真是前言不搭后语。今天的时间又不早了,睡一觉起来之后通读一遍,没有什么大问题后再发表。


看完了,基本上、应该是、可能是没什么问题了。如果有问题的话即使没有问题的话,也真是贻笑大方了。之后可能还需要记录就不是很成系统的内容了。但还想做或者了解的工作呢,还有SSH安全加固、操作系统的无人值守更新、备份文件同步到Dropbox或Google Drive等在线存储、增加Syncthing同步等。有空再玩吧。

最近这段时间,脑子实在不怎么工作,就连组织语言,比如“说话”,对我来说也成了一个很大的挑战。写日志也是想到很多,但化成文字却是很困难。还是随随便便耍嘴皮子比较容易,或者不说话。

Leave a Reply

Your email address will not be published. Required fields are marked *