本期作者
刘瑞洲
哔哩哔哩资深开发工程师
背景
先说明下什么叫“全量在播”,指的是直播范围内,当前所有在线主播的信息,尤其是主播的房间id和uid,对于很多上游业务来说,是必要的数据,是业务逻辑的数据基础。
直播之前虽然有一套这样的系统,但是从目前运行状态看并不能面向更高体量业务支撑,而且线上也因为这种过时的在播架构工作异常发生过几次线上事故。
面向未来的一两年我们有个很明确的目标,千万在线十万在播。其中后者十万在播,就是对于我们新的在播系统最大的挑战。有了目标,我们先要了解历史的在播系统是怎么样的,有哪些缺陷,这些都是很好的素材,对于新的在播系统的架构设计有很多的启发。
挑战一,历史的在播系统,由于一些历史原因和房间基础服务的共用资源,所以是个生长发育不太好的伴生品,需要解耦。
挑战二,在播数据的生成比较暴力,定时全量扫表,5s甚至都无法跑完sql,非常容易影响房间基础db的吞吐,从而使基础房间接口回源超时,发生过几次线上事故,需要解决慢查询风险。
挑战三,提供的在播相关接口比较分散,链路也比较复杂,造成上游调用混乱,而且还存在非标接口,提供给一些历史服务全量拉取所有在播的所有房间信息,一次拉取的包极大,超过10MB,需要收敛统一,并且足够轻量以及较小的接入成本。
挑战四,由于全量在播的信息体比较大,42k在播下,纯粹房间id有400k左右的大小,存放在redis,造成大key的风险,而我们的目标是十万在播,所以数据存储本身也需要有更加弹性的设计。
挑战五,历史在播信息生成任务由于本身就在两个zone下跑,生成的信息也是在独立的,所以是一个多活架构设计,新的方案也要继承这个特点。
挑战六,历史的在播受制于方案本身,无法做到更高的实时性,并且无法解决横向扩容的问题,一旦业务调用量上去直接造成热key,需要解决使用层io的瓶颈。
挑战七,需要具备可观测性的建设,覆盖整个链路,并且需要具备数据备份的能力,帮组业务在时间过期后相关问题定位上提供数据支持。
系统设计
历史系统总结了诸多挑战,结合对Google系统架构解密中关于设计系统的理解,新的系统架构设计需要从这几个方面考虑:
面向易于理解的设计
适应变化的设计
弹性设计
面向恢复性的设计
所以在技术选型上和方案设计上都要遵循这些优秀的原则来指导实践。
技术选型
存储在播信息的技术选型上,我们选择了自研的taishan kv,支持大key存储引擎,支持持久化存储,最重要的是天然支持多活,可以做好挑战五,这里放下自研taishan多活的架构设计:
数据传输上,没有使用普通的gRPC Unary模式,而是选择更加适合大包数据传输的gRPC Server streaming模式。
新的在播系统架构,整体上采用生产消费这一比较经典的模型,整体架构设计如下:
整个架构设计一共分成三部分,非常易于理解。其中数据生产层,负责静态数据和动态数据的收集和处理,其实就是对一个周期内的在播数据和开关播信息做处理;数据消费层,主要是拉取数据并做处理,最终暴露给sdk;sdk层,封装了一系列的数据拉取逻辑,数据更新逻辑,数据处理逻辑以及丰富的api。新的在播系统独立于原有系统,和业务彻底解耦,解决挑战一。
下面结合设计原则和挑战分别说说每一部分的关键实现。
生产系统
这里的主要逻辑是对动态数据的处理,最终目的是一次周期内,以基础数据为底,累加开关播信息形成比较实时的信息。采用这种结合基础数据加mq增量的方案,可以更好解决挑战六。
在分布式调度平台执行全新一次周期的任务调度,主要逻辑过程如下:
获取bucket配置信息,用来对庞大的在播信息进行切分
初始化增量key,用来给右边消费程序提供新的周期的key来写数据
设置当前周期为生效状态,右边消费程序就可以开始处理新周期的增量数据
分批执行sql,查询最新周期内的在播数据
对数据进行聚合和转化处理,通过bucket配置做最终的分片切割
将这些分片数据写入taishan,做持久化处理
清空异常补偿数据集,更新元信息,写入最新周期的相关信息
这里的第1步,通过动态配置bucket的方式,在生产和消费层都做分批处理,避免了大的size数据引起的一系列存储,传输等问题,很好的解决了挑战四。
第4步,通过将表以两百万为一个范围,最大并行10个查询任务来执行分段查询,高峰耗时250ms左右,相比5-6s的全量查询耗时表现优秀很多。最后将每个分段结果聚合,利用mapreduce的思想,分而治之,从而避免慢查询的问题,这里是为了解决挑战二。
同时我们做了sql查询的监控。
整个流程比较长,并且有对外部的依赖,所以需要做异常处理,来确保即使这个周期有异常,上个周期依然可以利用补偿数据集来获得正确的信息。
右边消费侧逻辑就比较简单,通过第3步设置的flag,来判断写新周期的增量信息还是继续写上个周期的增量数据,并且由于使用mq,防止异常情况下mq延迟过大,做了消息过期处理,来确保新周期消息的实时性,不混入过期信息。增量信息同样存入taishan。我们也做了mq延迟的监控,便捷观察数据实时性的健康度。
mq消息会有多个消费线程处理写入同一个增量的key,所以需要原子写入,并且增量数据也比较大,这里并不合适对这个大key进行cas操作,可以利用taishan提供的casMultiKey api,绑定一个小key做cas,taishan内部做一次事务,从而实现原子更新数据。
消费层可以很方便的通过基础分片数据加增量数据获取一次完整的数据,并且得益于taishan可持久化的特性,我们将在播的数据以非常小的存储成本按照7天的时间周期来存储,作为数据的备份,必要时可以帮助上游业务来定位一些过期问题。
消费系统
消费层,这里最核心的操作就是获取最新周期的元信息,通过元信息可以获取到最新周期的所有基础分片数据和增量数据,通过合并处理后就是最终提供给上游sdk层使用的数据。
消费层引入了本地内存来存储在播数据,本身由于在播数据一直在变化,不需要所有在播服务提供的数据在同一时间点强一致,而需要更加实时的数据,所以在本地数据生成的时候引入了版本的概念,只要拉取生成一次数据就会产生一个数据版本。数据版本的设计在下面会专门说明。
正是由于引入本地缓存,所以消费处理层的服务可以横向扩容,几乎不存在io瓶颈。
在数据传输方面,虽然从生产层获取数据走的分片传输方式,但是供给上游使用同样存在数据body过大的问题。当然我们没有使用那种传统的分页传输,这种本身就是有状态的,不利于分布式部署,而且存在数据一致性的问题,还有客户端和服务端的传输限制等诸多缺陷和不稳定性问题。
我们最终使用gRPC server stream模式,同样采用分批传输的思想,并且保证一次版本数据完整分段传输,十份契合我们的需求。虽然stream模式比一般unary模式要复杂,但是它带来的收益以及我们本来就要使用sdk的封装方式,使得这个决策有充分的理由。
通用grpc监控中没有stream模式的监控,所以针对此我们做了stream维度的请求指标上报,弥补这块缺失。
sdk设计
引入sdk,主要是为了解决收敛统一的问题,以及干掉不合理的非标接口,彻底解决挑战三,而且我们也同时封装了grpc stream的复杂处理逻辑,并且提供了丰富的api,满足业务的不同诉求。
sdk本身也引入了本地缓存来存储在播数据,和消费层一样的底层原则,数据更注重实时性,所以也引入了数据版本的设计。引入本地缓存也是合理的,有些面向C端的大流量业务,本身就是需要缓存这类大型的数据,避免过高的QPS穿透而引起网络io的瓶颈。
由于上游业务比较丰富,涵盖了几乎所有直播的业务后端技术团队,所以封装在sdk中有利于后期升级,来适应业务的发展,同时在sdk层也有利于做各个指标监控上报,构成整体全量在播系统全链路监控版图的一环。
数据版本
由于我们在消费层,sdk层都引入本地内存缓存,消除了io的瓶颈,可以几乎无限制的横向扩容,提供最大的服务承载能力,而数据本身的实时性就显得更加重要,因此在整个生产,消费,使用阶段都引入了版本的概念。目前数据版本主要使用时间戳,可以忽略不同机器时间戳的微小差异,时间戳整体是个递增的趋势,比较适合我们这种时刻更新的数据。
最开始的生产层包含基础版本,在一次周期内不变,持续递增,如下
数据消费层,每个服务实例,每次拉取基础数据和增量数据后,都会生产一个数据版本,并且会同时上报这两个数据版本指标,来监控服务层拉取数据是否异常以及本身生产是否异常,如下
sdk层,属于业务侧的服务层,同样会上报消费层数据版本指标,以及本身生产内存缓存时的数据版本,起到同样监控上游异常和本身异常的目的,如下
如果上游基准数据版本一直维持一条线,那么可能上游出现异常,同时可以从上游数据版本监控来佐证判断;如果基准数据保持递增,而本身数据版本维持一条线,说明本身服务运行异常,导致本身获取到非实时数据。
所以通过数据版本的设计,可以非常直观监控到数据的健康度,配合事件告警可以方便快捷做好线上保障。
数据正确性验证
新的在播系统的方案虽然理论上可以实现更好的实时性,但是需要在实际环境中与历史在播系统的数据做对比,这里利用一个计算公式,设置newData为新的在播数据,oldData为旧的在播数据,newData对比oldData的差集为newDiff,并且计算出newDiff中开播数 newOn 和关播数 newOff;同理oldData对比newData的差集合 oldDiff,并且计算出oldDiff中开播数 oldOn 和关播数 oldOff;然后就可以得到newData的相对延迟数据为oldOn + newOff,而oldData的相对延迟数为newOn + oldOff;新的在播系统sdk层可以调节数据更新周期,我们选择了1-5s进行对比,如下
可以看出4s,5s新的比旧的历史在播数据实时性略强,3s基本就和旧的在播数据拉开比较大的差距了,所以对于实时性的要求可以超预期满足。
可观测性建设
全链路监控作为新系统在可观测性建设上必不可少,解决挑战七。我们在横向上有很全面的支持,覆盖了生产,消费,sdk各层的直播监控,如下
在每层的垂直方向上,做了通用性监控,如实际的数据状态,本层服务的数据健康度,依赖层的数据健康度等。
生产层包含了基础信息大盘,以及生产链路各个阶段的核心指标监控等。在播数据本身就是我们直播核心生产系统大盘的重要体现,当中有很多数据值得深挖,在这方面的数据可观测性上我们做了很多工作,丰富了很多维度的监控。
包含基础盘类的数据指标:最新在播数及其变化趋势,最近开播数及其变化趋势,房间id创建分配趋势,短位号数据
包含生产类数据指标:查询耗时趋势,mq延迟趋势,cas碰撞数,以及数据版本健康度
消费层,由于基础监控缺失grpc stream模式的监控,我们加入了针对性的埋点,补齐了这块,主要包含了grpc stream请求的监控,sdk api总体使用qps的监控,以及数据健康度的监控。
sdk层,我们添加了任务耗时监控,业务使用api的监控,以及配置监控等,业务可以选择自己的服务进行观察,可以详细了解当前服务的在播数据状态和使用数据。
总结
新的直播在播数据分发系统,不仅新的业务受益,历史服务也会受益。如图历史房间服务job优化在播数据获取的方式,使其内存占用明显下降。
目前直接接入或者间接接入新的在播系统的业务服务有30+,线上运行大半年,历经各个线上大型活动,S赛事,跨年晚会,拜年祭等,以及各个不同部门不同业务场景的考验。
在十万在播挑战的topic上,新的在播系统不仅彻底解决了历史债务,也标准化统一了使用方式。在目标上支持十万在播也只是个开始,新的在播系统足够弹性,面向适应变化的设计,并且每个关键点都考虑到无瓶颈的设计,在相当长一段时间以及业务规模发展上,可以提供足够的支撑。
参考文档
架构师应该知道的37件事 (豆瓣)(https://book.douban.com/subject/35062026/)
Core concepts, architecture and lifecycle | gRPC(https://grpc.io/docs/what-is-grpc/core-concepts/#server-streaming-rpc)
Building Secure and Reliable Systems(https://google.github.io/building-secure-and-reliable-systems/raw/toc.html)
以上是今天的分享内容,如果你有什么想法或疑问,欢迎大家在留言区与我们互动,如果喜欢本期内容的话,欢迎点个“在看”吧!
往期精彩指路
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...