Uber的底层存储从Postgres换成MySQL之后

  背景

  早期的Uber后台软件由Python写成,数据存储使用Postgres。后期随着业务的飞速发展后台架构也变化巨大,演进成了微服务加数据平台。数据存储也由Postgres变成了 Schemaless ——Uber自主研发的以MySQL做为底层的高可用数据库。Uber的数据库主要存储的是Trip数据,就是一个叫车订单从下单起,到上车、下车、付费等的全过程跟踪及处理。从2014年初起,由于业务增长迅猛,Uber的原有基础架构已经无法继续支撑业务。改进的项目花了将近一年时间。

物联网

  对于新的数据库存储系统,Uber的主要关键需求是:

  要有能力通过增加服务器而线性地增加容量。增加服务器不但要增加可用的硬盘容量,还要减少系统的响应时间。

  需要有写缓冲能力,万一持久化到数据库失败时,仍可以稍后重试。

  需要通知下游依赖关系的方式,数据变更要能无损的通知出去。

  需要二级索引。

  系统要足够健壮,可以支持7*24服务。

  在调查对比了Cassandra、Riak和MongoDB等等之后,Uber技术团队没有发现能完全满足需求的现成解决方案。而再考虑到数据可靠性、对技术的把握能力等因素,他们决定自己开发一套数据库管理系统——Schemaless,一个键值型存储库,可以存放JSON数据而无需严格的模式验证,是完全的无模式风格。用MySQL作底层存储,其中只有顺序写入,在MySQL主库故障时支持写入缓冲。并有一个数据变更通知的发布-订阅功能(命名为trigger),支持数据的全局索引。

  Schemaless项目技术负责人Jakob Thomsen 认为 :

  Schemaless的强大与简单更多是因为我们在存储节点中使用了MySQL。Schemaless本身是在MySQL之上相对较薄的一层,负责将路由请求发送给正确的数据库。借助于MySQL第二索引及InnoDB的BufferPool,Schemaless的查询性能很高。

  在Evan Klitzke的文章中,他是从Postgres与 Innodb 的底层存储机制对比开始的,后面提到了他们碰到的若干Postgres问题:

  写入效率不高

  数据主从复制效率不高

  表损坏问题

  从库上的 MVCC 支持问题

  难于升级到新版本

  在Postgres的底层设计中,它的行数据是不可修改的,每个不可修改的行都叫做“元组”,每个唯一的元组都由一个唯一的 ctid 标志,ctid也就实际指出了这个元组在磁盘上的物理偏移量。这样对于一行修改过的数据来说,就会对应着在物理上有多个元组。表是有索引的,主键索引和第二索引都以B树组织,都直接指向ctid。

  除了ctid之外还有一个关键字段prev,它的默认值为null,但对于有数据修改的记录,新的元组里面的prev字段里存储的就是旧元组的ctid值。

物联网

  与Postgres相对应的是,MySQL的InnoDB引擎主键索引和第二也都以B树组织,但是索引指向的是主键,而主键才真正指向数据记录。而且,InnoDB的数据是可以修改的。两者实现MVCC的机制不同,MySQL依靠UNDO空间中的 回滚段 ,而不是象Postgres依靠在数据表空间对同一条数据保持多份。

物联网

  Postgres和InnoDB都通过 WAL (Write Ahead Log)来保证数据可以在数据库上安全写入,但对于主从库的数据复制实现原理并不同。Postgres会直接把WAL发送到从库上,让从库也执行WAL来复制数据。而MySQL则是发送Binlog,在从库上应用Binlog。

  由此,再来看看Uber对于Postgres有哪些不满意:

  写放大

  一般来说大家介意 写放大 的问题是由于对SSD磁盘的使用。SSD磁盘是有寿命的,它的写入次数是有限的(虽然数字很大)。这样如果应用层只是想写入少量数据而已,但数据落入磁盘时却变大了许多倍,那大家就会比较介意了。比如你只是想写入1K的数据,可是最终却有10K数据落盘。

  Postgres的写放大问题主要表现在对有索引的表进行数据更新上。因为Postgres的索引都是指向元组的ctid,而元组又是不可更新的,所以当你更新一条记录时,它会创建一个新的元组存入磁盘,并且要针对所有的索引,为每个索引都创建一条新记录来指向新的元组,不管你更改的字段和这个索引有没有关系。这样对于WAL来说,Postgres更改一条记录操作会写入新的完整记录,再加上多条索引记录。