DynamoDB是Amazon基于《 Dynamo: Amazon’s Highly Available Key-value Store 》的NoSQL数据库服务。可以满足数据库的无缝扩展,可以保证数据的持久化和高可用。开发者不用担心DynamoDB的维护、扩容、性能等一系列问题。完全由亚马逊管理,开发者可以更专注于架构和业务层面。本文主要介绍笔者团队在具体业务中遇到的挑战,基于这些挑战,最终选择AmazonDynamoDB的原因,在实践中遇到了哪些问题,以及如何解决。本文不会详细讨论AmazonDynamoDB的技术细节,也不会涵盖AmazonDynamoDB的所有功能。背景与挑战TalkingDataAdTracking作为广告主与媒体之间的广告监测平台,每天需要接收大量的促销样本信息和实际效果信息,最终将实际效果归因于促销样本。例如,当我们通过手机上的新闻应用程序浏览信息时,我们会在信息流中看到穿插的广告。广告可以是文字、图片或视频形式,无论是哪种广告形式。与用户互动。如果广告推送比较精准,恰好是用户感兴趣的内容,用户可能会点击广告了解更多信息。一旦广告被点击,监控平台就会收到用户触发的点击事件。我们把这个点击事件携带的所有信息都称为样本信息,可能包括点击的广告来源,点击广告的时间等等。通常,用户点击广告后,会被引导进行相关操作,比如下载广告推荐的APP。用户下载并打开APP后,移动广告监测平台会收到APP发送的效果信息。到目前为止,它被认为是成功的广告转换。DynamoDB实践:当数据量巨大且不可预测时,如何保证高可用和实时性?移动广告监控平台需要接收源源不断的样本信息和效果信息,并进行一次又一次重复、不间断的实时处理和转换。对于监控平台来说,责任重大,不能多记录也不能少记录。如果转化数据记录的太多,广告主需要向媒体支付更多的广告费,如果转化数据不记录,媒体就会蒙受损失。这给平台带来了几大挑战:数据量大:部分媒体可能会通过非正常手段制造虚假样本,产生“虚假流量”以实现利润最大化,因此广告监测平台不仅会收到真实的用户样本中此外,还会收到大量假样本,影响正常监测和归属。在最“疯狂”的时候,我们平台一天会收到40亿+的点击样本事件请求。要知道这些点击样本事件是为后续的效果归因预留的,样本的有效期差别很大,从最短12小时到最长90天不等。不可预测的数据量:对于广告主的大促、应用商店竞价排名等一系列的推广,会突然涌入大量的样本数据,面对这些不可预测的流量情况,我们还是需要保证系统正确、稳定、实时。实时处理:广告主依靠广告监测平台的实时处理结果来调整广告推广策略。因此,广告监测平台需要支持实时数据处理,为广告主更快优化推广策略提供有力支持。同时,广告监测平台的处理结果必须实时反馈给媒体和广告主。可见,准确性和实时性是系统必须满足的基本条件。样本存储:我们业务的核心功能是归属。比如我们需要明确是哪个推广活动样本带来了用户下载打开APP的转化效果——也就是上图中的第7步。当用户安装APP后,监控平台需要在步骤1中找出样本对应的推广活动,这是一个查询匹配的过程。对于庞大的归因样本数据,有效期是不同的。我们应该如何存储样本,以便系统可以快速归因而不影响实时结果?这也是一个很大的挑战。在初始形态中,我们的业务处理服务是在2017年6月之前部署在机房的,使用Redis2.8版本来存储所有的样本数据。Redis采用多节点分区,每个分区采用主从方式部署。一开始,我们部署了多个节点的Redis,分成多个分区,每个分区一主一从。这种方式一开始没有问题,但是随着用户设置样本有效期的延长,监控样本数量的增加,当时的节点数量逐渐不足以支撑业务存储层面。如果用户监控量和推广量急剧增加,我们的系统存储就会崩溃,我们的业务也会瘫痪。于是我们进行了第一次扩容。由于之前的部署方式,我们只能将多个Redis节点的容量翻倍,这一切都需要手动操作,期间我们尝试了各种方法来保护用户的样本数据。DynamoDB实践:当数据量巨大且不可预测时,如何保证高可用和实时性?这种部署方式会随着监控量的增加和用户设置的有效期变长而越来越不堪重负。当不可预知的爆发发生时,可能会导致严重的后果。而且人工扩容方式容易出错,时效性差,成倍增加成本。当时由于机器资源有限,不仅Redis需要扩容,广告监测平台的一系列服务和集群也需要扩容。解决挑战经过讨论和评估,我们决定将样本处理等服务迁移到云端进行处理,存储方式重新选择AmazonDynamoDB,可以满足我们大部分的业务需求。结构调整后的系统如下图:DynamoDB实践:当数据量巨大且不可预测时,如何保证高可用和实时性?应对庞大且不可预测的数据量:我们平台需要接受推广监控连接请求,Persistence用于后续的数据归属处理。从理论上讲,系统有多少广告监控数据请求,DynamoDB就可以存储多少数据,存储任何量级的数据只需要一张表。不用担心DynamoDB的扩展。我们无法在系统运行时感知到存储在扩展。这也是亚马逊官方宣称的完全托管和无缝扩展。高可用性:AmazonDynamoDB作为存储服务提供极高的可用性。所有写入DynamoDB的数据都会存储在固态硬盘中,并自动同步到多个AWS可用区,以实现数据的高可用性。这些任务也完全由AmazonDynamoDB服务管理,用户可以专注于业务架构和编码。实时处理:AmazonDynamoDB提供极高的吞吐性能,支持秒级配置的任意级别的吞吐量。对于写多读少的应用,每秒写入数据数可以调整到1000条或更高,每秒读取数可以减少到10条甚至更少。吞吐量可由用户任意设置。除了可以在web管理后台随时调整吞吐量外,还可以通过DynamoDB提供的客户端进行动态调整。例如,系统在运行时的写入能力不足。我们可以选择在web管理后台手动增加调整,也可以在代码中调用客户端API实现自动调整。采用客户端动态调整的方式,可以让系统具有很高的收缩能力,同时保证数据处理的实时性。当系统数据流量变高时动态调整,当数据流量变低时动态调整。与手动调整相比,动态调整更加灵活。基于以上几点,我们认为AmazonDynamoDB能够轻松支持系统的核心业务能力。业务端需要做的就是整理好业务逻辑,把数据写入DynamoDB,剩下的交给DynamoDB。另外:TTL:我们使用AmazonDynamoDB提供的TTL特性来管理有生命周期的数据。TTL是一种为表中即将过期的数据设置特定时间戳的机制。一旦时间戳过期,DynamoDB会在后台删除过期数据,类似于Redis中的TTL概念。有了TTL的能力,我们减少了业务中很多不必要的逻辑判断,同时减少了存储量带来的成本。Stream:我们业务中没有开启Streaming来抓表动作,但是我们觉得DynamoDBStreaming是一个很好的特性。当存储在DynamoDB表中的数据发生变化(新增、修改、删除)时,会通知相关服务/程序。比如我们修改一条记录的某个字段,DynamoDB可以捕捉到这个字段的变化,并将变化前后的结果写入流记录中。真正的知识来自实践。在使用一些开源框架或者服务的时候,我们总会遇到一些“陷阱”。DynamoDB和所有服务一样,有自己的使用规则。这里主要分享一下我们在实际使用过程中遇到的问题和解决方法。数据偏移在DynamoDB中创建表时,需要指定表的主键,主要是为了数据的唯一性、快速索引、增加并行度。主键有两种,“单独使用分区键”作为主键和“使用分区键+排序键”作为主键,后者可以理解为组合主键(索引),是唯一确定的/由两个字段检索。DynamoDB底层根据主键的值对数据进行分区存储,可以均衡负载,降低各个分区的压力。同时,DynamoDB也会尝试对主键值进行“合理”的分区。一开始我们并没有对主键值做任何处理,因为DynamoDB会将分区键值作为内部哈希函数的输入,其输出将决定数据存放在具体的分区中。但是随着操作,我们发现数据开始写offset,而且非常严重。结果是DynamoDB表的读写性能下降。具体原因将在后面详细讨论。发现这类问题后,我们考虑了两种解决方案:所以我们选择了第二种方法,调整业务代码,写入时对主键值进行哈希处理,查询时对主键条件进行哈希处理。解决数据偏移后,自动扩容潜规则的读写性能恢复,但运行一段时间后,读写性能再次下降。查询数据写入不偏移。当时我们把写入性能提高到60000+/秒,但是没有用。实际写入速度仅为20,000+/秒。最后发现我们的分区太多了,DynamoDB后台自动维护的分区数已经达到200+,严重影响了DynamoDB表的读写性能。DynamoDB自动扩容,支持用户自定义吞吐量,这是基于其两个自动扩容规则:单个分区大小限制和读写性能限制。单个分区大小限制DynamoDB会自动维护数据存储分区,但每个分区的最大大小为10GB,一旦超过此限制,DynamoDB将拆分分区。这也是数据偏移的影响。当数据严重偏移时,DynamoDB会默默的为你的偏移分区拆分分区。我们可以根据以下公式计算分区数:总数据大小/10GB向上取整=分区总数。比如表的数据总量为15GB,15/10=1.5,向上取整=2,分区数为2,如果数据偏移量分布不均匀,则两个分区各存储7.5GB的数据。读写性能限制DynamoDB为什么要拆分分区?因为它需要保证用户预设的读写性能。如何保证?靠的是把每个分区的数据控制在10G以内。另一种情况是,当分区不能满足预设的吞吐量时,DynamoDB也会对分区进行扩容。DynamoDB对每个分区的读写容量定义如下:writecapacityunit:写入容量单位(WCU:writecapacityunits),按每条数据最大1KB计算,最大1000条每秒可以写入数据。Readcapacityunit:读取容量单位(RCU:readcapacityunits),按每条数据最大4KB计算,每秒最大3000条。也就是说,一个分区的最大写入容量单位和读取容量单位是固定的,超过分区的最大容量单位就会分裂分区。因此,我们可以根据以下公式计算分区数:(预置读容量/3000)+(预置写容量/1000)向上取整=总分区数。例如预设读容量为500,写容量为5000,则(500/3000)+(5000/1000)=5.1,则向上取整=6,则分区数为6。需要注意的是,对于一个单个分区超过10G,拆分后的新分区共享原分区的读写容量,不共享每个表的单独读写容量。因为预设的读写容量决定了分区的数量,但是因为单个分区的数据量达到了上限,所以分裂了两个新的分区。因此,当数据偏移严重时,读写性能会急剧下降。上面的数据冷热导致的问题,是因为我们一开始是在单表上操作的。这样,即使数据不移位,随着时间的推移,数据量越来越大,自然而然去除的分区也越来越多。因此,我们根据业务对表进行了合理的拆解,建立了冷热数据表。这有两个好处:提高性能:根据上述规则,这是显而易见的。热表的数据量不会继续无限增长,所以分区也稳定在一定数量级内,保证了读写性能。降低成本:不必要地增加单表的读写性能,不仅没有明显的效果,反而会急剧增加成本。使用成本的增加是任何人都无法接受的。DynamoDB存储也是需要成本的,所以将冷表数据存储在S3或其他持久化服务中,删除DynamoDB表也是一种降低成本的方式。表限制表中数据的大小和数量没有限制,可以无限制地向表中写入数据。但是对于AWS中的账户,每个DynamoDB使用区域有256个表的限制。对于一个公司来说,如果他们共享同一个账号,可能会有限表的风险。所以,如果开启冷热表策略,除了删除冷表降低成本外,也是解决256张表限制的一个办法。上面提到的属性名的长度,写单元中每条数据最大为1KB,读单元中每条数据最大为4KB。单条数据的大小不仅占字段值的字节,还占属性名。因此,在保证可读性的同时,尽量减少表中的属性名。综上所述,使用DynamoDB也是有成本的,主要体现在写入和读取的成本上。我们制定了一套策略,根据实际流量实时调整读写上限。随着发展,DynamoDB也推出了AutoScaling功能,实现了可以自定义策略动态调整读写上限的能力,可以为开发者省去大量的研发工作。目前我们部分业务使用了AutoScaling功能,但由于该功能的局限性,在实际使用中动态调整的实时性略有欠缺。
