阿里云的表格存储服务(http://www.aliyun.com/product/ots)是一款面向pb级结构化/半结构化数据存储和百万级高并发读写访问的nosql数据库服务,在移动社交场景中有着非常广发的应用,如今非常火热的钉钉也将后台的消息推送和存储功能从mysql迁移到表格存储上,以获得更加优秀的高并发和规模扩展能力;同时也有非常多的创业企业将企业自身针对客户的消息推送能力基于表格存储来构建。本文将详细介绍表格存储在移动社交中的技术实践。本文的主要内容已经在2016年云栖大会深圳场的存储论坛上进行过分享。
随着移动互联网的发展,移动社交应用日益的丰富,很多移动应用中也都加入了社交的元素,在构建这些社交应用的过程中,会面临一些相似的技术问题和挑战。
图1
以即时消息通讯场景为例,我们来深入的探究一下具体会遇到哪些技术挑战,我们是怎么样来使用表格存储服务解决这些问题和挑战的。
图2
首先,我们简单定义一下这个场景,基本的元素有用户、群组、消息、客户端等,有三个方面的需求:1)群组中用户发送的消息能够即时到达所有其他用户;2)不仅送达其他用户,而且是送达这些用户的多个客户端,因为现在用户使用多个移动客户端的情况越来越频繁;3)在多个客户端上,用户具有一致的体验,比如说在一个客户端上阅读过的消息,这个状态信息就能够同步到其他的客户端上,哪些群组有未读消息,哪些没有在多个客户端上的视图要一致起来。
图3
构建这样一个场景,技术架构通常会有这么四层,最上面是客户端,然后是服务接入层,负责管理网络链接,之下是服务的逻辑层,主要是负责消息的分发和到各个客户端的同步,最下面也是最基础的部分是数据存储层,负责用户和消息数据的持久化存储,通常会用到数据库存储用户和群组的关系,使用nosql存储消息的具体内容和状态,以及文件存储服务来保存消息的多媒体内容(如图片、视频等)。
图4
而这里面挑战最大的海量消息的存储,汇总一下,消息存储实际上面临着这么几个主要的需求和特点:1)规模的跨速扩展,很多应用在极端时间内就会积累大量的客户和消息,因此要求存储具备快速无缝的水平扩展能力;2)高并发低延时的需求,活跃用户数很大,每天产生的消息量有几百亿条,峰值甚至达到几十上百万的tps;3)高可用容灾的需求,用户对于移动社交的依赖越来越大,服务的不可用对用户体验的影响非常大;4)功能模型相对简单,消息数据是典型的kv模型,没有事务和复杂sql查询的需求。这也是为什么nosql技术在这样的场景中应用的非常广泛。那么接下来我们就来看看表格存储服务是如何解决这些技术问题的。
图5
首先我们还是简要的了解一下表格存储服务的基本功能和模型,首先它是一个具备规模无缝扩展、高并发、高可用、多租户共享的nosql存储服务,提供的功能相对简单,主要是单行的读写,批量的读写,主键范围上的查询,提供的api也比较简洁直观,一共有7个数据读写的api。
图6
数据模型上,以表的方式来组织数据,每一行包含主键和属性列,主键是一行的标识,可以由多列组成,在创建表的时候需要定义,属性列则是非常自由的,不同行的属性列可以不相同,可以动态的增加和减少,主键的第一列是分区键,整个表按照分区键的范围进行分区,不同的数据分区会调度到不同的服务节点上进行服务。
图7
服务的整体架构有四层,分别是应用层,使用我们提供的sdk,用户服务层,负责协议的解析,安全,计量,权限等,第三层是表存储引擎,负责表和分区的管理,负载均衡,存储的索引,高可用容灾,最底下是飞天分布式系统,包含分布式文件系统(盘古),分布式资源调度(伏曦),分布式锁服务(女娲)以及硬件运维和部署系统(天基)。
图8
使用表格存储服务来构建前面提到的场景,我们需要定义两张表,第一张表是消息同步表:用户消息的存储和同步,表的主键有两列,第一列是用户id的md5值,使用md5值是为了让表的数据分布更加均匀,第二列是消息的id号,是一个单调递增的数字,越晚接收到的消息对应的数字越大,属性列是消息的内容和状态,文本内容直接存放在表格存储中,如果是多媒体内容,则放在oss中,然后将内容的链接放在属性列里面。这张表就记录了每个客户接收到的所有消息,这张表会主要用在两个工作流程中:1)当群中的用户发送一条消息,消息分发服务会将这条消息写入到群中复制成多条记录写入这张表中,每一条记录对应一个群中的一个用户;2)消息同步服务从这张表中读取每个用户接收到的消息,并同步给多个客户端。这里我们只要为每个客户端记录一个消息同步点,也就是同步到哪一条消息id,通过给定用户id+消息id的范围,就能够准确的将新接收到并且未同步的消息读取出来,同步给多个客户端,然后更新每个客户端的同步点。客户端的同步点信息可以放在另外一张表中。
图9
第二张表是会话的状态表,存储每个用户的每个群组的会话状态信息,比如这个群组中有多少条未读消息,最后一条消息的内容和时间等等,再来看一下表的结构,主键有三列,群id的md5,群id和用户id,这三个主键列的组合唯一标识一个用户的一个群会话。分区键是md5也是处于同样的原因。这张表的使用场景包括:1)当用户使用一个新的客户端登录时,需要将该用户的所有群组状态从表中读取到,并显示在客户端上;2)当每个群组中有消息产生时,需要针对每个用户去更新这个群的会话,也就是说群里面产生一条消息,实际上会放大很多倍(群中用户的个数)。这里面存在的技术挑战是假设一个群很大,有几千人,并且群中假设很多人同时发送消息,那么在一个这个时刻,对系统的并发性要求会非常高。我们在后面会专门来看这个问题。这里面还有一个小细节,是使用一个列的多版本机制来记录未读消息数,每次更新这个列时,就增加一个版本,每一个版本代表这个群组有一条未读消息,那么在读取的时候只要拿到这个列值的版本个数,就可以确定这个群组中用户有多少条消息未读。
在系统的运行过程中,表的数据量会持续的增大,系统会自动的对分区进行分裂和负载的均衡,从而提升了单表的数据规模和并发度能力,我们简要看一下这个过程。下面的4张图描述的是数据分区从p0分裂成p1和p2,再逐渐分裂为p3、p4和p5、p6的过程。整个分裂过程对应用访问数据本身几乎是没有影响的,也无需人工的参与,相对于常见的对数据库做固定的分库分表方式有很大的优势。
图10
图11
图12
图13
在回到刚才提到的第二表对系统高并发的要求,实际上我们在设计表的主键时就已经考虑到了,因为我们有两种选择,一种是将群组id作为分区键,一种是使用用户id作为分区键,这两种设计都能达到标识一个群组的目的,但是并发性上相差很大,我们来具体对比一下。
图14
图14采用的是第一种方法,使用群组id作为分区键,在这种设计下,用户在群里面发送一条消息,实际上会转化为一个会话状态表的batchwrite操作,每一行记录对应群中的一个消息接收者,可以看到这些记录因为具有相同的分区键所以归属表中的同一个数据分区,结果是一个batchwrite动作只发送给后端的一个存储节点来处理,到存储底层只会涉及到几个io操作,开销很小。当有上万个用户同时并发的发送消息时,上万个batchwrite操作分别发送到不同的后端存储节点并发处理。在这个设计中数据记录写入的聚合度非常高,
图15
图15采用的是第二种方法,使用用户id作为分区键,在这种设计下,用户在群里面发送一条消息,也转化为一个batchwrite操作,每一行记录对应群中的一个消息接收者,但是这些记录因为具有不同的分区键所以很大可能归属于表的不同数据分区,结果是一个batchwrite操作要发送数据给多个后端存储节点(取决于后端存储节点的数目和表的大小),使得网络操作和io操作的次数成倍的增加,开销变得很大。当有上万个用户同时并发的发送消息时,上万个batchwrite操作可能会扩散出几十万设置上百万的网络来回。在这个设计中数据记录写入的聚合度很低。在实际测试过程中,两种设计在性能上会相差一个数量级。
可以看到选择一个合理的主键和分区键设置,对并发性能的影响非常大。总结一下,有两个经验:1)分区键的取值要足够的离散,以确保没有热点分区;2)选择合理的分区键,使得并发写的聚集效果更好,减少batchwrite操作的数据分散度。
高可用容灾的能力对于社交服务也至关重要,我们提供在一个region内的双集群同步机制,这里有两个层面的容错和容灾机制:1)单集群内,数据本身是有3分冗余,当一个节点出现问题,数据仍然有2分拷贝可以使用,第三份也会很快的恢复;2)在集群间设置了数据的异步复制,新的更新会实时的同步到备集群上,当主集群发生灾难时,可以快速的切换到备集群上。整个过程可以做到自动化,对应用透明,当然在切换过程中会出现部分时间的不可用,通常不超过几分钟的时间。
图16
很多时候,我们还会碰到一个需求,就是对消息进行关键词的搜索,由于表格存储本身不提供全文索引功能,我们在实际生产中推荐应用使用表格存储+开放搜索opensearch的组合来解决问题,表格存储存储原始数据,opensearch存储关键词的索引,两者之间的数据最终一致由应用来保证,查询时先根据输入的关键词从opensearch中获取消息的id,然后再反查表格存储获取最终的内容。大概的流程如下图所示。这对于应用来说还是有不小的开发代价,所以我们今年也有计划将这两个服务更好的进行融合,从而给开发者提供更加简便的开发体验。
图17