AWS系列:Dynamodb深度体验(1)

编者按:本文系InfoQ中文站向陈天的约稿,这是AWS系列文章的第三篇。以后会有更多文章刊出,但并无前后依赖的关系,每篇都自成一体。读者若要跟随文章来学习AWS,应该至少注册了一个AWS账号,事先阅读过当期所介绍服务的简介,并在AWS management console中尝试使用过该服务。否则,阅读的效果不会太好。DynamoDB看似简单,用起来则相当麻烦。本文将探讨如何进行DynamoDB的数据库(表)设计,如何使用 hash key / range key 使得查询更方便快捷?如何缓冲对DynamoDB的突发读写,如何自动scale up / scale down。

基本概念

DynamoDB 是 AWS 独有的完全托管的 NoSQL Database。它的思想来源于 Amazon 2007 年发表的一篇论文:Dynamo: Amazon’s Highly Available Key-value Store。在这篇论文里,Amazon 介绍了如何使用 commodity hardware 来打造高可用、高弹性的数据存储,这篇文章影响了很多 NoSQL 数据库的设计,如 cassandra / riak,也最大程度地将 consistent hashing 这个概念从学术界引入了工业界。欲理解 DynamoDB,首先要理解 consistent hashing。consistent hashing 的原理如下图所示:

AWS系列:Dynamodb深度体验(1)_第1张图片

它的概念是:我有一个足够大的keyspace(2的160次方,比较一下:IPv6是2的128次方),我们记作X,然后将X放在一个环形的空间里划分成大小相等的Y个 partition,依次循环排列(如图),每个 partition 由一个vnode(riak的概念)管理,当你有M个database server(node),Y个vnode再平均映射到M个node上。当数据要插入时,将其主键(hash key)映射到K中的一个地址(addr),对应到某个vnode,再进一步对应到某个node,如果这个数据需要N个replica,则将数据写入addr(vnode a),addr + 1(vnode b), …​,add + N(vnode n)。在这种设置下,M就是你的shards,N是replica。以后添加新的node时,映射发生变化,只需要把相应的变化了的vnode迁移到新的node上即可。在这种结构下,sharding/replica对程序员基本上是透明的。

在 DynamoDB 中,为持久化,一份数据被写入到三个 replica 中。至于需要多少个 partition(或者说 shard),则由开发者为每个 DynamoDB table 设置的读写属性来确定。对于一个有三个 partition,三个 replica 的表,数据是这样分布的:

AWS系列:Dynamodb深度体验(1)_第2张图片

DynamoDB 组织数据的基本单元是 table,类似于 MongoDB 的 collection,或者 RDBMS 里的 table,每个 table 里可以有任意数量的 item,类似于 MongoDB 的 document,或者 RDBMS 里的 row。而每个 item,必须有一个 hash key,用于做 consistent hashing,还可以有一个可选的 range key,用于查询。如果 range key 存在,则 hash key 可以重复,hash key + range key 唯一确定一个 item 即可;如果 range key 不存在,则 hash key 必须唯一。

我们知道,一个 hash key 只会存在于同一个 partition 上,因此,相同 hash key 不同 range key 的 item 也会在同一个 partition。如果 hash key 选择不好,容易产生过热的 partition,影响效率。

对于 hash key,查询的动作很单一,就是 Get,或者 BatchGet,在同一个 partition 内部,数据是按照 range key 进行排序的。

如果你要使用 hash key / range key 以外的字段进行查询,你必须为要查询的字段创建索引(LocalIndex,Global Secondary Index)。DynamoDB 里面的 Global Secondary Index 实质上是一张表,只不过和主表联动更新而已。

从其他数据库迁移到 DynamoDB

很多用户在第一次接触 DynamoDB 时,已经有了很多其他 DB 的工作经验,这时候,照搬已有的经验去使用 DynamoDB,会遇到很多挑战。

Race Condition

在 MySQL 或者 MongoDB 里,同一个 db client 的若干次写请求是排队的,这个特性会导致应用程序的一些 race condition 在 DB 层面被掩盖了。比较经典的问题是 Session Store。大部分的应用框架都会使用 DB 来存储 Session,然后在每个 request 到来时刷新 Session。其中,第一次访问的 request 和用户登录的 request 处理起来会特别一些,前者创建 Session,后者会在 Session 中附加用户信息,而其它的 request 对于 Session 而言,只是刷新过期时间而已。框架一般会将这些操作区分开来,比如,在 nodejs 的 express 框架中,Session Store 定义了几种基本的操作:getsettouchget 用于获取 Session,set 用于创建/更新(replace)Session,touch 用于刷新(update timestamp)Session。很多 DB 的实现采用偷懒的方式,不定义 touch,将刷新的行为和更新的行为等同起来,这会导致同一个用户的多个并发的 request 进入 Session 更新的 race condition,如果恰巧其中的一个 request 是处理用户登录,那么用户登录在 SessionStore 里所做的操作有可能会被其他 request 的操作覆盖,导致登录不成功:

get.set.....done         // 登录
.......get.set.....done  // 刷新

这个问题在 express-mysql-sessionexpress-mongo-session 里都存在,只不过被数据库的串行访问给掩盖了:

get.set.....done
................get.set.....done

