from:https://coyee.com/article/10690-why-uber-engineering-switched-from-postgres-to-mysql

介绍

早期的 Uber 架构是由 Python 编写的,使用的是 Postgres 数据库存储。从那时起,Uber 的架构就一直在变化,变成微服务模型和新的数据平台。具体的说,很多我们以前使用 Postgres 的地方现在改用 Schemaless,这是 Uber 基于 MySQL 构建的一个数据库分片层。在这篇文章中,我们将向你介绍我们在使用 Postgres 碰到的一些不足,并解释为什么要决定使用 MySQL 来构建 Schemaless 和其他后端服务。

第 1 段(可获 1.34 积分)

Postgres 的架构

我们遇到很多 Postgres 的问题:

  • 写效率低下
  • 数据复制效率低下
  • 表损坏的问题
  • 对复制的 MVCC 支持比较差
  • 更新到新版本很麻烦

我们将通过分析 Postgres 在磁盘上的存储的表和索引数据的存储来分析这些问题,同时我们也将与 MySQL 通过其 InnoDB 存储引擎在处理这些存储的方式进行比较。需要提醒的是本文中进行的分析是基于老的 Postgres 9.2 版本。据我们所知,在 Posgres 新版本中内部的架构并没有显著变化,而 9.2 版本中的磁盘存储的基本设计从 8.3 版本就没再有大的变化,这已经有将近 10 年的时间了。

第 2 段(可获 1.97 积分)

磁盘存储格式

关系数据库几个最关键的任务是:

而且这些任务必须能在一起工作,这就是一个数据库在设计磁盘上数据存储时候必须考虑问题。

Postgres 一个核心的设计思路就是不可变的行数据。这些不可变的行按照 Postgres 的说话被称为“元组”。Postgres 把这些元组称为 ctid. 一个 ctid 在概念上表示元组在磁盘的位置(例如物理磁盘偏移)。多个 ctid 可以用来描述单行数据(例如当用于 MVCC 的需求存在多个版本的行,或者老的行还没有被 autovacuum 进程回收)。一组有组织的元组就形成了一个表。表包含索引,索引数据通常使用B树进行组织,映射到实际数据上。

第 3 段(可获 2.49 积分)

通常 ctids 对用户来说是透明的,但是了解它们是如何工作的有助于理解 Postgres 表在磁盘的存储结构。为了查看某行数据的 ctid 值,我们可以在查询语句中增加 ctid 字段即可,例如:

uber@[local] uber=> SELECT ctid, * FROM my_table LIMIT 1;  -[ RECORD 1 ]--------+------------------------------  ctid                 | (0,1)  ...其他字段...

为了解释这个布局的详情,让我们假设一个简单的用户表。对每个用户我们有一个自增长的用户 ID 作为主键,还有用户的姓名以及出生年份。同时我们给姓名定义了一个复合索引,另外一个索引是用户的出生年份。该表的 DDL 定义 SQL 如下所示:

第 4 段(可获 1.71 积分)
CREATE TABLE users (     id SERIAL,     first TEXT,     last TEXT,     birth_year INTEGER,     PRIMARY KEY (id) ); CREATE INDEX ix_users_first_last ON users (first, last); CREATE INDEX ix_users_birth_year ON users (birth_year);

注意着上面的三个索引,逐渐索引加上我们定义的两个二级索引。

这篇文字的示例中我们的数据表中使用如下的数据,包含了一组有影响力的历史数学家:

如我们之前描述的,每一行都隐式的包含一个唯一但不可见的 ctid 字段。因此这个表的内部呈现如下表所示:

第 5 段(可获 0.99 积分)

主键索引映射到 ctids,定义如下:

B树索引定义在 id 字段上,B树中每个节点包含 ctid 值。注意在这种情况下,B树中字段的顺序刚好与表中的顺序是一致的,因为使用的是自增长ID,但这并不一定需要这样。

第 6 段(可获 1.04 积分)

二级索引看起来都差不多,最大的不同就是存储顺序的差别。B树索引是采用字母书序进行组织。下面是采用字母顺序对名字进行排序:

同样的,出生年份是簇集索引,按升序的排序:

如你所见,这些情况下二级索引的使用并不会让 ctid 字段按字母顺序排序,这与使用自增长的主键不同。

第 7 段(可获 1.17 积分)

