如何在Ubuntu16.04上使用Percona将MySQL数据库备份到对象存储

来自菜鸟教程
跳转至:导航、​搜索

介绍

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

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

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

先决条件

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

  • 使用 Ubuntu 16.04 进行初始服务器设置:本指南将帮助您配置具有 sudo 权限的用户帐户并配置基本防火墙。
  • 以下 MySQL 安装指南之一: 如何在 Ubuntu 16.04 上安装 MySQL:使用 Ubuntu 团队提供和维护的默认包。 如何在 Ubuntu 16.04 上安装最新的 MySQL:使用 MySQL 项目提供的更新包。
  • 如何在 Ubuntu 16.04 上使用 Percona XtraBackup 配置 MySQL 备份:本指南使用 Percona XtraBackup 工具设置本地 MySQL 备份解决方案。

除了上述教程之外,您还需要生成访问密钥和秘密密钥,以使用 API 与您的对象存储帐户进行交互。 如果您使用的是 DigitalOcean Spaces,您可以按照我们的 如何创建 DigitalOcean Space 和 API 密钥 指南了解如何生成这些凭据。 您将需要保存 API 访问密钥和 API 机密值。

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

安装依赖项

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

刷新我们的本地包索引,然后使用 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

之后,我们可以安装 boto3pytz 模块,我们将使用对象存储 API 返回的偏移感知格式来准确比较时间:

sudo -H pip3 install boto3 pytz

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

创建对象存储配置文件

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

last guide 中,我们创建了 /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 本地备份脚本相同的功能,具有更基本的组织结构(因为不需要在本地文件系统上维护备份)以及上传到对象存储的一些额外步骤。

如果您没有从存储库下载脚本,请在 /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 [ "$(id --user --name)" != "$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 "$(date -d "${now}" "+%D")" +%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 [ "$(id --user --name)" != "$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),以便脚本可以尝试使用新的存储桶名称进行完整备份。 准备好后,重新运行上面的命令再试一次。


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

OutputUploaded /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
Outputbackup-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
OutputUploaded /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
Outputbackup-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

接下来,以 backup 用户的身份调用 download-day.sh 脚本。 输入您要下载的档案的日期。 日期格式相当灵活,但最好尽量明确:

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

如果有与您提供的日期匹配的存档,它们将被下载到当前目录:

OutputLooking 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
Outputfull-10-17-2017_19-09-30.xbstream  incremental-10-17-2017_19-19-20.xbstream

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

提取并准备备份

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

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

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

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

cd restore
sudo -u backup prepare-mysql.sh
OutputBackup 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,所以这次我们可以将文件移动到/tmp/mysql-remote

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

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

sudo mkdir /var/lib/mysql

现在,我们可以键入 prepare-mysql.sh 命令提供的 xtrabackup 恢复命令,将备份文件复制到 /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;'
Output+----+---------+-------+--------+
| 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 作业来进行远程备份,然后禁用本地备份作业。 通过启用或禁用 [X96X] 脚本,我们可以根据需要在本地和远程备份之间轻松切换。

首先,在 /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

请过几个小时再回来查看,以确保正在按计划进行额外的备份。

备份提取密钥

您必须处理的最后一个考虑因素是如何备份加密密钥(位于 /backups/mysql/encryption_key)。

恢复使用此过程备份的任何文件都需要加密密钥,但将加密密钥存储在与数据库文件相同的位置会消除加密提供的保护。 因此,将加密密钥的副本保存在单独的位置非常重要,以便在数据库服务器出现故障或需要重建时仍然可以使用备份存档。

虽然非数据库文件的完整备份解决方案不在本文的讨论范围内,但您可以将密钥复制到本地计算机以便妥善保管。 为此,请键入以下内容查看文件的内容:

sudo less /backups/mysql/encryption_key

在本地计算机上打开一个文本文件并将值粘贴到其中。 如果您需要将备份恢复到不同的服务器,请将文件的内容复制到新机器上的 /backups/mysql/encryption_key,设置本指南中概述的系统,然后使用提供的脚本进行恢复。

结论

在本指南中,我们介绍了如何每小时备份 MySQL 数据库并将它们自动上传到远程对象存储空间。 系统将每天早上进行一次完整备份,然后每小时进行一次增量备份,以提供恢复到任何每小时检查点的能力。 每次备份脚本运行时,它都会检查对象存储中超过 30 天的备份并将其删除。