低代码到底是不是行业毒瘤?一线大厂怎么做的?戳此了解>>> 了解详情
写点什么

内容系统服务的三个架构原则和操作范式

2021 年 3 月 12 日

内容系统服务的三个架构原则和操作范式

面向用户的业务系统中,最重要的服务之一是内容资源服务。内容资源是以数字化形式记录全部信息,本文把依赖内容资源为用户提供各种服务的软件系统定义为内容使用系统。用户手机上大部分 App 的基础功能,都可以归类为内容使用系统,比如腾讯新闻,为成千上万用户提供新闻资讯服务功能;极客时间 APP、喜马拉雅、得到等,为用户提供课程、音频资源服务;Bilibi、腾讯视频等,为用户提供视频类资源服务等;甚至商品详情介绍等也都属于内容使用系统。这些基础功能所对应的内容使用系统,其目标是对用户输出大量的有价值的信息资源,这一特性,和注册登录系统、支付系统等其他非内容使用系统,都有着明显的区别。


内容使用系统,高度依赖其系统内存储的结构化信息资源,以及在资源基础上的各种创新,为用户提供服务。面对这类系统进行架构和设计时候,有没有比较好的抽象、原则和操作范式,帮我们去理解这类系统,并解决实际问题呢?


有的。本文对内容资源使用系统进行抽象分析,并阐述一些架构中应该遵循的原则和实操范式。类似下面的实际问题,都可以被解答:


  • 问题一:内容系统各微服务/模块之间该如何划分,有没有和该系统特性相关的、较高抽象的原则?

  • 问题二:内容系统的各微服务/模块之间,相互引用资源时,该如何存储引用,以 ID 引用是否是最合适的?有没有必要冗余层级关系?

  • 问题三:内容系统内,不同服务在传递资源信息时,将用户之于资源的状态直接传递是否合适,有没有更好的方式?

  • 问题四:内容系统的业务体系内,经常有各种资源组合,有什么办法可以合理地提高开发效率?


内容资源系统分析


我们先思考下系统对外输出的信息资源的抽象划分,抽象是将复杂物体的一个或几个特性抽出去,而只注意共性的描述,抽象更接近事务的本质,架构应该面向抽象。


如上文所述,信息资源是内容使用系统的基石。内容使用系统给所有用户输出的所有信息资源,是有边界的。而这个有边界的空间,可以从构成上,抽象出三个维度:基础资源维度、用户维度、以及用户与资源之间的关系维度。如下图所示,这三个维度构成一个内容使用系统对外输出的资源空间。



纯粹的基础资源是一维的,当它添加上用户维度后,两者组成二维资源用户平面。再结合关系维度,衍生为立体三维服务空间。关系维度的掺入,使得原本资源界面发生量级变化,带来了服务多样性。内容使用系统,对用户提供的所有信息,都可以在资源界面空间里定位。我们从一维、二维、三维不断扩展的角度来定义资源空间。


基础资源。基础资源是资源服务或模块对外输出的纯粹资源信息,其内容由 PGC 或 UGC 生产而成。它是一维的。基础资源通过电脑端、客户端等软件,可以被用户使用,比如新闻信息的本体数据、音频、图片、视频的源数据等。这部分通常也是资源服务的基石。


资源用户平面。一个成熟的内容系统往往服务于成千上万个用户,其对所有用户输出的纯粹的基础资源信息,可以看作系统输出的资源界面。它是二维的。该平面较为简单,每个用户获得的资源也是一致的。如传统的资讯类网站,对所有用户无差别展示信息,只对外输出了资源用户平面。内容系统单纯提供资源用户平面,对用户缺乏定制化,无法提供更有效的服务和价值,正在逐渐被替代。


资源用户空间。内容系统的目标是为用户提供优质服务。随着服务的升级,业务内容系统,往往会将用户和资源之间拓展出一定的关系,即针对用户进行定制化资源,或对资源附属一些用户特有的属性,以更好的服务用户。如音频资源附加上用户的收听进度、某个视频附加上是否已经收藏、或者依赖用户偏好推荐出特定的资源等。伴随着关系的出现,直接将资源平面,提升为资源空间,极大丰富了资源的多样性,提升用户体验。


