事故
一个阳光明媚的下午,程序员小七一如往常地写着bug。
突然收到客服消息,说收到大量用户投诉,无法再打开页面。小七最近发布了新代码,我认为部分代码存在问题。
快速将流程切换到备用数据库并回滚代码。接下来查看错误日志,发现数据库连接池报大量超时错误。这种情况通常有两种可能:
一是数据库或者与其连接的网络出现问题,导致无法连接数据库,且已达到超时时间。另一个是有很多线程运行缓慢的查询。旧线程仍在执行查询,而新线程陷入等待状态,只能等待达到超时时间。最终发现数据库中的查询速度慢是失败的原因。由于高频查询不会命中索引,因此每次查询至少需要一秒或更长时间,因此会累积大量查询请求并超时。
复盘
吸取这次经验后,小七决定到当地调查一下这种病症。
首先,让我们使用一个非常简单的演示表创建一个不正确的索引年龄、分数。
create table Demon( id int auto_incrementprimary key, name varchar(255) null,age int null, core int null); 在demo 中创建索引idx_age_score (age, core); 启用慢速SQL 日志记录。
SET GLOBAL throw_query_log=1; 接下来,使用Python创建一个包含500万条随机数据的SQL文件。问题大概是:
import randomif __name__=='__main__': SQL_file=open('./batch_jq.SQL', 'w',encoding='utf-8') a1=['张', '金', '李', '王', '赵'] a2=['余', '明', '荣', '范', '君', '林'] a3=[', '李', '林', ', '国', ''] _len=5000 # 循环5k 次while _len=1: line='Insert into demo(name,age,score) values ' arr=[] # 每个时间范围批量插入1,000 个项目到i (1 , 1001): name=random.choice(a1)+random.choice(a2)+random.choice(a3) arr.append((name, random.randint(1, 100), random.randint(1) , 10000000) )) _SQL=line + str(arr).strip('[]') SQL_file.write(_SQL + ';\n') _len -=1PS: 这里,不是将数据一一插入,而是使用批量插入。所以运行SQL会快一点。
接下来,运行SQL插入500万条数据。
.[2020-04-19 20:05:22] 636 毫秒内影响24000 行.[2020-04-19 20:05:23] 638 毫秒内影响24000 行。 [2020-04-19 20:05:] 23] 193 毫秒内影响了8000 行[2020-04-19 20:05:23] 摘要3 分42 秒989 毫秒内执行了5000 条语句中的: 条(文件中有106742400 个符号) ,建立一个简单的使用SpringBoot + JdbcTemplate 的应用程序。
@RestControllerpublic class DemoController { private static Final Logger LOGGER=LoggerFactory.getLogger(DemoController.class); @Resource private JdbcTemplate jdbcTemplate; //触发慢查询业务的入口@GetMapping('trigger') public Stringtrigger() { long ago=System .currentTimeMillis(); jdbcTemplate.query('select * from demo.demo where Score 20 limit 50', (set) - { }); 很久之后=System.currentTimeMillis() {} ms', after - before ); return 'success' }} 我尝试调用http://localhost:8080/trigger,发现需要一秒多的时间。虽然有点慢,但还是可以接受的。
于是就去AB做了压测。
$ ab -n500 -c20 http://localhost:8080/trigger# 表示总共500 个请求,一次20 个并发请求。在压力测试中,我们发现日志打印时间基本在15秒左右。已经很慢了,但是没有报错。业务中可以正常使用,不会出现数据库查询慢的情况。
2020-04-19 20:56:21.665信息18908 --- [nio-8080-exec-3] c.e.s.controller.DemoController :调用时间: 15260 ms20:56:21.779信息18908 --- [io -8] 080-exec-10] c.e.s. 控制器.DemoController : 调用时间: 15445 ms.并增加并发量。
$ ab -n500 -c50 http://localhost:8080/trigger#表示总共500个请求,每次并发请求数为50个。您会注意到控制台中显示的通话时间开始缓慢增加。打印一些异常信息。
2020-04-19 21:02:55.277 错误17100 --- [io-8080-exec-45] o.a.c.c.C.[.[.[/].[dispatcherServlet] : servlet [dispatcherServlet] 的Servlet.service() 在路径[] 的上下文中抛出异常:[无法处理请求。嵌套异常是org.springframework.jdbc.CannotGetJdbcConnectionException:获取JDBC 连接失败。嵌套异常是java.SQL.SQLTransientConnectionException: HikariPool-1 - 连接不可用。请求在30000 毫秒后超时。 ] 根本原因存在java.SQL.SQLTransientConnectionException: HikariPool-1 - 连接不可用。 com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:689) ~[HikariCP-3.4.2.jar:na] 上的请求在30000 毫秒后超时。 zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:196) ~[HikariCP-3.4.2.jar:na] 示例代码使用SpringBoot自带的数据库连接池Hikari。默认超时为30 秒。抛出异常。无论连接池如何,功能都会略有不同,但本质上都有超时设置。
此时数据库中还存在一些较慢的SQL 记录。
SHOW GLOBAL STATUS LIKE '%Slow_queries%';| Slow_queries | 51 |下一步是运行以下SQL 来打印当前连接状态并查看哪些SQL 语句正在消耗时间。
如果你运行SHOW FULL processlist;,你可以很容易地看到SQL执行时间超过1秒。当我用SQL 解释这一点时,我意识到这是对整个表的扫描。
当然,实际的项目表并没有那么简单,SQL语句和索引也更加复杂。这只是为了演示目的而创建的一个简单示例。
现在有很多很棒的数据库监控工具,比如阿里巴巴的Druid,可以让你更方便、更优雅地查看日志、排查数据库问题。
调优后我还进行了50并发的本地压力测试,发现响应时间基本停留在10ms左右,完全没有压力。
调优
使用索引
慢SQL 通常可以通过使用索引来解决。
通过问题定位,您发现您对某个字段有高频查询需求,但您并没有为该字段建立索引。 MySQL中的所有索引都使用“最左匹配原则”,因此现有的连接索引已经过时,并且不会在高频查询上获得命中。
当然,创建太多索引也有缺点。根据你的业务来决定。
使用缓存
分析发现这些慢SQL执行的查询条件完全相同。这意味着查询结果可以存储在缓存中,允许后续查询直接从缓存中获取,从而显着提高性能。
事实上,目前主流的ORM框架都支持缓存,甚至可以用于多级缓存。 Spring还提供了一个缓存框架Spring Cache,您可以根据自己的需求进行配置和使用。
反思
回顾调整后,思考过去。
利用好explain
在使用SQL语句之前,可以先解释一下SQL语句,看看是否命中索引。如果没有命中,考虑是否是高频语句,是否需要调优。
进行充分的压测
网上没有任何问题。不要盲目地假设您的程序没问题;将其直接部署到您的生产环境中。如果能在上线前进行压力测试,就可以尽早发现性能问题,延缓损失。
利用好日志和监控
通常在晚上用户较少的时候在线发布。如果您可以配置错误日志收集以及数据库监控和警报,您可能能够在大量用户之前发现此问题。这样我们就能尽快解决问题,减少用户和企业的损失。
公众号
如果您看到这篇文章,就表示您同意我的文章。如果这有帮助,请支持我们:
版权声明:本文由今日头条转载,如有侵犯您的版权,请联系本站编辑删除。