【升职加薪秘籍】我在服务监控方面的实践(6)-业务维度的mysql监控

大家好,我是蓝胖子,关于性能分析的视频和文章我也大大小小出了有一二十篇了,算是已经有了一个系列,之前的代码已经上传到github.com/HobbyBear/performance-analyze,接下来这段时间我将在之前内容的基础上,结合自己在公司生产上构建监控系统的经验,详细的展示如何对线上服务进行监控,内容涉及到的指标设计,软件配置,监控方案等等你都可以拿来直接复刻到你的项目里,这是一套非常适合中小企业的监控体系。

在上一节我们是讲解了如何对应用服务进行监控,这一节我将会介绍如何对mysql进行监控,在传统监控mysql(对mysql整体服务质量的监控)的情况下,建立对表级别的监控,以及长事务,复杂sql的监控,并能定位到具体代码。

监控系列的代码已经上传到github

github.com/HobbyBear/easymonitor

无论是前文提到的 机器监控还是应用监控,我们都提到了四大黄金指标原则,对mysql 建立监控指标,我们依然可以从这几个维度去对mysql的指标进行分析。

四大黄金指标是流量,延迟,饱和度,错误数。

对于流量而言可以体现在mysql数据库操作的qps,数据库服务器进行流量大小。对于延迟,可以体现在慢查询记录上,饱和度可以用数据库的连接数,线程数,或者磁盘空间,cpu,内存等各种硬件资源来反映数据库的饱和情况。错误数则可以用,数据库访问报错信息来反应,比如连接不足,超时等错误。

由于我们是用的云数据库,上面提到的这些监控维度以及面板在云厂商那里其实都基本覆盖了,我称这些监控面板或者维度是数据库的传统监控指标。 这些指标能够反应数据库监控状况,但对于开发来讲,去进行问题排查还远远不足的,下面我讲下如果只有此类型的监控会有什么缺点以及我的解决思路。

传统监控指标痛点

在使用它们对mysql进行监控时当异常发生时,不是能很好的确定是哪部分业务导致的问题。比如,当你发现数据库的qps突然升高,但是接口qps比较低的时候,如何确定数据库qps升高的原因呢? 这中间存在的问题在于mysql的数据监控指标和应用服务代码逻辑没有很好的关联性,我们要如何去建立这种关联系?

答案就是建立表级别的监控,你可以发现传统的监控指标都是对mysql整体服务质量进行的监控,而应用业务逻辑代码本质上是对表进行操作,如果建立了表级别的监控,就能将业务与数据库监控指标联系起来。比如按表级别建立单个表的qps,当发现数据库整体的qps升高时,可以发现这是由于哪张表引起的,进而定位到具体业务,查看代码逻辑看看是哪部分逻辑会操作这张表那么多次。

下面我们就来看看如何建立表级别的监控。

建立表级别的监控

mysql的performance schema其实已经暴露了表级别的某些监控项,不过由于某些原因我们线上并没有开启它,并且由于直接使用performance schema暴露的监控指标不能定制化,所以我将介绍一种在应用服务端埋点的方式建立表级别的qps。我们生产上是golang的应用服务,所以我会用它来举例。

用github.com/go-sql-driver/mysql 在golang中开启一个mysql连接是这样做:

db, err = sql.Open("mysql", connStr)

sql.Open的第一个参数是驱动名,默认的驱动名是mysql,这个驱动是引入github.com/go-sql-driver/mysql时自动去创建的。

func init() {  
   sql.Register("mysql", &MySQLDriver{})  
}

所以,我们完全可以包装默认驱动,自定义一个自己的驱动,驱动实现了open接口返回一个连接Conn的接口类型。

type Driver interface {  
  Open(name string) (Conn, error)  
}

type Conn interface {
Prepare(query string) (Stmt, error)
Close() error
Begin() (Tx, error)
}

type Stmt interface {
Close() error
NumInput() int
Exec(args []Value) (Result, error)
Query(args []Value) (Rows, error)

}

自定义的驱动类型在实现Open方法时,也可以自定义一个Conn连接类型,然后再实现它的查询接口,进行sql语句分析,解析表名后进行埋点统计。

完整的定义驱动代码已经上传到文章开头的github地址,总之,你需要明白,通过对默认驱动的包装,我们可以在sql执行前后做一些自定义的监控分析。我们定义了3个钩子函数,分别针对sql执行前后以及报错做了监控分析。

对于sql埋点的原理更详细的讲解可以参考go database sql接口分析及sql埋点实现

// sql执行前做监控
if ctx, err = stmt.hooks.Before(ctx, stmt.query, list...); err != nil {  
   return nil, err  
}  
// sql执行  
results, err := stmt.execContext(ctx, args)  
if err != nil { 
   // sql 报错时做监控
   return results, handlerErr(ctx, stmt.hooks, err, stmt.query, list...)  
}
// sql执行后做监控
if _, err := stmt.hooks.After(ctx, stmt.query, list...); err != nil {  
   return nil, err  
}

