大家好,我是蓝胖子,关于性能分析的视频和文章我也大大小小出了有一二十篇了,算是已经有了一个系列,之前的代码已经上传到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上完成表级别的监控面板。

对于数据库还有要需要注意的地方,那就是长事务和复杂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的监控。

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

  1. DB监控-mysql监控

    Mysql监控属于DB监控的模块之一,包括采集.展示.监控告警.本文主要介绍Mysql监控的主要指标和采集方法. Mysql监控和Redis监控的逻辑类似,可参考文章<Redis监控>. ...

  2. MYSQL进阶学习笔记十六:MySQL 监控!(视频序号:进阶_35)

    知识点十七:MySQL监控(35) 一.为什么使用MySQL监控 随着软件后期的不断升级,myssql的服务器数量越来越多,软硬件故障的发生概率也越来越高.这个时候就需要一套监控系统,当主机发生异常时 ...

  3. 工作不到一年,做出了100k系统,老板给我升职加薪

    看了下自己上一次发技术文还是在6月15日,算了算也是两个来月了.别怕,短暂的离开,是为了更好的相遇. 来到新公司以后啊,发现公司的搜索业务是真的太多了,大大小小有几百个搜索业务.来了之后得先梳理.熟悉 ...

  4. 不懂DevOps!他在升职加薪的那天下午,提出了离职

    不久前我们一个已毕业的学员向班主任老师分享了前几天他遇到的一件事: 一个许久未联系他的朋友突然打电话给他,寒暄了几句后突然说,想来北京找工作,问能不能帮忙给介绍一些工作. 在接下来的通话中,我们学员了 ...

  5. jmeter & 性能测试:从0到实战(实操易用、面试造火箭、升职加薪必备)

    [性能基础] 性能测试概念.术语:https://www.cnblogs.com/uncleyong/p/10706519.html 性能测试流程(新):https://www.cnblogs.com ...

  6. 一文搞懂秒杀系统,欢迎参与开源,提交PR,提高竞争力。早日上岸,升职加薪。

    前言 秒杀和高并发是面试的高频考点,也是我们做电商项目必知必会的场景.欢迎大家参与我们的开源项目,提交PR,提高竞争力.早日上岸,升职加薪. 知识点详解 秒杀系统架构图 秒杀流程图 秒杀系统设计 这篇 ...

  7. 改造断路器集群监控Hystrix Turbine实现自动注册消费者、实时监控多个服务

    在上一篇文章中,我们搭建了Hystrix Dashoard,对指定接口进行监控.但是只能对一个接口进行监听,功能比较局限: Turbine:汇总系统内多个服务的数据并显示到 Hystrix Dashb ...

  8. 《一头扎进》系列之Python+Selenium框架实战篇7 - 年底升职加薪,年终奖全靠它!Merry Christmas

    1. 简介 截止到上一篇文章为止,框架基本完全搭建完成.那么今天我们要做什么呢????聪明如你的小伙伴或者是童鞋一定已经猜到了,都测试完了,当然是要生成一份高端大气上档次的测试报告了.没错的,今天宏哥 ...

  9. &#128218;C#/.NET/.NET Core推荐学习书籍(升职加薪,你值得拥有)

    前言: 作为一名程序员,我们无时无刻都要考虑着如何通过不断地学习来提升自己的核心竞争力.古人有云:"书中自有黄金屋,书中只有颜如玉",说明了书籍的重要性,没错工作多年来,发现身边那 ...

  10. TDengine在浙商银行微服务监控中的实践

    作者:楼永红 王轩宇|浙商银行    浙商银行股份有限公司(简称"浙商银行")是 12 家全国性股份制商业银行之一,总部设在浙江杭州,全国第13家"A+H"上市 ...

随机推荐

  1. .NET 通过源码深究依赖注入原理

    依赖注入 (DI) 是.NET中一个非常重要的软件设计模式,它可以帮助我们更好地管理和组织组件,提高代码的可读性,扩展性和可测试性.在日常工作中,我们一定遇见过这些问题或者疑惑. Singleton服 ...

  2. MC我的世界模拟城市模拟大都sim-u常见问题总结

    title: 常见问题 date: 2022-08-22 09:58:01 permalink: /course/2 sidebar: auto article: true copyright: tr ...

  3. 【GiraKoo】Android Studio调试时,提示port无法打开

    Android Studio调试时,提示port无法打开 现象描述 在Android Studio在进行调试时,无法正常运行.App闪退. IDE提示: Error running 'app': Un ...

  4. UCOS-II 任务栈空间合理分配

    最近利用空闲时间跑了一下正点原子的stm32f4开发板的实时操作系统demo,发现了一个比较有意思的东西,分享如下: 硬件平台:正点原子stm32f4开发板 软件开发平台:MDK uVision v5 ...

  5. vscode 配置代码自动格式化加修复

    子曰:"工欲善其事,必先利其器", 编码必须的就是有一个顺手的ide,然而光有还不行,还要懂得配置,毕竟不同的团队代码规范不同,如目前用得较多的就是eslint,今天就顺便记录下v ...

  6. 代码随想录算法训练营Day40 动态规划

    代码随想录算法训练营 代码随想录算法训练营Day40 动态规划| 343. 整数拆分 96.不同的二叉搜索树 343. 整数拆分 题目链接:343. 整数拆分 给定一个正整数 n,将其拆分为至少两个正 ...

  7. 曲线艺术编程 coding curves 第二章 三角函数曲线(TRIG CURVES)

    第二章 三角函数曲线(TRIG CURVES) 原作:Keith Peters 原文:https://www.bit-101.com/blog/2022/11/coding-curves/ 译者:池中 ...

  8. CKS 考试题整理 (15)-镜像扫描ImagePolicyWebhook

    Context cluster 上设置了容器镜像扫描器,但尚未完全集成到cluster 的配置中. 完成后,容器镜像扫描器应扫描并拒绝易受攻击的镜像的使用. Task 注意:你必须在 cluster ...

  9. FPGA加速技术在人机交互界面中的应用及优化

    目录 引言 随着人工智能.云计算.大数据等技术的发展,人机交互界面的重要性也越来越凸显.作为用户与计算机之间的桥梁,人机交互界面的性能和效率直接影响用户的体验和使用效果.为了优化人机交互界面的性能,我 ...

  10. 从逻辑门到 CPU

    目的,造一个很简单的,概念上的 CPU,虽然简单,但是是五脏俱全的 CPU 从最基础的逻辑门开始造,零基础可以看 制造基本武器:与门.非门.或门 现在计算机都是二进制,那二进制是一开始就能想到的吗?显 ...