今天看到一篇技术文章,介绍twitter在早期(用户数小于等于1.5亿),架构的主要思路,读了以后,第一个感觉就是,项目的工程化,在很多时候都是相似的,不同的公司,不同的国家,不同的文化,不同的工程师,但是却有着相同的工程化思路。
时间线
首先,我们要普及一个叫“时间线”的概念。
我理解的时间线就是外国的“朋友圈”,朋友圈是我的朋友以发圈文章的时间从晚到早依次排列,而时间线就是我关注的用户发的消息,按时间降序排列。
社交类的应用,时间线通常有两种,一种是我的时间线,另外一种是别人的时间线,我的时间线就是我自己的推文,而别人的时间线就是我关注用户的推文。
twitter在1.5亿用户的规模时,每秒钟写入消息的条数是6000,我们可以认为每秒钟有6000人每人都发了一条twitter,大部分的人还是以读为主,这是一个典型的读多写少的应用。
每秒钟6000条的写入在强大的硬件设备上不是问题,但是要读就不行了,你能想象有大量用户同时在一个库上执行下面的SQL语句么?
select m.content
from follow f
inner join message m
on m.user_id = f.user_id
where f.follow_id = ?
limit ?;
数据库必死无疑。(这条SQL是我自己猜测的,内部的结构可能不是这样)
于是twitter的工程师想了一个办法来减少数据库的读操作,就是把每个用户的时间线都存放在redis集群中。具体的操作是每当有新的消息写入时,如果关注这个用户的关注者少于5000,那就把这条消息写到redis集群中,每个用户的时间线都从redis集群读取。
这看起来是个好方法,但是为什么说是5000个用户呢?因为一旦超过这个数字,即使是写入redis,也需要大量的时间,假设一个超级巨星的关注者有5000万,这个操作会耗费巨大的代价。
所以对于大于5000粉丝的用户,并不会把ta的twitter通过redis集群同步给每个关注者,而是要等用户自己请求,只有当用户访问自己的时间线时,twitter的系统才会去查询。
实际上,如果连续30天用户甚至没有登录twitter的话,连redis集群中也不会再保存不活跃用户的时间线,如果用户后来又登录了,就去数据库查询,但为了保护脆弱的数据库,也只会查最新的800条消息。
维护在redis中用户的时间线,有个小技巧,就是只存消息的ID,不存消息的具体内容,但是我猜测,消息仍然是存到redis中,只是不是同一个key而已,用户数量如此庞大的基础上,是不可能直接查库的。
我们
我现在所在的项目组也在维护一个社交产品,它其中一部分功能与twitter非常相似,是一个叫关注的功能。也有个别大V的关注者非常多。
当我们在最开始规划这个功能的时候,首先考虑的就是当一个用户发表了消息,通过消息中间件或者是redis同步给用户,但是同步大V消息的时候,可就不那么简单了。
我们重新捋了一下思路,觉得我们的应用是在一个初期的阶段,虽然有一些大V的关注者非常多,但是大部分人远不能和大V相比,加上活跃用户数占比并不太高,扪心自问,是否真的需要使用复杂的方案?
最后我们确定的方案是,所有用户的时间线都通过主动查询的方式来维护,同样为了保护脆弱的数据库,主动查询的消息会被缓存起来,并且缓存的有效期设置了一个折中的时间,有缓存的也是有坏处的,比如我关注的用户推送了新的消息,但我因为缓存的缘故,可能无法马上读到。
维护用户的时间线,存的也是消息的ID,具体的内容存到redis的另一个key中,这点和twitter也非常相似。
当时的想法是,如果用户没有规模大到一个地步,我们暂时就使用这种相对简单的方案。在redis能解决问题的情况下,尽量保持架构的简单。大量的使用redis,在表示关系的时候,尽量使用ID和具体内容分离的方案。
归纳
当我看到介绍twitter架构的那篇文章后,想到原来做项目的时候做的决策,虽然当时我们并不知道twitter的作法,但现在看来有“心有灵犀”的感觉,架构是跟随用户规模迭代的,在每个时期构建最合适、最接地气的架构,不贸然扩大问题规模,是我们共同的选择。
twitter有一个地方和我们不同,就是使用了图数据库来保存用户之间的关注关系,但我们使用的还是传统的关系型数据库,为了查找关系的时候更快,我们把所有用户之间的关系也都放在了redis中,考虑到关注之间的关系还有取消关注的情况,我们使用的redis数据结构是zset,用score来表示是否关注的状态。
问题
不知道你是否接触过社交类的应用呢?你在处理时间线的时候,方案是什么?