在sql执行前,通过SqlMonitor.parseTable对sql语句的分析,解析出当前sql涉及的表名,以及操作类型,是insert,select,delete,还是update,并且如果sql涉及到了多张表,那么会对其打上MultiTable的标签(这在下面讲sql审计时会提到),sql执行前的钩子函数如下所示:

func (h *HookDb) Before(ctx context.Context, query string, args ...interface{}) (context.Context, error) {  
   ctx = context.WithValue(ctx, ctxKeyBeginTime, time.Now())  

   // 得到sql涉及的表名,以及这条sql是属于什么crud的哪种类型
   tables, op, err := SqlMonitor.parseTable(query)  
   if err != nil || op == Unknown {  
      log.WithError(err).WithField(ctxKeySql, query).WithField("op", op).Error("parse sql fail")  
   }  
   if len(tables) >= 2 {  
      ctx = context.WithValue(ctx, ctxKeyMultiTable, 1)  
   }  
   if len(tables) >= 1 {  
      ctx = context.WithValue(ctx, ctxKeyTbName, tables[0])  
   }  
   if op != Unknown {  
      ctx = context.WithValue(ctx, ctxKeyOp, op)  
   }  
   return ctx, nil  
}

分析出了表名并且记录上了sql的执行时长,我们可以利用prometheus的histogram 类型的指标建立表维度的p99延迟分位数,并且能够知道表级别的qps数量,如下,我们可以在sql执行后的钩子函数里完成统计,MetricMonitor.RecordClientHandlerSeconds封装了这个逻辑。

