文章目录
前言
数据库备份与恢复是数据库管理中最重要的方面之一。如果数据库崩溃后却没有办法恢复,那么对企业造成的毁灭性结果可能会是数据丢失、收入减少、客户不满等。不管公司是使用单个数据库还是多个数据库来存储数百 GB 或 TB 的数据,它们都有一个共同点,即需要有一个备份与恢复方案或脚本来备份重要数据并使自身免于灾难。
以下脚本主要用于全量备份,已验证MySQL-5.7版本可正常使用,如果了解golang语言,可以轻松看懂该脚本,有不对的地方请大佬们指教!!!
一、数据库备份
1、备份方式
数据库备份一般有逻辑备份与物理备份:
逻辑备份包含使用 mysqldump 命令导出并存储在二进制文件/SQL文件中的数据,只能还原至备份的时间点,例如mysqldump,操作过程中会锁表。
物理备份是物理数据库文件的副本,可以利用全备份加增量binlog备份还原至任意时间点,例如xtrabackup,操作过程中不会锁表。
2、备份工具
Percona Xtrabackup是一款针对MySQL数据库的开源、免费的热备工具,支持增量备份,压缩备份和流备份等,功能十分强大,
主要有有以下优点:
1.备份速度快,且可靠
2.备份过程中不会打断正在执行的事务
3.能够基于压缩等功能节约磁盘空间和流量
4.自动备份校验
5.备份还原的速度更快
6.可以使用流备,将备份传输到另一台机器
二、备份脚本解析
详细golang脚本见下载
0、前言
原本该脚本是一个简单的shell脚本,执行起来简单轻巧,且shell脚本可读性强。
为什么会写出一版golang形式的脚本?
主要是因为目前正在学习golang语言,为了更好的学习golang中的相关语法及golang的标准库使用方法,因此才对shell脚本进行了改写。
1、脚本目录详情
2、configs配置文件解读
程序在启动后,通过golang的os标准库首先读取到对应的配置文件,将其反序列化为struct结构体形式,并对yaml文件中的模板变量进行解析,详情见utils.go文件
代码如下(示例):
username: "xx"
password: "xxxx"
host: "xx.xx.xx.xxx"
mysql_port: "xx"
expired_day: "{{ .expired_day }}" #yaml文件中指定模板变量
current_day: "{{ .current_day }}"
backup_data_path: "/xx/xx/xx/xx/my{{ .mysql_port }}/xtrabackup/data"
backup_log_path: "/xx/xx/xx/xx/my{{ .mysql_port }}/xtrabackup/log"
backup_binlog_path: "/xx/xx/xx/xx/my{{ .mysql_port }}/xtrabackup/binlog"
backup_log_file: "full_backup_data_{{ .current_day }}.log"
backup_binlog_file: "full_backup_binlog_{{ .current_day }}.log"
binlog_pathfile_src: "/xx/xx/xx/my{{ .mysql_port }}/binlog"
innobackupex: "/xx/xx/innobackupex"
xtrbackup: "/xx/xx/xtrabackup"
mycnf: "/xx/xx/xx/my{{ .mysql_port }}/my.cnf"
mysock: "/xx/xx/xx/my{{ .mysql_port }}/run/mysqld.sock"
parallel: "4"
username | 数据库备份用户,需要对所有库表拥有权限 |
---|---|
password | 数据库备份用户密码 |
host | 数据库地址 |
mysql_port | 数据库端口 |
expired_day | 备份过期删除时间(即保留7天的备份) |
current_day | 当前时间 |
backup_data_path | 备份数据存放目录 |
backup_log_path | 备份日志存放目录 |
backup_binlog_path | binlog备份存放目录 |
backup_log_file | 备份日志存放位置 |
backup_binlog_file | binlog备份存放位置 |
binlog_pathfile_src | 生成binlog的位置 |
innobackupex | innobackupex备份工具命令位置(备份并未使用它) |
xtrbackup | xtrbackup备份工具命令位置 |
mycnf | 数据库配置文件存放位置 |
mysock | 数据库sock文件存放位置 |
parallel | 备份时指定多线程数量 |
3、Init初始化目录解读
该目录下的init.go文件主要用于备份前进行相关初始化操作,例如:数据库连接是否正常检测、yaml配置文件加载
首先导入标准库 go get database/sql
package Init
import (
"MySQL_BackUp/service"
"MySQL_BackUp/utils"
"database/sql"
"fmt"
"log"
"time"
)
// Init 函数用于初始化服务,包括检查数据库连接、yaml模板文件加载
func Init(yamlFilePath string) (*service.MySQLBackupService, error) {
// 加载基本配置(拿到yaml文件中的相关配置)
config, err := utils.LoadConfig(yamlFilePath, nil)
if err != nil {
log.Fatalf("failed to load config: %v", err)
}
/*
给yaml文件中的三个模板变量赋值
*/
// 获取当前日期
currentDay := time.Now().Format("2006-01-02")
// 假设过期日期是当前日期后7天
expiredDay := time.Now().AddDate(0, 0, 7).Format("2006-01-09")
data := map[string]string{
"current_day": currentDay,
"expired_day": expiredDay,
"mysql_port": config.MySQLPort,
}
// 再次加载配置,替换掉yaml中的模板变量 {{ . xxx }}
finalConfig, err := utils.LoadConfig(yamlFilePath, data)
if err != nil {
log.Fatalf("failed to open database connection: %v", err)
}
/*
1、检查数据库连接--连接数据库自带的mysql库验证即可
2、创建数据库对象
3、进行ping操作,验证数据库通信是否正常
*/
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s",
finalConfig.Username, finalConfig.Password, finalConfig.Host, finalConfig.MySQLPort, "mysql")
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("failed to open database connection: %v", err)
}
if err = db.Ping(); err != nil {
log.Fatalf("failed to ping database: %v", err)
}
log.Printf("mysql connect success: %v", dsn)
// 初始化 MySQLBackupService 实例,并返回该对象
}
这段代码主要引用了自己写的utils.go文件,针对yaml文件中的模板变量进行解析并赋值
4、utils工具目录解读
主要引用到了gopkg.in/yaml.v2、text/template标准库的用法,具体使用详情自行学习即可
package utils
import (
"MySQL_BackUp/models"
"bytes"
"fmt"
"os"
"text/template"
"gopkg.in/yaml.v2"
)
/*
用于解决yaml文件包含{{ .xxx }}这种模板变量的
主要思想就是读取 YAML 文件并将其内容解析为包含 结构体,然后结合"text/template" 标准库对其模板变量进行解析赋值
*/
// 用于解析和填充配置模板字符串
func ParseConfigTemplate(configStr string, data map[string]string) (string, error) {
/*
template.New("config") 创建一个新的模板对象,其名称为 "config"
Parse(configStr) 方法解析传入的模板字符串 configStr。
*/
tmpl, err := template.New("config").Parse(configStr)
if err != nil {
return "", err
}
/*
var buf bytes.Buffer 创建一个缓冲区,用于存储模板执行后的输出结果。
tmpl.Execute(&buf, data) 方法将模板与数据结合并将结果写入 buf 中。
&buf 是指向缓冲区的指针。
data 是包含要填充模板的数据的映射。
将缓冲区中的内容转换为字符串,并且返回结果和 nil 表示没有错误
*/
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}
// 读取yaml文件并进行模板加载
func LoadConfig(yamlFilePath string, data map[string]string) (*models.Config, error) {
// 读取yaml文件
configFile, err := os.ReadFile(yamlFilePath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
/*
yaml.Unmarshal(configFile, &config) 将 YAML 文件的内容反序列化为 models.Config 结构体。
如果反序列化过程中出现错误,则返回相应的错误信息。
*/
var config models.Config
err = yaml.Unmarshal(configFile, &config)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
// yaml文件中包含 {{ .xxx }} 模板变量的结构体字段
fields := []*string{
&config.ExpiredDay,
&config.CurrentDay,
&config.BackupPath,
&config.LogPath,
&config.BinlogPath,
&config.LogFile,
&config.BinlogFile,
&config.BinlogPathFileSrc,
&config.MyCnf,
&config.MySock,
}
/*
对于结构体中的每个字段,都使用 ParseConfigTemplate 函数处理其中可能包含的模板变量。
对字段的值进行了处理,并将处理后的值赋回到原始的结构体字段中。
如果处理过程中出现错误,则返回相应的错误信息。
*/
for _, field := range fields {
processedValue, err := ParseConfigTemplate(*field, data)
if err != nil {
return nil, fmt.Errorf("failed to process template: %w", err)
}
*field = processedValue
}
return &config, nil
}
5、models目录解读
package models
//与yaml文件中的变量一致,使用了结构体中的tag标签,将结构体解析为对应的yaml文件名称
type Config struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
Host string `yaml:"host"`
MySQLPort string `yaml:"mysql_port"`
ExpiredDay string `yaml:"expired_day"`
CurrentDay string `yaml:"current_day"`
BackupPath string `yaml:"backup_data_path"`
LogPath string `yaml:"backup_log_path"`
BinlogPath string `yaml:"backup_binlog_path"`
LogFile string `yaml:"backup_log_file"`
BinlogPathFileSrc string `yaml:"binlog_pathfile_src"`
BinlogFile string `yaml:"backup_binlog_file"`
Innobackupex string `yaml:"innobackupex"`
Xtrabackup string `yaml:"xtrbackup"`
MyCnf string `yaml:"mycnf"`
MySock string `yaml:"mysock"`
Parallel string `yaml:"parallel"`
}
// 定义相关操作方法存放到接口中,只有实现了接口中方法,才等于实现了该接口
type BackupService interface {
CheckAndCreateDirectories(directories []string, binlogFile string) error //创建目录
CheckXtarPid() bool //检查是否存在备份进程
CheckDevUsage() //检查磁盘使用率
BackupDatabase() //全量备份
BackupBinlog() //binlog备份
}
6、service目录解读
这个目录下主要实现了上述目录中的接口
package service
import (
"MySQL_BackUp/models"
"database/sql"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/shirou/gopsutil/disk"
)
// 定义结构体
type MySQLBackupService struct {
Config *models.Config
Db *sql.DB
}
// 检查备份路径是否存在,如果不存在则创建
func (b *MySQLBackupService) CheckAndCreateDirectories(directories []string) error {
for _, dir := range directories {
//获取目录信息,并判断目录是否存在,如果不存在则创建
if _, err := os.Stat(dir); os.IsNotExist(err) {
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
return fmt.Errorf("failed to create directory %s: %v", dir, err)
}
log.Printf("Directory %s created\n", dir)
} else {
log.Printf("Directory %s already exists\n", dir)
}
}
return nil
}
// 检查当前是否存在备份进程
func (b *MySQLBackupService) CheckXtarPid() bool {
//golang执行shell命令标准库 exec.Command("xx","xx")创建新的*cmd对象
cmd := exec.Command("pgrep", "-f", "xtra")
// 使用Output方法执行该命令并收集其输出
output, err := cmd.Output()
if err == nil && len(output) > 0 {
log.Printf("# 当前存在备份进程:\n%s", output)
return true
} else {
return false
}
}
//检查数据库存储备份数据文件的磁盘大小
func (b *MySQLBackupService) CheckDevUsage() {
//使用第三方disk库文件获取磁盘使用率
usageStat, err := disk.Usage(b.Config.BackupPath)
if err != nil {
log.Fatalf("Failed to get disk usage: %v", err)
}
devUsage := usageStat.UsedPercent
//判断磁盘使用率是否超过了80%
if devUsage >= 80 {
log.Printf("# disk dev_usage more than 80%%: %.2f\n", devUsage)
//遍历执行目录下的所有文件 filepath.Walk()函数本身具有递归调用功能
err := filepath.Walk(b.Config.BackupPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
//判断指定目录下的文件及文件日期
if !info.IsDir() && time.Since(info.ModTime()).Hours() > 24*7 {
if err := os.Remove(path); err != nil {
return err
}
}
return nil
})
if err != nil {
log.Fatalf("# failed to delete old backup files: %s\n", err.Error())
}
log.Printf("# delete backup files older than 7 days\n")
}
}
// 执行全量备份
func (b *MySQLBackupService) BackupDatabase() {
//判断备份文件是否存在,如果不存在则创建
log.Printf("# 开始全备份...")
backupFile := fmt.Sprintf("%s/full_%s.tar.gz", b.Config.BackupPath, time.Now().Format("20060102"))
file, err := os.Create(backupFile)
if err != nil {
log.Fatalf("# failed to create backup file: %s\n", err.Error())
}
defer file.Close()
/*
1、拼接日志文件路径
2、使用os.OpenFile()函数打开日志文件,如果不存在则创建
*/
logFilePath := filepath.Join(b.Config.LogPath, b.Config.LogFile)
logFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatalf("# failed to open log file: %s\n", err.Error())
}
defer logFile.Close()
/*
执行xtrbackup备份命令,并启用多线程
$xtrabackup --defaults-file=${mycnf} --user=$username --password=$passwd --slave-info --backup --lock-ddl-per-table --stream=tar 2>> $log_file | gzip - > ${backup_path}/full_${current_day}.tar.gz
*/
cmd := exec.Command(b.Config.Xtrabackup,
"--defaults-file="+b.Config.MyCnf,
"--user="+b.Config.Username,
"--password="+b.Config.Password,
"--slave-info",
"--backup",
"--lock-ddl-per-table",
"--stream=tar",
"--parallel="+b.Config.Parallel,
)
//执行gzip压缩命令
gzipCmd := exec.Command("gzip")
//因为原备份命令是多命令组合而成的,因此使用管道 cmd.StdoutPipe()
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
log.Fatalf("# failed to get stdout pipe: %s\n", err.Error())
}
defer stdoutPipe.Close()
//执行命令,并区分stdin、stdout 和 stderr
/*
这行代码将gzipCmd命令的标准输入(stdin)设置为另一个命令的标准输出管道(stdoutPipe)。
stdoutPipe是一个io.Reader,通常由前一个命令创建,用于从其标准输出读取数据。
这样做的目的是将前一个命令的输出直接作为gzipCmd的输入,实现数据流的连接。
*/
gzipCmd.Stdin = stdoutPipe
gzipCmd.Stdout = file //标准输出 到备份文件backupfile中
cmd.Stderr = logFile //备份命令标准错误 输出到日志文件full_backup_data_{{ .current_day }}.log
gzipCmd.Stderr = logFile //gzip压缩命令标准错误 输出到日志文件full_backup_data_{{ .current_day }}.log
/*
cmd.Run()和cmd.Start()的区别:
cmd.Run() 执行 Run 会立即阻塞当前goroutine 等待 5 秒种
Run() 方法适用于需要顺序执行外部命令
cmd.Start() 不用等命令执行完成,就结束, 然后在 Wait() 方法阻塞等待 5s
使用 Start() 方法适用于需要并发执行多个命令的情况,
wait()方法必须与start结合使用
*/
}
// 备份binlog
func (b *MySQLBackupService) BackupBinlog() {
log.Printf("# 开始增量备份binlog...")
binlogFilePath := filepath.Join(b.Config.BinlogPath, b.Config.BinlogFile)
binlogFile, err := os.OpenFile(binlogFilePath, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatalf("# failed to open binlog file: %s\n", err.Error())
}
defer binlogFile.Close()
// 函数写入日志
writeLog := func(message string) {
log.Println(message)
binlogFile.WriteString(message + "\n")
}
// 查找所有匹配的binlog文件,并按升序排序
cmd1 := exec.Command("sh", "-c", fmt.Sprintf("for i in $(find %s -name 'mysql-bin.*' | grep -v mysql-bin.index | sort -n | sed '$d'); do if [ ! -e %s/$(basename $i) ]; then cp -p $i %s; cp -p $i %s; fi; done", b.Config.BinlogPathFileSrc, b.Config.BinlogPath, b.Config.BinlogPath, b.Config.BinlogPath))
output1, err := cmd1.CombinedOutput()
if err != nil {
writeLog(fmt.Sprintf("# %s 备份binlog失败: %s\n%s", time.Now().Format("2006-01-02 15:04:05"), err.Error(), output1))
} else {
writeLog(fmt.Sprintf("# %s 备份binlog成功:\n%s", time.Now().Format("2006-01-02 15:04:05"), output1))
}
// 删除过期备份binlog数据,只保留一个备份周期7天的数据
deleteCmd := exec.Command("find", b.Config.BinlogPath, "-name", "mysql-bin.*", "-ctime", "+6", "-exec", "rm", "-f", "{}", ";")
output3, err := deleteCmd.CombinedOutput()
if err != nil {
writeLog(fmt.Sprintf("# %s 删除过期备份binlog数据失败: %s\n%s", time.Now().Format("2006-01-02 15:04:05"), err.Error(), output3))
} else {
writeLog(fmt.Sprintf("# %s 删除过期备份binlog数据成功:\n%s", time.Now().Format("2006-01-02 15:04:05"), output3))
}
writeLog("# 增量备份完毕...")
}
7、cmd目录解读
此处是程序的入口处
package main
import (
"MySQL_BackUp/Init"
"fmt"
"log"
"os"
"sync"
_ "github.com/go-sql-driver/mysql"
)
// 程序入口
func main() {
//命令行参数 ./xxxx config.yaml <-- yamlFilePath
if len(os.Args) < 2 {
fmt.Println("Usage: ./mysql_backup <yaml_file_path>")
os.Exit(1)
}
yamlFilePath := os.Args[1]
// 初始化服务
backupService, err := Init.Init(yamlFilePath)
if err != nil {
log.Fatalf("Initialization failed: %v", err)
}
//延迟关闭数据库
defer backupService.Db.Close()
//定义并检查BackupPath、LogPath、BinlogPath路径是否存在
directories := []string{backupService.Config.BackupPath, backupService.Config.LogPath, backupService.Config.BinlogPath}
if err := backupService.CheckAndCreateDirectories(directories); err != nil {
log.Fatalf("Failed to check and create directories: %v", err)
}
//检查是否存在备份进程
if backupService.CheckXtarPid() {
log.Println("# 备份程序退出~~")
os.Exit(1)
}
var wg sync.WaitGroup
var mu sync.Mutex
// 检查磁盘空间
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
defer mu.Unlock()
backupService.CheckDevUsage()
}()
// 全量备份数据库操作
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
defer mu.Unlock()
backupService.BackupDatabase()
}()
// 备份 binlog
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
defer mu.Unlock()
backupService.BackupBinlog()
}()
// 等待所有 goroutine 完成
wg.Wait()
log.Println("# 所有备份任务完成")
}
程序入口函数使用了goroutine、锁,主要为了确保备份脚本的多线程执行,提高备份速率,确保数据的准确性
8、Makefile解读
在Go语言的开发过程中,项目构建是一个关键环节,它涉及代码编译、打包、测试等多个步骤。 go build作为官方提供的命令行工具,提供了基本的构建功能,而Makefile则是一种更灵活的自动化构建脚本,适用于复杂项目的需求。
以下makefile文件中定义了构建完成后程序的二进制名称、指定了构建所需要的go相关命令、程序的入口目录、三个不同阶段实现的不同功能
build阶段
等价于执行go build xx.go 命令
clean阶段
清除掉构建后的二进制程序文件及多余的文件
run阶段
构建完成后执行,等价于go run main.go
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOGET=$(GOCMD) get
BINARY_NAME=mysql_backup
CONFIG_FILE=configs/config.yaml
# Directories
CMD_DIR=cmd
all: build
build: //
$(GOBUILD) -o $(BINARY_NAME) $(CMD_DIR)/main.go
rm -rf ./xtrabackup_backupfiles
clean:
$(GOCLEAN)
rm -rf $(BINARY_NAME)
rm -rf ./xtrabackup_backupfiles
run: build
./$(BINARY_NAME) $(CONFIG_FILE)
rm -rf ./xtrabackup_backupfiles
9、Scripts目录解读
主要是为了方便运维人员的可操作性,以及将输出到控制台的相关信息保存到对应的文件中,方便查看对应的日志文件,减少控制台不必要的输出
前提是linux服务器中必须要先安装gcc等相关命令,否则脚本会执行报错
#!/bin/bash
# 检查是否传递了参数
if [ "$#" -ne 1 ]; then
echo "Usage: $0 {build|clean|run}"
exit 1
fi
# 获取脚本所在的目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 推断 Makefile 所在的目录(假设 Makefile 在脚本目录的上一级)
MAKEFILE_DIR="$(dirname "$SCRIPT_DIR")"
# nohup.out 文件路径
NOHUP_FILE="$SCRIPT_DIR/nohup.out"
# 检查 nohup.out 文件是否存在,如果不存在则创建
if [ ! -f "$NOHUP_FILE" ]; then
touch "$NOHUP_FILE"
fi
# 根据参数执行相应的 make 命令
case "$1" in
build)
echo "===== Starting build at $(date) =====" >> "$NOHUP_FILE"
make -C "$MAKEFILE_DIR" build &>> $NOHUP_FILE
echo "===== Finished build at $(date) =====" >> "$NOHUP_FILE"
;;
clean)
echo "===== Starting clean at $(date) =====" >> "$NOHUP_FILE"
make -C "$MAKEFILE_DIR" clean &>> $NOHUP_FILE
echo "===== Finished clean at $(date) =====" >> "$NOHUP_FILE"
;;
run)
echo "===== Starting run at $(date) =====" >> "$NOHUP_FILE"
make -C "$MAKEFILE_DIR" run &>> $NOHUP_FILE
echo "===== Finished run at $(date) =====" >> "$NOHUP_FILE"
;;
*)
echo "Invalid option: $1"
echo "Usage: $0 {build|clean|run}"
exit 1
;;
esac
10、程序执行步骤解读
程序调用流程
main.go
-->Init.go
-->config.yaml
-->models.go
-->utils.go
-->models.go
-->service.go
-->launch.sh
1、将整个脚本目录放置在对应的服务器目录下,例如: /data/目录下
2、执行以下命令,前提是服务器已安装golnag环境,此处以golang 1.19环境为示例
cd /data/xx/
go mod init xxx目录名 #产生go,mod文件
go mod tidy #服务器将会下载程序中依赖的golang标准库
3、执行scritps目录下的脚本文件
cd /data/xx/scripts/
./launch.sh build #对golang程序进行构建
./launch.sh run #执行golang备份脚本程序
4、查看scripts目录下的nohup.out文件,查看执行命令是否有报错
三、备份脚本执行示例
1、服务器准备
可以ping通公网的vmware服务器,并提前安装好mysql-5.7版本、golang1.18版本
2、上传golang备份脚本至服务器目录
3、执行命令下载标准库
go mod init xx
go mod tidy
4、执行scripts目录下的脚本
`cd /xx/xx/scripts/ && ./launch.sh build`
./launch.sh run
查看scripts/nohup.out日志文件,无报错即备份成功
查看备份数据保存的目录是否已存在新的备份
至此整个备份程序演示完成!成功的对数据库进行了全量备份及备份日志做出了保存
总结
以上就是我今天分享的数据库备份脚本–golang脚本,结合当前自学的golang知识,对shell脚本进行了改写,主要是加深对golang的语法的理解以及标准库的学习,虽然实现了对应的功能,但是自我感觉整个代码可读性较差,且代码冗余浮肿,大家看看即可!