假设我们需要更新表中的一行记录,例如我们需要将 al-Khwārizmī 的出生年份更改为 770 CE。如前面我们提到的,行的元组是不可变的。因此修改这个记录时我们需要添加一个元组到表中,新的元组就有一个新的 ctid 值,将被标识成为 I。Postgres 需要区分新的 I 上元组与老的元组 D。Postgres 在每个元组的内部存储了一个版本字段和一个指向前一个元组的指针(如果存在的话)。因此新的表结构看起来如下所示:

第 8 段(可获 1.66 积分)

只要存在两个版本的 al-Khwārizmī 行数据,索引就必须相应的保留对两个版本的数据映射。为了简单描述,我们省略了主键索引,这里只显示二级索引,如下所示:

第 9 段(可获 0.54 积分)

我们使用红色来标识老版本的数据,使用绿色标注新的数据。Postgres 使用另外一个字段来存放行的版本信息,用来确定哪个元组是最近使用的。这个增加了的字段可以让数据库确定哪个行元组用于事务处理,可能不被允许看最新行版本。(译者注:此句翻译很便扭,求纠正)

Postgres_Tuple_Property_

Postgres 是将主键索引和二级索引直接指向元组在磁盘的偏移,当一个元组的位置发生变化,所有的索引都必须被更新。

第 10 段(可获 1.21 积分)

数据复制

当我在表中插入一行新数据时,Postgres 需要在启用流复制时将数据复制到从节点。为了故障恢复考虑,数据库维护了一个 写前日志 (WAL) 并使用它实现 两阶段提交。就算流复制不启用的话数据库也必须维护这个 WAL,因为 WAL 使得数据库支持 ACID 的原子性和持久性。

我们可以通过考虑当数据库宕机时所发生的事情来理解 WAL,例如突然断电的情况。WAL 相当于是数据库计划对磁盘存储的数据进行修改时的一个分类账。当 Postgres 守护进程第一次启动时,进程会将这个分类帐与磁盘中实际存储的数据进行比较,如果分类账中包含了一些磁盘上没有的数据,那么数据库就会自动纠正这些通过 WAL 反应的元组和索引数据的差异。然后回滚出现在 WAL 但未提交的事务中的数据(意味着事务没被提交)。

第 11 段(可获 2.53 积分)

另外一方面,Postgres 也利用 WAL 将其在主从之间发送来实现流复制功能。每个从库复制数据与上述崩溃恢复的过程类似。流复制与实际崩溃恢复之间的唯一区别是,在恢复数据过程中是否能对外提供数据访问服务。

由于 WAL 实际上是为崩溃恢复目的而设计,它包含在物理磁盘的低级别更新的信息。WAL 记录的内容是在行 tuple 和它们的磁盘偏移量(即一行 ctids) 的实际磁盘上的代表级别。如果暂停一个 Postgres 主库,从库数据完全赶上后,在从库的实际磁盘上的内容完全匹配主库。因此,像工具 rsync 都可以恢复一个同步失败的从库。

第 12 段(可获 2.67 积分)

Postgres 上述设计的大坑

Postgres 的上述设计给 Uber 在 PG 的使用上,导致了效率低下和其他很多问题。

写放大(Write Amplification)

在 Postgres 设计的第一个问题是已知的写入放大 。

通常的写入放大是指一种问题数据写入,比如在 SSD 盘上,一个小逻辑更新(例如,写几个字节)转换到物理层后,成为一个更大的更昂贵的更新。

同样的问题也出现在 Postgres,在上面的例子,当我们做出的小逻辑更新,比如修改 al-Khwārizmī 的出生年份时,我们不得不执行至少四个物理的更新:

  1. 在表空间中写入新行的 tuple;
  2. 为新的 tuple 更新主键索引;
  3. 为新的 tuple 更新姓名索引 (first, last) ;
  4. 更新 birth_year 索引,为新的 tuple 添加一条记录;
第 13 段(可获 2.14 积分)

事实上,这四步更新仅为了反映一个到主表的写操作;并且每个这些写入也同样需要在 WAL 得到体现,所以在磁盘上写入的总数目甚至比 4 步更大。

值得一提的是这里更新 2 和 3。当我们更新了 al-Khwārizmī 的出生年份,我们实际上并没有改变他的主键,我们也没有改变他的名字和姓氏。然而,这些索引仍必须与创建在数据库中的行记录了新的行的 tuple 的更新。对于具有大量二级索引的表,这些多余的步骤可能会导致巨大的低效。举例来说,如果我们有一个表上定义了十几个二级索引,更新一个字段,仅由一个单一的索引覆盖必须传播到所有的 12 项索引,以反映新行的 CTID。

