与数据库性能作斗争:间歇性超时问题

news2024/12/26 0:09:14

今年早些时候,当我们与数据库互动时,我们的应用程序在两周的时间里出现了间歇性的超时问题。

尽管我们尽了最大的努力,但我们不能立即确定一个明确的原因;我们并没有进行任何明显改变数据库使用方式的代码更改,也没有突然的流量变化,我们的日志、追踪或仪表板中也没有任何警告性的内容。

在那两周内,我们部署了24个不同的以性能和可观测性为重点的更改来解决这个问题。

在这篇文章中,我将分享一些关于这些更改是什么以及我们从中获得的价值。

初步调查 

当我们注意到这些减速并收到一个客户的报告,以及在我们的错误报告工具Sentry中看到了大量的“context canceled”错误后,我们首先对这些减速进行了调查。

我们的值班工程师,Aaron,启动了一个事件并开始进行调查。他打开了我们在Grafana中的API仪表板,该仪表板提供了我们API健康状况的高级概览。

他确认我们确实在某些API请求上超时了,但在一分钟之内,我们已经恢复了正常服务。

更多技术干货请关注公号【云原生数据库】

在更新事件以让每个人都知道事情似乎正常之后,他开始调查是什么原因导致的这个问题。

打开一个单一失败的追踪,Aaron注意到这个HTTP请求几乎等待了20秒才从连接池中获取到一个可用的连接。

什么是连接池?当我们的应用程序与我们的数据库通信时,它使用一个在database/sql Go包中实现的客户端连接池。该包使用这些池来限制我们的应用程序中可以随时与数据库通信的进程数。当一个操作使用数据库时,它将该查询发送到database/sql包,该包试图从其连接池中获取一个连接。如果所有可用的连接都在使用中,该操作实际上会被阻塞,直到它可以获得一个连接。

这种阻塞是Aaron在追踪中看到的20秒的延迟。幸运的是,我们已经有了可观测性来识别这个问题。我们使用一个go.patch文件来修补database/sql包并向ConnectionPoolWait方法添加追踪来实现它。这可能不是最稳健的方法,但为了在追踪中添加一个单一的跨度,它做得很好。

从我们的追踪中,Aaron发现我们有各种各样的请求卡在等待连接池上。在这一点上,我们转向Kibana,以更好地了解这些请求的类型分布。

--- /tmp/sql.go    2022-08-01 23:45:55.000000000 +0100+++ /opt/homebrew/Cellar/go/1.19/libexec/src/database/sql/sql.go    2022-09-16 13:06:58.000000000 +0100@@ -28,6 +28,8 @@     "sync"     "sync/atomic"     "time"++    "go.opencensus.io/trace" )  var (@@ -1324,6 +1326,9 @@         return conn, nil     } +    ctx, span := trace.StartSpan(ctx, "database.sql.ConnectionPoolWait")+    defer span.End()+     // Out of free connections or we were asked not to use one. If we're not     // allowed to open any more connections, make a request and wait.     if db.maxOpen > 0 && db.numOpen >= db.maxOpen {

这将使我们能够确认这是少数几个端点争夺一个数据库连接池的争用,还是许多端点可能都使用不同的池。 

我们发现的是,这个问题相当普遍——没有一个单一的连接池受到影响。

我们曾希望它是一个单一的池,因为那样就更容易锁定那个池中的工作并对其进行优化。

鉴于此,我们开始查看一般的数据库健康情况。历史HTTP流量和PubSub指标并没有表明我们在那个时候收到的任何东西都是非常普通的。Heroku的Postgres统计数据也显示了一个相当正常的数据库负载,尽管它确实突出了一些被忽视的查询,随着我们的数据库的增长,这些查询变得越来越慢。

由于没有明显的出发点,我们决定修复任何看起来缓慢并且是快速胜利的东西。我们已经发布了许多优化,包括:

  • 将策略违规转移到使用一个物化视图,而不是必须拉取策略和所有相关的事件,只是为了为每个请求进行那种计算。 

  • 添加一些新的数据库索引来加速缓慢的查询。 

  • 重写了一些首先连接,然后过滤那些没有被索引的列的查询,当它们可以在一个已经存在于连接中的索引表上过滤同一列时。 

