当前位置: 首页 > 后端技术 > Java

天天刷bilibili,了解他们的评论系统是怎么设计的?

时间:2023-04-01 17:13:57 Java

今天给大家分享哔哩哔哩评论系统的组件化、平台化建设。通过架构设计的不断演进,可以管理系统不断增加的复杂性,从而更好地满足各种用户的需求。basicfunctionmodulereview的基本功能模块比较稳定。1.发表评论:支持无限建回复。2.阅读评论:按时间和热度排序;显示评论数量、楼中楼等3、删除评论:用户删除、UP主删除等4、评论交互:点赞、踩、举报等5、管理评论:置顶、选择、后台操作管理(搜索、删除、审查等)。结合哔哩哔哩等互联网平台评论的产品特性,评论一般包括一些更高级的基础功能:1、评论的富文本展示:如表情、@、分享链接、广告等2、评论标签:例如UP主点赞、UP主回复、好友点赞等3.评论装扮:一般用于突出评论者的身份等4.热点评论管理:结合AI和人工智能打造更好的评论区为用户营造氛围。建筑设计审查是主要内容的延伸。因此一般设计成独立的系统拆分。架构设计-概述架构设计-reply-interfacereply-interface是评论系统的接入层,主要服务两类调用者:一类是客户端的评论组件,一类是基于评论系统的二次开发或其他相关业务业务后端。针对移动端/WEB场景,设计一套基于view-model的API。BFF层利用客户端提供的布局能力,负责组织业务数据模型,将其转化为视图模型,布局后发送给客户端。对于服务端场景,设计的API需要体现清晰的系统边界,基于最小可用性原则对外提供数据,同时做好安全验证和流量控制。对于评论业务,业务数据模型是最复杂的。哔哩哔哩评论系统历史悠久,承载了相当多的功能模块,核心是发布接口和列表接口,一次写入一次读取,数据字段多,依赖服务多,调用复杂关系,尤其是一些依赖关系。更改很容易导致整个系统损坏。所以我们把整个业务数据模型组装成两步,一是服务整理,二是数据组装。服务编排分为几个层次。同层可以并发调用,前置依赖多的可以一个pipeline调用,从结构上提高了复杂调用场景下的接口性能下限;为不同的依赖服务提供不同的SLA。不同的降级处理、超时控制和业务限流方案,保证了在少数弱依赖抖动甚至完全不可用的情况下评论服务可用。数据组装在业务编排之后进行,比如批量查询评论发布者的粉丝勋章数据后,转换组装到每张评论卡片中。架构设计-reply-admin评论管理服务层,为内部多个管理后台提供服务。运营商的数据查询有:1、组合、关联查询条件复杂;2、关键词检索能力刚需;3、先写后读的可靠性和实时性要求高。对于这样的查询需求,ES几乎是最好的选择。但由于业务数据量大,需要针对多个不同的查询场景建立多个索引分片,数据更新的实时性不高。因此,我们在ES的基础上做了一层封装,提供统一的数据检索能力,结合在线数据库,对一些实时性要求高的字段进行刷新。架构设计-reply-service评论基础服务层,专注于评论功能的原子实现,如查询评论列表、删除评论等。一般来说,该层很少改变业务逻辑,但需要提供极高的可用性和性能吞吐量。因此reply-service集成了多级缓存、Bloomfilter、热点检测等性能优化手段。架构设计-reply-job评论异步处理层主要有两个职责:1.配合reply-service补充架构,实现基本评论功能的原子化。为什么基础功能的原子实现需要架构来补充?最典型的情况就是缓存的更新。一般采用CacheAside模式,先读取缓存,再读取DB;缓存的重构是指读请求未命中缓存而穿透到DB,从DB中读取内容后再写回缓存。这套流程对外提供了原子数据读取功能。但由于某些缓存数据项重建成本较高,比如评论列表(因为列表是分页的,重建时会开启预加载),如果短时间内多个服务节点的大量请求缓存未命中的时间,很容易引起DB抖动。解决方案是使用消息队列实现“单个评论列表,只重建一次缓存”。综上所述,所谓架构补充就是“用单线程解决分布式无状态服务的通病”。另一方面,reply-job也作为数据库binlog的消费者,进行缓存更新操作。2、配合reply-interface对一些耗时长/高吞吐量的调用进行异步/调峰处理。对于发表评论等操作,基于安全/策略的考虑,会有非常繁重的pre-call逻辑。对于用户来说,这种长时间的耗时几乎是无法接受的。同时,当前的热点很可能造成发表评论的瞬间流量高峰。因此,reply-interface处理完一些必要的验证逻辑后,会通过消息队列发送给reply-job进行异步处理,包括提交审核、写入DB、发送通知等。同时,这里也利用了消息队列的“有序”特性,对单个评论区的评论进行串行处理,避免了一些并行处理带来的数据混乱风险。那么异步处理后的用户体验如何保证呢?首先,C端评论接口会返回显示新评论所需的数据内容,客户端根据此显示新评论,完成一次用户交互。如果用户再次刷新页面,因为发表评论异步处理的端到端延迟基本在2s以内,此时所有数据都准备好了,不会影响用户体验。一个有趣的问题是,早年的评论显示的是楼层数,其实就是一个计数器,评论区内不能重复。所以这个发布楼层号的操作必须在一个评论区内进行序列化(或者用更复杂的锁实现),否则同时发布的两条评论获取的楼层号会重复。但是分布式部署+负载均衡网关在处理评论请求时无法实现这种序列化,需要在消息队列中进行处理。存储设计数据库设计结合评论的产品功能需求,评论至少需要两个表:第一个是评论表,主键是评论id,键索引是评论区id;二是评论区表,主键为评论区id,平台化后增加评论区类型字段,与评论区id构成“联合主键”。由于评论内容是一个大字段,相对独立,很少修改,所以独立设计了第三张表。主键也是评论ID。评论表和评论区表的字段主要包括四种:1.关系类,包括发布者、家长评论等。这些关系数据在发布时已经确定,基本不会修改。2、统计类别,包括总评论数、根评论数、子评论数等,一般在评论发布或删除时修改。3、状态类,包括评论/评论区状态、评论/评论区属性等。评论/评论区状态是一个枚举值,描述了正常、审核、删除等可见状态;comment/commentareaattributes是一个Integer位图,可以用来描述comment/commentarea的一些关键属性,比如UP主likes等。4.其他,包括meta等,可以用来存储一些key附属信息。评论回复的树状关系如下图所示:以访问评论列表为例,我们的查询SQL可能是(简化):1.查询评论区的基本信息:SELECT*FROMsubjectWHEREobj_id=?和obj_type=?2。查询时间序列一级评论列表:SELECTidFROMreply_indexWHEREobj_id=?和obj_type=?ANDroot=0ANDstate=0按楼层排序=?限制0,203。批量查询根评论的基本信息:SELECT*FROMreply_index,reply_contentWHERErpidin(?,?,...)4.并发查询楼内评论列表:SELECTidFROMreply_indexWHEREobj_id=?和obj_type=?和根=?ORDERBYlike_countLIMIT0,35。批量查询楼评基本信息:SELECT*FROMreply_index,reply_contentWHERErpidin(?,?,...)产品形式方面,单页只有二级列表(嵌套层数较多,对应嵌套多次点击),评论数只有两级。如果回复的数量也没有限制,则子评论的发布需要级联更新所有父评论的回复。当前的数据库设计不能满足这个要求。更进一步,产品端的定义是,删除一级评论,其回复等同于删除全部。如果直接删除,此时也可能会出现写放大。所以结合查询逻辑,并不是需要更新回复数,而是评论区的计数更新操作需要减去一级评论的回复数。评论系统的数据库选择要求有两个基本且重要的特征:1.必须有交易;2.容量要大。一开始我们使用MySQL分表来满足这两个需求。然而,随着哔哩哔哩社区的突破,原有的MySQL分表架构很快达到了存储瓶颈。所以从2020年开始,我们逐渐迁移到TiDB,TiDB具备横向扩展的能力。缓存设计我们根据数据库设计来设计缓存,选择redis作为主要缓存。缓存主要有3个:1.subject,对应“查询评论区基本信息”,redisstring类型,value以JSON序列化存储。2、reply_index,对应“查询xxx评论列表”,redis排序集合类型。member为评论id,score对应ORDERBY的字段,如floor,like_count等。3.reply_content,对应“查询xxx评论的基本信息”,存储的内容包括reply_index表的两个字段和reply_content表对应相同的评论id。reply_index是一个排序集。为了保证数据的完整性,必须先确定key存在,然后才能增量添加。由于存在判断和增量添加不是原子的,缓存可能在判断存在之后增量添加之前失效。所以使用redis的EXPIRE命令进行存在性判断,避免这种极端情况造成数据丢失。另外,缓存的一致性依赖于binlog的刷新。有几个关键的细节:1.binlog投递到消息队列,shardkey选择评论区,保证单个评论区和单个评论的更新操作是串行的。后者是顺序执行的,保证对同一个成员的zadd和zrem操作不会乱序。2、数据库更新后,程序主动写入缓存,binlog刷新缓存。两者都删除缓存而不是直接更新缓存,以避免并发写操作时数据混乱,尤其是在binlog延迟、网络抖动等异常场景下。如何解决大量写操作后读操作缓存命中率低的问题?这时候可以使用singleflight来控制,防止缓存崩溃。写作阅读的可用性设计热点2020年,腾讯的辣椒酱不香了[1],引发了评论区的狂欢。由于上面提到的各种“评论区维度序列化”,当时评论发布的吞吐量很低,面对如此大的流量,出现了严重的延迟。痛定思痛后,我们分析了瓶颈,做了如下优化:1、对于评论区评论数的更新,先合并内存再更新,可以减少热点场景下的SQL执行次数;评论表的插入改为批量写入。2.其他非数据库写操作的业务逻辑拆分为pre-和post-两部分,从数据写入主线程中剥离出来,交给其他线程池并发执行。改造后,系统的并发处理能力有了很大的提升。同时支持并行/聚合粒度的配置,在吞吐量上有更大的灵活性。热评区评论TPS提升10多倍。除了写热点,评论的阅读热点也有一些典型的特征:1.因为大量的接口需要读取评论区的基本信息,存在阅读放大,所以这个操作是最先感知到存在的的阅读热点。2、由于评论业务下游依赖较多,而且大部分是批量查询,对下游也是一种读放大。另外,很多依赖都是比较小的业务单元,数据稀疏,难以承载评论的大流量。3、评论阅读热点集中在评论列表首页,热评热搜。4、评论列表的业务数据模型也包含一些个性化信息。因此,我们使用《直播场景下 高并发的热点处理实践》[5]一文中使用的SDK,在阅读评论区基本信息阶段进行热点检测,并将热点识别传递给BFF层;热点后读取本地缓存,然后加载个性化信息。热点检测的实现是基于单机的滑动窗口+LFU,那么如何定义和计算对应的热点条件阈值呢?首先我们设计系统容量,列出容量计算的数学公式,主要包括各个接口QPS的关系,服务集群总QPS与节点数的关系,接口QPS与节点数的关系CPU/网络吞吐量等;连同对应依赖方的一些热点相关的统计信息,检测数据项的单机QPS热点阈值是通过上面列出的公式计算出来的。最后通过热点压测验证对应的热点配置和代码实现是否符合预期。冗余与降级上面提到,评论库服务层集成了多级缓存。上级缓存未命中或网络错误后,可根据具体场景需求降级为下一级缓存。各级缓存的功能可能略有不同,但都保证了基本的用户体验。另外,评论系统是同城阅读的双活架构。数据库和缓存均独立部署在两个机房,均支持多副本,具有水平扩展的灵活性。针对双机房架构特有的二级机房数据延迟故障,支持入口层切换/跨机房重试/应用层补偿,保证用户在极端情况下尽可能不敏感。在功能层面,我们做了重要的层级划分,将对应的依赖分为强依赖(比如审计)和弱依赖(比如粉丝勋章)。对于弱依赖,我们一方面坚决限流,异常情况下熔断。另一方面,我们通过超时控制、请求预过滤、优化调用安排,甚至技术方案重构等方式,不断优化和提升非核心功能的可用性。评论区得到更好的曝光和展示。安全设计评论系统的安全设计分为“数据安全”和“舆情安全”。数据安全除了数据安全法的要求外,评论系统的数据安全还包括“合规要求”。评论数据合规性,一方面是审计和风险控制,另一方面,工程侧的主要需求是“状态一致性”。比如有害评论删除后,不能在客户端展示,也不能通过API对外暴露。这就对数据的一致性提出了更高的要求,包括缓存。在设计层面,主要有两种做法:1.在数据读写阶段考虑一致性风险,严格保证时序。2.为各种数据写入操作定义优先级,防止高优先级的操作被低优先级的操作覆盖。例如,审核删除的有害评论不能通过其他普通运营商/自动化策略发布。3.通过冗余检查避免有风险的数据泄露。比如评论列表暴露后,读取sortedset中的id列表后,需要验证对应评论的状态。只有当它可见时才能发送。舆论安全舆论安全问题更具普遍性。界面错误导致用户操作失败、评论区关闭、评论统计不准确,甚至新功能上线、用户不满意的评论被推送到热门评论前排等,都可能引发舆论问题。在系统设计层面,我们主要通过几个方面来避免。1、不要暴露用户无法处理、不值得处理的错误。例如,评论、点赞、投反对票等轻量级操作,或者读取某个数据项失败,不值得用户重试,此时告知用户操作失败是没有意义的。系统可以考虑自行重试,甚至忽略。2.优化产品功能及其技术实现,如评论统计、热评排名等。这里重点介绍评论统计的优化思路。计数不一致的根本原因是数据冗余造成的。一般出于性能考虑,会在评论列表之外为这个列表记录一个长度。也就是说,使用SELECTcountFROMsubject而不是SELECTcount(*)FROMreply_index。基于这种冗余设计,大部分计数字段都是增量更新的,即+1、-1,容易产生错误累积。评论统计不准确有以下几个原因:1.并发事务导致的“写偏”,比如根据评论的状态更新评论区的评论计数。在事务A中读取的评论状态,在事务B中可能会被修改,此时事务A中计数更新的前提被破坏,从而导致错误的增量更新。此时计数可能太高或太低。2.运行时异常、脏数据或非常规的显示端控件导致某些数据被过滤。此时计数可能过高。3.统计冗余同步到其他系统,比如视频表的评论数。延迟导致进程不一致,同步失败直接导致最终不一致。并发事务的解决方案主要有两种:1.事务锁定。在综合评价方面,它对性能的影响更大,尤其是在存在“锁放大”的情况下。需要的锁越多,“锁冲突”的可能性就越大。2.连载。评论区的所有操作都抽象成一个排队的DomainEvents,串行处理,不易出错。那为什么不能按照评论维度来拆分,在评论区维度就更不可能出现热点了呢?如前所述,删除一级评论时,其回复实际上会从计数中删除;删除二级评论时,其根评论的计数也会更新。多个评论的操作相互影响,所以按照评论维度拆分还是存在并发事务问题。热评设计什么是热评早期的热评其实是按照评论的点赞数降序排列的。后来又衍生出更复杂的热评:不仅包括像“秒评”这样用户推荐、运营选择、标识显着展示的产品形态,还包括热评的各种排名算法,以及应用场景热评排名算法的排序不限于评论主列表的热度排序,还包括楼中楼(暴露子评论)、动态暴露评论等。热评排序逻辑一般包括数量点赞数、回复数、内容相关性、??差评数、“时间衰减因子”、字数权重、用户等级权重等【如何在B站评论区脱颖而出?[7]](https://mp.weixin.qq.com/s?__...)本文从内容运营层面介绍了什么样的评论更容易排在热评前列。说白了,我们对“热”的理解大致可以分为几个阶段:1、高点赞就是高人气。→解决热评问题2.基于用户的正负样本投票,高加权平均意味着高人气。→解决负面热评高赞高反感的问题3、短时间内高点赞率意味着高人气。→解决高赞永远高赞的马太效应4、热评用户流量大,社区影响力也大。需要权衡社会价值导向、公司战略定位、商业利益、UP主和用户的“情绪”。→追求用户价值平衡的挑战及解决方案显然,我们对不同阶段热门评论的理解,也在系统设计上提出了不同的需求:1.按照点赞的绝对值排序,即实现ORDERBY的页面排序喜欢计数。点赞数是一个经常更新的数值。对于MySQL,尤其是TiDB,由于扫描行数约等于OFFSET,当OFFSET很大时,查询性能特别差,很难找到完美的优化方案。另外,由于like_count的分布,同一个值可能会堆叠多个元素。比如评论区的所有评论都不点赞。我们更多的是依赖于redis的sortedset来进行分页查询,这需要非常高的缓存命中率。2.根据正负样本加权平均,即Reddit:Wilson排序[6],现阶段数据库已经无法实现如此复杂的ORDERBY,热评开始几乎完全依赖数据排序集、预计算排序等结构的分值和写入。因此在架构设计上,增加了feed-service和feed-job来支持热评榜单的读写。3、按点赞率排序,需要实现点赞率的近实时计算。点赞率=点赞数/曝光数。曝光数据来源为客户端上报的展示日志。量级非常大。可以说是写多读少的场景:exposure只有在重新计算排序的时候才会读。数字。4.追求用户价值的平衡,需要应对各种细分场景下的差异化需求。热门评论排序与提要排序类似,但有一个根本区别:提要排序往往是个性化的,每个人看到的都不一样,但评论往往没有那么激进。总的来说,我们希望大家看到的评论按照大致相同的顺序排序。由于排序问题的解决是探索性的,系统设计层面需要提供更多样化、更具可扩展性的工程能力,包括算法和策略的快速迭代、实验能力等,提高整个热评模块的可观察性,完善的监控、丰富的数据报表、可解释的排序流程等。在架构上,增加了strategy-service和strategy-job来承接这部分策略探索业务。此外,数据量级的增加也对系统的吞吐量提出了更高的要求:无论热评算法如何变化,一般来说,热评榜需要能够访问到所有的评论,并且基本保持不变热评榜。评分逻辑。在百万甚至千万条评论的评论区,热评论排序面临的主要挑战是:1.大key问题:比如单个sortedset过大,会影响读写性能(时间复杂度的底数可以看作是O(logN));全量更新时,也可能会遇到redispipeline的瓶颈。2.存储压力实时放大:多样化的数据源对特征的导入和更新带来挑战。需要支持更丰富的数据结构和尽可能高的写入吞吐量(想象曝光数的蜕变作为排序特征的需求);与推荐排序不同,热评排序是全量排序。这时候需要读取所有评论的所有特征,查询压力会很大。现阶段,我们还在持续优化,在项目实现层面尽可能还原理想的排序算法设计,以保证用户受欢迎的浏览体验。目前形成的整体系统架构如下图所示:图中的“评论策略层”负责建立一套系统化的热评监管能力,并通过召回机制达到想要的“平衡”。也就是通过策略工程,召回一批本该沉到底部的差评或本该排到前排的好评,然后在排名计算阶段根据召回结果达到这样的效果。这样做的好处是可以保留一个通用的底层排序算法,然后利用迭代细分场景下的召回策略,实现差异化评论的均衡排序。召回策略的工程设计按照分层设计的原则分为三个部分:1.Factormachine。主要职责是维护策略所需的所有“因素”,包括一些现有的在线/离线数据,以及新开发的用于策略迭代的流式窗口聚合数据。factormachine最难的点是管理各种数据获取的拓扑关系,通过缓存保护下游(数据提供者很难也不应该承担热评业务的巨大流量)。所有因素可以形成一个有向无环图,通过梳理依赖关系和推导计算,可以提高并发性,减少冗余。2.规则机。实现了一套声明式规则语法,可以直接引用因子机预定义的因子,组合各种逻辑运算符组成正则表达式。规则机执行命中后,会向下游传递预先声明的召回决策,如排序提权。3.召回处理中心。该层的职责是接收并执行规则机返回的各种决策。需要处理不同决策的优先级PK、不同规则的重叠效应、决策豁免等问题。热评排序涉及的特点是多数据源,数据更新方式、更新频率、查询性能也有很大差异。因此,我们根据数据源的特点做了多级缓存,通过多级冗余和跨级合并,提高了特征读取的稳定性和性能极限。当然,数据的实时性、一致性和可用性还处于一个动态权衡的过程中。例如,使用redis计数器维护曝光数,由于成本限制,这些计数器不是持久化的;各种静态模型有4到5层冗余。此外,还应用了内部稀疏数据布隆过滤器查询、数据局部性集中与哈希相结合、近实时大窗口聚合计数等多种性能优化方法。需要指出的是,召回和排序两个阶段都需要查询因子/特征,这样才能复用“因子机”,完成对每个特征的差异化实现和维护。热门评分排序最关键的计算模块是首先引入一种自适应冷却算法,根据评论区的评论数和活跃度来预估重新计算排序的收益,拦截大部分低价值的重排请求。其次,在全打分排序阶段,“排序策略”贯穿上下文,既支持传统的静态经验打分公式,也支持动态模型打分,支持AI模型的快速部署和快速迭代。通过组合和继承,支持排序策略的叠加和微调。结合评论网关层排序策略的路由,可以实现各种自定义排序,完成热门评分排序系统的平台化。除了点开评论区就能看到的“热评”,我们还将这套热评系统应用在楼中楼、动态曝光评论、评论详情页等类似场景中,实现统一工程中的建筑。