由于移动互联网的兴起,人与人之间的交流、信息分享能够以电子信号的速度传递在各个终端设备之间,每个人也都能够成为一个信息发布平台和信息接收平台,像朋友圈、微博、Twitter等社交平台的出现,大大方便和丰富了人们的日常生活。人人分享、人人参与也必然要求社交平台能够具有大规模、高并发、低延时的能力。通过本文,我们来看看如何搭建一个能够承受亿级活跃用户的社交信息分享平台。
一个最基础的方案
我们先来定义一个社交平台的基础的功能:
- 用户能够发布自己的状态、心情、照片、小视频等信息
- 用户能够关注自己的朋友、大V、明星等,时刻查看社交圈内的最新状态
为了满足这个最简单的需求,除了最基础的用户信息表,我们还需要添加用户关注表以及状态发布表:
// 用于记录每个用户关注的朋友、大V、明星等人物信息
CREATE TABLE Followers(
user_id VARCHAR(20) NOT NULL,
--关注人id
attention_id VARCHAR(20) NOT NULL,
--备注
remark VARCHAR(100),
PRIMARY KEY(user_id, attention_id)
);
// 用于记录每个状态的发布信息
CREATE TABLE Information(
message_id INT NOT NULL AUTO_INCREMENT,
--发布人id
user_id VARCHAR(20) NOT NULL,
--发布时间
posted_time INT NOT NULL,
--发布内容
content VARCHAR(20) NOT NULL,
PRIMARY KEY(message_id)
);
然后每个用户在刷新自己的朋友状态时,后台可以使用如下的 SQL 来获取信息:
SELECT message_id, poster_id, posted_time, content
FROM
( SELECT user_id, attention_id
FROM AttentionInfo WHERE user_id=5
)user
JOIN
( SELECT message_id,user_id as poster_id,posted_time,content
FROM Information
WHERE posted_time > ${上次刷新时间}
)message
on user.attention_id = message.poster_id
order by posted_time desc
limit 10;
好了,当有好友更新了状态,用户马上就可以通过刷新看到了。
挑战
这个设计在用户数量较小的时候是没有问题的,随着业务的成熟,不管是用户数量的增加还是历史状态数据的积累,我们都会面临如下几个挑战:
1.并发
状态的发布和分享功能是社交平台的核心功能,几乎承担了社交平台95%以上的读写访问量,单个数据库已经无法承担所有用户的访问并发了。
这时候我们可以采取的是将状态发布表按照用户也就是发布人进行拆分,假如将所有的用户按照哈希分成10份,每一份对应一个新的数据库,这样理想情况下,每一个数据库只需要支撑 1/10 的读写并发,也就是能够支持现在10倍的用户量。
但从并发这个问题上来看,分库分表可以解决这个最棘手的问题,我们再看看下面几个挑战。
2.性能
关系型数据库的访问性能与表中数据的大小是有直接关系的,同样的查询场景,数据量越多,访问性能也就越慢,跨表的JOIN查询受影响更为明显。
分库分表可以将数据量打散,维持住单库或者单表的数据量上限,但是当一个用户的关注人被路由到了多张子表或多个子库之后,单个用户的社交圈刷新会发起跨库的查询事务,一方面让查询逻辑会变的更加复杂,跨的子表或者子库个数到一定数量之后,反而查询性能会更慢;另一方面,跨子库子表的查询也加剧了每个子库或者子表的访问压力,最坏情况下,每个用户的刷新动作都会查询所有的子表或者子库,这样又与分库分表的初衷相悖了。
3.稳定
当活跃用户量上来之后,为了应对高并发的访问和保证低延时的性能,分库分表又好像是唯一的方法。子库子表的个数越多,管理的代价和复杂度也会越来越大,任何一个子库所出现访问热点会影响整个服务,而一次分库分表会需要极高的运维代价,停服停写、重新设置路由规则都会让这种实时在线服务的稳定性异常脆弱。更不用说服务器磁盘故障甚至是宕机了,如何保证业务的稳定也会是一个大大的问题。
4.成本
好了,假设我们使用一个良好的方案解决了并发、性能以及稳定性上的问题,服务运行良好,往往这个时候业务也度过了野蛮生长的时期,下面就是要考虑成本了。
我们也发现社交平台上的用户活跃时间段有着明显的规律性,比如一日三餐的时段,比如周末,比如节假日,再在比如有重大活动的时候。在2016年除夕当天微信红包收发总量80.8亿,共有4.2亿人参与,平均每人收发20个,而在年初一凌晨00:06:09,一秒钟发送的红包数量达到了40.9万个。
对于社交平台而言,高峰时段的并发访问量可能比一般的时段高出了一个甚至是多个量级,如果按照高峰时段来准备硬件资源将会产生非常大的浪费。
那么我们就需要一款大规模、高并发、低延时同时又能够弹性资源按量付费的数据库产品了!
按照上述需求,我们找到了阿里云的表格存储。
表格存储是一款能够提供单表PB级以及百万TPS能力的NoSQL数据库服务,服务端自动对数据进行分区和负载均衡,让单表数据从 GB 到 TB 再到 PB__,访问并发从 __0 至 百万 都无需繁琐的扩容流程,按量付费又避免了访问低谷时空闲资源的浪费。对象存储提供了 近乎无限大小的存储空间 ,数据的增量无需任何扩容流程。
架构设计
我们将用户关注的信息、发布的状态、照片视频的缩略图等存放在表格存储中,利用表格存储的高并发低延时的特点,在极短的时间内展示出最新的状态、照片等信息,将照片、短视频的源数据存放在对象存储中,通过CDN加速,在用户点击照片或者打开视频时,照片、视频通过CDN加速迅速在设备端进行加载, 这样既大大提高了用户体验,又很好的节省了不必要的数据传输。
方案实现
移植原连接查询方案
表格存储目前只提供SDK及API操作,还不支持 SQL 语句查询,而目提供的Hive、SparkSQL的查询方式主要偏向于离线分析场景,无法满足实时在线查询的需要,上述的跨表JOIN查询需要通过API来实现,当用户查看社交圈的最新状态时:
1.先从关注表中拿到所有的关注信息
2.调用 BatchGetRange 拿到所有关注人的新状态
3.对拿到的状态信息根据发布时间进行Merge,然后在客户端进行展示
4.记录此次查看的时间,用于下次刷新
挑战
虽然使用表格存储之后不再需要关心并发、性能、稳定和成本这些头疼的事情了,但是我们也发现这种方案对业务层的服务端有着很高的要求,为了达到极致的用户体验,用户的每一次刷新我们都需要起多个线程进行并行的 GetRange 查询,再在服务端按照发布的时间进行 Merge,并发的线程越多,用户体验到的延时也就越短,但是服务端的压力也就越大,多次联合查询能够提供的性能都是有极限的,关注的人物越多,刷新的效率也就越低。
如何能够提供一个真正达到极致的用户体验的社交平台呢?
基于表格存储我们不用再考虑规模、并发、性能等等方面的问题,那该如何设计呢?
一个新的方案
在系统的架构设计上,越复杂的系统,需要做的tradeoff也就越多,为了达到极致的用户体验,我们可以考虑牺牲状态的强一致性,不再要求一个状态在发出后能够马上被所有的用户看到,主要有两个原因:
- 大部分用户对状态的时效性的要求并不是强一致性的,秒级的延时为大家所接受
- 在针对海量用户的方案设计上,保证状态的强一致性也基本不可能,事务的隔离级别和数据库并发性是对立的,隔离级别越高,数据库的并发性也就越低,这也导致了无法保证状态信息的强一致性
基于这个前提下,我们将每个用户都作为一个 Session,其关注的所有状态都写入这一个Session内,那么用户在查看自己社交圈的更新时,只需要查看自己的这一个 Session 中新的状态即可。
我们使用一张新的 状态分享表 ,以用户的 user_id 为分区键,状态的 发布时间 为第二个主键,状态发布及查看流程如下:
1.用户在发布状态时,将状态写入到该用户所有粉丝的 Session。
为了能够应对突发的写入流量,我们可以先将状态写入队列服务中,在后端异步将状态信息同步到所有的粉丝 Session内,当写入流量增加,我们增加后端的消费者就能够降低写入延时。
2.我们已将数据做了解耦,用户刷新自己的社交圈时,不再有其他的数据依赖,使用单次 GetRange 查询就能获取所需要的数据。
经过实测,表格存储的高性能实例一次读取连续的50行数据的平均延时也在20ms以内,完全能够满足设计需要。
更多花样
状态发布、朋友的最新状态查看都是社交圈最基础的功能,在这些功能之上还需要提供各种各样的小功能才能不断提高用户的粘性,比如新状态的推送、多平台信息查看状态的同步等等,这里不再一一详述,在高并发IM系统架构优化实践以及表格存储服务在社交应用场景的实践中,都有相关方案介绍,大家也不妨参考一下。
结语
表格存储天然支持单表PB级数据量及百万级的访问TPS,以及与基础数据量大小无关的访问性能,弹性资源和按量付费让业务在各个阶段都无需为不必要的资源付费。
把数据规模和高并发这些头疼的问题交给表格存储吧,需要快速迭代快速跟随用户需求优化的上层业务才是宝贵精力应该focus的地方!