上段提到了关系,关系是将用户资源平面质变为资源空间的核心。那关系的本质是什么?关系,本质上是人与物(信息资源可视为虚拟物)的相互作用、相互影响的状态,可以视为资源维度的用户态。内容使用系统引入关系,是为了增加资源的价值。关系的介入让资源空间的输出,成为附加用户属性的资源信息,我们把这些用户属性称为用户态。如使用(App 中展示的状态可能是浏览历史记录、音频收听进度、次数等)、权益(App 中表现的是否已经购买)、收藏等等。


由于关系的介入,对单个用户而言,资源空间对其输出不再是空间内的某个资源线,而是空间内的各种形态切面,如下图中蓝色的切面。



虽然人和物的关系,可以不断衍生,但通常在一个商业体系内,真正有商业价值的关系屈指可数。一种新的对用户有价值关系被创造出来,会形成新的商业模型,并带来的商业价值。基础关系,通常可以认为是一张二维表即可表达的关系为基础关系。它的种类往往也能枚举出来,基础关系可以成为资源的属性,附属在资源上输出给用户,如收藏、权益都属于基础关系。而复杂的关系可以理解为基于各种关系衍生的关系,常见的如根据用户的各种关系数据,推荐出有偏好关系的信息资源等。



内容使用系统类产品的发展成长,必然涉到三个维度的增长或创新,其产品层面的需求大都也来自于这三个维度,虽然不同时机侧重点会有不同:


  • 三个维度自身的创新增长有:从用户维度为用户量级的增长,用户日活的增长;从基础资源的维度为资源的增长及多样化发展,不断生产新资源,还可以不断扩展资源的形态,如极客时间扩展出从音频、视频,从课程扩展出训练营等;从用户和资源的关系维度为不断扩展出新的关系形态,如收藏、追更等。

  • 维度内的衍生组合,会让资源输出的形式更加丰富:资源内部的组合(文稿内部结合音频、视频等、资源的打包售卖、资源的分类组织等)、用户内部的组合(如用户的关注关系、群组关系等),关系内部的组合(购买列表、收藏列表等)也会成为创新点。

  • 维度间的衍生组合,从产品进化角度来看是系统继续成长的必经之路,因为给用户的提供更好的个性化服务是内容系统发展的必然。以资源服务、关系为原材料,经过各种策略及算法的不断深度组合,则会衍生出更为复杂的聚合层服务,如智能推荐类产品形态糅合了各种关系推荐出用户感兴趣的资源池;学习清单类产品形态糅合了用户、关系、资源三个维度,向某个用户推荐其他用户整理的资源列表(关系)等等。整个系统就像分子组成细胞,细胞又衍生出五彩缤纷的生命世界一样,关系让资源平面衍生为资源空间,而再经过衍生组合多样化后,系统便可以为用户提供更为丰富多彩的服务形态,极大的提升资源的价值及用户体验。


这样探讨的意义在哪里呢?在于让我们对内容系统进行架构时,可以清晰的认识到内容系统的构成本质,并对体系内的每个微服务的定义和归类有更清晰的认识;也可以按该抽象,对未来产品的发展,及技术规划做一定的预测。


服务拆分


按照分析,我们将系统划分为:资源服务群组、基础关系服务群组、用户服务群组、和衍生组合群组。其中,资源服务群组还可以内部拆分出底层资源服务。用户服务群组,也可以内部拆分出底层用户群组。一个产品初期基础关系服务和底层资源服务发展较快,成熟期后,衍生组合服务群组需求变化较多。


在群组内服务拆分层面上,按照单一职责原则,可以将不同类型的资源拆分为不同的服务或者模块。通常每种基础关系,也是一个单独的领域,适合成为单独的服务出现。同时,衍生组合层服务通常应当独立出来,不与基础资源/基础关系混淆,专注自身逻辑及拼装组合场景。



