原文:http://codecampo.com/topics/61
我的上一个网站(已下线)包含一个类似豆瓣广播的功能,当时我搜索了不少资料,发现网上对好友状态广播的完整描述并不多。现在想把看过的一些资料总结起来,看看一个好友状态广播会大概包括一些什么内容。
我看过的资料包括但不限于:
- infoq 中文关于豆瓣架构的文章
- infoq 中文关于新浪的文章(其中新浪架构师的一个视频特别有用,但是我搜不出来了)
- Scaling Twitter: Making Twitter 10000 Percent Faster
- Twitter-Architecture
我的总结可能会忽略一些重要但我还没遇见的现实问题,并且包括我的一些猜测,请不要当作最佳实践。文中引用的一些数据或情节都从上面的链接中看到,但比较琐碎,不再一一标注出处。
描述:什么是好友状态
很多地方都用到好友状态,像 SNS 的好友近况,Twtter 的 timeline,豆瓣的广播,其实都是好友状态的应用。它通常有这样的特点:
- 一个用户可以跟踪多个用户的状态。根据豆瓣、新浪、twitter的相关文章,一个成熟平台的用户平均 followings 大概在 130 人上下。
- 一个用户状态可以被很多人跟踪。名人的跟踪人数通常会很恐怖,比如 sina 的明星粉丝会上数百万。
- 好友状态是一个很琐碎的消息片段。延迟和缺失是可以忍受的。
那么,下面来逐步搭建一个好友状态广播。
第一阶段:基于数据库查询的广播时间线
这跟 SQL/NOSQL 的关系不大,这里为了个人方便用 mongodb 的形式描述。
模式
> db.statuses.findOne() { _id : ObjectId(...), user_id: ObjectId(...), content: "Hi, I'm Rei.", ... }
假设某用户 user 有一个 following_ids 数组,保存他(她)关注的人的 ID 值。那么查询他(她)的广播时间线是这样:
db.statuses.find( { user_id : { $in : user.following_ids } } )
新浪一开始只花了一周用 PHP 写出的微博原型,用的是这个原理。很明显,这只是权宜之计,不能用于大用户量消息广播。加上考虑 twitter 的 @ 语法和其他复杂的阅读权限的需求,用数据库查询不能满足状态广播的要求。
第二阶段:消息投递取代数据库查询
用数据库查询来实现复杂的广播时间线是不划算的。从用户直觉上来说,一条状态进入自己的时间线,其位置就是不变的了,数据库查询提供的排序功能几乎没有意义。并且为了实现复杂的阅读权限,用消息投递去取代数据库查询是更合理的方案。
那么,用户的时间线此时是一个实际存在的数据结构,里面需要存储什么呢?状态消息本身在数据库中存储一次就行了,用户时间线中只需保存 ID 值。下面用 ruby 代码描述负责状态分发的逻辑:
# after create status after_create :deliver_status def deliver_status self.user.followers.each do |user| if self.access_allow? user user.home_timelines << status.id end end end
此后,订阅用户只需根据自己的 home_timelines 储存的 ID 值,去数据库或者缓存中读取状态消息就行了。
不过 home_timelines 储存在哪里?这是第三阶段的内容,暂时将它理解为每个用户的一个附加数据就行了(比如可以存在数据库中)。
第二阶段改进型:异步投递
容易观察到,即使某个用户的 follower 很少,但是将投递广播的时间耗费嫁接在发布状态的用户身上也是不合理的。如果 follower 成千上万,那么用户发布状态的耗时将不可忍受。这个时候要引入异步投递。
# after create status after_create :enqueue_status def enqueue_status Resque.enqueue(StatusDeliver, self.id) # 假设使用 resque 做任务队列, StatusDeliver 定义了执行 Status#deliver_status 的逻辑 end def deliver_status .... end
现在,后台的消息投递会交给独立的 worker 完成,不影响前端用户的操作。(resque 是一个基于 Redis 的 rails 队列系统)
第三阶段:忽略不活跃用户
如果用户的 follower 成千上万,即使用独立的 worker 进行消息投递,对系统的CPU和空间消耗来说依然很不划算。一个不活跃用户,可能数周登录一次,登录之后只看最近几天的状态广播,那么之前为他(她)投递的所有状态都是浪费。
这时候需要有计划的忽略不活跃用户。
之前没有详细说用户的 home_timeline 存放在哪里,如果之前是存在数据库中的,那么现在是将它取出来,放到内存缓存的时候了,比如 memcached。
将 timeline 数据存放到内存缓存中,设定一个过期周期,当缓存过期后(这是由于用户长时间没有活动造成的),消息投递方法就可以从缓存中知晓(或者更进一步,维护一个活跃 follower 列表),从何忽略对该用户的消息投递。
那么用户长时间不活动后归来,发现自己的 timeline 空空如也,不会导致用户流失么?
基本上,每个网站都会有重新激活用户 timeline 的方法,比如在页面上添加一个按钮“更新时间线”,让用户点击主动要求激活 timeline。但要怎么重建 timeline,我没看到有网站将自己的方法分享出来。我想到的最笨方法,就是执行一次查询,获取用户一部分 following 的状态数据,然后即时的添加到 timeline 中。
这里值得留意的是,用户对时间线是否完整并不敏感(他/她也没办法知晓完整的时间线是怎样),时间线可以不符合“数据完整性”(啊呀,真羞)。这是网站全局的考量了,你愿意为一个不活跃用户储存所有的时间线吗?
一个例子是,我前天登录人人的时候,时间线是空空如也的,即使点击“更新”也一样。今天去到,时间线就有了内容,因为我在前天激活了我的 timeline,新的状态会被投递。(他们甚至不愿意在我手工激活的时候投递哪怕几条好友消息给我,真懒!不过 facebook 也一样。)
结尾
现实中的广播系统当然不止这么些内容,还有跨数据中心的部署,均衡负载,数据库分片等等。这不是我一个业余程序员接触得到的,但遵守循序渐进的原则,技术问题可以需时解决。
以上是我对我以前查阅的消息广播方面的资料总结,方便我不断完善,也希望对有需要的人提供一点帮助。