2
头图

场景需求

大多数企业的数据库都有备份需求。然而,运维人员常见的做法是编写一个shell脚本通过xtrabackup这个mysql数据库备份工具,结合linux的crontab这个定时任务命令,实现周期性地对数据库进行备份,如果不监控这些数据库每次的备份是否成功,很可能会在后期的工作中出现问题。比如xtrabackup 加了--safe-slave-backup和--ftwrl选项,在备份失败后可能引起主从中断且不自动恢复。
因此,今天围绕这个话题,介绍一下如何改造prometheus的exporter插件来监控mysql和mongodb的备份状态。

方案设计

我们现有在用的应用日志系统有sentry,loki,elasticsearch等。经过调研,这些虽然提供了丰富的多语言支持,错误分析功能强大。但不足之处在于,这些日志系统的告警方式仅有邮件通知,无法发送短信。若要开通,还将涉及复杂的接口测试。而我们的数据库的备份日志中,备份状态只是xtrabackup执行后返回值,接入这些日志系统无异于高射炮打蚊子。
最终回到mysql本身,现有情况是,每个实例都做了exporter+prometheus+grafana的监控方案。因此,在原生的exporter插件基础上,增加备份状态的监控指标,才是代价最小的方案。

实现过程

项目地址为mysqld_exporter-withBackup,基于prometheus的mysqld_exporter v0.15.0改造,以下进行详细说明。
在collector/目录下新建一个文件backup_status.go,作为我们添加的指标收集器,其主要内容如下。

const (
    // Subsystem.
    backup = "backup"
)

这是监控指标的子系统(subsystem)名称,比如我们curl http://[IP]:9104/metrics看到的输出,如下图所示,其中的'global_status'就是一个子系统。

// Metric descriptors.
var (
    backupStatusDesc = prometheus.NewDesc(
        prometheus.BuildFQName(namespace, backup, "status"),
        "Status of database backups.",
        []string{}, nil,
    )
)

这是指标描述符(metric descriptors),对应下图标黄部分

// ScrapeBackupStatus collects from `SHOW BINARY LOGS`.
type ScrapeBackupStatus struct{}

// Name of the Scraper. Should be unique.
func (ScrapeBackupStatus) Name() string {
    return "backup_status"
}

这种写法实际就是为结构体(ScrapeBackupStatus)定义了一个方法(Name),参考:how-to-add-a-method-to-struct-type-in-golang
这里的name()返回值就是scraper的名称,它是指标的一部分,跟在上文提到的subsystem之后,对应下图标示部分,

后面的几个Help(),Version(),Scrape()同理,都是结构体的方法。

// Help describes the role of the Scraper.
func (ScrapeBackupStatus) Help() string {
    return "Collect the status of database backups"
}

Help()的返回值,对应mysqld_exporter --help的以下输出

// Version of MySQL from which scraper is available.
func (ScrapeBackupStatus) Version() float64 {
    return 5.1
}

Version()的返回值,定义了mysql的最低版本,版本过低会报错。

// Scrape collects data from database connection and sends it over channel as prometheus metric.
func (ScrapeBackupStatus) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error {
    var status float64
    var delblank string
    filename := "/tmp/mysqlBakStatus.txt"
    content, err := os.ReadFile(filename)
    if err != nil {
        status = 10000
        ch <- prometheus.MustNewConstMetric(
            backupStatusDesc, prometheus.GaugeValue, status,
        )

        return nil
    }
    delblank = strings.Replace(string(content), " ", "", -1)
    delnewline, _ := strconv.ParseFloat(strings.Replace(delblank,"\n", "", -1), 64)
    if delnewline == 1 {
        status = 1
    } else {
        status = 0
    }
    ch <- prometheus.MustNewConstMetric(
        backupStatusDesc, prometheus.GaugeValue, status,
    )

    return nil

Scrape()就是采集器的核心实现,首先通过os.ReadFile读取本机的/tmp/mysqlBakStatus.txt文件,依据ReadFile的返回值判断:

  • 若为非空则读取失败,将状态值status设为10000,同时把status值传给ch <- prometheus.MustNewConstMetric来创建指标(代码其余部分会将此指标进行注册等后续步骤,此处无需过多关注。),然后结束方法;
  • 若为空则读取成功,继续剩余步骤。

对于读取到的内容,两次用strings.Replace方法,先删除空格,然后删除换行符。
然后将得到的值用strconv.ParseFloat进行浮点转换,得到的值存入状态值status,传给ch <- prometheus.MustNewConstMetric来创建指标。

// check interface
var _ Scraper = ScrapeBackupStatus{}

go语言中这种写法作用是编译时检查,确保这里的ScrapeBackupStatus的类型与Scraper接口一致。Scraper的定义在mysqld_exporter.go,如下图,

接下来,在Scraper定义中,添加上collector的方法名,并设置默认为关闭状态,

使用提示

  1. 首先,在备份脚本中添加判断备份是否成功的逻辑
    成功则echo "1" > /tmp/mysqlBakStatus.txt,
    失败则echo "0" > /tmp/mysqlBakStatus.txt
  2. 运行mysqld_exporter时需指定 --collect.backup_status标志
    mysqld_exporter --collect.backup_status
  3. 对应的metric指标为mysql_backup_status
    返回值及含义:0-Failed 1-Normal 10000-读取/tmp/mysqlBakStatus.txt文件失败

MongoDB的实现

mongodb_exporter的实现与mysqld_exporter类似,只是代码逻辑更加杂乱,没有很好地模块化,详细实现这里不再一一列举。项目地址为mongodb_exporter-withBackup,基于percona的mongodb_exporter v0.39.0改造。


Grainy
1 声望1 粉丝