依据内容系统的抽象划分。将有相似的特征的服务划归为同一群组,可以使群组内更深入研究这类特征,并提高复用度。团队的搭配可以考虑借鉴康威法则,将组织架构参照系统架构的进行拆分,不同的团队负责不同的服务群,以避免服务间的边界和职责随系统发展而变得模糊。


下文重点讲述在这个划分体系下,资源群组特征及其与其他群组交互的特征及设计。


第一层 资源请求无状态


原则:确保内容系统内的服务自身是无状态的(Stateless)。最经典的无状态场景,是 HTTP 协议的无状态,它是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。类似,服务无状态,可以理解为,该服务运行的实例是无状态的,并且多个实例对于同一个请求响应的结果是完全一致,每个请求与之前任何请求都无关。


无状态服务在架构设计中都被当作铁律。这是因为:无状态服务很容易横向扩展,遇到性能压力时,只需在负载均衡之后增加节点就能处理更多业务请求,而且,无状态服务逻辑也更为简单,其服务体自身(不考虑客户端、存储)不需要考虑复杂的 CAP 问题。做好这个“无状态”化的工作,依赖于在架构设计上的合理分层,【操作范式】尽量将数据状态的处理移动至在最前端,或者移动至最底层的存储层。中间的服务层应当将“无状态”作为一个普遍性的标准来执行。


  • 状态移动至最前端:将用户信息移动至客户端保存和维护,是无状态服务较为成熟的实践。处于系统面向用户最前端的 App 可以很好的承载签名和加密工作,天生支持页面维护及无状态存储,与此同时,由于会话状态集中在最前端,哪怕真的状态丢失了,也只是单个用户状态丢失,重建状态的成本很小。

  • 状态移动至最底层:数据存储通常是有状态的,比如对数据的增删查改操作,通常是有复杂的事务逻辑(时间状态)和一致性逻辑(空间状态)的,状态处理不当会造成错误。而数据库等中间件经过多年进化,不但已经较好的解决各种事务逻辑带来的数据状态问题,也解决了分布式存储的一致性数据状态问题。将状态下移至数据库,实现和数据的分离,服务自身可以不需要考虑维护请求的状态。


内容系统内的各种服务,都可以成为无状态服务。而资源群组内的服务,还能进一步利用弱状态。


通常,资源群组内的服务具有读取与写入量级悬殊巨大,用户对一致性要求不高的业务特性,使得其对数据存储要求,符合弱状态的场景,即允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,只要在可接受时间内达到最终一致性即可。在高并发的场景下,强一致要求的数据存储会存在扩展性问题,需要 “保卫数据库”。但基础资源服务,通常可以依据弱状态这一特点,方便地利用 NoSQL 对底层数据库进行扩展,牺牲一定的强一致性,避免出现数据存储的瓶颈。对于如运营人员更新某篇文章的内容,在较短时间内,不同用户访问出现一定程度的不一致,这是完全可以接受的。



资源服务,成为无状态后,是否还有进一步优化空间呢?我们继续深入下一层。


第二层、资源访问无用户态


原则:将用户态和资源服务解耦,确保更多的资源服务是无用户状态。在上文的内容资源系统分析中,我们分析了什么是用户态。要确保更多的资源服务是无用户态的,并非意味着,我们废弃空间的多样性,而是将资源与关系的组合,尽可能提至最上层。