第 14 段(可获 2.19 积分)

复制

因为复制发生在磁盘的变化上,因此写入放大问题自然会转化为复制层的放大。一个小的逻辑记录,如“更改出生年份为 CTID D 到 770”,WAL 会将上述描写的 4 步从网络上同步到从库,因此写入放大问题也等同一个复制放大问题,从而 Postgres 的复制数据流很快变得非常冗长,可能会占用大量的带宽。

在 Postgres 的复制发生一个数据中心内的情况下,复制带宽可能不是一个问题。现代网络设备和交换机可以处理大量的带宽,许多托管服务提供商提供免费或廉价的内部数据中心带宽。然而,当复制必须在不同数据中心之间发生的,问题都可以迅速升级。

第 15 段(可获 2.97 积分)

级联复制可以降低跨数据中心的带宽要求,只需要主库和一个从库之间同步一份数据所需的带宽和流量,即便在第二个数据中心配置了多个从库。然而,Postgres 的复制协议的详细程度,对于使用了大量二级索引的数据库,仍可能会导致数据的海量传输。采购跨国的带宽是昂贵的,即使有钱的土豪公司,也无法做到跨国的带宽和本地的带宽一样大。

这种带宽的问题也导致我们曾经在 WAL 归档方面出现过问题。除了发送所有从西海岸到东海岸的 WAL 更新,我们将所有的 WAL 记录归档到一个文件存储的 Web 云服务,这样当出现数据灾难情况时,可以从备份的 WAL 文件恢复。但是流量峰值时段,我们与存储网络服务的带宽根本无法跟上 WAL 写入的速度。

第 16 段(可获 2.73 积分)

3. 数据损坏

在一次例行主数据库扩容的变更中,我们遇到了一个 Postgres 9.2 的 bug。从库的切换时间顺序执行不当,导致他们中的一些节点误传了一些 WAL 记录。因为这个 bug,应该被标记为无效的部分记录未标记成无效。

以下查询说明了这个 bug 如何影响我们的用户表:

SELECT * FROM users WHERE ID = 4;

此查询将返回两条记录:修改出生年份之前的老记录,再加上修改后的新记录。如果将 CTID 添加到 WHERE 列表中,我们将看到返回记录中存在不同的 CTID 记录,正如大家所预料的,返回了两个不同行的 tuple。

第 17 段(可获 1.9 积分)

 

这个问题是有几个原因非常伤脑筋。首先,我们不能轻易找出这个问题影响的行数。从数据库返回的结果重复,导致应用程序逻辑在很多情况下会失败。我们最终使用防守编程语句来检测已知有这个问题表的情况。因为 bug 影响所有服务器,损坏的行在不同的服务器节点上可能是不同的,也就是说,在一个从库行 X 可能是坏的,Y 是好的,但对另一个从库,用行 X 可能是好的,Y 行可能是坏。事实上,我们并不确定数据损坏的从库节点数量,以及主库是否也存在数据损坏。

第 18 段(可获 1.81 积分)

虽然我们知道,问题只是出现在每个数据库的少量几行,但我们还是非常担心,因为 Postgres 复制机制发生在物理层,任何小的错误格式有可能会导致彻底损坏我们的数据库索引。B 树的一个重要方面是,它们必须定期重新平衡 ,并且这些重新平衡操作可以完全改变树的结构作为子树被移到新的磁盘上的位置。如果错误数据被移动,这可能会导致树的大部分地区变得完全无效。

最后,我们追踪到了实际的 bug,并用它来确定新的 master 不存在任何损坏行。然后再把 master 的快照同步到所有从库上去,这是一个艰苦的体力活的过程(小编:看到美帝的 DBA 也这么苦逼心理终于平衡一点了),因为我们每次只能从在线的池子里面拿出有限几台来操作。

第 19 段(可获 2.24 积分)

虽然我们遇到的这个 bug 仅影响 Postgres 9.2 的某些版本,而且目前已经修复了很久。但是,我们仍然发现这类令人担忧的 bug 可以再次发生。可能任意一个新的 Postgres 版本,它会带着这种致命类型的 bug,而且由于其复制的不合理的设计,这个问题一旦出现,就会立即蔓延到集群中所有复制链的数据库上。

