前几天 Uber 发表了一篇文章“为什么Uber 系统从PostgreSQL 换成了MySQL ”。因为当时手上还有项目要做,我并没有马上去读。可是不久之后我的邮箱就被大家的邮件撑爆了,铺天盖地全是类似这样的问题 :“PostgreSQL 真的有那么差吗”?因为我知道从一般意义上来说PostgreSQL 还是不错的,这些邮件就引起了我的好奇心,不知道那篇文章中到底写了些什么内容呢?这篇文章就想根据Uber 的文章来讲一讲。
我想Uber 的本意是说对于他们的业务需求来说,他们发现MySQL 比PostgreSQL 更合适,可惜他们的文章没能准确的表达出这个意思。本来他们可以写“PostgreSQL 对于更新操作很多的系统有些局限”,这篇文章却只说了“在架构上对写操作支持不好”。如果你的系统更新操作不是非常频繁的话,其实你不必担心Uber 的文章中写到的问题。
在这篇文章中我会解释一下为什么我不认为Uber 的文章能作为大家为数据库选型时的一般参考,为什么MySQL 可能的确是适合Uber 业务的,以及为什么企业成功了之后可能会碰到新问题而不仅仅是要扩展数据存储规模,等等。
关于更新操作
Uber 的文章中首先重点写可是又没有提供足够细节的问题是:在更新一条记录时 PostgreSQL 总会更新那张表里的所有索引。相对应的是 MySQL 的 InnoDB 则只更新那些包含了更新字段的索引。PostgreSQL 的底层会因为更新没有被索引到的字段而产生更多的磁盘 IO(文中提到的“写放大”问题)。如果这个对于 Uber 的确是个大问题的话,那这种更新操作也许要占他们系统负载的很大一部分。
但是,虽然 Uber 的文章中没有提到,我们还是可以推理出一些东西的。文章中没有提到 PostgreSQL 的 Heap-Only-Tuples (HOT) 机制。从 PosgreSQL 的源码中可以看到,HOT 机制只对某些场景适用:“当一个元组被不断地更改没有被索引到的字段时”。在这种场景下,如果新版本的元组可以和旧版本存储在相同的数据页里的话,PosgreSQL 可以不更新任何索引就完成更新。调节参数 fillfactor 可以达到这个效果。可能 Uber 的技术团队知道 HOT 机制对他们的问题没帮助,也许他们每次更新都会至少更新到一个被索引了的字段。
文章中有一段话也从另一个侧面印证了这个推理:“如果我们在表上定义了 12 个索引,那即使更新的字段只包含在一个索引中,这 12 个索引也都要更新 ctid 来指向新记录”。它明确地说到了“只包含在一个索引中”,这就是一种边界情况——只要有一个索引就行——要不然的话 PosgreSQL 的 HOT 机制是可以解决这个问题的。
注:我非常好奇他们的索引数量不能减少些吗?也就是说把他们的索引重新设计一下。不过,也很有可能有些索引虽然没什么机会用到,但只要用到时就是非常重要的。
看起来Uber 的系统会进行许多更新操作,每次都会至少更新一个被索引到的字段。如果他们占绝大多数的用例都是要做这样的更新,那文章中关于采用MySQL 而不是PostgreSQL 的观点就是有道理的。
关于查询操作
关于他们用例的另一句话引起了我的注意,文章提到MySQL/InnoDB 使用聚簇索引,并且承认:“这样的设计意味着在进行第二索引查询时,InnoDB 与PostgreSQL 相比稍有逊色,因为InnoDB 要进行两次索引查询,而PostgreSQL 只用一次”。我之前在分析SQL Server 时就曾分析过这个问题(“聚簇索引惩罚”)。
引起我注意的是他们的用词是“稍有逊色”。依我之见,如果你要通过第二索引进行大量查询的话,这个可是相当大的不足。如果他们认为这只是一点点问题的话,那也许就表明他们的第二索引很少会被用到。那就表明,他们主要是通过主键搜索(这种场景下没有第二索引惩罚)。注意我的用词是“搜索”(searching)而不是“查询”(selecting),原因是聚簇索引惩罚会影响任何带有where 子句的语句,而不仅仅是查询。这表明高频率的更新操作主要是依据主键的。
最后,还有一点他们没有提到的和查询相关的东西:他们没有提及PostgreSQL 的索引扫描的不足。尤其是在更新频繁的数据库中,PostgreSQL 的索引扫描的实现就更是无用。我甚至会说这一个问题就影响了我绝大多数的客户。我在 2011 年曾写过关于这个问题的文章。2012 年 PostgreSQL 9.2 对索引扫描的功能进行了有限支持(只适用于非常静态的数据)。在2014 年我甚至在PostgreSQL 大会上表达了我对这个问题的顾虑。可是Uber 并没有抱怨这个问题,看来查询速度不是他们的问题所在。我猜他们的查询主要是靠在从库上查询(见下文)来提升速度,而且非常有可能主要做的是主键查询。
到目前为止,总结起来他们的用例似乎比较适合键值型存储。要知道InnoDB 是个相当好用而且广受欢迎的底层存储,MySQL 和MariaDB 就是最广为人知的结合了InnoDB 存储并提供了一些非常有限的SQL 前端的数据库。严肃的说,如果你主要是需要一个键值型存储并且偶尔需要运行一些简单的SQL 语句,那MySQL 或者MariaDB 就是非常不错的选择。我猜它们至少要比任何类型的刚开始提供类SQL 语言查询的NoSQL 键值型存储要好。另外,Uber 刚刚基于MySQL 和InnoDB 搭建了他们自己的分布式数据库 Schemaless 。
关于索引重平衡
在文中描述索引时的最后一句,它在讲述 B 树索引时用到了“重平衡”这个词,它还引用了 Wikipedia 上一篇关于“删除操作之后的重平衡”文章。可惜,Wikipedia 上的文章并不普遍适用于数据库索引,因为它上面描述的算法隐含的前提是每个节点必须都是半满的。PostgreSQL 使用了 B 树的 Lehman, Yao 变种,它为了提高并发度支持稀疏索引。因此,PostgreSQL 仍然会从索引中删除空白页(见幻灯片“索引的内部原理”第15 页),但这只是一个次要问题而已。
真正令我担忧的是这一句:“B 树的一个重要特性是它们会定期做重平衡……”在这里我要澄清这并不是每天都会运行的一个周期性过程。每次索引改变(也可能更糟)都可能会引起索引重平衡,但文章中继续说:“当子树移动到磁盘其他位置时,这种重平衡操作会完全改变树的结构”。如果你认为“重平衡”的过程会导致大量的数据移动,那你就错了。
B 树最重要的过程是节点分裂。你从字面上也许就能理解,当一个节点中没办法再写入一条新记录时,它就会分裂。粗略地说在经过 100 次插入之后就会有一次分裂。要分裂的节点会再生成一个新节点,然后从自己这里挪一半的记录过去,再把新节点和旧的、下一个以及父亲节点都连接起来,这就是 Lehman,Yao 的算法节省了许多锁操作的地方。但在某些情况下,新节点并不能直接被加到父亲节点里,因为如果父亲节点也恰好满了而无法再添加新的子节点时,父亲节点也会分裂,上述所有过程都会重做。
在最差的情况下,分裂操作会一直向上传递到根节点,它也会分裂并在它的上一级再加上一个新的根节点。在这种情况下,B 树的深度会增加。注意根节点分裂时为了保持树的平衡会进行整棵树的调整,但这并不一定会导致大量的数据移动。在最差的情况下,每一层会修改三个节点,再加上个新的根节点。事实上,当今世界绝大多数的生产库 B 树索引都不超过 5 层,也就是说最差的情况——根节点分裂——可能在经过十亿次插入操作的过程中才会发生 5 次,而且也不会整棵树都受影响。总的来说,索引维护并不是周期性的,也并不频繁,它压根不会完全改动树的结构,至少是磁盘上的数据。
关于物理复制
Uber 的文章中提到了关于 PostgreSQL 的另一个我不赞成的主要问题:物理复制。文章提到索引“重平衡”问题的原因是 Uber 曾经碰到过一个 PostgrSQL 的复制 BUG,导致所有的从库数据都损坏了(“BUG 影响了 PostgreSQL 9.2 的好几个子版本,已经被修复很长时间了”)。
因为 PostgreSQL 9.2 只在内核中提供了物理复制功能,那一个主从复制的 BUG 的确“会导致树的大部分内容都完全错误”。解释一下,如果一个节点分裂没有成功的被复制出去,结果导致它找不到正确的子节点的话,那整棵子树都会失效。这是绝对正确的,就象一句“如果有 BUG 就要糟”这样的话一样正确。要破坏一个树的结构并不需要改很多数据:只要一个坏指针就够了。
Uber 的文章也提到了物理复制的一些其他问题:超大的复制流量(部分原因是更新操作导致的写放大)、升级到新 PostgreSQL 版本时需要过长的停服时间。第一个对我来说合理,但第二个我就实在无法评论了(但是在 PostgreSQL-hackers 邮件组里是有一些评论的)。
最后,文章声称“PostgreSQL 没有真正意义上的从库MVCC 支持”。幸运的是文章链接到了PostgreSQL 文档中解释这个问题的页面。问题主要是说主库完全不知道从库在做什么,所以对于某些要做大操作而延迟非常大的从库来说,有可能主库会删除从库仍未取走的日志。
按PostgreSQL文档来说,有两个办法解决这个问题: (1) 为了让读事务可以有机会完成操作,允许复制流在一定的超时时间内延迟应用数据。如果超时之后读事务仍然没有完成,就把事务杀死,让复制流应用数据。(2) 配置从库向主库发送回应消息说明自己当前的复制状态,避免主库清除掉任何从库仍然需要的历史版本数据。Uber 的文章直接排除了第一种办法,但压根没提第二种办法,还批评Uber 的开发者不了解数据库底层原理。
关于开发者
引用全部原文:“比如,一个开发写了一段代码来把一个收据通过邮件发给另一个用户。依代码的写法不同,可能会隐式地用到一个数据库事务,直到邮件成功发送之后事务才结束。当然,让你的代码在做一些不相干的阻塞式IO 时还打开一个事务并不好,但现实情况是绝大部分工程师们都不是数据库专家,可能并不会意识到这个问题的存在,尤其是当他们使用ORM 等架构的时候,那些开启事务之类的底层细节都被掩盖起来了。
不好意思,我理解并支持这样的说法,对于这句话:“大多数工程师都不是数据库专家”。我要说的是实际上大多数工程师的数据库知识都少得可怜。其实即使不是数据库专家,每一个要使用SQL 的工程师还是要知道事务的。
我工作的很大一部分内容就是给开发者们做SQL 培训,在各种规模的公司里我都做过。如果有一件事我敢肯定的话,那就是大家的SQL 知识实在是难以置信地差。在上文刚刚提到的“打开的事务”的问题中,我可以肯定很少有开发者知道只读事务是真的存在的,大多数开发者只知道事务是用来保护写操作的。我太多次碰到这样的问题了,我也准备了幻灯片来解释。幻灯片刚刚上传了,有兴趣的读者可以下载。
关于成功
这里有我要说的最后一个问题:一个公司雇的人越多,他们的能力就会越趋近平均水平。夸张的说,如果你把地球上所有人都雇了,那你们公司的水平就恰好达到那个平均值。雇的人多只是增大了样本集合而已。
有两个办法可以避免这种可能:(1) 只雇最好的人。这个办法的难点在于当暂时找不到水平高于一般的人时,只好耐心等待。(2) 雇水平一般的人再把他们在工作中培养起来。新员工要上手的话可能需要比较长的热身期,而且老员工也可能会需要培训。这两个方法的共同问题是都需要很长时间。如果你的业务迅猛增长而你的时间等不及的话,你就只好雇水平一般的人,他们对数据库不会很了解( 2014 年的经验数据)。换句话说,对于一个快速发展的公司,技术比人更容易改变。
随着在不同时期的业务需求不同,成功的因素也会影响团队的技术能力需求。在创业初期,公司需要现成的、可以立刻上手并且足够方便实现业务需求的技术。SQL 就是其中之一,因为它非常灵活(想怎么用就怎么用),而且要找懂点 SQL 知识的人也非常容易。好,咱们开工吧!许多——也可能是大多数——的公司就到此为止了。即使他们比较成功了,业务也发展了,他们可能还是很能接受 SQL 数据库的各种缺点而不求改变。这并不是在说 Uber。
有些幸运的初创公司最终会不满足于 SQL 的功能。到那时,他们已经有能力去接触更多的(或者说理论上无限的?)资源,这时候有趣的事就发生了:他们发现如果把现在用着的通用数据库换成一个专门为自己的需求而定制开发的系统的话,那很多问题就都可以解决了!这就是一种新的 NoSQL 数据库诞生的时刻。在 Uber,他们叫它 Schemaless 。
关于 Uber 对于数据库的选型
到目前为止,我并不认为 Uber 是和他们的文章中说的一样用 MySQL 替换了 PostgreSQL。看起来他们实际上是用他们的定制解决方案替换了 PostgreSQL,而且仅仅是用了 MySQL/InnoDB 做底层存储而已。
看起来那篇文章只是在说为什么 MySQL/InnoDB 比 PostgreSQL 更适合作 Schemaless 的底层存储。对于使用 Schemaless 的人来说可以听取他们的意见。但不幸的是,文章并没有说明白这一点,因为它没有提到是怎样的业务变化促使 Uber 引入了 Schemaless,要知道在2013 年他们刚从MySQL 迁移到PostgreSQL 上。
好可惜,现在读者们只留下了个PostgreSQL 很糟糕的印象……
阅读英文原文: On Uber’s Choice of Databases
评论