Google Cloud Spanner 学习报告

Google Cloud Spanner 学习报告

  • 17 Jan 2019

本人介绍,未使用过 Google Cloud Spanner,不是 DBA。

简介

Google 的 AdWords 曾经是靠 MySQL+手工Sharding来支撑,但在扩展和可靠性上不能满足要求。于是开发了全局一致性和自动 Sharding 的数据库 Spanner。

Spanner 带动了一波云原生数据库的热潮,云原生数据库的特点是,计算和存储分离,增加 node 就可以增加存储能力和性能;自动分片;支持分布式事务。

国内的云原生数据有阿里云的 云数据库POLARDB,腾讯云的
云数据库 CynosDB,这两个数据库的特点还有完全兼容 MySQL,其中 CynosDB 还可以支持 PostgreSQL 10(参考时间 2019年1月17日),现有应用无需更改 SDK,减少数据库迁移的成本。

TrueTime

Spanner 数据库告诉了人们,当数据容量增加,分片数量增加,依然可以享受高性能和事务全局一致性,依靠的最惊艳的技术就是 TrueTime。

注意,Spanner 的可序列化(serializability)依靠的还是 Lock,但是外部一致性(external consistency)依靠的是 TrueTime

事务依靠全局一致性的顺序 id 来保证事务执行顺序的正确性,Spanner 使用的是时间戳,而就算使用原子时钟,时间还是会有乱序的存在。TrueTime.now() 返回了 [earliest, latest] 间隔,牺牲了少许等待时间换来更高的一致性。下面 quote 一下 Google 的文章

Thus, if two intervals do not overlap, then we know calls were definitely ordered in real time. If the intervals overlap, we do not know the actual order.

Schema Design

不同的数据库间做数据迁移,都要理解两个数据库各自的优点,才能更好的用上新数据库。例如从 MySQL 迁移到 HBase,如果表设计一模一样的话,简直就是浪费了 Hbase 的特性,而且因为 Hbase 没有二级索引(虽然可以简单设计一下)可能业务性能会下降。

先简单说一下 Spanner 的存储架构

Spanner 的数据都是存放在节点机器,下面简称 node,而 node 存放分片(split),数据在 split 的访问顺序是有序的,参考过论文,数据在 split 中使用 KV 结构存储。node 之间的数据也是有序的,可以这样理解:node1{spli1, split2}, node2{split3}, node3{split4, split5, slit 6}

第一个问题,顺序写入 HotSpot

数据主键如果是有序的,例如用自增 id 或者 MongoDB 的 ObjectId,新数据很大可能会写入到第一个或者最后一个split,造成单个 node 的负载超高,而其他 node 很空闲,这样对于写性能不能做到增加 node 就增加性能。

Spanner 产品文档给出了几种解决办法:

1. 调换主键顺序

因为 spanner 是可以定义多个列来组合称主键,登录日志表如原主键是 (time, user_id), 因为 time 字段是自增的,可以用 (user_id, time) 来做主键。登录的 user_id 是无序的所以可以把数据写入到尽量多的 split

2. 增加 shard_id 字段

通过保存原主键的 hash 或者只是保存 hash。例如原主键是 (shop_id, user_id, order_id),可以增加 shard_id 字段 shard_id = md5(shop_id + user_id + order_id)[0:3],新主键为 (shard_id, shop_id, user_id),这里还可以考虑到只需要 shop_id 和 user_id 计算 md5,因为这样的话每个用户下的订单都可能放在同一个 split,加快用户端的订单查找速度。而根据 shop_id 查找订单可能是低频的,能接受的延时稍高。Spanner 文档是使用 hash() % N 来存储数字型 shard_id,我这里只是给出另一种实现,不一定是最好。

3. 使用 UUID v4

新表设计可以使用 UUID,但是如果从旧数据库转移过来,例如 MongoDB,直接把 ObjectId 替换成 UUID 的话,一般都是新增一个主键然,保留旧主键,并且为旧主键增加索引。但是更好的办法应该是使用前一种方法,增加 shard_id。不过,具体是否更好还是要看业务,增加 shard_id 的好处是服务使用方还是可以直接旧 id 来访问单条数据。

4. 按位反转

// 随手打的不要介意 
// 不是所有语言都能很好处理 64 位整数和二进制运算, 下面用 javascript 做例子

function getNewId(oldId) {
    let newId = 0;
    const MAX_BITS = 51;
    for (let i = 0; i < MAX_BITS; i++) {
        newId *= 2; // newId <<= 1;
        if (oldId & 1) {
            newId |= 1;
        }
        oldId = Math.floor(oldId / 2);
    }
    return newId;
}

还有一种做法做法其实跟 shard_id 差不多,就是把最高位的第 2 到 N + 1 位用来保存hash,原 id 保存在低 (63 - N) 位

出现 HotSpot 的其他因素

1. 索引

Spanner 的索引也是数据表,也就是数索引的设计也会影响写入性能

Table 主键设计好了,但是因为业务需要需要增加二级索引,而二级索引的字段的数据是自增的话,索引表的新数据都会写到同一个 split,从而造成 HotSpot。

2. 父子关系表设计

您可以在一个数据库中定义多个表,而且,如果希望 Cloud Spanner 以物理方式协同定位表的行,从而实现高效检索,您还可以选择定义表之间的父子关系。 https://cloud.google.com/spanner/docs/schema-and-data-model?hl=zh-CN

假设 root table(父表) shops(shop_id, shop_name), 字表 shop_orders(shop_id, order_id) INTERLEAVE IN PARENT shops,这样同一个 shop_id 下的所有订单都会放在同一个 split,就算 shop_orders 表加入了 shard_id 都没没有用的,当同一个店铺高并发写入订单的时候,会造成存放该 shop_id 数据的 split 出现 HotSpot。

父子表适合字表数据相对少的情况,例如一个订单一张发票,那么发票表可以是订单表的子表。

数据库选型考虑

由于 Google Cloud Spanner 是 GCP 托管的服务,存储费对于新旧数据都是一致的,一个电子商务网站,顾客一般不会经常看 1 年前的订单,这时候的订单数据如何可以存放在成本相对小的服务器,可以大大减少云服务费用。

另外一个考虑就是,不是所有应用都一定要用到 Spanner,Google 给出另一个选择合适数据库的方法:

https://cloud.google.com/storage-options/

image.png

图片来自 Google Cloud

参考资料

阅读原文

推荐阅读更多精彩内容