4. 从库无 MVCC

Postgres 没有真正的从库 MVCC 支持。在从库任何时刻应用 WAL 更新,都会导致他们与主库物理结构完全一致。这样的设计也给 Uber 带来了一个问题。

第 20 段(可获 1.71 积分)

为了支持 MVCC,Postgres 需要保留行的旧版本。如果流复制的从库正在执行一个事务,所有的更新操作将会在事务期间被阻塞。在这种情况下,Postgres 将会暂停 WAL 的线程,直到该事务结束。但如果该事务需要消耗相当长的时间,将会产生潜在的问题,Postgres 在这种情况下设定了超时:如果一个事务阻塞了 WAL 进程一段时间,Postgres 将会 kill 这个事务。

 

第 21 段(可获 1.36 积分)

这样的设计意味着从库会定期的滞后于主库,而且也很容易写出代码,导致事务被 kill。这个问题可能不会很明显被发现。例如,假设一个开发人员有一个收据通过电子邮件发送给用户一些代码。这取决于它是如何写的,代码可能隐含有一个的保持打开,直到邮件发送完毕后,再关闭的一个数据库事务。虽然它总是不好的形式,让你的代码举行公开的数据库事务,同时执行无关的阻塞 I / O,但现实情况是,大多数工程师都不是数据库专家,可能并不总是理解这个问题,特别是使用掩盖了低级别的细节的 ORM 的事务。(小编:美帝程序员代码习惯跟咱们也很类似)

第 22 段(可获 1.89 积分)

Postgres 的升级

因为复制记录在物理层面工作,这导致不能在不同的 Postgres GA 版本之间进行复制。运行的 Postgres 9.3 主数据库无法复制数据到 Postgres 9.2 的从库上,也无法在运行 9.2 的主数据库复制数据到 Postgres 9.3 的从库上。

我们按照以下这些步骤,从一个 Postgres 的 GA 版本升级到另一个:

  • 关闭主数据库。

  • 在主库上运行 pg_upgrade 命令,这是更新主库数据的命令 。在一个大的数据库上,这很容易需要几个小时的时间,执行期间不能够提供任何访问服务。

  • 再次启动主库。

  • 创建主库的新快照,这一步完全复制一份主库的所有数据,因此对于大型数据库,它也需要几个小时的时间。

  • 清除所有从库上的数据,将从主库导出的快照恢复到所有从库。

  • 把每个从库恢复到原先的复制层次结构。等待从库追上主库的最新的更新数据。

第 23 段(可获 2.67 积分)

我们使用上述方法将 Postgres 9.1 成功升级到 Postgres 9.2。然而,这个过程花了太多时间,我们不能接受这个过程再来一次。到 Postgres 9.3 出来时,Uber 的增长导致我们的数据大幅增长,所以升级时间将会更加漫长。出于这个原因,我们的 Postgres 的实例一直运行 Postgres 9.2 到今天,尽管当前的 Postgres GA 版本是 9.5

如果你正在运行 Postgres 9.4 或更高版本,你可以使用类似 pglogical,它实现了 Postgres 的一个逻辑复制层。使用 pglogical,可以在不同的 Postgres 版本之间复制数据,这意味着升级比如从 9.4 到 9.5,不会产生显著的停机时间。但这个工具的能力依然存疑,因为它没有集成到 Postgres 主干,另外对于老版本的用户,pglogical 仍然不能支持。

第 24 段(可获 2.36 积分)

MySQL 架构概述

为了更进一步解释的 Postgres 的局限性,我们了解为什么 MySQL 是 Uber 新存储工程 Schemaless 的底层存储 。在许多情况下,我们发现 MySQL 更有利于我们的使用场景。为了了解这些差异,我们考察了 MySQL 的架构,并与 Postgres 进行对比。我们特别分析 MySQL 和 InnoDB 存储引擎如何一同工作。Innodb 不仅在 Uber 大量使用,它也是世界上使用最广泛的 MySQL 存储引擎。

InnoDB 的磁盘数据结构

与 Postgres 一样,InnoDB 支持如 MVCC 和可变数据这样的高级特性。详细讨论 InnoDB 的磁盘数据格式超出了本文的范围;在这里,我们将重点放在从 Postgres 的主要区别上。

第 25 段(可获 1.8 积分)

