转:MySQL 对 Uber 来说可能是最合适的,但不一定适合你

https://coyee.com/article/10766-mysql-might-be-right-for-uber-but-not-for-you?fromoschina

前几天 Uber 发布了一篇文章 "为什么 Uber 工程师将 Postgres 数据库换成 MySQL?" ,我没有马上阅读这篇文章,因为我的内心告诉我还不如回家干点别的事呢。但是我的邮箱被塞满了这样的问题 —— PostgreSQL 真的很糟糕吗?因为知道 PostgreSQL 没那么烂,所以这些邮件促使我想知道这篇文章到底在写什么鬼。本文主要解读 Uber 这篇文章中的问题。

在我看来,Uber 的文章基本上是在说他们发现 MySQL 比 PostgreSQL 更适合他们的环境。不过这篇文章传递消息的方式却非常的糟糕。他们应该写“PostgreSQL 在重度更新操作场景下的一些限制” 而不是“写架构的缺陷”。例如,如果你的使用场景并非重度写操作的,就无需担心 Uber 描述的这些问题。

0我的这篇文字中将解释说为什么我认为 Uber 的文章不能作为一般数据库选型的建议文章;为什么 MySQL 对 Uber 来说比较适合,以及为什么成功会导致更多的问题,而不只是数据存储的缩放问题。

UPDATE 操作

Uber 文章描述的第一个大问题,但仍没有提供完整细节的问题是 PostgreSQL 在更新某个表的记录时,总是需要更新一个表的所有索引。而 MySQL/InnoDB 只需要更新被修改的列的索引。PostgreSQL 采取的方式导致了更新操作时需要执行更多的磁盘 IO 操作来更新那些非索引列(文章里说的是“写应用”)。如果这对 Uber 来说是一个大问题,这些更新操作可能是其整个负载的一大组成部分。

 
0然后,Uber 文章中有一点炒作成分的原因是:该文章并未提及 PostgreSQL 的 Heap-Only-Tuples (HOT) 。根据  PostgreSQL 官方资料, HOT 非常适合这种特定场景 "元组数据重复更新,但不改变它的索引列" ,在这种场景下 PostgreSQL 可以在不改动索引的情况下更新数据,前提是新的行版本数据和前一个版本的数据存在于同一个页中。后者可以通过  fillfactor 设置项来进行调整。我猜测 Uber 工程师已经意识到 HOT 并非他们问题的解决方案,因为其高频更新操作至少影响一个索引列。
 

这个假设也得到了文章的支持,请看这句话:“如果我们的一个表定义了十几个索引,对单一索引字段的更新需要传播到所有十几个的索引,以便 ctid 可以指向新的数据行。”,非常明显的一句话 “只受单个索引影响” ,这是一个边界使用场景 —— 只有一个索引 —— 否则,PostgreSQL 的 HOT 技术就可以解决这个问题。

旁注:我真的很好奇,他们所拥有的索引数量是否会减少 —— 我建议 重新设计索引。这完全有可能的,这些索引很少使用,但用时却非常重要。

 

看起来 Uber 运行了很多更新操作,改变至少一个索引列。但是对一个拥有很多索引列的表来说,这个数量还是相对要少很多。如果这是占主导的使用场景,那么文章中使用 MySQL 替代 PostgreSQL 的论点是有道理的。

SELECT 查询

这是 Uber 文章中另外一个引起我注意的声明:文章解释说 MySQL/InnoDB 使用的是簇集索引,同时承认了“InnoDB 的这种设计方案相对于 PostgreSQL 有一些轻微的不足,主要体现在二级索引的查找上,因为 InnoDB 要做两次索引的查找,而 PostgreSQL 只需要做一次。我之前写过一篇文章来描述这个问题 ("the clustered index penalty") in context of SQL Server.

 
0引起我注意的是他们描述簇集索引有一些轻微的劣势,但我觉得,如果你运行使用二级索引来运行很多查询,那么这个劣势就非常明显。如果对他们来说只是很轻微的劣势,这表明这些索引应该不怎么使用。也就是说大多数都是基于主键索引进行搜索(那么就没必要用簇集索引)。注意我这里写的是“搜索” 而不是“查询”,原因是簇集索引的缺点影响到任何一个带 where 的语句,而不仅仅是 select 语句。这同时也意味着其高频的更新主要是基于主键进行查询的。
 

最后在查询上这篇文章也存在一些疏漏:他们并未提及 PostgreSQL 在做索引扫描(index-only scans)的限制,特别是对更新非常频繁的数据库来说。PostgreSQL 对 index-only 扫描的实现几乎是没什么用的。这个简单的问题影响了我的大多数客户。我曾经在 2011 年写了一篇博客来描述这个问题。在 2012 年 PostgreSQL 9.2 有限的支持 index-only 扫描(只对大多数静态数据有效)。在 2014 年我在 PgCon 上重新提及此问题。但是 Uber 并未抱怨该问题。Select 查询的速度并不是他们关心的问题,我猜他们是通过在复制节点上运行查询来解决这个查询速度的问题的。

 