但 DynamoDB 的任何访问都是无状态的,它没有 Connection 的概念,所有的操作都是一个 HTTPS 请求。这是为何同样逻辑的代码(见:connect-dynamo,使用 DynamoDB 就会触发 race condition。这并非 DynamoDB 的毛病,只不过 DynamoDB 帮我们将这种问题显现出来而已。

数据更新

初学 DynamoDB 的用户,在使用 updateItem 时,胸毛上一定有万千个草泥马在奔腾。MySQL 的 update 语句简单直观,MongoDB 的 $set 操作稍稍有些绕,但逻辑还算比较正常,读一下手册,立刻知道该怎么写。可是 Amazon 不给 DynamoDB 好好地整 API,下面的查询代码在初次相见时,有一种如鲠在喉的感觉:

{
    TableName: 'AwesomeTable',
    Key: {
        id: { S: 'AwesomeID' }
    },
    UpdateExpression: 'SET #attrName =:attrValue',
    ExpressionAttributeNames: { '#attrName': 'attr_to_be_updated' },
    ExpressionAttributeValues: { ':attrValue': { S: 'value_to_be_updated'} }
}

更要命的是,API 复杂也就罢了,API 文档 还不给你好好写,初学者如无帮助,根本无法使用。

数据查询

在 DynamoDB 里,要想根据索引查询数据,必须显式指定 IndexName,这也是很多从其它 DB 迁移过来的用户感觉很绕的地方:

{
    TableName: 'AwesomeTable',
    IndexName: EXPIRES_INDEX,
    KeyConditionExpression: '#hash_key = :v_hkey AND #range_key < :v_rkey',
    ExpressionAttributeNames: {'#hash_key': '<hash-key>', '#range_key': '<range-key>'},
    ExpressionAttributeValues: {
      ':v_hkey': {'S': '<some-value>'},
      ':range_key': {'N': <some-num>.toString()}
    },
    ProjectionExpression: '<fields-you-want-to-projected>'
  }

另外,查询的时候 hashkey 是必须的,rangekey 是可选的。DynamoDB 通过 hashkey 确定在哪个 partion 下查询,然后通过 range key 再在该 partition 下进一步筛选。由于 hashkey 的特性,你只能进行相等的判断,不能使用大于、小于、或者 begin with。更丰富的查询操作只能通过 range key 来实现。这一点也给从 MySQL / MongoDB 阵营过来的用户带来很大程度的不适,原本会写的查询,在 DynamoDB 中突然不知道怎么处理了。

如果能够掌握更新和查询的别扭语法,那么其他的数据库操作都不在话下。很多语言在 DynamoDB 直接的 SDK 上提供了一层封装,可以更优雅地用人性化的方式使用 DynamoDB,多多少少也减轻了使用上的痛苦。

处理容量和可伸缩

由于 DynamoDB 与众不同的计费模式,使其使用方式和其它 DB 也有显著不同。其它 DB,你预先估计好容量,准备好 cluster,然后为整体使用的服务器资源买单,同时需要为下一次容量扩展做准备;而 DynamoDB 你只需要设置预估的读写容量,系统会帮你自动分配资源保证这个容量。在其它 DB 里,设计容量往往是峰值容量,在低谷期这些容量被大大地浪费了;而在 DynamoDB 里,你可以动态调整容量使其和系统一起伸缩。这是个讨厌的工作,它意味着工程师再也不能写完代码往运维那里一扔,说:「兄弟,抗不扛得住,看你了」;而是在代码中考虑系统要如何运维。

DynamoDB 增删改查等操作都能够返回使用 ConsumedCapacity,这样你可以了解目前还剩多少剩余的容量,进行「动态」调整。这个调整不是即时的,有一定的时间代价,因为 DynamoDB 需要根据 consistent hashing 把 key 重新分布。因此,调整最好是指数级的,以减少调整的频次。

另外,DynamoDB 可以累积一定的读写容量来帮你应对突发状况。如果你的写容量是 100/s,系统突然瞬间涌入过多的请求,达到 200/s,只要你有累积的容量,DynamoDB 会帮你短时间撑过这个突发访问。这让你的系统在应对波动上变得简单。

这个特性可以被很好的利用。比如你的数据表要每天定期做一些数据整理活动。如果在 MySQL 里,你在凌晨1点自动一次性整理,但在 DynamoDB 下,这么做意味着你得规划好这次访问的容量,免得过载。简化的办法是,把清理活动均匀分布在凌晨 0~5 点的每个半小时。这样,你可以使用 1/10 的容量完成这个任务,并且可以利用累积的容量,把这一容量需求进一步降低到 1/15。

比如你要读取 1M 数据,分 10 次读,每次 100k,400s 读完,250/s,你可以用 150~200 左右的读容量,而非 2500/s,这样会大大节省费用。

本文先讲到这里,下一篇讲讲 hashKey 和 rangeKey 如何选择和定义,以便充分利用 DynamoDB 的特性。另外,会讲一些使用的场景。

参考文档

  1. Dynamo: Amazon’s Highly Available Key-value Store
  2. Amazon DynamoDB deep dive

感谢魏星对本文的策划与审校。

你可能感兴趣的:(AWS系列:Dynamodb深度体验(1))