架构上最重要的差别是 Postgres 直接映射索引记录到数据元组的磁盘位置,而 InnoDB 维护了一个二级结构。不同于保存一个指向记录磁盘位置的指针(如 Postgres 的 ctid),InnoDB 的二级索引记录是一个指向主键值的指针。所以 MySQL 中的二级索引通过主键关联的索引:

第 26 段(可获 0.87 积分)

为了执行上的(first, last)索引查找,我们实际上需要做两查找。第一次查找表,找到记录的主键。一旦找到主键,则根据主键找到记录在磁盘上的位置。

这种设计意味着 InnoDB 对 Postgres 在做非主键查找时有小小的劣势,因为 MySQL 要做两次索引查找,但是 Postgres 只用做一次。然后因为数据是标准化的,行更新的时候只需要更新相应的索引记录。而且 InnoDB 通常在相同的行更新数据,如果旧事务因为 MVCC 的 MySQL 从库而需要引用一行,老数据将进入一个特殊的区域,称为回滚段

第 27 段(可获 2.04 积分)

如果我们更新 al-Khwārizmī 的出生年份,我们看会发生什么。如果有足够的空间,数据库会直接更新 ID 为 4 的行(更新出生年份不需要额外的空间,因为年份是定长的 int)。出生年份这一列上的索引同时也会被更新。这一行的老版本被复制到回滚段。主键索引不需要更新,同样姓名索引也不需要更新。如果在这个表上有大量索引,数据库需要更新包含了 birth_year 的索引。因此,我们并不需要更新 signup_date,last_login_time 这些索引,而 Postgres 则必须全更新一遍。

第 28 段(可获 2.16 积分)

这样的设计也使得 vocuum 和压缩效率更高。所有需要 vocuum 的数据都在回滚段内。相比之下,Postgres 的自动清理过程中必须做全表扫描,以确定删除的行。

MySQL 使用额外的间接层:二级索引记录指向主索引记录,而主索引本身包含在磁盘上的排的位置。如果一个行偏移的变化,只有主索引需要更新。

复制

MySQL 支持多个不同的复制模式:

  • 语句级别的复制:复制 SQL语句(例如,它会从字面上直译复制的语句,如:更新用户 SET birth_year = 770 WHERE ID = 4 )
  • 行级别的复制:复制所有变化的行记录
  • 混合复制:混合这两种模式
第 29 段(可获 1.79 积分)

这些模式都各有利弊。基于语句的复制通常最为紧凑,但可能需要从库来支持昂贵的语句来更新少量数据。在另一方面,基于行的复制,如同 Postgres 的 WAL 复制,是更详细,但会导致对从库数据更可控,并且更新从库数据更高效。

在 MySQL 中,只有主索引有一个指向行的磁盘上的指针。这个对于复制来说很重要。MySQL 的复制流只需要包含有关逻辑更新行的信息。复制更新如“更改行的时间戳 x 从 T_ 1 至 T_ 2 ”,从库自动根据需要更新相关的索引。

第 30 段(可获 1.87 积分)

相比之下,Postgres 的复制流包含物理变化,如“在磁盘偏移8382491,写字节XYZ。” 在 Postgres 里,每一次磁盘物理改变都需要被记录到 WAL 里。很小的逻辑变化(如更新时间戳)会引起许多磁盘上的改变:Postgres 必须插入新的 tuple,并更新所有索引指向新的 tuple。因此许多变化将被写入 WAL。这种设计的差异意味着 MySQL 复制二进制日志是显著比 PostgreSQL 的 WAL 流更紧凑。

第 31 段(可获 1.33 积分)

复制如何工作也会影响从库的 MVCC。由于 MySQL 的复制流使用逻辑的更新,从库可以有真正的 MVCC 语义; 因此,读库查询不会阻塞复制流。相比之下,Postgres 的 WAL 流包含物理磁盘上的变化,使得 Postgres 的从库无法应用复制更新从而与查询相冲突,所以 PG 复制不能实现 MVCC。

MySQL 的复制架构意味着,bug 也许会导致表损坏,但不太可能导致灾难性的失败。复制发生在逻辑层,所以像一个重新平衡 B tree 这样的操作不会导致索引损坏。一个典型的 MySQL 复制问题是一个语句被跳过(或较少一点的情况,重复执行)的情况下。这可能会导致数据丢失或无效,但不会导致数据库出现灾难问题。

第 32 段(可获 2.09 积分)

