基于Redis的Feed推送系统

转载于:http://littlexiang.me/architecture/4.html

之前我们的Feed聚合是基于纯数据库IN查询, 条件多还要加上排序, 当数据超过1kw之后, 就开始有慢语句产生. 做了索引优化拆分成两条语句, 第一句只取id, 保证查询是index only, 然后再循环取单条feed. 尽管如此不久以后还是渐渐不行, 某些语句会扫描30w行, 大概在2s左右.于是开始构思如何模仿新浪微博的推送体系.找了很多blog都没有找到稍微是具体的实现细节,苦逼的只能自己半猜想半模拟了.

终于某天在梦中, 得到了粗略的思路.

再具体细化一些后的思路

分为推和拉两部分, 姑且称为Push和Pull.

用户初始化上线的时候, 第一次刷新动态列表, 此时inbox是空白的. 此时Pull负责动态聚合第一页数据返回, 同时online标志位标记为1, 当下触发Pull取20条x50页数据填充inbox, 同时Push开始对此用户推送新feed. 此情况只发生在 offline->online 状态切换的时候.

用户上线完成后只需要读取inbox的内容, Push负责持续向online用户推送.

online标志位缓存时间72小时, 同时inbox也保持一致的缓存ttl.

删除feed的情况由前端输出时过滤, 系统本身不处理. 一是此情况很少, 二是代价太高, 没有必要.

当发生关注/取消关注时, 同步将本人的online标志清除, 下一次用户刷新动态列表时会自动rebuild.

 

P.S. 该系统目前已经构建完成, 基于gearman+php+redis, 具体的实现细节之后再分篇阐述.


动态聚合的部分全部基于redis的sorted set, 采用mapreduce计算. 这就是前文中的Pull, 主要用于初始化和rebuild.

每个用户都有个人feed的队列, 用户得到的动态就是把所有关注者的feed队列内容聚合起来. 这里采用了mapreduce的计算方式, 先取出每个人的Top 20, 得到N个关注者的20xN条feed, 再进行排序得到最终的Top 20.

对性能做测试的时候, 按照关注上限3000人, 每人队列10000条做了模拟. 循环取3000次15s, 改用pipeline后0.5s, 最后变态地使用了邪恶的eval, 用lua脚本一次性在服务端做完再返回, 结果是0.2s.

复制代码
do
    local ret = {}
    local t = loadstring('return ' .. ARGV[1])()
    for k,v in pairs(t) do
        local key = 'u:' .. v .. ':photos'
        local tmp = redis.call('ZREVRANGEBYSCORE', key, '(' .. ARGV[2], '-inf', 'WITHSCORES', 'LIMIT', '0', ARGV[3])
        table.insert(ret, tmp)
    end
    return ret
end
复制代码

悲催的是redis的单线程的, 所以这个操作的并发性能很差. 根据redis文档描述 Time complexity: O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. 耗时主要取决于每页的feed条数和关注的人数.

当然可以通过多个read slave来提高并发, 也可以将个人feed队列分布存储在多个redis中, 通过二次mapreduce并行计算.

需要翻页时提供上一页最后一条的score, 取小于score的20条, 才可以保证mapreduce翻页结果正确. 实际中简单地使用了精度到毫秒的时间戳作为score.

intval((microtime(true) - $epoch_ts) * 1000)



分发器负责向所有在线的用户推送feed, 即前文中的Push.

用户产生一条新feed, 先同步插入自己的inbox, 然后交给Distributor分发给所有粉丝.

这里worker有2层, worker1负责接收任务, 按照每3000个粉丝一组分拆成很多子任务, 交给下一级worker2处理. worker2并行处理完成后汇报给worker1, 同时做一些计时和延误告警的工作. 

worker2负责具体的分发流程, 获取粉丝id->判定online->推送并未读数++. 为了追求速度, online是一次性批量获取的, 这里还碰到过一个坑, 参见 http://littlexiang.me/php/3.html .

同时worker2也有大小两个池, 初步设定threshold是100w, 超过100w粉丝的任务会交给large pool处理, 避免某些大佬发了照片来不及处理导致任务积压, 影响了大部分普通用户的分发.

关于性能, 模拟了100w粉丝全体offline和online的情况. 100个worker+2个redis在普通pc机上的分别是1~2s和10~11s.这里扯一个别的问题, 之前测试1个redis的时候, 分发速度是6~7w/s, 分布到2个redis之后居然还是6~7w/s, 最后发现是计算分布写的太罗嗦了, 后来把floor(crc32(id) % 360 / (360 / N))改成crc32(id) & (N-1), 达到了10w+/s, 缺点就是shard数量只能是2^n. 每次扩容必须翻倍, 不过不一定得加物理机, 直接多开一组redis实例就可以了, 因为是全缓存, 完全不用aof和rdb, 一台物理机上开个4个总是可以的.

关于inbox的分布和扩容, 直接添加配置就可以了. 因为online标志和inbox在一起, 所以不需要人工干预迁移, 失效的部分会在下一次用户操作时自动rebuild.



扩展性/性能/高可用

gearman大概每秒可以接收4k个任务, 单台的能力都是足够了,配置成LVS backup-backup模式切换. 任务通过gearman的扩展写入mysql持久化, 后端的mysql也是Master-Master+LVS切换, 杜绝单点.

pull的性能可以通过单组多slave和多组分布+2次mapreduce扩展

push的性能可以通过增加worker机和inbox的redis实例提高. 

个人feed的redis需要持久化, 每个节点都有3台: 主->备->持久, 通过LVS切换, 主和备都是纯内存, 持久机不参与工作,只负责写aof.

inbox缓存的redis全部都是纯内存, 可以有多台互相作为备机.

worker管理/健康检查

一开始是用pcntl接管了kill信号, 保证不会在任务进行中被kill掉, 不过strace发现有很多system call比较浪费, 于是改成从数据库读信号.worker做完一个任务后去数据库更新状态, 然后检查kill信号并记log.

start/stop/restart都统一由Manager脚本控制, 除非挂了, 正常情况下不许直接kill. Manager还带有watch功能, 每分钟检查一次所有进程的状态.

Monitor脚本循环跑一个空任务, 做一个sleep一会新增一个, 检查gearman server的状态.

吐槽

尼马gearman 1.1.4是坑爹的啊!mysql断了不会重连, 编译drizzle怎么也搞不上, 换0.41啥事都没有啊!

尼马php的gearman client是坑爹的啊! addServers两台有一台挂了直接就连不上啊!说好的自动failover呢!

尼马php的redis扩展是坑爹的啊!没有连接池吃CPU又高, 这是闹那样啊!

虽然php5.3换5.4性能有很大提高, 不过还是准备改写成python的, 毕竟后台worker嘛...


转载 :http://www.cnblogs.com/zl0372/articles/feed_5.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值