腾讯内容平台系统的架构实践
作者 | 孙子荀(授权)
编辑 | 小智
随着云结合微服务架构切实的提高了生产效率;深度学习不断深入内容处理的各个领域促进生产力的发展。 在消息系统,数据仓库,计算框架,存储系统等基础架构层建设逐步提升的基础上,大型互联网公司进一步提出了业务基础设施的需求。在基础架构和上层业务之间急需一个中台系统来承载。中台系统把业务层同性的算法能力,服务能力,业务能力高度集成,有效组织 ,动态规划。更好的帮助上层业务。
工程篇
前身
在 15 年的时候,内容平台(承载腾讯包括手 Q 等内容业务等的内容中台)最初来源于 QQ 公众号系统(公众号系统承载了包括 QQ 服务号,订阅号的关注关系,红包等大型活动的推送,订阅号消息下发,素材内容管理等)。当时公众号系统有几个子系统:资料子系统,消息子系统,关系链子系统 和 素材子系统。一个号主如果需要把自己的内容给粉丝,需要经过这 4 个子系统。
(蓝色部分)
(子系统就是有独立的存储 逻辑 数据流 接口体系。概念来自 Systems Analysis & Design 的 DFD sub-system)
在最简单的粉丝发送的场景,首先使用素材系统管理群发任务的内容,然后用关系链子系统拉取粉丝数据,通过消息子系统创建群发任务进行消息发送,过程中需要和资料子系统交互获得各自参数。
在 15 年下半年,内容战略升级,除了来自平台的号主发送内容,我们还有大量来自其他外部合作平台。他们通过公司其他平台对接进来,当时我们复用这套基于消息发送的场景,让对方创建群发任务,内容进入素材库进行处理,然后就可以触达粉丝了。
但是后来整个业务形态从订阅变成了 Feeds 流,原来的粉丝关系变成了推荐,随着内容处理服务的越来越多,内容量的不断增加,老的这套系统就无法承载了。于是我们需要改造老的系统。
我们希望是有一套统一的多源内容库,在良好的扩展性框架下,各类型服务通过实现预定义接口,完成对内容的加工处理,人机结合,输出给订阅方。
内容的处理服务包括了内容安全质量(质量评价,暴力色情,低俗,标题党,错别字等),内容建模特征(分类,主题,标签等),内容理解生成(封面图,摘要,结构化,剪辑等)。
(内容平台顶层图)
文本介绍一下其中主要的架构部分工作。
存储
物理存储
原来存号主发送内容的素材系统就变成了内容平台的最早雏形,素材还是通过 MySql 来存储用户发送的内容,所有的文本和 html 生成的页面样式也存储在一张表中,单表不堪重负,进行了 partition Sql 执行优化等工作,但是无济于事。在进入内容时代我们需要有一个性能更高的存储系统来支持。 当时的技术选项的考虑基于过去素材系统的痛点和未来需要支持的规模。
- 我们需要我们的存储系统能支持任意的字段扩展 ,Schema Free。便于扩展根据列的定位效率需要在 O(1)
- 存储系统一定要支持永久存储,同时能满足基础的并发读,虽然不要求像 Redis 一样上万 /s ,至少也要是千级别。
- 需要支持多机 水平扩展。
- 公司有团队成熟运维。
当时考虑过 Mongodb 和 Hbase ,Cassandra 以及其他 KV 存储。
Mongodb 的好处很多 。但是他的高效率访问带来的是内存资源的极大开销。冷热不均的分配,不可控的并发写入和副本存储都使得他无法承载未来几年更大的发展。
其他的就是 Hbase ,当初能预料到的是如果我们需要把 Hbase 当作 KV 对在线服务,是无法承受的,但是我们可以在其之上增加一个 KV 的 cache 解决这个问题,剩下的事情就是我们去打造一个中间层支持 Hbase 和内存 KV 的数据同步。
Hbase 的 row key + column family + column qualifier + timestamp + value 是 HFile 中数据排列依据。HFile 据此,对数据的索引到 data block 级别,而不是行级别。另外当初还考虑过一个方案就是基于 LevelDB 的全新内容中间件方案,这样能做到内存 KV 和永久存储合二为一,可是在那个时候的环境下,我们就算之前做过初级版本,也无法快速开发来,但是 Hbase 的好处是他可以支撑一段时间的 KV 访问,未来扛不住再优化上增加 Cache,事实上后来我们也是这样走的。
关于存储这里的工作我们后面还会提到,我们怎么进化到存储中间件 RCS。存储有了接下来就是如何设计存储层的数据模型。
数据建模
在设计存储模型的时候,在 16 年的时候 ,确认的事情有几个:
- 内容处理的肯定会有大量的模块并行的需要对内容进行处理加工;
- 这些模块有共性的属性获取,也有特化的属性获取需求。
- 模块自身彼此会产生输出给其他模块用。
我们的目标:
在架构上,打造统一存储来托管所有模块需要读写存储的场景,这样每个模块的同学统一存储。无论是业务同学的业务逻辑字段。还是算法同学的模型业务输出 or 模型特征输出。开发人员需要更加关注于策略本身,存储上的事情统一收拢提供 API 就行。
在表结构上:
如果第 1 点做到了,那么我们未来可以基于这个宽表进行天然的单表检索,单表基础内容特征挖掘。甚至是算法实验字段都可以统一在一张宽表里。
于是我们做了几个重要的设计:
1、推广新的唯一 ID 体系,废除公众号的自增 articleID,ID 能支持以下特征:
ID 体系 = 预留字段 + 时间 + 自增 ID + 内容类型 + 业务来源
于是有了 rowkey,拿到任意一个 rowkey 我们至少能第一眼知道来源大概的时间和类型,便于路由。
2、规范列名,所有列名分为【状态类】和【内容特征类】,前者用于标记状态,处理情况。后者用于保存内容的基础元信息,模块处理过程中产生的结果信息,中间信息。当时列的结构约定的格式是:
列名 = 列属性(状态类 or 业务类 or 模型类) + 字段属主 + 字段描述状态类
模型类
当时所不能预估的事情,现在思考有几点:
- 业务字段可能会根据不同的业务场景产生「多态」,这个在语言中很好解决的问题,落到存储层会有不少问题。 业务场景之间,多个业务之间对同一个内容的标题和封面图都可能有自己的子类,需要增加场景概念。
- 当初假设的是执行是树或者图这样的深度遍历 DAG,不会产生回路重遍历,事实上居然真的出现了这种场景。
- 随着字段的成倍扩张字段,列名一直没有很好的规划收拢分配,造成开发人员组织架构复杂之后不可控,需要有个合理的收拢分配方式。
我们在这里的数据模型使用了宽表格式,相比复杂的 EVA 存储,我们觉得宽表更加利于数据汇总统计。后续 RCS 部分会再次介绍。
宽表事务性更好。HBase 对一行的写入(Put)是有事务原子性的,一行的所有列要么全部写入成功,要么全部没有写入。但是多行的更新之间没有事务性保证。线上当前真实的情况是,单文章表已经有超过 500 多稀疏 column ,并且随和业务场景增加不断增加。测试数据验证并不会随着 column 增加而影响查找开销。
相关对比可阅读:
https://stackoverflow.com/questions/16447903/table-design-wide-table-vs-columns-as-properties
过程建模
主要介绍服务的设计区分,这里的服务包括业务能力服务,比如转码,业务逻辑处理,也包括模型算法相的服务。还有数据流的架构方法。
服务分层架构
我们把任何一个服务分层L0-L2三层,0层就是原子化的,不适合业务场景的无状态基础能力,如果在 NLP 的应用上,类似下图:
类似 kernel 支持 IO_DIRECT ,VFS 提供直接访问操作块设备层。我们的模型服务也可以支持直接越过业务逻辑访问底层基础能力。
(具体 linux io 相关参考 http://km.oa.com/group/17746/articles/show/208771)
从开发任何一个服务的时候,我们先梳理好层次关系。[对外的协议和接口]这里可以统一代码框架,由纯工程人员提供,包括并行化云化容灾过载保护熔断等。
业务测的算法团队使用其他公共库的 L0 or L1 层的能力,进行 L1-L2层的业务开发,工程人员也可以一起进行 L2 层的开发。
(服务模块内分层示意图)
我们根据以下几个维度可以来区分一个服务,下面提到的调度中心根据这些维度进行不同的处理策略。
是否需要全局的信息:
指的是针对一个内容的处理,是否需要历史上所有已经处理的内容的输出。比如去重,或者同质化内容控制,一定需要和历史上已经积累的内容处理状态做逻辑。增量 + 全量同步。
是否强业务逻辑
所谓强的业务逻辑 就是说这个服务是否和业务逻辑没有强关联,可以独立于业务场景。如封面图抽取。
不一样的有如:同质化的内容控制,这个和业务策略强相关,不同的业务场景 ,策略有存在特化的可能性,类似文件系统的实现有 ext4 xfs。同样标签抽取服务,也会对不同的垂类有不同的业务策略。
是否可以服务化
一般来说服务本身的业务逻辑越弱,又不依赖于其他业务全局信息的,可以提供对第三方的统一服务,比如图片标签抽取, 标题党。
这类服务可以充分输出服务自身的信息提供给上层业务方进行使用。
是否可以异步处理
是否能异步取决于,服务处理的时间效率,如果处理时间能保证实时返回那异步的需求不大,有的服务在特定业务场景下是严格依赖的,比如封面图,一定是需要处理完成才能继续。有的服务是可以在内容上线之后在执行并且更新,例如同质化控制,可以先曝光再下架。
是否有前置依赖性
如果有前置依赖的服务,一定需要在前置的若干服务处理完成之后才能进行本服务。
比如视频拍重,一定依赖之前的视频抽 frame,音频指纹服务都完成之后,才能继续进行。该服务需要任务图中依赖前置服务父节点都完成之后启动。
我们认为当开发一个服务之前,必须搞清楚这里的几个维度。比如我们现在需要引入 xLab 的标签能力,我们综合看这几个维度,
过去我们有几个思路,服务是否需要深入业务场景,那这里的问题就是,如果和模型能力强相关的业务逻辑,是需要放在服务自身实现的,因为这样可以更加有利于业务逻辑和算法输出相互配合,形成整体,对于开发人员来说,理解业务的需求,是业务部门的算法人员必须关注的,比如标签,去重, 模块内部算法特征使用和业务逻辑高度内聚。
流程处理
Brendan(也是 Kubernetes 的作者,现在微软,之前在谷歌 Cloud)在 18 年的新书 Designing Distribution System 中提到了我们从 15 年开始尝试的几个路径, 几乎做了类似的模式和事情和经验总结,感慨的是这本书 18 年才出版。 (道路自信 理论自信 文化自信 ☺)
Brendan 称这种多阶段多模块协作的处理方式为 Batch Computational Patterns . 包括 Work Queue Systems ,Event-Driven Batch Processing, Coordinated Batch Processing。
(Event-Driven Batch Processing workflow 示意图)
(内容中心简化版的视频处理 workflow 示意图)
我们的处理框架经历了几个阶段。
批处理模式(2016):
开始时完全的去中心化的思路。当时背景是时间紧,需要构建的模块多。在 16 年,团队不到十人的情况下,基情满满的开发了接近一百多个模块,出入库,图文的去重,封面图,外部能力对接,一些早期质量模型,视频视频转码,爬虫等,同时还在兼顾老的公众号系统的开发工作。
内部: 这个时候所有的事情都需要快速往前,模块与模块之间通过存储的状态变更进行交互,开发同学高度自治负责模型能力和业务场景,每个服务功能单元内高内聚低耦合。 依赖扫描存储的状态字段监听状态,生产变更。
外部: 16 年中旬,因为下游业务的增多,彼此交互需要松耦合,单模块可分配的处理时间不断压缩, 具备基础的多生产,多消费模式。我们引入消息队列机制来对接多个下游业务方 CMS ,推荐等。通知内容的出入库,状态变更。
事件驱动批处理模式(2017):
在 16 年那个时候有任务调度和 讨论所有模块以后都预留管理接口,能接受紧急情况下的调度。也考虑过直接改造增加消息队列的方式。到了 17 年初热点内容对内容处理提出了很高的时效性,我们当时几位同学反复讨论是否需要有中心化的调度服务来进行触发和扭转。产生了以下几种方案:
方案 1.1 的优势是:
全局无中心模块。任何模块都可以在故障情况下进行自我处理。
模块之间松耦合通过消息队列彼此通信。
(1.1 模型)
当时我们在 1.1 和 2.0 两套方案中有过很大的讨论。
目标是支持事件驱动,但是到底继续延续无主的消息队列方式(拓扑配置可以通过配置中心管理)还是把服务的触发逻辑收拢到调度中心来控制,其实是一个技术导向和业务导向的问题。最终我们选择了业务。
我们对调度中心的定位:
调度中心 = 事件驱动器(拓扑管理)+ 流量控制 + 容灾 + 监控(性能,调用链跟踪)方案 2.0 的优势是:
- 业务模块之间没有处理的时序依赖。
- 调度中心可以根据结合容量模型,动态调整模块并发处理请求的数目。
- 调度中心可根据来源维持多个拓扑,起到配置服务器的作用。
(2.0 的调度中心 右下角)
我们选择了 2.0 一个良好的调度框架,可以和存储层结合,服务自我注册。
通用的框架
我们可以抽象和定义了一批操作子,使得调度服务可以在这些操作,并且和底层的 RCS 存储结合起来。上层界面可以直接拖拽 各种服务,并且确定操作子,可视化的构建 workflow。
Filter:
对于经过的内容进行强制过滤处理,一般用于基础安全过滤和内容高准确率下的去重,降低入库被攻击的风险。
Copier:
支持一份内容 fork 成多个场景内容,继承已有字段。这样可以对一个内容的处理后分拆成多份对等继续接下来的处理。在存储层就产生了多条具有同样外键指向的父记录的记录。
Divider:
多路划分,根据存储层已经产生的通用字段,对业务的处理进行分支分流的逻辑。比如当对于低于多少分质量分的号主发文。分高质量和低质量两路进行不同处理(是否灰度,是否审核等)。
调度使用各个服务实现的模版模式操作服务接口,外部服务可以结合适配器模式保证接口统一。通过抽象这类操作让存储天然和调度和服务框架能互相结合。
(workflow 和服务构建的示意图)
扩展
在 workflow 的设计中有 2 个主要阵营,管弦编排方式 Orchestration 和 舞蹈编排方式 Choreography。
参考:
https://wso2.com/blogs/thesource/2016/03/orchestration-and-choreography-when-to-use-an-esb-vs-a-workflow-engine/
前者注重中心化的调度服务,便于统一控制所有业务逻辑,便于监控及时干预。坏处是,耦合度过高,变得臃肿,而各个服务退化为单纯能力模型,容易失去自身价值。
后者纯事件驱动各个监听改事件的服务,会主动获取事件处理,并可以按需发布自己的消息。优点是低耦合高内聚,技术人员充分自主。坏处是,业务流程是通过订阅的方式来体现的,需要增加额外的调用链跟踪框架做监控,且可能有服务掉链子 Process interruption 还是需要额外介入。
我们的 2.0 模式吸收两者优点,调度中心起到事件发生器的作用,在规定的操作原语的场景下进行流程逻辑编排,同时各个服务自身保留了 L2 层的业务逻辑,避免出现 coordinates 业务复杂化。
关于调度
每次我们同学在过公司技术评审的时候都会遇到一个问题,你们为什么不用 storm。这里其实是这样,Storm 本身其实是一套实时数据流式的调度框架,并不包含业务逻辑,类似 Torque 这样的作业调度。用户需要在 Topology 中编写自己的调度逻辑。 Spout 和 bolt 单元需要用户在固定的框架下面进行数据流的交互。
这基本局限了我们的自主空间:
- 增加修改 blot 要重启 topology,不能热更新。
- 适合数据的实时统计,而不是多维度业务对象的算法逻辑处理,不适用自定义事物原语的场景。
- 系统运维相比自研框架会更加曲折,后者对算法模型的同学仅需要规范协议,且 storm 语言主要还是支持 Java。
- 对于上面提到的异步返回的服务没有天然的支持。
内容数据流
说完 Workflow, 我们再来看看 Dataflow。在剑桥 Martin Kleppmann(分布式系统专家,也是另外一本 Stream Processing 书的作者)的神作:Designing Data-Intensive Application 一书中,Martin 提到数据流系统把数据(我们这里指内容数据)从一个服务发送到另外一个服务有几种方式:
(原书截图)
在软件工程里所有的数据流动都要设计好数据流图(Data Flow Diagram)内容订阅
我们内部服务之间的早期数据流通共享,就是 Model_1 基于 Databases ,我们通过字段的 column 区分,严格时序,保证数据的一致性。
后期随着 Workflow 2.0 改造,我们对于宽表的模型字段和业务字段分开访问。在调度中心和服务之间之间对于宽表的业务字段,统一通过 Model_2 接口传递的方式。
在系统与系统之间 (下图虚线框),我们通过 Model_3 的消息队列总线的方式。对外统一通过消息队列总线的方式进行同步,数据包括增量和全量,采用多状态的版本控制,不同的处理状态使用不同的版本号。同时随着业务一致性等场景要求,外部也可以通过我们提供的数据中间件服务 RCS 读写业务数据即 Model_3。
(内容平台 Dataflow 示意图)
内容中间件
到了 17 年下半年,我们发现随着需要数据同步的业务场景逐渐变多,多个业务场景之间的内容数据出现了不一致。同时多个业务会有直接访问底层数据库的需求。于是将一套存储搭配多业务场景副本,变成了一套存储,统一支持多业务场景访问,同时合并了之前对外同步的 Model_2 消息传递方式,打包进一个组件,也就是上图的 RCS。
存储的数据中间件,同样分为 L0,L1,L2 三层,
L0:Cproxy 专注于对底层存储的访问数据层异地容灾。
L1:Sproxy 用于存储介质访问路由,OR-Mapping 对外统一场景字段,比如一个 cover 可能根据策略的不同,mapping 到不同模型实验组的 cover column,对外屏蔽的。
L2 最上层 Eproxy 用于整个业务层的逻辑控制。
(RCS 组件,三层 proxy 对应 L0,L1,L2)
(HBase 本身的 replication 会拖慢主库的写入速度,数据又是我们的核心资产,我们在 17 年开始增加了 proxy 写入异地冷备,并且定期和架平进行切换演习)关于 RCS 的细节我们后续再另起展开,有几点需要提一下为什么,他能称之为存储中间件。
收拢所有对存储层的操作行为。
支持任意字段的直接访问,并且加了权限控制 。
支持通过内置的 MQ 对接外部实时数据统计服务,通知流水,更新数据操作。
比 Hbase 的 replication 更好的副本机制。
类似的工作: 小米在 2018 年 9 月发布了他们的开源组件 Pegasus 1.11.0。
内容检索
我们做了一个工作让 RCS 的数据能实时同步到 ES,通过 ELK 进行多维度检索,趋势巡检。同步到 Hive 进行全量的内容挖掘等。有了宽表,我们很容易执行如下命令:
稀缺度实验 B 挖掘出来的某个时间段内封面图评分>8 分并且标签数少于 3 个的视频总数。
(基于 ELK 的监控视图)
写在最后
感谢过去曾为这套系统付出的工程开发同学 alexcxu carrickliu ericxjiang guangyupeng jianxunzou jamescxchen mamoyang maplechang marcopeng taoyang tedqian xiaoccwang yuliangshen 以及能力模型同学 chenchwang lshzhang haodeye leafxin louislwang loopingwang jordanyu tiantianfan yaoyaoyu yurunshen vincentqliu 还有 AI 特征工程师 dongdong 和勇哥。
作者介绍
孙子荀,(腾讯/SNG内容平台部/平台产品中心/算法平台和后台一组组长),11 年在百度从事高性能算法方面的工作。12 年加入腾讯,15 年开始负责QQ 公众号平台和内容中心后台,并和团队从无到有一起搭建了QQ的内容平台。16 年开始从事内容处理能力的算法研究和落地工作。