用户态介入后,系统会发生一定变化:


  • 资源存储的强弱状态可能发生变化。用户态服务和基础资源服务的存储状态要求有一定的不同:资源自身的存储是弱状态,而用户态存储是有较强的状态。如商品详情是和用户态无关,但用户相关的数据的服务,是有强状态的,如对资源有权益、阅读进度数据,都受用户行为的影响。不同的存储状态,应对高并发的架构方式也不相同,如上文所述,弱状态的存储可以方便的利用 NoSQL 对底层数据库进行扩展。强状态的存储,应对高并发,则较多依赖底层存储自身的扩展,适宜选择一些自身扩展较好能力的中间件,如支持分片集群的 Mongo 等,让中间件来解决复杂的 CAP 问题。

  • 服务界面量级发生变化。用户态的介入使服务界面量级发生量级的增长,同时也带来复杂度的非线性增加,如缓存问题,在未掺入用户角度时,还有可能把 PGC 全部资源存入缓存,但是一旦掺入用户,往往不可能把所有用户信息存入缓存,需要处理如冷热数据、空间和性能的平衡问题。


用户态介入使得存储状态的复杂度增加,和资源界面量级的提升。这意味着,我们应当要减少有用户态资源界面的输出,使得资源维度的服务输出尽可能保持纯粹。简单而言,内容使用系统,有尽可能多的服务或模块,可以忽略用户标识,即不同的用户返回是一样的,这样会使整个系统里保持更多的简单稳定服务或模块。


用户态组合提至上层


上文所述,用户态加入后,资源空间复杂度会提升,我们应对策略是需要保持尽量多的服务或模块简单性,将组合尽可能提至最上层,即向上移动至客户端,或者向上移动至最接近客户端的模块、或服务,以避免出现组合的传递。


【操作范式】资源和用户态组合提至最上层。尽量将用户态相关的处理上浮到最前面的层,因为如果只有最前面的层负责对用户处理状态,如此一来,其它的下层就可以将“无用户态”作为一个普遍性的标准去做。


服务的演化中,我们发现,经常会有微服务携带大量用户相关的关系进行传递,将用户态提至最上层后,可以解决以下问题:


  • 减少流量放大问题。可以从下图中对比出,改造前的进度服务,承担来自不同类型资源服务的流量请求,改造后,单条请求,可以批量处理不同资源类型的请求,减少流量放大问题。

  • 降低基础资源服务/模块复杂度,避免基础服务要考虑的用户态问题。如上文提到,基础资源属于弱状态,可以使用 NoSQL 进行缓存,但用户态属于强状态,两者应对高并发的处理方式并不一致。

  • 有利于用户降级统一处理。从用户态服务和资源服务的业务特点来看,用户态属性通常可以被降级的。降级隐藏越深,系统复杂度及排查问题难度越高。


实操中,在内容使用系统内,【操作范式】可以提供组合的状态挂载组件(SDK) 以提高开发效率。微服务之间的服务调用获取用户态时,优先采用状态挂载组件,挂载不同类型状态。客户端直接调用的状态聚合,可以采用微服务 API 方式聚合,由聚合服务或模块对外输出。统一的组件,还可以方便的对降级进行统一处理。



此处补充说明下资源权益的特例。理论上,权益也属于用户属性,理应上移动至最上层,但如果资源的安全要求较高,权益需要收敛,且没有更好的解决方案的话,该用户态可以下移,是属于在安全和合理的架构之间进行权衡取舍。


用户态解耦


将用户态和资源服务解耦会降低资源服务复杂度。此处,我们介绍一种采用加密 Token 的解耦方式,希望对读者去解耦用户态提供借鉴。


【案例】将用户态移动至最上层客户端。假设,如下图左侧,服务 A 是基础资源服务,提供用户某个课程内,有权益的音频 ID 列表;服务 B 是核心底层资源服务,提供通过音频 ID 来换取的音频访问地址及解密秘钥。对用户小明使用 APP 而言,APP 通过服务 A 获取到小明有权益的资源 ID 列表,再通过请求服务 B,来获取具体的资源访问地址及秘钥。左侧服务 A 和服务 B 都需要来判断该请求的用户态,即小明是否有权益。这种情况下,如何将服务 B 改造为无用户态服务呢?


