DynamoDB 是 AWS 的托管 NoSQL 数据库,可以当作简单的 KV 数据库使用,也可以作为文档数据库使用.
Data model
组织数据的单位是 table, 每张 table 必须设置 primary key, 可以设置可选的 sort key 来做索引.
每条数据记作一个 item, 每个 item 含有一个或多个 attribute, 其中必须包括 primary key.
attribute 对应的 value 支持以下几种类型:
- Number, 由于 DynamoDB 的传输协议是 http + json, 为了跨语言的兼容性, number 一律会被转成 string 传输.
- Binary, 用来表示任意的二进制数据,会用 base64 encode 后传输.
- Boolean, true or false
- Null
- Document 类型包含 List 和 Map, 可以互相嵌套.
- List, 个数无限制, 总大小不超过 400KB
- Map, 属性个数无限制,总大小不超过 400 KB, 嵌套层级不超过 32 级.
- Set, 一个 set 内元素数目无限制, 无序,不超过 400KB, 但必须属于同一类型, 支持 number set, binary set, string set.
选择 primary key
Table 的 primary key 支持单一的 partition key 或复合的 partition key + sort key. 不管哪种,最后的组成的primary key 在一张表中必须唯一.
简单说 partition key 用来做 hash, 决定了数据存储在底层哪个 partition 中. sort key 用来做排序, 如果几个item 共享相同的 partition key, 他们 的 sort key 必须不同, sort key 用来在同一个 partition key 下快速定位数据。
partition key 的选择比较关键,需要能将数据尽量打散,同一张 table 中每个 partition key 中的 item 个数要尽量均匀, 数据倾斜太严重或单个partition key 是热点 的话会导致 DynamoDB 的 read/write capacity 无法完全利用.
partition key 的错误设计:
如果用 DynamoDB 记录用户的活动记录, 用月份作 partition key, user id 作 sort key, 由于这些数据很有实效性, 一般都是比较近的时间点读写频繁,老数据基本不动,这样就会导致严重的数据倾斜和热点,只有最近几个月的数据所在 partition 在使用.
正确的做法,按月分表, user id 作 partition key, 老数据所在表设置设置较低的 read/write capacity, 要删除可以直接删表.
正确的 partition key 选择: user_id
, device_id
等区分度比较高的属性.
错误的 partition key 选择: 年月日, product type 等分布,访问模式不均匀,或区分度小的属性.
Read/Write capacity 和价格
DynamoDB 的并发能力和价格由 read/write capacity 决定.
一个 write capacity 在 1s 内可以处理一个写入请求.
一个 read capacity 在 1s 内可以处理1个 strong consistent read 或 2 个 eventually consistent read.
价格:
- WCU (write capacity unit), 0.47$/月
- RCU (read capacity unit), 0.09$/月
- 存储价格 0.25$/GB/月
假设系统设计容量 300 write/s, 3000 strong consistent read/s, 价格: 0.47 * 300 + 3000 * 0.09 = 411$/m
如果 read 全部可以用 eventually consistent read 代替,价格: 0.47 * 300 + 1500 * 0.09 = 276$/m
DynamoDB 可以购买 reverse capacity (以100 为单位购买), 整体价格可以再减少 53%, 变成 193.17$/m 和129.72 $/m
Partition
partition 的个数由 WCU/RCU 还有数据量共同决定: 一个 partition 最多存储 10GB 数据,支持3000 RCU 或 1000 WCU
WCU/RCU 和 partition 数目的公式:
( readCapacityUnits / 3,000 ) + ( writeCapacityUnits / 1,000 ) = initialPartitions (rounded up)
我们上面的例子:
(3000 / 3000) + (300 / 1000) = 1.3
向上取整,就需要 2 个 partition.
假设我们的系统需要存储50GB 数据,3000 RCU, 300 WCU, 最终就需要 5 个 partition. 每个 partition 的处理能力会被均分, 可以处理 600r/s, 60w/s, 这也是为什么我们选择 partition key 的时候要尽量打散数据,避免热点,分散不均匀的话,单个 partition 可能达到处理上限,但并没有达到系统的总上限.
如果单个 partition 的数据量超过了10GB, 这个 partition 会在后台自动分裂成两个 partition, 这个过程对上层透明,不会影响应用.
TTL (time to live)
创建表的时候可以指定一个 attribute 作为ttl 字段,这个字段里面写入的必须是 unix timestamp.
DynamoDB 会在后台定时扫描数据,过期数据会在 48 小时内删除(如果应用对 ttl 有精度要求,必须自己检查 ttl 字段). 如果 ttl 写入了一个5年前的值(eg: 0), DynamoDB 将不会删除这条数据.
Limits
1 RCU 最多可以读取4KB 的数据,如果返回的 item size 是 6KB, 就需要消耗2个RCU.
1 WCU 最多可以写入1KB 数据,如果写入 2KB 的数据, 就要消耗2 个 WCU
在 us-east-1 region, 每张表最高40000 RCU 和 40000 WCU, 每账户最高 80000 RCU 和 80000 WCU.
每个 item 的 size 不能超过 400KB.
BatchWriteItem api 一次最多写入25 个 item, 总大小不能超过16MB.
BatchGetItem api 一次最多读取 100 个 item, 返回数据不能超过 16MB.
Strong consistent read & eventually consistent read
- strong consistent read, 写入成功之后, 后续读取都会读到最新值.
- eventually consistent read, 写入后立刻读取有可能会读到旧的值,但最终能读到最新值.
关于 eventually consistent read:
- 写入一个之前不存在的key, 立刻读, 一定能读到该值
- 更新一个已存在key 的值,立刻读,可能读到旧的值
- 删除一个key, 立刻读,一定读不到该值
简单测试下 DynamoDB 的eventually consistent read 延迟情况怎么样.
对于同一个key, 循环写入100 次,每次写入一个随机值,然后立刻读取, 比较得到的是否是刚刚写入的值.
value_list = []
for i in range(100):
value = random.randint(1, 1000)
value_list.append(value)
table.write('key', value)
return_value = table.read('key')
if return_value != value: # 检查读到的值
print 'fail at count {}, read value at {}'.format(i, value_list.index(return_value))
time.sleep(0.1) # 等待 0.1s 再次读取
return_value = table.read('key')
assert return_value == value
在100 次读写中,大概有2 次会碰到读到上一次写入的值, 如果等待 0.1s 后再读, 在测试中读到的一定是最新值.
注意, AWS 并没有对 eventually consistent read 的延迟做任何保证,这可能会受到 RCU/WCU 等各种因素影响, 应根据业务情况决定是否使用 strong consistent read.
为了保证数据的持久性,每次写入一个值,DynamoDB 会将它写入后端 N 台服务器副本(N >= 3), 写入过程是并行的,只要有一个节点写入成功就可以返回, DynamoDB 后台会检测数据,如果有节点写入失败,会从写入成功的节点上同步最新的数据, 这也是最终一致性的由来, 如果全部节点还没有完全更新成功,client 就可能读到含有老数据的节点.
问题, 向N 台服务器同时写入数据,怎么判断有一个节点数据写入成功了呢(为了保证数据没写坏,写入后需要触发下读取做完整性校验)? 有个简单公式: W + R > N, W 是写入成功的节点个数, R 是读取成功的节点个数.
N 是副本个数, 如果N = 3, W = 2, R =2 就成功, 两次写入和读取不一定是一样的2 个几点,但由于总数是3,说明一定有一台机器是既写入成功又读取成功(关键就是保证 overlap), 表明数据一定已经正确得写入了系统.