到现在为止,根据 Uber 的使用场景,我觉得使用 Key/Value 存储更适合。你猜怎么着? InnoDB 本身就是一个非常可靠而且受欢迎的 Key/Value 存储。甚至还有很多 InnoDB 的封装包,并提供一些(非常有限)的 SQL 前端,其中 MySQL 和 MariaDB 就是使用非常普遍的产品。请原谅我的讽刺,但认真的说,如果你只是需要一个 Key/Value 存储并且偶尔需要运行简单的 SQL 查询,那么 MySQL 或者 MariaDB 是一个合理的选择。我想在提供有限的 SQL 查询支持的要求下,MySQL 是比其他任何随机的 NoSQL 存储系统更好的选择。而 Uber 从另外一个角度讲就是在构建他们自己的(Schemaless)基于 InnoDB 和 MySQL 的架构。

 

重做索引

文章索引描述的最后一个注意点:它使用了 rebalancing 这个词,用在 B 树索引上。同时这个词链接到了维基百科上的文章 "Rebalancing after deletion." 不行的是维基百科的文章不适用于数据库索引,因为维基百科的文章上描述的算法要求每个节点至少要有半满的要求。为了提升并发处理能力,PostgreSQL 使用 Lehman, Yao 的 B-trees 变种, 从而支持稀疏索引。作为一个侧面说明,PostgreSQL 仍从索引中移除空页(请看 slide 15 of "Indexing Internals"). 然后,这仅仅是一个侧面的问题。

 

真正让我担心的是这句话:“ 使用 B 树索引一个至关重要的地方就是,它们必须定期重做 (rebalancing)。。。” 这里我想澄清的是,这并不是每天都要执行的过程。索引的平衡是在每次索引变更的时候维护的(有更糟糕的吗?),但是文章继续写到 “ 索引重做的操作会改变树的结构,因为子树会被移动到磁盘上新的位置。” 如果你以为 rebalancing 操作会引发大量的数据迁移,那你就理解错了。

B 树最重要的操作是节点分割。节点分割发生在当一个节点无法挂接属于它的一个新节点时。节点分割一般大概发送在大约 100次插入操作之后。节点分割时会分配一个新的节点,移动当前节点大约一半的条目到新的节点上,然后将新的节点与上一个节点、下一个节点以及父一级节点关联。这就是 Lehman Yao 算法节省了大量的锁资源的处理方式。在某些情况下,新的节点无法直接添加到父节点,因为父节点没有足够的空间来存放新的子节点条目,那么父节点就会开始进行节点分割,并依此类推。

 

最坏的情况下,分割操作会直接上升到根节点,那么根节点也会被分割,并在其上产生一个新的根节点。只有在这种情况下 B 树不会变得更深。注意,一个根节点的分割高效的将整棵树下移,因此保持了树的平衡。不管怎样,这并不会导致大量的数据移动。最坏的情况它可能会触及每个级别上的三个节点以及新的根节点。不过很明确的一点是:真实环境下的索引一般不会超过 5 级。更明确的是:最坏的情况下,也就是根节点分割只会在每10亿次插入才会发生一次。其他的情况无需遍历整棵树。毕竟,索引的维护不是周期性的,甚至不是很频繁的,它也不会完全的改变树的结构,至少不会发生磁盘上的物理操作。

 

物理复制

这是我对那篇文章关于 PostgreSQL 部分的另外一个关注点 —— 物理复制。文章中谈及索引重做是因为 Uber 碰到了 PostgreSQL 一个复制方面的 Bug 导致下游服务器的数据损坏(此 Bug 只影响特定版本的 PostgreSQL 9.2 ,而且很久以前就已经修复)。

因为 PostgreSQL 9.2 是核心中提供物理的复制功能,一个复制的 Bug “会导致树的大部分大部分不可用”。详细点说就是:如果一个节点分割没有正确的被复制,它就无法指向正确的子节点,那么子树就不可用。这完全正确,就像这么一句话:“如果存在一个 Bug,就大事不好了”。你无需修改大量的数据来破坏整个树结构,一个简单的坏指针足以。

 

Uber 文章中提及另外一些物理复制的问题是:1. 超大的复制流量;2. 因为更新操作导致的写操作放大;3. 更新 PostgreSQL 版本导致的停机时间太长。第一个问题我比较敏感,第二个问题我不能评论什么(但是可以通过 PostgreSQL-hackers 邮件列表中的声明 了解更多细节)。

