DynamoDB 是 Amazon 基于《 Dynamo: Amazon’s Highly Available Key-value Store 》实现的 NoSQL 数据库服务。它可以满足数据库无缝的扩展,可以保证数据的持久性以及高可用性。开发人员不必费心关注 DynamoDB 的维护、扩展、性能等一系列问题,它由 Amazon 完全托管,开发人员可以将更多的精力放到架构和业务层面上。
本文主要介绍作者所在团队在具体业务中所遇到的挑战,基于这些挑战为何最终选型使用 Amazon DynamoDB,在实践中遇到了哪些问题以及又是如何解决的。文中不会详细讨论 Amazon DynamoDB 的技术细节,也不会涵盖 Amazon DynamoDB 的全部特性。
TalkingData 移动广告效果监测产品(TalkingData Ad Tracking)作为广告主与媒体之间的一个广告投放监测平台,每天需要接收大量的推广样本信息和实际效果信息,并最终将实际的效果归因到推广样本上。
举个例子,我们通过手机某新闻类 APP 浏览信息,会在信息流中看到穿插的广告,广告可能是文字形式、图片形式、视频形式的,而不管是哪种形式的广告它们都是可以和用户交互的。
如果广告推送比较精准,刚好是用户感兴趣的内容,用户可能会去点击这个广告来了解更多的信息。一旦点击了广告,监测平台会收到这一次用户触发的点击事件,我们将这个点击事件携带的所有信息称为样本信息,其中可能包含点击的广告来源、点击广告的时间等等。通常,点击了广告后会引导用户进行相关操作,比如下载广告推荐的 APP,当用户下载并打开 APP 后,移动广告监测平台会收到这个 APP 发来的效果信息。到此为止,对于广告投放来说就算做一次成功转化。
DynamoDB实践:当数据量巨大而不可预知,如何保证高可用与实时性?
移动广告监测平台需要接收源源不断的样本信息和效果信息,并反复、不停的实时处理一次又一次转化。对于监测平台来讲,它的责任重大,不能多记录,也不能少记录,如果转化数据记多了广告主需要多付广告费给媒体,记少了媒体会有亏损。这样就为平台带来了几大挑战:
在 2017 年 6 月前我们的业务处理服务部署在机房,使用 Redis Version 2.8 存储所有样本数据。Redis 使用多节点分区,每个分区以主从方式部署。在最开始我们 Redis 部署了多个节点,分成多个分区,每个分区一主一从。刚开始这种方式还没有出现什么问题,但随着用户设置的样本有效期加长、监测样本增多,当时的节点数量逐渐已经不够支撑业务存储量级了。如果用户监测推广量一旦暴增,我们系统存储将面临崩溃,业务也会瘫痪。于是我们进行了第一次扩容。
由于之前的部署方式我们只能将 Redis 的多个节点翻倍扩容,这一切都需要人为手动操作,并且在此期间我们想尽各种办法来保护用户的样本数据。
DynamoDB实践:当数据量巨大而不可预知,如何保证高可用与实时性?
这种部署方式随着监测量的增长以及用户设定有效期变长会越来越不堪重负,当出现不可预知的突发量时就会产生严重的后果。而且,手动扩容的方式容易出错,及时性低,成本也是翻倍的增长。在当时由于机器资源有限,不仅 Redis 需要扩容,广告监测平台的一系列服务和集群也需要进行扩容。
经过讨论和评估,我们决定将样本处理等服务迁移到云端处理,同时对存储方式重新选型为 Amazon DynamoDB,它能够满足我们的绝大部分业务需求。经过结构调整后系统大概是下图的样子:
DynamoDB实践:当数据量巨大而不可预知,如何保证高可用与实时性?
此外还有:
我们在使用一些开源框架或服务时总会遇到一些“坑”,这些“坑”其实也可以理解为没有很好的理解和应对它们的一些使用规则。DynamoDB 和所有服务一样,也有着它自己的使用规则。在这里主要分享我们在实际使用过程中遇到的问题以及解决办法。
在 DynamoDB 中创建表时需要指定表的主键,这主要为了数据的唯一性、能够快速索引、增加并行度。主键有两种类型,「单独使用分区键」作为主键和「使用分区键 + 排序键」作为主键,后者可以理解为组合主键(索引),它由两个字段唯一确定 / 检索一条数据。DynamoDB 底层根据主键的值对数据进行分区存储,这样可以负载均衡,减轻单独分区压力,同时 DynamoDB 也会对主键值尝试做“合理的”分区。
在开始我们没有对主键值做任何处理,因为 DynamoDB 会将分区键值作为内部散列函数的输入,其输出会决定数据存储到具体的分区。但随着运行,我们发现数据开始出现写入偏移了,而且非常严重,带来的后果就是导致 DynamoDB 表的读写性能下降,具体原因在后面会做详细讨论。发现这类问题之后,我们考虑了两种解决办法:
所以我们选择了第二种方法,调整业务代码,在写入时将主键值做哈希,查询时将主键条件做哈希进行查询。
在解决了数据偏移之后读 / 写性能恢复了,但是运行了一段时间之后读写性能却再次下降。查询了数据写入并不偏移,当时我们将写入性能提升到了 6 万 +/ 秒,但没起到任何作用,实际写入速度也就在 2 万 +/ 秒。最后发现是我们的分区数量太多了,DynamoDB 在后台自动维护的分区数量已经达到了 200+ 个,严重影响了 DynamoDB 表的读写性能。
DynamoDB 自动扩容、支持用户任意设定的吞吐量,这些都是基于它的两个自动扩容规则:单分区大小限制和读写性能限制。
DynamoDB 会自动维护数据存储分区,但每个分区大小上限为 10GB,一旦超过该限制会导致 DynamoDB 拆分区。这也正是数据偏移带来的影响,当数据严重偏移时,DynamoDB 会默默为你的偏移分区拆分区。我们可以根据下面的公式计算分区数量:
数据总大小 / 10GB 再向上取整 = 分区总数
比如表里数据总量为 15GB,15 / 10 = 1.5,向上取整 = 2,分区数为 2,如果数据不偏移均匀分配的话两个分区每个存储 7.5GB 数据。
DynamoDB 为什么要拆分区呢?因为它要保证用户预设的读 / 写性能。怎么保证呢?依靠将每个分区数据控制在 10G 以内。另一个条件就是当分区不能满足预设吞吐量时,DynamoDB 也会将分区进行扩充。DynamoDB 对于每个分区读写容量定义如下:
也就是说,一个分区的最大写入容量单位和读取容量单位是固定的,超过了分区最大容量单位就会拆分区。因此我们可以根据下面的公式计算分区数量:
(预设读容量 /3000)+(预设写容量 /1000)再向上取整 = 分区总数
比如预设的读取容量为 500,写入容量为 5000,(500 / 3000) + (5000 / 1000) = 5.1,再向上取整 = 6,分区数为 6。
需要注意的是,对于单分区超过 10G 拆分后的新分区是共享原分区读写容量的,并不是每个表单独的读写容量。
因为预设的读写容量决定了分区数量,但由于单分区数据量达到上限而拆出两个新的分区。
所以当数据偏移严重时,读写性能会急剧下降。
产生上面的问题是由于我们一开始是单表操作。这样就算数据不偏移,但随着时间推移数据量越来越多,自然拆出的分区也越来越多。
因此,我们根据业务做了合理的拆表、设定了冷热数据表。这样做有两大好处:
表对于数据的大小以及数量并没有限制,可以无限制的往一张表里写入数据。但对于 AWS 的一个账户,每个 DynamoDB 使用区域的限制为 256 张表。对于一个公司来说,如果共用同一个账号的话可能会存在创建表受限的风险。所以如果启用了冷热表策略,除了删冷表降低成本外,也是对 256 张表限制的一种解决办法。
上面提到了写入单位每条数据最大 1KB、读取单位每条最大 4KB 的限制。单条数据的大小除了字段值占用字节外,属性名也会占用字节,因此在保证可读性的前提下应尽量缩减表中的属性名。
DynamoDB 的使用也是存在成本的,主要体现在写入和读取的费用。我们自己研发了一套按照实际流量实时调整读、写上限的策略。随着发展 DynamoDB 也推出了 Auto Scaling 功能,它实现了自定义策略动态调整写入与读取上限的能力,对于开发者来说又可以省去了不少研发精力。目前我们也有部分业务使用了 Auto Scaling 功能,但由于该功能的限制,实际使用上动态调整的实时性略显欠缺。
作者介绍:
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70000523/viewspace-3120717/ ,如需转载,请注明出处,否则将追究法律责任。