如何在Ubuntu 16.04上使用Percona将MySQL数据库备份到对象存储

数据库通常在您的基础架构中存储一些最有价值的信息。因此,在发生事故或硬件故障时,要有可靠的备份来防止数据丢失,这一点非常重要。 [Percona XtraBackup备份...

介绍

数据库通常在您的基础架构中存储一些最有价值的信息。 因此,在发生事故或硬件故障时,要有可靠的备份来防止数据丢失,这一点非常重要。

Percona XtraBackup备份工具提供了在系统运行时对MySQL数据进行“热”备份的方法。 他们通过在文件系统级复制数据文件然后执行崩溃恢复来实现数据集内的一致性。

之前的指南中 ,我们安装了Percona的备份实用程序,并创建了一系列脚本来执行旋转本地备份。 这适用于将数据备份到不同的驱动器或网络安装卷以处理数据库计算机的问题。 但是,在大多数情况下,数据应该在异地进行备份,以便维护和恢复。 在本指南中,我们将扩展先前的备份系统,将我们的压缩加密备份文件上传到对象存储服务。 我们将在本指南中使用DigitalOcean Spaces作为示例,但基本程序也可能适用于其他兼容S3的对象存储解决方案。

先决条件

在开始本指南之前,您将需要一个MySQL数据库服务器,配置上一个指南中概述的本地Percona备份解决方案。 你需要遵循的全套指南是:

除上述教程之外,还需要生成访问密钥和密钥,以便使用API​​与对象存储帐户进行交互。 如果您正在使用DigitalOcean Spaces,可以按照如何创建DigitalOcean空间和API密钥指南了解如何生成这些证书。 您将需要同时保存API访问密钥和API秘密值。

完成前面的指南后,请以sudo用户身份登录到服务器以开始使用。

安装依赖项

我们将使用一些Python和Bash脚本来创建我们的备份,并将它们上传到远程对象存储以便妥善保管。 我们将需要boto3 Python库来与对象存储API进行交互。 我们可以使用Python的包管理器来下载。

刷新我们的本地包索引,然后使用apt-get通过键入以下命令安装Ubuntu默认存储库中的Python 3版本的pip

sudo apt-get update
sudo apt-get install python3-pip

由于Ubuntu保持了自己的软件包生命周期,因此Ubuntu存储库中的pip版本不会与最近的版本保持同步。 不过,我们可以使用工具本身更新到更新版本的pip 我们将使用sudo进行全局安装,并包含-H标志以将$HOME变量设置为值pip

sudo -H pip3 install --upgrade pip

之后,我们可以安装boto3以及pytz模块,我们将使用pytz模块来准确地使用对象存储API返回的offset-aware格式进行比较:

sudo -H pip3 install boto3 pytz

现在我们应该拥有所有需要与对象存储API进行交互的Python模块。

创建一个对象存储配置文件

我们的备份和下载脚本将需要与对象存储API进行交互,以便在需要恢复时上载文件并下载较旧的备份工件。 他们将需要使用我们在先决条件部分中生成的访问密钥。 我们不是将这些值保存在脚本本身中,而是将它们放在一个专用文件中,这个文件可以被我们的脚本读取。 这样,我们就可以分享我们的脚本,而不必担心暴露我们的凭据,我们可以比脚本本身更重要地锁定凭据。

上一篇指南中 ,我们创建了/backups/mysql目录来存储备份和加密密钥。 我们将把配置文件放在这里和我们的其他资产一起。 创建一个名为object_storage_config.sh的文件:

sudo nano /backups/mysql/object_storage_config.sh

在里面,粘贴以下内容,将访问密钥和密钥更改为从对象存储帐户和存储桶名称获取的值为唯一值。 将端点URL和区域名称设置为您的对象存储服务提供的值(我们将在此处使用与DigitalOcean的NYC3区域相关的值):

/backups/mysql/object_storage_config.sh
#!/bin/bash

export MYACCESSKEY="my_access_key"
export MYSECRETKEY="my_secret_key"
export MYBUCKETNAME="your_unique_bucket_name"
export MYENDPOINTURL="https://nyc3.digitaloceanspaces.com"
export MYREGIONNAME="nyc3"

这些行定义了两个名为MYACCESSKEYMYSECRETKEY环境变量来分别保存我们的访问和密钥。 MYBUCKETNAME变量定义了我们想用来存储备份文件的对象存储桶。 存储桶名称必须是通用唯一的,所以您必须选择一个没有其他用户选择的名称。 我们的脚本将检查存储桶值,看看它是否已被另一个用户声称,如果可用,将自动创建它。 我们export我们定义的变量,以便我们在脚本中调用的任何进程都可以访问这些值。

MYENDPOINTURLMYREGIONNAME变量包含API端点和对象存储提供程序提供的特定区域标识符。 对于DigitalOcean空间,端点将为https:// region_name .digitaloceanspaces.com 您可以在DigitalOcean控制面板中找到Spaces的可用区域(在撰写本文时,只有“nyc3”可用)。

完成后保存并关闭文件。

任何可以访问我们的API密钥的人都可以完全访问我们的对象存储帐户,所以限制对配置文件的访问对于backup用户是非常重要的。 我们可以给文件的backup用户和组所有权,然后键入以下命令撤消所有其他访问:

sudo chown backup:backup /backups/mysql/object_storage_config.sh
sudo chmod 600 /backups/mysql/object_storage_config.sh

我们的object_storage_config.sh文件现在只能被backup用户访问。

创建远程备份脚本

现在我们有一个对象存储配置文件,我们可以继续开始创建我们的脚本。 我们将创建以下脚本:

  • object_storage.py :该脚本负责与对象存储API进行交互,以创建存储桶,上传文件,下载内容以及修剪较旧的备份。 我们的其他脚本会在需要与远程对象存储帐户交互时调用此脚本。
  • remote-backup-mysql.sh :这个脚本通过加密和压缩文件成一个工件,然后上传到远程对象存储来备份MySQL数据库。 它会在每天开始时创建一个完整备份,然后在每个小时之后进行一次增量备份。 它会自动修剪超过30天的远程存储桶中的所有文件。
  • download-day.sh :这个脚本允许我们下载与某一天相关的所有备份。 因为我们的备份脚本每天早上都会创建一个完整备份,然后全天进行增量备份,所以此脚本可以下载恢复到每小时检查点所需的所有资产。

随着上面的新脚本,我们将利用前面指南中的extract-mysql.shprepare-mysql.sh脚本来帮助恢复我们的文件。 您可以随时在GitHub上查看本教程存储库中的脚本。 如果你不想复制和粘贴下面的内容,你可以直接从GitHub下载新的文件:

cd /tmp
curl -LO https://raw.githubusercontent.com/do-community/ubuntu-1604-mysql-backup/master/object_storage.py
curl -LO https://raw.githubusercontent.com/do-community/ubuntu-1604-mysql-backup/master/remote-backup-mysql.sh
curl -LO https://raw.githubusercontent.com/do-community/ubuntu-1604-mysql-backup/master/download-day.sh

请务必在下载后检查脚本,确保它们已成功检索,并确认您将执行的操作。 如果您满意,请将脚本标记为可执行文件,然后通过键入以下命令将它们移动到/usr/local/bin目录中:

chmod +x /tmp/{remote-backup-mysql.sh,download-day.sh,object_storage.py}
sudo mv /tmp/{remote-backup-mysql.sh,download-day.sh,object_storage.py} /usr/local/bin

接下来,我们将设置每个脚本并更详细地讨论它们。

创建object_storage.py脚本

如果您没有从GitHub下载object_storage.py脚本,请在/usr/local/bin目录中创建一个名为object_storage.py的新文件:

sudo nano /usr/local/bin/object_storage.py

将脚本内容复制并粘贴到文件中:

/usr/local/bin/object_storage.py
#!/usr/bin/env python3

import argparse
import os
import sys
from datetime import datetime, timedelta

import boto3
import pytz
from botocore.client import ClientError, Config
from dateutil.parser import parse

# "backup_bucket" must be a universally unique name, so choose something
# specific to your setup.
# The bucket will be created in your account if it does not already exist
backup_bucket = os.environ['MYBUCKETNAME']
access_key = os.environ['MYACCESSKEY']
secret_key = os.environ['MYSECRETKEY']
endpoint_url = os.environ['MYENDPOINTURL']
region_name = os.environ['MYREGIONNAME']


class Space():
    def __init__(self, bucket):
        self.session = boto3.session.Session()
        self.client = self.session.client('s3',
                                          region_name=region_name,
                                          endpoint_url=endpoint_url,
                                          aws_access_key_id=access_key,
                                          aws_secret_access_key=secret_key,
                                          config=Config(signature_version='s3')
                                          )
        self.bucket = bucket
        self.paginator = self.client.get_paginator('list_objects')

    def create_bucket(self):
        try:
            self.client.head_bucket(Bucket=self.bucket)
        except ClientError as e:
            if e.response['Error']['Code'] == '404':
                self.client.create_bucket(Bucket=self.bucket)
            elif e.response['Error']['Code'] == '403':
                print("The bucket name \"{}\" is already being used by "
                      "someone.  Please try using a different bucket "
                      "name.".format(self.bucket))
                sys.exit(1)
            else:
                print("Unexpected error: {}".format(e))
                sys.exit(1)

    def upload_files(self, files):
        for filename in files:
            self.client.upload_file(Filename=filename, Bucket=self.bucket,
                                    Key=os.path.basename(filename))
            print("Uploaded {} to \"{}\"".format(filename, self.bucket))

    def remove_file(self, filename):
        self.client.delete_object(Bucket=self.bucket,
                                  Key=os.path.basename(filename))

    def prune_backups(self, days_to_keep):
        oldest_day = datetime.now(pytz.utc) - timedelta(days=int(days_to_keep))
        try:
            # Create an iterator to page through results
            page_iterator = self.paginator.paginate(Bucket=self.bucket)
            # Collect objects older than the specified date
            objects_to_prune = [filename['Key'] for page in page_iterator
                                for filename in page['Contents']
                                if filename['LastModified'] < oldest_day]
        except KeyError:
            # If the bucket is empty
            sys.exit()
        for object in objects_to_prune:
            print("Removing \"{}\" from {}".format(object, self.bucket))
            self.remove_file(object)

    def download_file(self, filename):
        self.client.download_file(Bucket=self.bucket,
                                  Key=filename, Filename=filename)

    def get_day(self, day_to_get):
        try:
            # Attempt to parse the date format the user provided
            input_date = parse(day_to_get)
        except ValueError:
            print("Cannot parse the provided date: {}".format(day_to_get))
            sys.exit(1)
        day_string = input_date.strftime("-%m-%d-%Y_")
        print_date = input_date.strftime("%A, %b. %d %Y")
        print("Looking for objects from {}".format(print_date))
        try:
            # create an iterator to page through results
            page_iterator = self.paginator.paginate(Bucket=self.bucket)
            objects_to_grab = [filename['Key'] for page in page_iterator
                               for filename in page['Contents']
                               if day_string in filename['Key']]
        except KeyError:
            print("No objects currently in bucket")
            sys.exit()
        if objects_to_grab:
            for object in objects_to_grab:
                print("Downloading \"{}\" from {}".format(object, self.bucket))
                self.download_file(object)
        else:
            print("No objects found from: {}".format(print_date))
            sys.exit()


def is_valid_file(filename):
    if os.path.isfile(filename):
        return filename
    else:
        raise argparse.ArgumentTypeError("File \"{}\" does not exist."
                                         .format(filename))


def parse_arguments():
    parser = argparse.ArgumentParser(
        description='''Client to perform backup-related tasks with
                     object storage.''')
    subparsers = parser.add_subparsers()

    # parse arguments for the "upload" command
    parser_upload = subparsers.add_parser('upload')
    parser_upload.add_argument('files', type=is_valid_file, nargs='+')
    parser_upload.set_defaults(func=upload)

    # parse arguments for the "prune" command
    parser_prune = subparsers.add_parser('prune')
    parser_prune.add_argument('--days-to-keep', default=30)
    parser_prune.set_defaults(func=prune)

    # parse arguments for the "download" command
    parser_download = subparsers.add_parser('download')
    parser_download.add_argument('filename')
    parser_download.set_defaults(func=download)

    # parse arguments for the "get_day" command
    parser_get_day = subparsers.add_parser('get_day')
    parser_get_day.add_argument('day')
    parser_get_day.set_defaults(func=get_day)

    return parser.parse_args()


def upload(space, args):
    space.upload_files(args.files)


def prune(space, args):
    space.prune_backups(args.days_to_keep)


def download(space, args):
    space.download_file(args.filename)


def get_day(space, args):
    space.get_day(args.day)


def main():
    args = parse_arguments()
    space = Space(bucket=backup_bucket)
    space.create_bucket()
    args.func(space, args)


if __name__ == '__main__':
    main()

此脚本负责管理对象存储帐户中的备份。 它可以上传文件,删除文件,修剪旧备份,并从对象存储下载文件。 我们的其他脚本不是直接与对象存储API交互,而是使用这里定义的功能与远程资源进行交互。 它定义的命令是:

  • upload :上传到对象存储每个作为参数传入的文件。 可能指定多个文件。
  • download :从远程对象存储器下载单个文件,该文件作为参数传入。
  • prune :从对象存储位置删除比特定年龄更早的每个文件。 默认情况下,这将删除超过30天的文件。 在调用prune时,可以通过指定--days-to-keep选项来调整它。
  • get_day :使用标准的日期格式(如果日期中有空格,使用引号)下载作为参数的日子,工具将尝试解析并从该日期下载所有文件。

该脚本尝试从环境变量中读取对象存储凭证和存储桶名称,因此我们需要在调用object_storage.py脚本之前确保从object_storage_config.sh文件填充这些凭证。

完成后,保存并关闭文件。

接下来,如果您尚未这样做,请通过输入以下命令来使脚本可执行:

sudo chmod +x /usr/local/bin/object_storage.py

既然object_storage.py脚本可以与API交互,我们可以创建Bash脚本来使用它来备份和下载文件。

创建remote-backup-mysql.sh脚本

接下来,我们将创建remote-backup-mysql.sh脚本。 这将执行许多与原始backup-mysql.sh本地备份脚本相同的功能,具有更基本的组织结构(因为不需要在本地文件系统上进行备份)以及一些上传到对象存储的附加步骤。

如果您未从存储库下载脚本, remote-backup-mysql.sh/usr/local/bin目录中创建并打开一个名为remote-backup-mysql.sh的文件:

sudo nano /usr/local/bin/remote-backup-mysql.sh

在里面,粘贴下面的脚本:

/usr/local/bin/remote-backup-mysql.sh
#!/bin/bash

export LC_ALL=C

days_to_keep=30
backup_owner="backup"
parent_dir="/backups/mysql"
defaults_file="/etc/mysql/backup.cnf"
working_dir="${parent_dir}/working"
log_file="${working_dir}/backup-progress.log"
encryption_key_file="${parent_dir}/encryption_key"
storage_configuration_file="${parent_dir}/object_storage_config.sh"
now="$(date)"
now_string="$(date -d"${now}" +%m-%d-%Y_%H-%M-%S)"
processors="$(nproc --all)"

# Use this to echo to standard error
error () {
    printf "%s: %s\n" "$(basename "${BASH_SOURCE}")" "${1}" >&2
    exit 1
}

trap 'error "An unexpected error occurred."' ERR

sanity_check () {
    # Check user running the script
    if [ "$USER" != "$backup_owner" ]; then
        error "Script can only be run as the \"$backup_owner\" user"
    fi

    # Check whether the encryption key file is available
    if [ ! -r "${encryption_key_file}" ]; then
        error "Cannot read encryption key at ${encryption_key_file}"
    fi

    # Check whether the object storage configuration file is available
    if [ ! -r "${storage_configuration_file}" ]; then
        error "Cannot read object storage configuration from ${storage_configuration_file}"
    fi

    # Check whether the object storage configuration is set in the file
    source "${storage_configuration_file}"
    if [ -z "${MYACCESSKEY}" ] || [ -z "${MYSECRETKEY}" ] || [ -z "${MYBUCKETNAME}" ]; then
        error "Object storage configuration are not set properly in ${storage_configuration_file}"
    fi
}

set_backup_type () {
    backup_type="full"


    # Grab date of the last backup if available
    if [ -r "${working_dir}/xtrabackup_info" ]; then
        last_backup_date="$(date -d"$(grep start_time "${working_dir}/xtrabackup_info" | cut -d' ' -f3)" +%s)"
    else
            last_backup_date=0
    fi

    # Grab today's date, in the same format
    todays_date="$(date -d"$(echo "${now}" | cut -d' ' -f 1-3)" +%s)"

    # Compare the two dates
    (( $last_backup_date == $todays_date ))
    same_day="${?}"

    # The first backup each new day will be a full backup
    # If today's date is the same as the last backup, take an incremental backup instead
    if [ "$same_day" -eq "0" ]; then
        backup_type="incremental"
    fi
}

set_options () {
    # List the xtrabackup arguments
    xtrabackup_args=(
        "--defaults-file=${defaults_file}"
        "--backup"
        "--extra-lsndir=${working_dir}"
        "--compress"
        "--stream=xbstream"
        "--encrypt=AES256"
        "--encrypt-key-file=${encryption_key_file}"
        "--parallel=${processors}"
        "--compress-threads=${processors}"
        "--encrypt-threads=${processors}"
        "--slave-info"
    )

    set_backup_type

    # Add option to read LSN (log sequence number) if taking an incremental backup
    if [ "$backup_type" == "incremental" ]; then
        lsn=$(awk '/to_lsn/ {print $3;}' "${working_dir}/xtrabackup_checkpoints")
        xtrabackup_args+=( "--incremental-lsn=${lsn}" )
    fi
}

rotate_old () {
    # Remove previous backup artifacts
    find "${working_dir}" -name "*.xbstream" -type f -delete

    # Remove any backups from object storage older than 30 days
    /usr/local/bin/object_storage.py prune --days-to-keep "${days_to_keep}"
}

take_backup () {
    find "${working_dir}" -type f -name "*.incomplete" -delete
    xtrabackup "${xtrabackup_args[@]}" --target-dir="${working_dir}" > "${working_dir}/${backup_type}-${now_string}.xbstream.incomplete" 2> "${log_file}"

    mv "${working_dir}/${backup_type}-${now_string}.xbstream.incomplete" "${working_dir}/${backup_type}-${now_string}.xbstream"
}

upload_backup () {
    /usr/local/bin/object_storage.py upload "${working_dir}/${backup_type}-${now_string}.xbstream"
}

main () {
    mkdir -p "${working_dir}"
    sanity_check && set_options && rotate_old && take_backup && upload_backup

    # Check success and print message
    if tail -1 "${log_file}" | grep -q "completed OK"; then
        printf "Backup successful!\n"
        printf "Backup created at %s/%s-%s.xbstream\n" "${working_dir}" "${backup_type}" "${now_string}"
    else
        error "Backup failure! If available, check ${log_file} for more information"
    fi
}

main

此脚本处理实际的MySQL备份过程,控制备份计划,并自动从远程存储中删除较旧的备份。 您可以通过调整days_to_keep变量来选择要备份多少天。

我们在上一篇文章中使用的本地backup-mysql.sh脚本为每一天的备份维护了不同的目录。 由于我们远程存储备份,因此我们只会在本地存储最新的备份,以尽量减少专用于备份的磁盘空间。 以前的备份可以根据恢复需要从对象存储中下载。

与前面的脚本一样,在检查了一些基本要求得到满足并且配置了应该采取的备份类型之后,我们将每个备份加密并压缩到单个文件存档中。 之前的备份文件将从本地文件系统中删除,并且所有比days_to_keep中定义的值更早的远程备份将被删除。

完成后保存并关闭文件。 之后,通过输入以下命令确保脚本是可执行的:

sudo chmod +x /usr/local/bin/remote-backup-mysql.sh

此脚本可用作该系统上backup-mysql.sh脚本的替代,以从本地备份切换到远程备份。

创建download-day.sh脚本

最后,在/usr/local/bin目录下载或创建download-day.sh脚本。 该脚本可用于下载与特定日期相关的所有备份。

如果您之前没有下载,请在文本编辑器中创建脚本文件:

sudo nano /usr/local/bin/download-day.sh

在里面,粘贴下面的内容:

/usr/local/bin/download-day.sh
#!/bin/bash

export LC_ALL=C

backup_owner="backup"
storage_configuration_file="/backups/mysql/object_storage_config.sh"
day_to_download="${1}"

# Use this to echo to standard error
error () {
    printf "%s: %s\n" "$(basename "${BASH_SOURCE}")" "${1}" >&2
    exit 1
}

trap 'error "An unexpected error occurred."' ERR

sanity_check () {
    # Check user running the script
    if [ "$USER" != "$backup_owner" ]; then
        error "Script can only be run as the \"$backup_owner\" user"
    fi

    # Check whether the object storage configuration file is available
    if [ ! -r "${storage_configuration_file}" ]; then
        error "Cannot read object storage configuration from ${storage_configuration_file}"
    fi

    # Check whether the object storage configuration is set in the file
    source "${storage_configuration_file}"
    if [ -z "${MYACCESSKEY}" ] || [ -z "${MYSECRETKEY}" ] || [ -z "${MYBUCKETNAME}" ]; then
        error "Object storage configuration are not set properly in ${storage_configuration_file}"
    fi
}

main () {
    sanity_check
    /usr/local/bin/object_storage.py get_day "${day_to_download}"
}

main

可以调用该脚本从特定的一天下载所有的档案。 由于每一天都从完整备份开始,并在一天的其余时间积累增量备份,因此这将下载恢复到每小时快照所需的所有相关文件。

该脚本采用一个日期或一天的单一参数。 它使用Python的dateutil.parser.parse函数来读取和解释作为参数提供的日期字符串。 该功能相当灵活,可以解释各种格式的日期,例如包括“Friday”之类的相关字符串。 为了避免含糊不清,最好使用更明确的日期。 如果您希望使用的格式包含空格,请务必使用引号括住日期。

准备好继续时,保存并关闭文件。 通过键入以下脚本来执行脚本:

sudo chmod +x /usr/local/bin/download-day.sh

我们现在有能力从对象存储中下载备份文件,以便在我们要恢复的特定日期。

测试远程MySQL备份和下载脚本

现在我们已经有了我们的脚本,我们应该测试以确保它们按预期运行。

执行完整备份

通过与backup用户调用remote-mysql-backup.sh脚本开始。 由于这是我们第一次运行这个命令,它应该创建MySQL数据库的完整备份。

sudo -u backup remote-backup-mysql.sh

注意:如果您收到一个错误,指出您选择的存储桶名称已被使用,您将不得不选择一个不同的名称。 /backups/mysql/object_storage_config.sh文件中更改MYBUCKETNAME的值,并删除本地备份目录( sudo rm -rf /backups/mysql/working ),以便脚本可以使用新的存储桶名称尝试完整备份。 准备好后,重新运行上面的命令重试。

如果一切顺利,您将看到类似于以下内容的输出:

Uploaded /backups/mysql/working/full-10-17-2017_19-09-30.xbstream to "your_bucket_name"
Backup successful!
Backup created at /backups/mysql/working/full-10-17-2017_19-09-30.xbstream

这表示已在/backups/mysql/working目录中创建了完整备份。 还使用object_storage_config.sh文件中定义的存储区将其上传到远程对象存储。

如果我们看一下/backups/mysql/working目录,我们可以看到类似于上一个指南中backup-mysql.sh脚本生成的文件:

ls /backups/mysql/working
backup-progress.log  full-10-17-2017_19-09-30.xbstream  xtrabackup_checkpoints  xtrabackup_info

backup-progress.log文件包含xtrabackup命令的输出,而xtrabackup_checkpointsxtrabackup_info包含有关所用选项,备份的类型和范围以及其他元数据的信息。

执行增量备份

让我们对我们的equipment表进行一些小改动,以创建我们第一次备份中找不到的额外数据。 我们可以在表格中输入一个新的行:

mysql -u root -p -e 'INSERT INTO playground.equipment (type, quant, color) VALUES ("sandbox", 4, "brown");'

输入数据库的管理密码以添加新记录。

现在,我们可以进行额外的备份。 当我们再次调用这个脚本的时候,只要它和上一次备份的时间一样,就应该创建一个增量备份(根据服务器的时钟):

sudo -u backup remote-backup-mysql.sh
Uploaded /backups/mysql/working/incremental-10-17-2017_19-19-20.xbstream to "your_bucket_name"
Backup successful!
Backup created at /backups/mysql/working/incremental-10-17-2017_19-19-20.xbstream

以上输出表明备份是在本地在同一目录中创建的,并再次上传到对象存储。 如果我们检查/backups/mysql/working目录,我们会发现新的备份已经存在,并且之前的备份已被删除:

ls /backups/mysql/working
backup-progress.log  incremental-10-17-2017_19-19-20.xbstream  xtrabackup_checkpoints  xtrabackup_info

由于我们的文件是远程上传的,因此删除本地副本有助于减少使用的磁盘空间量。

从指定的一天下载备份

由于我们的备份是远程存储的,如果我们需要恢复我们的文件,我们将需要下拉远程文件。 为此,我们可以使用download-day.sh脚本。

首先创建并移动到backup用户可以安全写入的目录中:

sudo -u backup mkdir /tmp/backup_archives
cd /tmp/backup_archives

接下来,调用download-day.sh脚本作为backup用户。 通过你想下载的档案的一天。 日期格式相当灵活,但最好尽量明确:

sudo -u backup download-day.sh "Oct. 17"

如果存档与您提供的日期相匹配,则会将其下载到当前目录:

Looking for objects from Tuesday, Oct. 17 2017
Downloading "full-10-17-2017_19-09-30.xbstream" from your_bucket_name
Downloading "incremental-10-17-2017_19-19-20.xbstream" from your_bucket_name

验证文件是否已经下载到本地文件系统:

ls
full-10-17-2017_19-09-30.xbstream  incremental-10-17-2017_19-19-20.xbstream

压缩的加密档案现在又回到服务器上。

提取和准备备份

一旦文件被收集,我们可以像处理本地备份一样处理它们。

首先,使用backup用户将.xbstream文件传递给.xbstream脚本:

sudo -u backup extract-mysql.sh *.xbstream

这将解密和解压档案到一个名为restore的目录。 输入该目录并使用prepare-mysql.sh脚本准备文件:

cd restore
sudo -u backup prepare-mysql.sh
Backup looks to be fully prepared.  Please check the "prepare-progress.log" file
to verify before continuing.

If everything looks correct, you can apply the restored files.

First, stop MySQL and move or remove the contents of the MySQL data directory:

        sudo systemctl stop mysql
        sudo mv /var/lib/mysql/ /tmp/

Then, recreate the data directory and  copy the backup files:

        sudo mkdir /var/lib/mysql
        sudo xtrabackup --copy-back --target-dir=/tmp/backup_archives/restore/full-10-17-2017_19-09-30

Afterward the files are copied, adjust the permissions and restart the service:

        sudo chown -R mysql:mysql /var/lib/mysql
        sudo find /var/lib/mysql -type d -exec chmod 750 {} \;
        sudo systemctl start mysql

现在应该准备/tmp/backup_archives/restore目录中的完整备份。 我们可以按照输出中的指示来恢复我们系统上的MySQL数据。

将备份数据还原到MySQL数据目录

在恢复备份数据之前,我们需要移除当前的数据。

首先关闭MySQL以避免数据库被破坏,或者在我们替换数据文件时崩溃服务。

sudo systemctl stop mysql

接下来,我们可以将当前的数据目录移动到/tmp目录。 这样,如果还原有问题,我们可以轻松地将其移回原处。 由于我们在上一篇文章中将文件移动到了/tmp/mysql-remote ,因此我们可以将这些文件移动到/tmp/mysql-remote

sudo mv /var/lib/mysql/ /tmp/mysql-remote

接下来,重新创建一个空的/var/lib/mysql目录:

sudo mkdir /var/lib/mysql

现在,我们可以输入xtrabackup命令提供的xtrabackup restore命令将备份文件复制到/var/lib/mysql目录中:

sudo xtrabackup --copy-back --target-dir=/tmp/backup_archives/restore/full-10-17-2017_19-09-30

一旦该过程完成,修改目录权限和所有权以确保MySQL进程有权访问:

sudo chown -R mysql:mysql /var/lib/mysql
sudo find /var/lib/mysql -type d -exec chmod 750 {} \;

完成后,再次启动MySQL并检查我们的数据是否已正确恢复:

sudo systemctl start mysql
mysql -u root -p -e 'SELECT * FROM playground.equipment;'
+----+---------+-------+--------+
| id | type    | quant | color  |
+----+---------+-------+--------+
|  1 | slide   |     2 | blue   |
|  2 | swing   |    10 | yellow |
|  3 | sandbox |     4 | brown  |
+----+---------+-------+--------+

数据可用,表示已成功恢复。

恢复数据后,返回并删除恢复目录很重要。 未来的增量备份无法应用于完整备份,因此我们应该删除它。 此外,出于安全原因,备份目录不应该保留在磁盘上不加密。

cd ~
sudo rm -rf /tmp/backup_archives/restore

下次我们需要备份目录的干净副本时,我们可以从备份存档文件中再次提取它们。

创建一个Cron作业来每小时运行一次备份

我们创建了一个cron作业来自动备份上一个指南中的本地数据库。 我们将设置一个新的cron作业来执行远程备份,然后禁用本地备份作业。 通过启用或禁用cron脚本,我们可以根据需要轻松地在本地和远程备份之间切换。

首先,在/etc/cron.hourly目录下创建一个名为remote-backup-mysql的文件:

sudo nano /etc/cron.hourly/remote-backup-mysql

在里面,我们将通过systemd-cat命令与backup用户调用我们的remote-backup-mysql.sh脚本,该命令允许我们将输出记录到journald

/etc/cron.hourly/remote-backup-mysql
#!/bin/bash 
sudo -u backup systemd-cat --identifier=remote-backup-mysql /usr/local/bin/remote-backup-mysql.sh

完成后保存并关闭文件。

我们将启用我们的新cron作业,并通过处理两个文件的executable权限位来禁用旧作业:

sudo chmod -x /etc/cron.hourly/backup-mysql
sudo chmod +x /etc/cron.hourly/remote-backup-mysql

通过手动执行脚本测试新的远程备份作业:

sudo /etc/cron.hourly/remote-backup-mysql

一旦提示返回,我们可以用journalctl检查日志条目:

sudo journalctl -t remote-backup-mysql
[seconary_label Output]
-- Logs begin at Tue 2017-10-17 14:28:01 UTC, end at Tue 2017-10-17 20:11:03 UTC. --
Oct 17 20:07:17 myserver remote-backup-mysql[31422]: Uploaded /backups/mysql/working/incremental-10-17-2017_22-16-09.xbstream to "your_bucket_name"
Oct 17 20:07:17 myserver remote-backup-mysql[31422]: Backup successful!
Oct 17 20:07:17 myserver remote-backup-mysql[31422]: Backup created at /backups/mysql/working/incremental-10-17-2017_20-07-13.xbstream

Check back in a few hours to make sure that additional backups are being taken on schedule.

Backing Up the Extraction Key

One final consideration that you will have to handle is how to back up the encryption key (found at /backups/mysql/encryption_key ).

The encryption key is required to restore any of the files backed up using this process, but storing the encryption key in the same location as the database files eliminates the protection provided by encryption. Because of this, it is important to keep a copy of the encryption key in a separate location so that you can still use the backup archives if your database server fails or needs to be rebuilt.

While a complete backup solution for non-database files is outside the scope of this article, you can copy the key to your local computer for safekeeping. To do so, view the contents of the file by typing:

sudo less /backups/mysql/encryption_key

Open a text file on your local computer and paste the value inside. If you ever need to restore backups onto a different server, copy the contents of the file to /backups/mysql/encryption_key on the new machine, set up the system outlined in this guide, and then restore using the provided scripts.

结论

In this guide, we've covered how take hourly backups of a MySQL database and upload them automatically to a remote object storage space. The system will take a full backup every morning and then hourly incremental backups afterwards to provide the ability to restore to any hourly checkpoint. Each time the backup script runs, it checks for backups in object storage that are older than 30 days and removes them.