改造如下图右侧。该场景下,由于服务 A 已经将权益下发至客户端,下发前,将用户、权益时间等状态信息进行加密,生成加密串,下发至客户端。客户端保请求服务 B 时候,携带自身保存的加密串,服务 B 只对加密串进行鉴定是否合法,如果合法则返回所需资源。通过这种方式,将用户态上移至客户端,将原本服务 B 需要考虑的用户态问题,改造为无用户态的解密问题,服务 B 也成为了无用户态的服务。由于底层核心资源服务 B 被使用范围非常广泛,去除用户态的话,大大降低了核心资源服务的复杂度,提高稳定性。



第三、资源服务消除冗余


原则:减少资源服务或模块之间相互引用中的冗余。冗余会带来各种异常,应对异常会增加资源服务或模块的复杂度,降低稳定性,本节来分析如何减少服务间的冗余。


内容系统输出的数据是整体数据结构化的数据,不仅数据内部结构化,而且数据之间是有联系的。设计的哲学是相通的,这一特性和数据库有类似,我们可以假设,整个业务系统的输出资源的边界(资源空间)是一个库,每个资源服务自身的输出界面,可以认为是一张宽表。从而借鉴成熟的数据库范式,来减少冗余。冗余会带来类似违反范式后造成的各种异常,插入异常,修改异常,删除异常的问题。由于数据库数据极度规整,而微服务之间对数据的使用限制几乎不存在,数据库违反范式带来的危害有可能会被放大。


先来复习下数据库的范式,第二范式:消除非主属性对码的传递函数依赖。 第三范式:消除主属性对码的部分和传递函数依赖。资源服务的拆分中,可以参考该原则:


  • 【操作范式】同一服务内如果有部分依赖或和传递依赖的话,考虑将这部分抽离为单独的服务。特别是多个资源服务都有相同的冗余,那这部分资源可以提出为独立的微服务,并继续下沉。该拆分可以看做是单一职则这一方法论的子集。



  • 【操作范式】在不同服务之间传递数据的过程中,只传递最小冗余集合,不传输所引用的完整资源,并将拼装提至最上层。这样做,可以减少数据冗余,与数据不一致的场景出现,并降低底层服务复杂度。如不同服务之间的输出界面,不传输整个资源,只传递所引用的 ID,如我们的业务体内相互引用和传递的是类型和 ID,将拼装提至最上层进行。当然,可以考虑一定程度的反范式以提高性能,但要权衡利弊。



资源组合提至上层


实践中,【操作范式】资源的组合也需要尽可能提至上层,如客户端或对客户端输出资源的服务层。这一操作范式,对资源内部的组合,和聚合层服务组合都适用。如同一资源服务的输出中,用户资料和大多数资源服务的输出,都存在部分依赖,将用户资料拆分为独立微服务,各服务只传递用户 UserID,只在最上层进行聚合,已经是比较成熟的实践。类似,音视频媒体服务,涉及音频标题、时长、不同码率 URL 等,和文章资源存在传递依赖,也应进行独立拆分,由客户端直接依赖 ID 来进行请求具体资源。


最后,对不同类型的资源的组合,也需要提至聚合层,同时可以采用 SDK,以提高开发效率。根据倒置原则,由底层服务方进行字段或者业务逻辑的适配。



总结


本文探讨了内容系统服务的三个架构原则,和它们对应的操作范式,对我们的思考和实践都有一定的指导意义。


首先,资源服务设计中,应当尽可能地利用资源服务特点,做到服务无状态。操作中,可以将状态移动至顶层和底层数据库,并进一步利用 NoSQL 提升性能。


其次,资源服务设计中,将用户态和资源服务解耦,确保更多的资源服务是无用户态。要尽可能地减少有用户态资源界面的输出,确保资源维度的服务输出尽可能保持纯粹。操作中,需要资源和用户态组合提至最上层,如将用户态移动至最上层客户端,还可以提供组合的状态挂载组件(SDK) 以提高开发效率。


