我们都是架构师!
架构师订阅号,关注获取更多技术分享
现已开通多个微信群,有兴趣交流学习的同学
可加若飞微信:1321113940进群
合作邮箱:[email protected]
1 背景数据是微博推荐引擎的基础, 无论是未加工,过滤的由微博平台部门推送过来的原始数据还是由推荐引擎实时,准实时,离线计算出的微博关键词,推荐候选集等非原始数据, 在整个微博推荐引擎运作的过程中, 数据的读写操作随处可见. 推荐引擎在不断的进化过程中, 也逐步将数据层单独剥离开, 抽象出数据导入, 存储和对外访问的业务模型. 数据导入部分(Rin)已经较为完善, 采用消息中间件解决了经典的削峰填谷(发布, 订阅吞吐能力不对等)和上下游解耦问题, 统一采用memcache协议发布订阅; 存储部分则是主要依赖于Redis和Lushan; 数据的对外访问业务则采用了twemproxy做代理的解决方案. 代理在目前的互联网行业中应用十分广泛, 本文所关注的是7层业务代理, 即将访问请求按照一定策略路由到后端是App Server或者Data Server集群的一类服务, 换句话说, 所有叫云DB或者分布式DB的服务, 流量一定是经由此类代理来转发的, 这里不再赘述, 有兴趣的同学自行google学习. 2 问题在微博业务增量发展的过程中, 使用twemproxy遇到了如下的一些场景:
同时, 从纯技术角度来看, twemproxy也存在一些问题:
PS: twemproxy已经不再作为twitter公司内部的数据访问解决方案 3 技术调研在有了业务需求和技术驱动的双重push下, 接下来要做的很明显: 实现一个支持高并发, 易扩展, 易维护的代理. 首先从业界开源的代理服务来着手, 除了前文说的twemproxy, 这里另外着重调研了两个:
由于McRouter大量采用了Facebook自己二次开发的库, 所以整个代码量较之twemproxy更为庞大, 虽然已经声明此开源软件FB公司内部一直在用, 也在不断维护, 不过从我们自身的项目需求而言, 依然不是一个清爽的解决方案, 更倾向于把其当做一个良好的学习用的开源框架.
再次结合twemproxy来看, 三种开源的代理各自有一些技术上或者业务上胜出的地方, 但直接在其之上做二次开发的可行性必要性均不高, 微博推荐业务的需求需要一个从零开始的特色代理. 经过调研以及和同事们的技术讨论, 最终决定用Golang进行开发, 原因如下:
4 代理设计从业务需求出发, 主干功能如下:
除去主干功能, 配置,日志和监控也是做为24*7代理服务的不可或缺的部分, 这里花些时间描述下 4.1 配置由于最先接触的是twemproxy的线上环境, 对其配置格式(yaml)和规范印象较深, 在设计代理配置时也借鉴了其一些声明规范, 最终拍板用toml格式, 相比yaml的好处不多言, 样例配置如下: 上图中需要特殊说明的是连接池列表, 实例中用连续四个中划线分隔开, 是为了表明左右的服务分别属于不同机房, 数据通常是全量备份, 这样可以有效预防单机房硬件网络故障导致的服务不可用, 代理会透明切换到另一个机房上去. 当然如果某类存储服务就是一个单机房的单点, 删除掉四个中划线及其以后的部分就可以了 4.2 日志IO密集型的日志本身需要缓冲 异步写的功能, 防止日志写性能影响qps, 这里Golang原生的日志库是无法满足需求的, 自己也没有从头造轮子, 直接捡了现成的库seelog, 功能很强大, 除了异步, 还支持日志按大小, 或者日期(比如一小时切分一次)切分, 还会自行维护最大日志数(再也不用维护crontab logrotate了), 样例配置如下: 4.3 监控因为要保证24*7, 代理的工作状态是需要及时汇报给监控平台的, 例如当前连接数, qps请求数, 已经请求的连接数等等. 这里采用了一个简单的方式, 直接暴露一个监控http服务, 监控平台直接可以进行抓取. 有一些开源代理, 例如codis, 会在代理中嵌入一个大的Web MVC框架(martini), 结合js做一个漂亮的dashboard, 除了代理本身, 无状态代理自身的peer node(zookeeper协同), 代理管理的后端存储节点状态都会进行展示, 考虑到组内已经有相关的监控平台, 这个包袱就不扛了~ 4.4 代理模块代理在设计上大致划分为四层, 每层包含若干个模块, 模块在Go项目中体现为一个个单独的package, 即同一类功能的代理放置到一个folder下, 具体划分见下图: 这里着重介绍其中一些: 协议模块protocol: 包含redis和memcache两类, 因为Golang的net.conn可以绑定bufio.Reader, redis和memcache的协议解析函数入参均为bufio.Reader, 可以方便进行流式网络数据读取. 解析协议的函数配合官方协议描述即可看懂(操作符 操作数\r\n ), 目前还没有考虑支持二进制协议. hash模块: 目前抽象了一个hash函数类型 type HashFunc func(key string) (dbIndexNum int, serverPosition int, err error). 包含db索引是因为最早的需求里组内的Lushan服务是需要有DB号来进行访问的, 如果是像redis服务, 可以忽略db号, 目前支持的hash规则除了较为简单的取模取余还包括一致性Hash支持. tunnel模块: 这里指的是与上游客户端的一个物理连接建立后, 不断侦听通道数据, 做解析, 处理以及回复的处理函数. 注意只有同步编程模型才能够抽象出一个理解完整的通道函数, 通道里的处理函数也是整个代理的核心, 充分利用了多协程 本地map reduce的模型来处理一次请求, 这个后文会加以说明. entry模块: 由于proxy支持http,redis,memcache, 而后两者是直接基于tcp连接的编程, 所以这里抽象了两类入口服务, tcp和http, 其中tcp采用了任务队列加协程池的方式来避免协程在运行期的大量创建和销毁, 占用内存, 影响性能; http是golang内建的功能, 这里做封装, 主要是加入对gzip,deflate请求的数据压缩, 以及平滑关闭http服务, 同时封装一个context上下文, 让tunnel模块中的业务处理函数能够得知某一个请求对应的实例名, 对应后端连接池等信息 conn-pool模块: 连接池模块是用来维护初始固定长度的和后端数据存储服务的连接资源, 避免每一次client请求触发一个和后端DB服务的新连接, 除此之外, 连接池模块还要做是应对DB服务不稳定, 超时等带来的连接失效问题, 以及跨机房切换, 心跳检测. 这里涉及最多的就是锁的运用, 例如心跳检测失败事件导致的连接池资源关闭,重置和不断来的获取连接池资源请求的race condition, 让获取连接池资源变得透明(例如:后端机器无响应能很快实时返回; 有跨机房节点, 返回跨机房的连接; 本机房数据服务恢复后能再次切换回来) common模块: common模块就是上图中的蓝色基础层, 此模块的子模块包含了封装日志;配置;Mysql DB操作(因为目前没有支持mysql协议代理的需求, 只是后端存储可能会用到, 所以直接用了现成稳定的client库);监控服务;异常库;util工具库, 看似此模块最边缘, 其实起的作用非常大, 没好轮子的车绝对跑不快. 业务逻辑模块: 上图中最上层标黄的业务层对应代理main函数所在的hyper_proxy package, 完成的事情包括但不限于: 加载外部配置文件; 启动监控以及调试服务; 初始化连接池并绑定实例; 启动对应实例; 在程序结束时, 走关闭实例监听连接, 关闭对应连接池的操作. 4.5 业务在立项之初, 能够完成一次典型的multi-key读操作就是代理业务层(tunnel模块)的设计核心, 考虑到Golang的并发模型, 采用层次化的多协程worker协同工作对于此类需求易理解, 易开发, 易扩展(见下图) 其它的写操作, 获取DB状态操作或者取single-key的操作会自动收敛成单任务链完成 例如, 上游的推荐计算层需要一次性的获取多个key(可能是用户uid或者是微博mid)的内容, 返回结果内容后应用计算策略计算并生成推荐结果. 如果是走redis协议, 命令会是mget key1,key2,key3…keyN, 这N个键值会hash到不同的后端DB服务上去, 我们的处理流程如下:
以上的三个步骤看似简单, 用分治的方法去各自解决问题, 实际上背后还隐藏着Golang语言的同步编程模型 异步IO高性能的特性, 不过这是另一个话题, 不在本文的讨论范围之内. 其实以上的处理流程很容易扩展成解决多层次存储的代理解决方案, 例如步骤2中可以递归嵌套下一层中相似的1,2,3的处理逻辑, 这也是@传鹏一直强调的分形(fractal)设计思想. 如果还不大明白, 请看下图: 上图是一个推荐业务中常见的面向二级存储的代理工作流, 比如一级存储是有热点数据的内存数据库, 二级存储是有全量数据的传统关系型数据库
从工程实现的角度来说, worker是一个goroutine(轻量级线程), 数目小于等于存储节点, channel就是一个线程/协程安全的先进先出队列(语言层面已经实现), 不同的worker取完后端数据就会push进当前层队列, 整合数据的逻辑则是不断从队列里尝试取数据(没有的话, 会阻塞等待, 所以不影响性能) 上图中workerN是一个无法在一级存储查询中命中的goroutine, 一旦遇到这种情况, workerN必须先计算非命中keys在二级存储中的内容, workerN通过二级channel的数据汇总, 把二级查询到的keys内容填充到打算反馈给一级channel的数据单元中, 然后再push进一级channel, 最终handler2会处理一级worker 1到N的所有push消息, 因为消息次序依赖于下游数据存储服务的响应, 所以汇总的时候要根据事先绑定key的offset来将内容在对应位置填充, pack完毕后返回给上游. 5 性能整个代理的开发过程中, 从一开始的demo压测(redis-benchmark, memtier_benchmark)到后期代理原型完成后进行的python脚本高并发压测一直贯穿. 最终的吞吐性能瓶颈依赖于后端数据存储的类型, 内存数据库的IO要明显高于文件系统的IO, 而代理的目标是让业务层直接访问数据存储服务和访问代理的请求耗时没有量级差距 6 总结目前新代理在线上的部署已经覆盖了很多业务线, 从主干功能开发完成后, 应对业务变化的上线业务基本可以在不改动代码或者少量修改的前提下完成. 问题和不足:
来源:微博推荐 原文:http://www.wbrecom.com/?p=649 转载文章,向原作者致敬!如有侵权或不周之处,敬请劳烦联系若飞(微信:1321113940)马上删除,谢谢! END 我们都是架构师! 架构师订阅号,关注获取更多技术分享 现已开通多个微信群,有兴趣交流学习的同学 可加若飞微信:1321113940进群 合作邮箱:[email protected] 本文转载自:微信公众账号 - 架构师,版权归原作者所有! |
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|