【翻译】为什么我的数据库变得这么慢?

这是原文,本翻译仅供学习与参考,如有侵权请联系我,我会迅速删除的。

本文将教你如何与降低数据库性能的敌人们较量,以及如何避免使你的数据流停滞在泥沼中。

随着数据的增长,性能会有所下降——这并不是什么秘密,但是幅度经常十分惊人。

如果这是一个常见的问题,那么理解“数据库速度下降”的底层原因就十分有必要,因为这可以使你尽早发现性能下降的预兆并加以预防。具体来讲,你需要了解数据库性能的三大敌人。

这些“敌人”是我在职业生涯中遇到的最常见的一些问题,正确处理这些问题能常常能使你在许多情况下避免遭遇数据库性能下降的境地。

数据库性能的三大敌人

这里有三个最常见的数据库性能的“大敌”。我们都对于传统关系型数据库可能出现的问题十分熟悉,但是这三个问题很容易出现在错误建模或不正确使用NoSQL、“NewSQL”以及几乎所有种类数据库存储引擎的情况。
事实上,我最近刚刚在一个会议上听一位参会者对我说,他用一个NoSQL引擎跑一个map-reduce任务,跑了整整十天!所以这三个敌人会影响我们所有人。

一号敌人:表扫描

让我们从最拖累数据库性能的一项开始:表扫描。
当一次对DBMS的查询请求一行(更多的情况可能是一个范围内的若干行)数据时,会发生一次表扫描。如果这次查询没有可用的索引,或者DBMS优化器没法选择可用的索引,那么引擎将不得不一次对整个数据集(或者表)进行一次从头到尾的顺序扫描。

表扫描经常导致性能的迅速下降——就像我在上个月的文章中展示的“曲棍球杆”曲线一样。
来,让我们看一个例子:假设我们有一个如下结构的表:

1
2
3
4
5
6
7
CREATE TABLE purchase_transaction (
transaction_id INTEGER PRIMARY KEY,
customer_id INTEGER,
order_id INTEGER,
order_date DATE,
order_amount DECIMAL(10,2
)

这个表上唯一的索引就是主键(在一个NoSQL数据库中就是object key(译者注:这个不知道怎么翻译)自身)。现在让我们进行一次如下的SQL查询:

1
2
3
4
5
SELECT *
FROM purchase_transaction
WHERE order_date BETWEEN1/1/2012AND1/31/2012
AND order_amount >= 1000.00
AND order_amount <= 2000.00

我们只是单纯地想选出一月份订单量在1000到2000之间的行。但是这个表对于order_date和order_amount并没有索引,所以DBMS必须进行一次表扫描。
这个不经过优化的查询可能顺利运行了很久,直到有一天你发信啊数据库性能急剧下降(就像从悬崖上掉下去了一样),那么是什么导致了这次性能下降呢?

原因在于其实表扫描是一直在进行的,只不过之前表的规模相对较小所以感受不到表扫描的时间花销。最终在某一个时间点,表的规模超过了一个适应内存或磁盘缓存大小的临界值,于是DBMS开始疯狂地从硬盘读取数据。规模的增长将会进一步使情况恶化,每次查询更加频繁而剧烈地从硬盘读取数据,要知道通常情况硬盘的速度可是比内存慢100倍以上的,于是你就可以感受到它的影响了。如果这时再增加几个运行同样查询的用户,整个DBMS会迅速卡死,将所有资源优先供应给其中一个查询上。

从我个人经验来讲,日常中任何一个执行时间超过一秒的查询最终都会酿成一个大问题,所以保持对运行情况的监视是十分重要的。

二号敌人:并发竞争

并发竞争是表扫描的近亲,其实表扫描经常也同时导致这个问题。
所有的数据库需要给多个用户或需求提供服务,毕竟大数据经常被多个用户用同一个应用同时访问。对于大多数组织来讲,更多的用户意味着更多的利润和更大的成功,所以从商业角度来讲这是他们梦寐以求的事。
并发竞争可以表现为很多种形式,但是其本质就是多个用户竞争同一个资源,而数据库会上锁来等待其中一个用户。通常有这么几种情况会导致并发竞争:
传统的关系数据库引擎:通常是多线程的,设计用于同时对多个请求提供服务。这很棒,但是当然我们希望事务完整性得到保证。然而,为了满足这个要求,引擎会锁住某些行来保证互斥的写操作(有些更坏的情况甚至可能锁住整个表)。
当这些行被一个用户或连接锁住,其他的用户就没法读这些行了。导致这种情况最常见的原因是数据库事务低下的效率,使单一事务持续的时间太久。
举个例子,想象一个对表中部的(mid-stream,不知道是不是这么翻译)读查询——这个查询会导致一次表扫描(甚至是一次表更新),这会导致一次地地道道的灾难,因为整个表会在这次查询漫长的执行过程中完全锁住。

  • 今天,大部分NoSQL引擎希望通过自身单线程的实现来避免这种情况。也就是说,它们可能非常快,但一次只能为一个请求提供服务。这在大部分场合都能很好的运行,但是如果每个请求都耗时若干秒,然后呢有成百上千的请求同时出现,整个数据库引擎就会在须臾之间跪掉。
  • 另一种会发生竞争的资源是可用的连接数。同样这个情况一般发生在关系数据库上,这样的数据库通常对同时连接数设定了上限。如果超过了上限,应用就会被数据库拒绝访问。
  • 对付竞争最通用的法则是限制事务的规模,请确认你的每个查询语句和写入语句都是有效的。让每个事务最多运行一秒钟是个很好的规则(当然对于某些高实时性系统一秒钟太长了,不过大致原则都一样)。

三号敌人:慢速写操作

这个“敌人”是非常具有误导性的,优势难以预测或预防。随着单个表或数据集规模的膨胀,写入速度往往也会呈现出“曲棍球杆”曲线。导致这种情况的原因一般是索引的使用。在传统的关系数据库引擎中,这始终都是个问题,尤其当多个不同的索引用在同一张表上时。

最常见的索引类型是B树,这种索引随着树的深度增加会耗费更多的计算和硬盘资源。最终的结果就是,对这个索引过的表进行写操作时速度会莫名其妙地下降。我注意到NoSQL和NewSQL引擎已经添加了类似索引的特性来满足开发者的需求。我预测因为关系型数据库这么多年来拓展的新特性已经很好地满足了用户的需求,其他许多种类的DBMS引擎会在以后变得更为简单(B树的索引原理我将在接下来的文章中进行讲解)。

这个问题的解决办法是限制索引的使用,只有在真正需要的时候再去用它们。同时当你的数据规模变大时时刻保持警惕。

总结

在这篇文章中,我讲解了三种数据库性能的敌人。请确定你始终在关注这些问题,尤其是当磁盘I/O增长的兆头或数据库性能滑坡的征兆出现时。尽早发现问题永远是这个问题最好的解决办法,只要你有一双具有洞察力的眼睛你就很有希望做到这一点。