最后,MySQL 的复制架构使得它可以在 MySQL 不同版本之间进行复制。MySQL 只在复制格式改变的时候才增加版本号,这对 MySQL 来说很不常见。MySQL 的逻辑复制格式也意味着,在磁盘上的变化在存储引擎层不影响复制格式。做一个 MySQL 升级的典型方法是在一个时间来更新应用到一个从库,一旦你更新所有从库,你可以把它提为新的 master。这个操作几乎是 0 宕机的,这样也能保证 MySQL 能及时得到更新。

第 33 段(可获 1.47 积分)

其他 MySQL 设计优势

到目前为止,我们集中于 Postgres 和 MySQL 在磁盘上的架构。MySQL 的架构导致性能比 Postgres 有显著优势。

 

缓冲池设计

首先,两个数据库缓冲池的工作方式不同。Postgres 用作缓存的内存比起内存的机器上的内存总数小很多。为了提高性能,Postgres 允许内核通过自动缓存最近访问的磁盘数据的页面缓存。举例来说,我们最大的 Postgres 的节点有 768G 可用内存,但只有大约 25G 的内存实际上是被 Postgres 的 RSS 内存使用,这让 700 多 GB 的可用内存留给 Linux 的页面缓存。

第 34 段(可获 1.86 积分)

这种设计的问题是,相比访问 RSS 内存,操作系统的页面缓存访问数据实际上开销更大。从磁盘查找数据,Postgres 执行 lseek 和 read 系统调用来定位数据。这些系统调用的招致上下文切换,这比从主存储器访问数据更昂贵。事实上,Postgres 在这方面完全没有优化:Postgres 没有利用的 pread(2)系统调用,pread 会合并 seed + read 操作成一个单一的系统调用。

相比之下,InnoDB 存储引擎实现了自己的 LRUs 算法,它叫做 InnoDB 的缓冲池。这在逻辑上类似于 Linux 的页面缓存,但在用户空间实现的,因此也显著比 Postgres 设计复杂,InnoDB 缓冲池的设计有一些巨大的优势:

第 35 段(可获 2.01 积分)

     1、使得它可以实现一个自定义的 LRU 设计。例如,它可以检测到病态的访问模式,并且阻止这种模式给缓冲池带来太大的破坏。

     2、它导致更少的上下文切换。通过 InnoDB 缓冲池访问的数据不需要任何用户/内核上下文切换。最坏的情况下的行为是一个的出现 TLB miss,但是可以通过使用 huag page 来搞定。

 

连接处理

MySQL 的实现是对每个连接生成一个线程,相对来说开销较低;每个线程拥有堆栈空间的一些内存开销,再加上堆上分配用于连接特定的缓冲区一些内存。对 MySQL 来说扩展到 10,000 左右的并发连接不是罕见的事情,实事上我们现在的 MySQL 接近这个连接数。

第 36 段(可获 2.03 积分)

Postgres 使用的是每连接一个进程的设计。这很明显会比每连接每线程的设计开销更大。启动一个新的进程比一个新的线程会占用更多的内存。此外,线程之间进行通讯比进程之间 IPC 开销低很多。Postgres 9.2 使用系统V IPC为IPC原语,而不是使用线程模型中轻量级的 futexes,futex 的非竞争是常见的情况,比 System V IPC 速度更快,不需要进行上下文切换。

除了与 Postgres 的设计相关联的内存和 IPC 开销,即使有足够的可用内存可用,Postgres 对处理大连接数的支持依然非常差。我们已经碰到扩展 Postgres 几百个活动连接就碰到显著的问题的情况,在官方文档中也没有确切的说明原因,它强烈建议使用独立的连接池来保证大连接数。因此,使用 pgbouncer 做连接池基本可行。但是,在我们后端系统使用过程中发现有些 BUG,这会导致开启大量的原本不需要的活跃连接,这些 BUG 也已经造成好几次宕机。

第 37 段(可获 3.01 积分)

结论

Postgres 在 Uber 初期运行的很好,但是 PG 很遗憾没能很好适应我们的数据增长。今天,我们有一些遗留的 Postgres 实例,但我们的数据库大部分已经迁移到 MySQL(通常使用我们的 Schemaless 中间层),在一些特殊的情况下,也使用 NoSQL 数据库如 Cassandra。我们对 MySQL 的使用非常满意,后续可能会在更多的博客文章中介绍其在 Uber 一些更先进的用途。

第 38 段(可获 1.17 积分)

MySQLPostgresUber