背景
现象
公司的APP突然出现响应很慢,并伴有登录不了的情况。
服务端架构
公司服务端采用分布式架构,服务间通过 RPC 访问,使用公司自研 RPC 框架。
问题分析
业务日志
通过查看日志文件,发现有很多服务间的调用时间消耗在 2s 以上,这些调用基本上都是访问 MySQL 数据库,通过分析发现其中一个更新数据库的方法调用频繁且每次耗时都在 2s 以上。这时就猜测是不是更新的SQL语句where 条件列没有加索引导致的,然后立即查看数据库表,发现确实没有加索引。
为什么没有加索引就会耗时那么多呢?我们知道在使用 update … where … 语句更新表数据时,为了防止出现并发问题,会对要更新的记录的索引上加锁。在查找过程中只要有扫描过的记录都会加锁,由于 where 条件列上面没有加索引,因此更新时将会扫描全表,也就相当于锁住了全表,这时对于这个表的更新就变成串行了,所以就大大增加了更新时间。
线程堆栈信息
通过 jstack 命令输出对外提供 HTTP 访问的 rest 服务线程堆栈信息,分析发现有很多业务相关的线程状态为 WAITING,都是发送完 RPC 请求后进入等待状态。rest 服务要通过 RPC 调用 rds 服务来操作数据库。 这里先解释下公司自研的 RPC 框架都是基于同步调用的,从调用请求发出到获取结果这个过程中,存在异步转同步的过程。底层使用 Netty 框架,采用的是异步传输,因此线程在请求后进入等待状态,当有数据响应回来后再唤醒线程。由于更新数据耗时长,所以线程处于等待状态也可以解释的通。但是发现一些查询数据库的 rpc 调用也处于等待,从业务日志上看也有耗时超过 2 s 的。那为什么查询数据库且用到了索引,还这么耗时呢?这就要通过全局分析 rpc 框架的实现来找原因了。
自研 RPC 框架线程模型
rest 服务 rpc 调用 rds 服务来操作数据库,rds 和 rest 直接是通过 channel 进行通信的,rds 每次建立 channel 连接后,都会把 channel 分配 EventLoop,每一个 EventLoop 可以持有多个 channel,一个 channel 在生命周期内只会绑定一个 EventLoop,每个 EventLoop 对应一个线程,每个 EventLoop 都有它自已的任务队列,独立于任何其他的 EventLoop。
假设 rds 有 4 个 EventLoop,那么创建的 16 个 channel 会平均分配到 4 个 EventLoop上,任务队列上的任务执行顺序是先进先出的顺序。假设任务队列里有 4 个任务 A、B、C、D,A 和 B 是执行 update 操作,C 和 D 是查询操作。在业务线程池没有可用线程的情况下,这四个任务将会依次执行,那么任务从创建到执行完成的时间将累计增加,这就是为什么有的查询操作也会耗时很久的原因,也就能够解释系统响应慢,甚至登录失败的问题。
其他问题
索引一直没有加,为什么刚开始时没有出现,而是运行一段时间后突然出现问题。我们的产品是安防摄像机,设备需要与两套系统建立连接,其中一个是视频服务系统,由另一个团队负责。设备建立连接后,需要上报信息,更新另一个系统MySQL表数据,也就是上面讲的。出问题时,视频服务系统重启了,因此大量设备需要重新建立连接,就会有大量的更新MySQL表请求,所以就会导致上述现象。
解决问题
问题的解决就很明确了,对相应的列加索引就可以了。下面是 MySQL 加锁的一些规则,在写 SQL 语句时可以做参考:
- 原则1: 加锁的基本单位是 next-key lock。next-key lock是前开后闭区间。
2. 原则2: 查找过程中访问到的对象才会加锁
3. 优化1: 索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁
4. 优化2: 索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
5. 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
总结
在写程序时就应该考虑哪些列是应该加索引的,特别是要注意更新语句,可能会导致锁住全表的情况。就会严重影响系统性能,导致应用不可用。对于一些耗时的操作,应该采用异步的方式。