知乎 姚钢强 - 知乎 feed 流架构演进

Chamber

2017/12/18 发布于 技术 分类

背景:知乎首页信息流根据业务类型(实时性,资源节省,算法排序)选择了 PULL 类型的架构,而且系统中存在大量过滤,会产生反复到底层 feed source 拉取的问题。响应时间受到了比较大的影响达到了 1.6s。 解决方案选型:尽量使计算接近存储,节省网络和序列化开销。使用最新的 Redis module 技术,定义针对首页业务逻辑的 Redis 接口。 方案介绍:将一部分计算逻辑迁移到 Redis 内部,使数据(使用 protobuf 压缩)和计算完全在内存中进行,减少无效网络传输,大大提高计算效率,降低了响应时间。而且 Redis 内部的计算逻辑改变时,不需要重新编译 Redis,只需要动态记载计算逻辑部分即可。 实施后效果说明:响应时间 p95 1.6s - 700ms,节省内存 1T,CPU 500 个,减少首页依赖服务 1/2 的 qps。

文字内容
1. 知乎Feed流架构演进
5. 姚钢强 2013 年年加⼊入知乎,知乎 Feed 流技术负责⼈人,负责期间 Server 端 P95 响应时间从 1.6S 降低到 700Ms,稳定性由 99.9% 提升到 99.995%
6. 提纲 A. Feed 流的需求和特点 B. ⽼老老 Feed 流的构架遇到的问题 C. 新构架 Redis module 技术⽅方案 D. Redis module ⽅方案遇到的问题 E. 新的问题与挑战
7. 知乎 Feed 流的需求和特点 A. DAU 2600万(2017.9) B. 个性化推荐,每次请求返回内容不不同(与搜索不不同, cache 难做) C. ⽤用户动态准实时分发
8. ⽆无 个 性 化 ( 热 ⻔门 榜 单 )
9. ⽤用 户 个 性 化 p u s h
10. push 存在的问题 A. 资源消耗严重,计算量量⼤大,存储量量⼤大 B. 智能推荐,排序难以实时调整 C. 过滤⽐比较难做(关注 or 被删除) D. 动态准实时分发难以达到 (⾼高粉丝⽤用户)
11. 实时 pull
12. pull 计算流程(慢) member 推荐的相关源 从指定的源拉取条⽬目 client N 过滤条⽬目 条⽬目 够⽤用 Y 拼装 meta 选前 10 条 算法排序
13. 提前计算,做缓存 client member 相关的源 N 缓存 Y 拼装 meta 从指定的源拉取条⽬目 N 过滤条⽬目 后 10 条 缓存 前 10 条 选前 20 条 条⽬目 够⽤用 Y 算法排序
14. 提前计算的问题 • 存在冗余计算,占⽤用资源多 • 冷启动 P95 响应时间⻓长 • ⽤用户⾏行行为分发延迟,体验差 • 离线计算策略略复杂,难以维护 • 推荐算法难以实时调整
15. 如 何 优 化 , 难 点 ⼉儿 在 哪 ⼉儿 ? • 依赖服务响应慢 • redis cache + local cache • gevent 并发 • 超时做降级 • Python 计算太慢 • Cython 模块替换 • 由于条⽬目不不够,反复访问底层源的存储 feesource
16. 可 能 的 解 决 ⽅方 案 • 拉取出更更多的条⽬目,防⽌止被过滤掉? • 拉取更更多的条⽬目也会浪费时间,过滤压⼒力力⼤大 • 根据算法拉取出更更精准的条⽬目? • 算法期望召回池越⼤大越好
17. 计算下推,接近存储
18. 新 feed 计算逻辑
19. Redis Module Redis modules make possible to extend Redis functionality using external modules, implementing new Redis commands at a speed and with features similar to what can be done inside the core itself.
20. Redis Module • 加载定制命令(MODULE LOAD module load lib_path/xx.so) • 执⾏行行定制命令 • 卸载定制命令(MODULE UNLOAD mymodule)
21. Client module load lib_path/xx.so 加载定制命令 START handle = dlopen("lib_path/XX.so") dlsym(handle,"RedisModule_OnLoad") RedisModule_Init 初始化模块 RedisModule_CreateCommand 注册定制命令 ... RedisModule_CreateCommand 注册定制命令 END redisServer attributes + modules: dict + commands: dict operations
22. Client AxxCmd argv1 argv2 … 执 ⾏行行 定 制 命 令 START 查找命令 RedisModuleCommandDispatcher 执⾏行行命令 模块 callback moduleHandlePropagationAfterCommandCallback 回收内存 moduleFreeContext END redisServer attributes + modules: dict + commands: dict operations
23. Client module unload ModuleName 卸载定制命令 START 清空注册符号 dictDelete(server.commands,cmdname) dlclose 卸载模块 moduleFreeModuleStructure END redisServer attributes + modules: dict + commands: dict operations
24. 定制的命令 • open_core, close_core • Redis module 更更新时打开 core dump,如果 crash ⽅方便便分析 • ts_query • Redis 内部的过滤,归并流程
25. t s _ q u e r y 接 ⼝口 设 计 • Request • [source_type, source_id]: 需要拉取 source • black_items: 需要过滤的⿊黑名单条⽬目 • merge_strategy: 合并策略略 • required_number: 需要返回的条⽬目数量量 • Response • [item_type, item_id, item_action, item_score]: 返回需要数⽬目的 feed 条⽬目
26. 带来的收益 • 响应时间 P95 降低 300ms • 去掉了了离线计算,节省 60% 的计算资源 • 内容候选池更更⼤大,为算法提供了了更更⼤大的空间 • ⽤用户动态实时分发,算法实时调整
27. 遇到的问题 • Module 更更新不不⽣生效,继续调⽤用⽼老老 Module • Redis 单线程 CPU 瓶颈,⾼高可⽤用 • Redis 的内存浪费
28. M o d u l e 更更 新 不不 ⽣生 效 • 加载新的 so 后,发现调⽤用的还是⽼老老逻辑 • GDB 发现存在符号被标记 DF_1_NODELETE • dlcose 仅仅声明 so 不不在被系统使⽤用,so 内存占⽤用依旧存在 • gcc 编译使⽤用 STB_GNU_UNIQUE ,防⽌止符号被标记为 DF_1_NODELETE • 按照依赖路路径加载 so,不不直接加载定制的 so
29. R e d i s 单 线 程 C P U 瓶 颈 , ⾼高 可 ⽤用 • 采⽤用 Redis Sentinel 部署集群,保证⾼高可⽤用 • ⼀一致性哈希 Redis shard,每个 shard 采⽤用 master slave 的⽅方式部署
30. Redis 内存浪费 • 采⽤用 protobuf 和 Redis ziplist 数据压缩 ,减少 shard • 有多个 slave ,还是浪费内存,没有根本解决问题 • Redis 仅仅作为任务队列列,任务分派给其他进程处理理? • 同⼀一台机器器伴随 Redis 部署计算节点?
31. Feed 架构的历程 • Feed 都⼀一样 • Feed 个性化,推模型 • Feed 个性化,拉模型 + 离线计算 • Feed 个性化,拉模型,采⽤用 Redis Module,计算接近存储
32. 参 考 资 料料 • Serving Facebook Multifeed: Efficiency, performance gains through redesign • dlclose - close a symbol table handle • dlclose doesn't really unload shared object, no matter how many times it is called • Redis Modules: an introduction to the API • Redis Loadable Modules System