最后,文章还声称 “Postgres 没有真正的复制 MVCC 支持” 。所幸的是文章链接到的 PostgreSQL 文档对这个问题有进行说明。问题的根本是复制的主节点不知道从节点在做什么,因此可能会删除一些仍然在副本上完成查询所需的数据。

 

根据 PostgreSQL 文档 的介绍,有两种方法来处理这些问题:

  1. 通过一个可配置的超时时间来延迟应用的复制流,因此读事务就有充足时间来完成查询。如果一个查询无法在要求的时间内完成,停止这个查询并继续进行复制。

  2. 配置复制节点给主节点发送关于查询执行的反馈,这样主节点就不会清空从节点所需的行版本数据。Uber 的文章使用了第一种方法,但是压根没提及第二种方法。这对 Uber 开发人员来说是打脸了。

 

关于开发者

引述一些常见的情况:"假设,开发者需要通过电子邮件发送收据给用户。这取决于开发者怎么写这个代码,该代码会打开一个数据库事务并在邮件发送完成后提交事务。让你的代码长时间打开一个数据库事务去等待一些毫无关联的堵塞操作是一种不好的做法,而现实中绝大多数开发者并非数据库专家,无法理解这个问题,特别是使用一些 ORM 框架屏蔽了底层操作时尤为如此。”

 

很不幸,我理解甚至是认同这种观点。与其说“绝大多数开发者不是数据库专家” 我想说的是绝大多数开发者对数据库的理解非常浅薄,每个接触到 SQL 的开发者都应该了解数据库事务 —— 不只是数据库专家。

我目前主要的工作是给开发者做一些 SQL 培训,我给各种规模的公司做过这样的培训。如果说有一件事我可以肯定的话,那就是多数人对 SQL 的知识是非常不足的。在“打开事务”的问题上,我可以确认几乎没有一个开发者知道只读事务的存在。大多数开发者知道事务可以在操作失败时候回滚写操作。我经常遇到有这样误解的开发者,所以我准备了一些幻灯片来解释这个问题。

 

关于成功

这是我最后想说的一个问题:一个公司雇佣越多的开发者,那么这个公司越趋近于其平均水平。夸张的说,如果你雇佣了整个星球的人,水平就是所有人的平均水平。招聘更多的人只是增加了这个取样的样本量。

打破这个规律的两种方法是:

  1. 只招聘最好的开发者,这种方法非常困难,因为你可能需要等很久才能找到很好的人。

  2. 招聘中等水平的人并提供培训。这就需要一个新员工入职后有比较长的上手时间,也可能会需要现有员工花时间去对这些新来的进行培训。两种方法共同的问题就是时间。因为你的业务快速发展,你可能没多少时间去等,所以你只能招聘那些对数据库了解并不多的开发者(2014年的经验数据)。换句话说,对一个快速增长的公司而言,变更技术比变更人员要容易得多。

 

随着时间的推移,成功的因素还会影响对技术栈要求的变化。在早期阶段,初创企业需要的是立即可用以及灵活的技术,这足以满足业务的需求。SQL 是一个很好的选择,因为它的确很灵活(你可以有很多种方式来做数据查询),而且很容易招聘到懂一点 SQL 知识的开发者。很好,那我们就开始吧。对很多公司,或者说是绝大多数的公司来说,这个故事就结束了。即使是这些公司比较成功,业务也进一步增长,它们仍将永远停留在 SQL 数据库的限制中。不过 Uber 不是如此。

 

一些幸运的初创企业最终会摆脱 SQL。如果这个事情发生了,就有更多的资源(或者几乎是无限的)然后奇妙的事情就发生了:他们意识到他们自己可以解决很多问题,例如替换一个通用的数据库系统,然后自己开发一个来代替它。这也是 NoSQL 数据库诞生的原因,在 Uber ,他们称之为 Schemaless.

Uber 的数据库选择

到现在为止,我相信 Uber 不需要像他们文章写的那样把 PostgreSQL 数据库替换成 MySQL。看起来他们只需要使用自己开发的解决方案去替换 PostgreSQL 就足够了,Uber 的这个解决方案恰好是 MySQL/InnoDB 目前所能提供的。

 

那篇文章只是解释了为什么作为 Schemaless 无模式的数据后端来说 MySQL/InnoDB 比 PostgreSQL 更加适合。如果你也在使用 Schemaless 的话,那可以听从 Uber 的建议。不过该文章并未清楚的说明其需求变更的情况,因为在 2013 年他们将数据库从 MySQL 移植到 PostgreSQL

可悲的是,这样的文章只留给读者一个糟糕的印象 —— PostgreSQL 很糟糕。

猜你喜欢

转载自suliran.iteye.com/blog/2317025
今日推荐