最近做项目,数据库选了DynamoDB。本想着,DynamoDB是AWS的分布式数据库,数据库容量大,查询速度快。没想到,做到一半,竟然发现有很多坑点。

这个项目是需求从消息队列中异步地获取不同商家的交易数据,对固定时间周期的交易历史记录做一些聚合操作,例如求和、按特征计数等等。这些历史记录需要存入数据库方便未来扩充聚合更多结果时使用,但只需要保留若干月份。更久时间的数据直接舍弃就好。数据量差不多在每天x百万条的水平上。而相应的实时聚合结果需要返回给其他微服务的查询请求中。

这些商家,毫无疑问,存在热点分布的情况。也就是个别商家的交易量极大,远超过很多小商家的交易合计。

这个项目的app层选择使用K8s的无状态pod部署,负责均衡策略按行为推断是轮询模式。

问题是什么呢?

这些聚合操作选择了使用了DynamoDB的Update表达式来更新聚合操作,而聚合是以商家为维度来执行的。由于前面所说的“交易热点分布”的问题,DynamoDB有可能会同时接收到同一个商家的多个聚合结果的更新请求。而DynamoDB是多主架构的数据库系统。如果多个实例同时接收到对同一条记录的更新请求,会发生脑裂冲突。DynamoDB的解决办法,便是抛出TransactionConflictException。根据对报错栈的分析,这个异常会在AWS的SDK端会延迟重试后再抛出来。也就是说APP层感知到的请求耗时其实是很高的

这样一来,原本为性能而选择的DynamoDB反而成了性能的拖累点。

其实问题很好分析,就是选择了与业务要求不相匹配的数据库系统。

多主的数据库系统,把更新冲突的解决外推到了app层。

而我们选择了轮询无状态的pod的app架构,完全没有办法处理这种问题。即使将负载均衡策略改成了按商家ID的哈希值来查询,也需要在pod内部处理好“同一商家的聚合结果需要顺序执行”的问题。一旦这么做,这对于大商户来说也成了一个痛点:因为每一笔交易的聚合结果都要顺序地更新。

解决办法应该是:调整数据库选型,选择一写多读的主从架构。“一写”,保证了数据写入的冲突可以在单一节点里被处理,不需要分布式协调。

站在更高的层次上来讲,整套系统的架构设计也有一定的问题。因为聚合操作使用了DynamoDB提供的原子化更新表达式来处理,相当与把数据库系统纳入了业务逻辑的一部分,并且严重依赖DynamoDB提供的API。数据库作为一个不受控的第三方系统,侵入app层以后会很对后续的开发维护形成很大的负担。早年见过的类似系统,真的是让人抓狂纠结到死。

正确的做法是:把聚合的逻辑完全做在app层。数据库只用来持久化,保持很好的可替换性。

话又说回来,每天百万级记录,其实使用Postgres完全足够了。因为历史数据很少使用到,高频查询的数据都是处于反复聚合的需要,都集中在当天。前一天的数据,只需要在其他表里存好聚合结果就好了,原始数据很少会被读取,除非有新的聚合需求。这种高频写入、低频读取的模式使用Postgres这种追加写入的数据库系统再好不过了。数据记录可以做好按天分表。超期数据直接删表就好,也很好维护。

总结教训是什么:一定要梳理好业务要求。