在这一点上,我们觉得我们已经投入了大量时间来调查这次中断,我们已经发布了大量的低挂果实;我们结束了这一天,并关闭了事件。

【squids.cn】数据库备份、迁移、同步工具

再次发生的减速 

在处理初次事件的几天内,闪电再次袭来——我们再次超时。这次轮到我值班,我被拉入了生成的事件。我打开了我们的仪表板,再次看到我们由于等待连接池而超时。看着Kibana和Google Cloud Trace,我们的慢请求中没有可辨识的模式。

我们的一名工程师,Lawrence,加入了事件,他建议,而不是玩打地鼠游戏,不停地修补查询,给我们所有的事务添加一秒钟的锁定超时。

由于我们的状态不佳,这至少让我们能够快速确定哪些请求比我们希望的持有事务的时间更长。

我们部署了这一变化,幸运的是,没有任何东西中断,但不幸的是,这意味着我们仍然没有更接近于确定我们减速的原因。

此时我们做出的一个显著变化是开始异步处理Slack事件。每当Slack频道中发生事件时,我们的机器人都可以访问它;我们通过webhook收到通知。此外,每当Slack同步工作区的用户时,我们都会为每次更改接收webhooks。这些可以加起来成为很多事件,而Slack经常一次性发送给我们大量的这些事件。

起初,当我们从Slack接收到此事件时,在该HTTP请求的生命周期内,我们会执行我们需要的任何响应,例如,为用户刚在频道中发布的GitHub链接提供附加链接。无论操作如何,我们总是会执行一些数据库查询,例如查找带有该Slack团队ID的组织。

为了帮助缓解高流量时段,我们开始异步处理这些事件。所以,当一个Slack webhook进来时,我们现在只是直接将其转发给PubSub,避免进行任何数据库查询。

通过使用PubSub,我们可以更多地限制对数据库的访问,如果我们愿意,我们可以免费获得一些重试逻辑。缺点是我们引入了处理事件所需的额外延迟,但我们认为这是一个合理的权衡。

这感觉像一个相当重大的变化,所以我们再次结束一天的工作,希望这是我们超时的最后一次。

提高我们的可观察性 

尽管我们尽了最大努力,第二天,我们看到了另一个超时时期。此时,我们已经做了一些感觉上会有帮助的更改,但并没有取得明显的进展。我们意识到我们必须加倍努力来提高我们的可观察性,这样我们就可以找出问题的根源。

我们想要的金蛋是能够在一段时间内对操作进行分组,并累加该操作持有连接池的总时间。这将使我们能够查询诸如“哪些API端点挂在数据库连接上时间最长?”这样的事情。

每当我们处理一个PubSub消息或处理一个HTTP请求时,我们都会记录一个指标。通过这个,我们知道“这个操作花了多长时间”,“这是什么操作”,“它属于哪组服务?”所以,我们考虑用连接池使用情况的额外信息更新该日志。

理论上,计算我们在连接池中花费的时间听起来很容易,但不幸的是,这并不像'start timer, run query, end timer'那么简单。首先,我们不能围绕我们运行的每一个数据库查询放一个计时器,所以我们需要一个全局应用的中间件。此外,每当我们打开一个数据库事务,连接池在事务的生命周期内都会被持有,所以我们需要设计一种方法来检测我们是否处于事务中,并根据需要更改我们的计数逻辑。

对于中间件,我们最初考虑在Gorm——我们的ORM中插入一些东西。但我们很快意识到Gorm提供的钩子包括我们在连接池上等待的时间,所以我们会计算我们已经知道的东西。

相反,我们在ngrok/sqlmw包中实现了一个中间件,它允许我们在查询或事务发生之前和之后的代码中钩入。在这里,我们调用了一个新方法我们添加的 - trackUsage ——它利用了go的Context来维护我们的新计数器。