func (h *HookDb) After(ctx context.Context, query string, args ...interface{}) (context.Context, error) {  
  // ....
   if tbnameInf := ctx.Value(ctxKeyTbName); tbnameInf != nil && len(tbnameInf.(string)) != 0 {  
      tableName = tbnameInf.(string)  
      MetricMonitor.RecordClientHandlerSeconds(TypeMySQL, string(ctx.Value(ctxKeyOp).(SqlOp)), tbnameInf.(string), h.dbName, now.Sub(beginTime).Seconds())  
      // .....
   }

我们可以利用这个指标在grafana上完成表级别的监控面板。

Pasted image 20230803181121.png

对于数据库还有要需要注意的地方,那就是长事务和复杂sql,慢sql的监控,往往出现上述情况时,就容易出现数据库的性能问题。现在我们来看看如何监控它们。

长事务监控

首先,来看下长事务的监控,我们可以为连接类型实现BeginTx方法,对原始driver.ConnBeginTx 事务类型进行包装,让事务携带上开始时间。

func (conn *Conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {  
   tx, err := conn.Conn.(driver.ConnBeginTx).BeginTx(ctx, opts)  
   if err != nil {  
      return tx, err  
   }  
   return &DriveTx{tx: tx, start: time.Now()}, nil  
}

接着,在提交事务的时候,判断时间是不是超过某个8s,超过了则记录一条错误日志,并把堆栈信息也打印出来,这样方便定位是哪段逻辑产生的长事务。由于我们的错误等级的日志会被收集起来自动报警,这样就完成了长事务的实时监控报警。

func (d *DriveTx) Commit() error {  
   err := d.tx.Commit()  
   d.cost = time.Now().Sub(d.start).Milliseconds()  
   if d.cost > 8000 {  
      data := log.Fields{  
         Cost:       d.cost,  
         MetricType: "longTx",  
         Stack:      fmt.Sprintf("%+v", getStack()),  
      }  
      log.WithFields(data).Errorf("mysqlongTxlog ")  
   }  
   return err  
}

sql审计

接着,我们看下sql审计如何做,mysql可以打开sql审计的配置项,不过打开后将会采集所有执行的sql语句,这样会导致sql太多,我们往往只用关心那些影响性能的sql或者让数据产生变化的sql。

代码如下,我们在sql完成执行后,通过sql的执行时长,对慢sql进行告警出来,并且对涉及到两个表的sql进行日志打印,也会对修改数据的sql语句(insert,update,delete)进行记录,这对我们排查业务数据会很有帮助。

func (h *HookDb) After(ctx context.Context, query string, args ...interface{}) (context.Context, error) {  
   beginTime := time.Now()  
   if begin := ctx.Value(ctxKeyBeginTime); begin != nil {  
      beginTime = begin.(time.Time)  
   }  
   now := time.Now()  
   tableName := ""  
   if tbnameInf := ctx.Value(ctxKeyTbName); tbnameInf != nil && len(tbnameInf.(string)) != 0 {  
      tableName = tbnameInf.(string)  
      MetricMonitor.RecordClientHandlerSeconds(TypeMySQL, string(ctx.Value(ctxKeyOp).(SqlOp)), tbnameInf.(string), h.dbName, now.Sub(beginTime).Seconds())
   }  
   // 对慢sql进行实时监控,超过1s则认为是慢sql
   slowquery := false  
   if now.Sub(beginTime).Seconds() >= 1 {  
      slowquery = true  
      data := log.Fields{  
         Cost:        now.Sub(beginTime).Milliseconds(),  
         "query":     truncateKey(1024, query),  
         "args":      truncateKey(1024, fmt.Sprintf("%v", args)),  
         MetricType:  "slowLog",  
         "app":       h.app,  
         "dbName":    h.dbName,  
         "tableName": tableName,  
         "op":        ctx.Value(ctxKeyOp),  
      }  
      log.WithFields(data).Errorf("mysqlslowlog")  
   }  
   op := ctx.Value(ctxKeyOp).(SqlOp)  
   multitable := ctx.Value(ctxKeyMultiTable)  
   if !slowquery && (multitable != nil && multitable.(int) == 1) && op == Select {  
      //  对复杂sql进行监控,如果不是慢sql,但是sql涉及到两个表也会日志进行记录
      data := log.Fields{  
         Cost:        now.Sub(beginTime).Milliseconds(),  
         "query":     truncateKey(1024, query),  
         "args":      truncateKey(1024, fmt.Sprintf("%v", args)),  
         MetricType:  "multiTables",  
         "app":       h.app,  
         "dbName":    h.dbName,  
         "tableName": tableName,  
         "op":        ctx.Value(ctxKeyOp),  
      }  
      log.WithFields(data).Warnf("mysqlmultitableslog")  
   }  
   // 对修改数据的sql进行日志记录  
   if op != Select && op != Unknown {  
      data := log.Fields{  
         Cost:        now.Sub(beginTime).Milliseconds(),  
         "query":     truncateKey(1024, query),  
         "args":      truncateKey(1024, fmt.Sprintf("%v", args)),  
         MetricType:  "oplog",  
         "app":       h.app,  
         "dbName":    h.dbName,  
         "tableName": tableName,  
         "op":        ctx.Value(ctxKeyOp),  
      }  
      log.WithFields(data).Infof("mysqloplog")  
   }  
   return ctx, nil  
}

总结

这一节我们完成了对mysql的监控,不过这个监控指标是在传统数据库监控项基础上建立的,目的是为了让监控指标更加容易反映到业务上,方便问题定位,在下一节我将会演示如何对redis进行监控,与mysql监控类似,我们也需要从业务维度思考对redis的监控。


http://www.niftyadmin.cn/n/4959912.html

相关文章

鹅厂涨薪开启,其它大厂会跟风吗?

从22年开始至今,网络上传言最多的就是“某某公司裁员了百分之多少的员工或是什么大幅度降薪等操作”,可谓是哀鸿遍野,每个人都在祈祷:“下一个被裁的不要是自己(老天爷保佑)”。 网友们吐槽:这些…

【golang】关于指针的有限操作

传统意义上来说,指针是一个指向某个确切的内存地址的值。这个内存地址可以是任何数据或代码的起始地址。在Go语言中有几种东西可以代表"指针"。其中最贴切传统意义的当属uintptr类型的了。该类型实际上是一个数值类型,也是Go语言内建的数据类型…

编写接口文档示例:从零开始,轻松掌握关键技巧

接口文档的编写是软件开发中至关重要的一环,本文将详细介绍如何编写接口文档示例,为您揭示从基础知识到高级技巧的全过程。通过实用的指导和比喻,让您轻松掌握编写接口文档示例的艺术。 在现代软件开发中,编写接口文档示例是确保项…

安防视频监控平台EasyNVR平台启用国标级联的操作步骤来啦!

安防视频监控汇聚EasyNVR视频集中存储平台,是基于RTSP/Onvif协议的安防视频平台,可支持将接入的视频流进行全平台、全终端分发,分发的视频流包括RTSP、RTMP、HTTP-FLV、WS-FLV、HLS、WebRTC等格式。 为提高用户体验,让用户更加便捷…

Shibboleth 笔记

目录 1. Shibboleth 笔记 1. Shibboleth 笔记 Shibboleth 是一个针对 SSO 的开源项目。Shibboleth 项目主要应用在校园内 Web 资源共享, 以及校园间的应用系统的用户身份联合认证。

Python实现SSA智能麻雀搜索算法优化Catboost回归模型(CatBoostRegressor算法)项目实战

说明:这是一个机器学习实战项目(附带数据代码文档视频讲解),如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 麻雀搜索算法(Sparrow Search Algorithm, SSA)是一种新型的群智能优化算法,在2020年提出&a…

ip地址的概念与构成

在今天的数字化时代,网络已经渗透到我们生活的方方面面。而IP地址(Internet Protocol Address)作为网络通信中不可或缺的一部分,扮演着连接世界各地设备和主机的重要角色。了解IP地址的概念和构成对于理解网络架构、数据传输和互联…

学习笔记|课后练习解答|电磁炉LED实战|逻辑运算|STC32G单片机视频开发教程(冲哥)|第八集(下):课后练习分析与解答

课后练习解答 增加按键3,按下后表示启动,选择的对应的功能的LED持续闪烁,表示正在工作,且在工作的时候无法切换功能。 需求分解 1 增加按键3 #define KEY3 P34 //增加按键32 按下后表示启动 电平控制3 工作状态锁定 表示正在…