最后,要减少资源服务或模块之间对资源相互引用的冗余。操作中,应当借鉴数据库的第二、三范式原则,同一服务内如果有部分依赖或和传递依赖的话,考虑将这部分抽离为单独的服务。在不同服务之间传递数据的过程中,只传递最小冗余集合,不传输所引用的完整资源,并将拼装提至最上层。将资源的组合也尽可能提至上层的实践中,可以采用 SDK,也可以采用聚合或转发服务,以提高开发效率。


关于作者:


奇正,曾在 Adobe 、百度任高级工程师,现任某互联网公司后端业务线 Leader,先后从事过 C++、Android、Golang 的开发工作。

2021 年 3 月 12 日 12:371732

评论

发布
暂无评论
发现更多内容

面试官:简单说一下RocketMQ整合SpringBoot吧

比伯

Java 编程 程序员 架构 计算机

可能会重塑未来移动支付市场的格局

CECBC区块链专委会

货币

《架构即未来:现代企业可扩展的Web架构流程和组织》.pdf

田维常

架构

100+大厂应届offer,从7个维度全面分析

程序员小灰

编程 面试 面经 腾讯大厂

2020最新最全的Java架构面试复习指南,掌握10%阿里P7没问题

Java架构之路

Java 程序员 架构 面试 编程语言

网络篇:朋友面试之TCP/IP,回去等通知吧

Crud的程序员

TCP 网络协议 IP

陪你手撕源码系列之 STL set 相关算法

herongwei

c++ 算法 set stl

《前端算法系列》如何让前端代码速度提高60倍

徐小夕

Java 算法 前端 前端进阶

Java开发者必读的〈Java开发手册(嵩山版)〉灵魂15问,深究Java规约背后的原理。

Java成神之路

Java 程序员 架构 面试 编程语言

Github上标星30K+的SpringBoot实战电商项目,简直不要太牛!

Java成神之路

Java 程序员 架构 面试 编程语言

2020的另一面:5G的斯普特尼克之年

脑极体

LeetCode题解:22. 括号生成,BFS,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

OSI七层模型与TCP/IP五层模型

Linux服务器开发

TCP/IP 网络协议栈 底层应用开发 Linux服务器开发 OSI

架构师训练营第 12 周学习总结

netspecial

极客大学架构师训练营

什么?还不知道该如何学习微服务?这份Github上星标55.9k的微服务神仙笔记真的太香了!

Java成神之路

Java 程序员 架构 面试 编程语言

架构训练营-week-12总结

于成龙

架构训练营

Java内存模型JMM详细解析

云流

程序员 并发编程 架构师 java面试

真的爱了!这份阿里P8整理的《Java核心技术整理》新版手抄本,简直把所有Java知识操作都写出来了

Java成神之路

Java 程序员 架构 面试 编程语言

spring2.5.6+java6升级到spring4+java8了

阿水

Java spring 升级

阿里聚划算5轮面试题:GC收集器、多线程锁、海量数据技术考核

Java架构之路

Java 程序员 架构 面试 编程语言

架构师训练营第 12 周作业

netspecial

极客大学架构师训练营

一只支持凡尔赛文学创作的摄影手机

脑极体

命令行搜索神器fzf

Rayjun

Linux

Gradle使用问题梳理

maijun

Gradle

2020年高频Java面试题集锦(含答案),让你的面试之路畅通无阻!

Java成神之路

Java 程序员 架构 面试 编程语言

架构师系列9: 找出单向链表合并节点

桃花原记

TCC Demo 代码实现

Java 分布式事务 Demo TCC

怎么保护自己的音乐作品不被盗用,用FL制作防盗水印片段

懒得勤快

版权保护 音乐 音乐制作 编曲

架构训练营-week12-作业1

于成龙

刚参加完阿里P6面试归来(Offer已斩获),6点面试经验总结

Java架构之路

Java 程序员 架构 面试 编程语言

架构师训练营第八周作业

丁乐洪

2021 ThoughtWorks 技术雷达峰会

2021 ThoughtWorks 技术雷达峰会

内容系统服务的三个架构原则和操作范式-InfoQ