func trackUsage(ctx context.Context) (end func()) {    inTransaction, _ := ctx.Value(InTransactionContextKey).(bool)    startTime := time.Now()    return func() {        duration := time.Since(startTime)        log.AddCounter(ctx, log.DatabaseDurationCounterKey, duration)        // If we're inside a transaction, the connection is held for the entire        // transaction duration, so we avoid double-counting each individual        // statement within the transaction        if !inTransaction {            log.AddCounter(ctx, log.DatabaseConnectionDurationCounterKey, duration)        }    }}

现在,我们可以根据每个操作在数据库连接池中保持的时间来过滤我们的日志和追踪。

使用像Grafana这样的工具,我们可以按操作类型分组,并在一段时间内累加值。但是,我们尚未真正利用这一点。当我们等待一些有用的数据时,我们发布了另一个改变,解决了我们减速的根本原因。

最终的修复 

当我们通过日志检查:“经过昨天的修复,我们现在看起来怎么样?”和“我们的连接池计数器是否工作?”时,我们注意到了每次处理Slack模态的提交时都会打开的不必要的事务。这是Slack在用户使用如/inc这样的斜杠命令时看到的视图中按下确认按钮时调用的HTTP端点。

我们为大多数模态提交移除了事务,在需要事务性保证的情况下,我们为这些代码路径明确地添加了事务。

直到几天后,我们才意识到这是我们问题的根源。由于超时是间歇性的,不是每天都发生,所以确认问题需要一点时间。但现在已经过去四个月,我们已经没有了数据库超时。

这确实证实了我们之前的怀疑——没有一个明显的慢事务导致了这个问题。相反,是许多短事务加在一起,给我们带来了一些真正的问题。

总结 

在与这个问题斗争了几天后,没有一个“啊哈!”的时刻,我们一下子解决了所有问题,这有点令人失望。

但是,从好的一面看,我们现在更有能力在未来诊断类似的问题。我们还进行了一些真正的性能改进,这应该会使我们的应用对用户更快。

如果我们为应用的不同部分使用了不同的数据库,我们很可能会更早地找到这个问题。但是,那也绝对不是免费的。有了那个,我们就必须开始考虑分布式事务,我们的开发环境会不那么流畅,但我们目前对我们做出的权衡感到满意。

作者:Rory Bain

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/982411.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SSL证书只有收费的吗?有没有免费使用的?

首先明白SSL证书是什么SSL英文全称:英文全称: Secure Socket Layer Certificate,中文全称:安全套接字层证书。 SSL是一种由数字证书颁发机构(CA) 签发的数字证书。它用于建立安全的加密连接,确保通过网络传输的数据在客户端和服务器之间的安全性和完整性…

不同供电系统下SPD浪涌保护器的用途差异与选择

浪涌保护器(SPD)是一种用于保护电气设备免受电力系统突发的电压浪涌或过电压等干扰的重要装置。在选择浪涌保护器(SPD)时,会有1P、1PN、2P、3P、3PN、4P等不同类型的产品,其中“P”是低压电器的一个专业术语…

新旧混战,内衣竞争终局在哪里?

从2016到2023,内衣混战没有结束,反而愈演愈烈。 近日,包括都市丽人、汇洁股份、爱慕股份、安莉芳等在内的内衣服饰企业发布2023年中期业绩,多数在净利润等关键财务指标上表现亮眼。比如,都市丽人、爱慕股份、汇洁股份…

NOMA学习

NOMA(非正交多址接入技术) NOMA基本概念上行NOMA与下行NOMA上行NOMA(MAC信道)下行NOMA(BC广播信道) SIC解码顺序叠加编码(SC)与串行干扰消除(SIC)叠加编码&am…

TypeScript类型守卫

概念 在语句的块级作用域【if语句内或条目运算符表达式内】缩小变量类型的一种类型推断的行为。 类型守卫可以帮助我们在块级作用域中获得更为需要的精确变量类型,从而减少不必要的类型断言。 类型判断:typeof实例判断:instanceof字面量相等…

手写Spring:第1章-开篇介绍,手写Spring

文章目录 一、手写Spring二、Spring 生命周期 一、手写Spring 💡 目标:我们该对 Spring 学到什么程度?又该怎么学习呢? 手写简化版 Spring 框架,了解 Spring 核心原理,为后续再深入学习 Spring 打下基础。在…

QMS系统在质量管理中的作用

一、QMS系统的定义: 质量管理体系(QMS)系统是一套用于规范和管理企业质量活动的框架和工具。它包括一系列互相关联的流程、程序和记录,旨在确保产品和服务符合质量要求、法规要求和客户期望,并持续改进质量绩效。 二、…

如何准确高效的对电商数据进行分析

体量庞大的电商数据,采集完成后应对数据进行合理利用,才能将采集到的数据数据物尽其用,这里说的利用数据,包含对数据的价格比较,或者分析使用,比较价格可输出低价分析报告,在品牌控价治理环节发…

关系型数据库和非关系型数据库

关系型数据库是以关系(表格)为基础的数据库,它采用了 SQL(Structured Query Language)作为数据操作语言,常见的关系型数据库包括 MySQL、Oracle、SQL Server 等。 非关系型数据库则是基于文档、键值、列族…

第4章 内核模块实验(iTOP-RK3568开发板驱动开发指南 )

在上一章节我们编写了最简单的helloworld驱动程序。有了驱动程序以后,要如何编译并使用驱动呢。编译驱动有俩种方法,分别是将驱动编译成内核和将驱动编译成内核模块。我们先来学习如何将驱动编译成内核模块、 4.1 设置交叉编译器 1 下载网盘资料下的交…

2023年9月软考高级信息系统项目管理师认证报名找弘博创新

信息系统项目管理师是全国计算机技术与软件专业技术资格(水平)考试(简称软考)项目之一,是由国家人力资源和社会保障部、工业和信息化部共同组织的国家级考试,既属于国家职业资格考试,又是职称资…

更改注册表exe值后的惨痛经历

装软件时由于执行性文件打不开,搜索教程更改了exefile的值,最后整个电脑崩了,所有EXE都打不开,折腾了5个小时,什么办法都试了,甚至重置电脑都不让,打算拿电脑城修电脑了,突然搜到了一…

文件上传之图片马混淆绕过与条件竞争

一、图片马混淆绕过 1.上传gif imagecreatefromxxxx函数把图片内容打散,,但是不会影响图片正常显示 $is_upload false; $msg null; if (isset($_POST[submit])){// 获得上传文件的基本信息,文件名,类型,大小&…

C++ continue 语句

C 中的 continue 语句有点像 break 语句。但它不是强迫终止,continue 会跳过当前循环中的代码,强迫开始下一次循环。 对于 for 循环,continue 语句会导致执行条件测试和循环增量部分。对于 while 和 do…while 循环,continue 语句…

MySQL聚簇索引与非聚簇索引

分析&回答 当数据库一条记录里包含多个字段时,一棵B树就只能存储主键,如果检索的是非主键字段,则主键索引失去作用,变成顺序查找了。这时应该在第二个要检索的列上建立第二套索引。这个索引由独立的B树来组织。有两种常见的方…

Redis优化 RDB AOF持久化

---------------------- Redis 高可用 ---------------------------------------- 在web服务器中,高可用是指服务器可以正常访问的时间,衡量的标准是在多长时间内可以提供正常服务(99.9%、99.99%、99.999%等等)。 但是在Redis语境…

图文版:以太网二层接口类型(含配套习题)

常见的以太网二层接口类型包括以下三种: 一、Access接口 access链路类型端口,一种交换机的主干道模式,2台交换机的2个端口之间是否能够建立干道连接,取决于这2个端口模式的组合。 Access端口在收到以太网帧后打开VLAN标签&#…

祝贺埃文科技入选河南省工业企业数据安全技术支撑单位

近日,河南省工业信息安全产业发展联盟公布了河南省工业信息安全应急服务支撑单位和河南省工业企业数据安全技术支撑单位遴选结果,最终评选出19家单位作为第一届河南省工业信息安全应急服务支撑单位和河南省工业企业数据安全技术支撑单位。 埃文科技凭借自身技术优势…

网络课程学习

计算机网络概述 计算机网络定义 一组自治计算机互联的集合 计算机网络基本功能 资源共享 综合信息服务 分布式处理与负载均衡 计算机网络的类型 局域网 LAN:由用户自行建设,使用私有地址组建的内部网络 城域网 MAN:由运营商或大规模…

得心应手应对 OOM 的疑难杂症

Java全能学习面试指南:https://www.javaxiaobear.cn/ 前面我们提到,类的初始化发生在类加载阶段,那对象都有哪些创建方式呢?除了我们常用的 new,还有下面这些方式: 使用 Class 的 newInstance 方法。使用…