码迷,mamicode.com
首页 > 其他好文 > 详细

哔哩哔哩「会员购」业务网关的研发赋能实践

时间:2020-11-13 12:36:00      阅读:7      评论:0      收藏:0      [点我收藏+]

标签:细分领域   设计理念   完全   项目   产生   变化   graph   copy   agent   

哔哩哔哩「会员购」业务网关的研发赋能实践

作者 沈佳伟 哔哩哔哩会员购架构师

写在前面

网关是个每隔一段时间就会被请出来「鞭尸」的概念,概念本身的起源已经无从考究。随着微服务和云原生的兴起,网关也伴随着不同的使用场景在各个领域进行细分和进化。

比较典型的细分领域有流量网关,比如耳熟能详的 Nginx/Tengine,通常承担着全域的 SLB(Server Load Balancing)能力,细分严格的公司还会将流量网关拆分为公网和内网。另一个典型领域是长连接网关,通常用于 IM 或 Push Message 等即时服务。

我们今天请来「鞭尸」的是业务网关。如何定义业务网关?相信业务网关如同哈姆雷特一般,在每个人心中都有不同的定义,大家耳熟能详的 Zuul, Dubbo 又或者是 Spring Cloud Gateway 是不是就属于业务网关呢?本文通过对哔哩哔哩「会员购」业务网关「大禹」设计和实现拆解,逐步阐述我们所理解的哈姆雷特是否和读者心中的哪个。

流量网关和业务网关

在系统架构日益复杂的今天,流量网关和业务网关就像好兄弟一般经常同时出现在我们的视野里。如果将网络请求比喻成一次银行服务,那么流量网关则扮演着大堂经理的角色,指引用户(Request)去需要的柜台(业务线)办理业务。业务网关则扮演着柜面服务员,通过柜面终端(业务服务组)满足用户的业务诉求。
技术图片

如同上述Case,大堂经理(流量网关)执行着按业务线分流请求的职责,当然同时也会承担一些不区分业务的常规职责,比如盘问用户(WAF,Web Application Firewall),比如安排大家排队等待(业务线限流),甚至关闭柜面(业务线熔断)又或者是张贴告示(静态路由转发)。

而承接特定业务的柜面服务员会根据客户的业务诉求(业务功能路由),打开某个文件柜(业务缓存)又或者在终端提交一张表格(请求业务服务),同时如果终端处理速度缓慢还可以提示后续用户稍作等待(业务功能限流)。

两人相辅相成,共同完成了用户(Request)的传递(路由)。

那不同业务线的业务网关可以是一个么?这个答案需要结合背后业务线的复杂程度来回答。业务网关扮演的角色除了稳定承接的业务线流量,更好服务用户(Request)外,还担负着业务线话事人的角色。什么样的用户应该享受什么样的服务(鉴权),是与用户一问一答的形式还是一次询问多个需求统一回答(串行/并行分发)。

由于不同柜面服务员(业务网关)对不同业务的熟悉程度不一样(特定业务支持能力),所以通常为了更好的服务特定业务用户都会为业务线单独安排柜面服务员(业务网关),但如果业务线同质化特点比较多,并且同一柜面服务员也可以高效的处理,那么的确是可以安排承接多个业务线的服务,又或者安排多个业务能力相同的柜面服务员分别为不同业务线单独服务(单元化部署)。
技术图片

业务网关的价值

展开这个话题前,先来看下哔哩哔哩「会员购」早些时的架构雏形以及日常研发中遇到的一些问题。(值得庆幸的是在业务从无到有伊始,「会员购」的前辈们就非常远见的选择使用SC进行服务拆分,并且服务的粒度也比较合理)见下图:
技术图片

其中 Search,C,Trade,Mng,UGC 等(数量还在不断增加)是按底层业务域粗粒度聚合后服务入口(也可以称之为聚合服务或聚合接口)。这些粗粒度的聚合服务均以独立的 SC 服务形式部署,并分别提供不同业务域的访问鉴权,业务限流,服务编排,数据视图转换等类似能力。与底层业务域通过 Feign Client + Http进行服务交互。

其中访问鉴权,业务限流等基础能力均通过统一的工具 Jar 包(Java Archive)和配置文件提供,而服务编排和数据视图转换则依赖下游服务提供的 Interface Jar 包硬编码实现。这里已经出现了业务网关的影子,而又不能称之为业务网关,因为这类聚合服务的确可以很好的完成流量转发的任务,但却无法发挥研发赋能的作用。

问题一:服务出口多。

由于上层的 Nginx/SLB 无法承担业务线的鉴权问题,所以通常这类功能通常下移至业务线入口进行(如图中 Search,C 等)。

由于聚合服务本身还承担着业务场景的聚合,那么随着业务场景数量的逐渐增加,聚合服务本身也会增加,加之聚合服务本身搭载的聚合接口数量之多,如果没有有效的服务出口管理机制仅通过几个配置文件(甚至是硬编码),那么随着时间推移,数据安全问题犹如墨菲定律早晚会出现。

而实现合理的业务网关则应该通过可视化的配置进行统一管理,在 Nginx/SLB 与业务服务间建立安全的桥梁,而无需在上线功能时增加额外的成本和风险。
技术图片

问题二:通用能力(如限流,熔断等)通常以 Jar 包形式安装在各个服务入口。

当初选择这种形式的好处是显而易见的:既统一代码库的实现,又仅需几个配置开箱即用。

但随着服务入口数量激增,维护代码库的成本是变低了,但部署/更新通用能力的成本却增加了。有的服务入口业务稳定,对更新没有动力。有的服务入口历史悠久,可能忘了更新。久而久之,各服务入口的通用能力版本五花八门,加上与之对应的配置文件可能依赖不同版本,对突发问题的及时修复和问题止损埋下了巨大隐患。

而实现合理的业务网关应该将通用能力内置并通过可视化配置应用于不同业务服务接口之上,既通过所见即所得的方式简化了配置难度降低了配置错误,又通过业务流量入口的角色统一控制了通用能力的更新迭代,减少业务系统即兼顾业务场景又兼顾通用能力升级的窘境。

问题三:多版本接口支持造成服务入口的腐烂,这个问题在与客户端(Native)交互场景中尤为突出。

由于客户端的特殊性,当业务服务新版本发布时并无法要求客户端整体升级,而需要一个相对缓慢的升级时间来逐步收敛版本。这就要求业务服务(大多是服务入口)进行多版本的数据格式兼容,以应对版本更迭时的中间状态。

同时站在服务提供方(Service Provider)的角度,由于服务接口的普适性,通常服务于多终端(Native,H5,小程序等),这类基于特定终端特定版本的特定处理由于没有有效的服务编排能力,只能依靠无穷无尽的 IF-ELSE 硬编码来兼容。同时由于这种普适性的接口设计无法很好的收集历史版本的流量,从而导致没有足够的数据来支持是否要删除那些丑陋的 IF-ELSE 硬编码,熵在增加,服务在腐烂。

而实现合理的业务网关应该通过透明代理,条件判断,服务编排,服务聚合,数据裁减等一系列能力,在终端与业务服务间建立一座基于配置的桥梁,向上提供兼容数据,向下屏蔽兼容细节。
技术图片

问题四:哔哩哔哩「会员购」现有的聚合服务(如 Search,C 等)与底层业务域使用Fiegn Client进行交互,通过加载业务域提供的服务接口存根(Interface Stub,通常打包成 Jar 的形式)进行硬编码的服务编排。

在业务初期团队规模较小的阶段,这种分层方式并没有什么不妥。但随着团队逐步壮大,业务日渐复杂,这种硬编码的方式就暴露出了低效、易错且不灵活的丑陋面目。

不但导致了几乎每新增或修改一个业务服务都需要同时调整聚合服务的硬编码并重新编译和部署,而且严重分散了工程师们对业务域的专注度,把宝贵的精力耗费在了不产生显著价值的请求路由上(这点在联调阶段尤为突出,定位、编译、重启和多版本协同工作时的部署覆盖经常把大家搞得焦头烂额,而解决这些问题不但耗时并且没有任何收益),同时这些聚合服务的请求路由在大多数场景下只是承担了透明代理的责任(甚至连服务编排都不需要)。

而实现合理的业务网关应该做到与下游框架,系统架构,代码语言无关,且不依赖任何代码存根,仅通过应用层协议(比如HTTP)即可与底层业务服务建立请求路由,同时提供即时生效的可视化配置,实时、动态的加载、暂停甚至关闭底层业务服务的请求路由而不需要对其做任何改动。

问题N:业务边界模糊,职责划分困难。

架构师们日常工作中一个很重要的职能就是为各业务团队理清业务边界,扫清职责不清晰的地方。但随着业务快速发展(比如哔哩哔哩「会员购」),新的想法不断崩出,很多时候新的业务场景会介于两个业务域之间的模糊地带,同时一旦涉及到业务流量入口(由哪个服务入口承接)这个话题时,通常都是公说公有理婆说婆有理。

如果仅仅是通过拍脑袋决定法强行划分,那么新的业务场景在通过不断迭代更新,逐步清晰后发现与拍脑袋结果不一致(业务服务所有权随着时间而变化),那么后续对已经支离破碎并且包含诸多硬编码的业务域再次收口将是难上加难。

而实现合理的业务网关应该通过合理的请求路由和服务编排将争议暂时上移(至业务网关),支撑研发快速验证,待后续业务边界逐步明朗后低成本甚至零成本重组业务域。业务规模扩大和系统复杂度的增加会造成研发和维护成本急剧上升,这是软件工程学早就得出的结论,而业务网关则可以通过更好的协调上下游合作方,有效提高协作体验,为合作赋能。
技术图片

如果展开业务「业务网关的价值」这个话题,可能整篇写满都不够。就如同系统架构中分层结构一般,在合理的位置(流量网关与业务系统间)增加一个中间层(业务网关)通常可以增加系统架构的灵活性。而业务网关本身也可以利用分层结构(见下文)增加配置的灵活度,从而更好地为研发赋能。

「大禹」业务网关

「大禹」是哔哩哔哩「会员购」面对持续变化的业务需求和日益复杂的系统架构时,用于降低系统之熵的业务网关方案。

我们的设计理念并不是构建一个大而全的万能解决方案,而是脱胎于业务场景,为业务服务提供性能稳定,场景适合且接入成本最小的技术实现。从业务中来,到业务中去,为研发赋能是「大禹」存在的价值。

「为什么要造轮子?」

因为如果需要兼容现有的内部中间件体系,并且具备契合当前业务需要的技术实现,对于对于任何一款开源网关产品都是一项非常大的改造,比较合适的是使用 SC 做底层框架进行再扩展(就如同本文之前描述的那些聚合服务)。但 SC 受限于 Servlet 体系,在扩展上是有较大局限的,所以综合考虑后无论从基础性能角度还是功能扩展难易度上均不是特别合适的选择。

我们的一些设计思路

「大禹」是采用 Netty4 作为服务端异步通信框架,Async Http Client 作为业务系统异步请求处理框架,Ongl3 作为内部动态表达式处理框架的「简单业务网关系统」。

为什么选择 Async Http Client 而不是直接使用 Netty Client?

因为由于现有架构特性,业务系统基本均采用 HTTP 协议,而 Async Http Client 非常棒的封装了所有底层细节,并且通过异步回调机制提供极其可观的性能,同时基本满足所有业务需求(如自定义 Header 等),那么使用 Netty 再造一个轮子也就不值得了。

全异步与半异步

很遗憾,「大禹」并不是在所有场景下均采用 100% 全异步无堵塞的处理模式。首先来看下什么是全异步无堵塞,什么场景会全异步无堵塞,全异步无堵塞的优势又是什么?
技术图片

以上是典型的「大禹」全异步无堵塞场景。首先请求经过网络进入服务,并通过Netty的事件处理机制进入 IO 线程。IO 线程经过一系列报文聚合(比如从多个报文包聚合 HTTP 请求)后推送至业务线程队列(从此处也可以看出服务处理能力和 IO 线程的密切关系以及 IO 线程的宝贵)。

而业务线程由业务线程池和等待队列组成,等待队列通常设置为一个容量相当大的 Queue,用于暂存当前无法被处理且还未网络超时的请求(通常网络超时会采用比 Nginx/SLB 略小的时间)。业务线程池在空闲或处理完上一次请求后会从等待队列获取下一个待处理请求,并在为请求装配必要组件后(比如 Response Header 容器,Ognl 上下文等)推送至 Pipeline 开始处理。

Pipeline 是一组遵循严格顺序的可扩展处理器,每个 Pipeline 均可决定继续执行还是立即终止并返回报文(或异常终止)。「大禹」的绝大部分研发赋能均是有Pipeline进行扩展的。

Pipeline 中有个比较特殊的处理器称之为 AggregatePipeline,承担着并行请求路由,响应数据聚合,最终报文重写等一系列重要工作。AggregatePipeline 会根据接口的配置参数拆分若干个并行请求,并通过Async Http Client 异步机制对多个并行请求的响应数据进行后处理(响应数据聚合等)。

如果 AggregatePipeline 仅需要处理一个请求(比如透明代理),那么将请求委托给 Async Http Client 后将终止后续 Pipeline,将业务线程交还给线程池用以处理后续请求。而当 Async Http Client 触发异步回调时(无论是成功,失败或者超时)均会利用 Async Http Client 异步回调线程向当前请求持有的 Netty 上下文(Netty Context)回写响应,并最终由 Netty 事件处理机制再次通过 IO 线程写入网络。

「大禹」内部称此类配置为「Direct」,通常用于无需后处理即可直接进行转发业务服务接口,甚至是静态资源文件。全异步无堵塞处理方式的优势非常明显,充分利用了 NIO 模型的特性仅在需要时唤起线程。极大的提高了业务线程的利用率。

与之对应的是半异步处理方式。「大禹」不仅支持单个服务的透明代理,同时也支持并行聚合服务甚至是具有依赖关系的并行聚合服务。首先解释下什么是聚合服务和具有依赖关系的聚合服务。
技术图片

AggregatePipeline 通过配置将原始的请求一拆为三,并通过 Async Http Client 将请求并行转发给三个下游业务服务,随后分别等待返回(或超时、异常)。

与此同时 AggregatePipeline 会为当前请求创建响应报文暂存区,并将异步回调后的多个响应报文分别放入暂存区不同位置(暂存区实际上是一个哈希表,拆分后不同响应报文存放在哈希表的位置也与配置相关),当三个异步回调均完成后(包括超时、异常)将开启后续对响应报文暂存区的深加工,如数据聚合(使用 Ognl 表达式进行调整)或是重新编排。

具有依赖关系的聚合并行服务也是类似的处理方式,只是在特定响应报文回调时再通过 Async Http Client 发起依赖请求并再次等待回调(通常具有依赖关系的聚合服务是需要使用被依赖服务某些响应报文作为自己参数的,在业务场景上的案例比如先使用 SPU_ID 访问商品业务域,然后使用响应报文的 SKU_ID 数据再次请求营销业务域后组合成一个聚合服务报文。
技术图片

同样类似的,等待被依赖服务返回一个数据集合,然后将响应报文中的数据集合拆分成多个并行请求后的数据聚合场景(业务场景上比如访问商品业务域返回多个 SKU_ID 后,再以 SKU_ID 作为参数拆分多个并行访问交易业务域的请求,最终合并响应报文,「大禹」称为对数据集合的 Fork-Join 操作)也是使用类似的方式进行并行请求控制的。

因为如何聚合服务,是否使用具有依赖关系的聚合服务,具有依赖关系时允许嵌套层级是多少。这些不确定因素完全取决于如何配置业务服务接口,从「大禹」的配置限制上来说是完全不受限制的(只要总体的响应时间没有超时,就允许不断的Fork请求),所以对于每个并行请求的控制至关重要。
技术图片

「大禹」为每个聚合服务创建了一个 Queue 用于控制与之对应的并行请求。

以上图为例:在首轮并行请求发起后 Queue 中将存在表示「/a」和「/b」两个响应报文的 Future(Future 为 Java 异步接口,调用其 Get 方法将获取 Future 的内容,如果内容尚不可用则进入堵塞),此时业务线程会不断尝试从 Queue 中获取 Future 并调用其 Get 方法获取响应报文(并立即放入响应报文暂存区而不做其他处理),获取响应报文的同时检查服务接口配置中是否存在具有依赖关系的聚合服务,如果存在则再次包装为 Future 形式加入 Queue 后发起并行请求。

以上几个步骤周而复始,直到总体响应时间超时或业务线程从 Queue 无法再获取到 Future。

可配置化服务编排的聚合服务是业务研发的利器,但是代价也非常明显:虽然是并行请求,但最大耗时取决于 RT 最长的下游业务服务,同时在等待聚合的过程中业务线程始终需要等待并处理聚合(虽然配合 Async Http Client 指定请求超时可以做到极短耗时),这也就是「大禹」在某些业务场景需要半异步的原因。

配置格式与同步策略

上文一直说到可视化的配置。在「大禹」中配置由 JSON 格式来存储。

JSON格式表示的服务接口配置内容会映射成「大禹」的内存对象(称之为Schema)。Schema 完整描述了对应服务接口生命周期,访问权限,聚合方式,响应重写等方方面面。

为什么没有选择 GraphQL?因为除了对请求路由的描述外,Schema 还承担着一些关于生命周期及附加能力的描述,这些配置更适合使用 JSON 而不是 GraphQL 来描述。而如果 JSON 中加载着 GraphQL 会让配置的方式不统一,造成难度增加。同时 JSON 的灵活性也为业务网关本身的迭代提供了很好的支撑。


{
  "parallel": [{
    "host": "open.mall",
    "path": "/helloword",
    "method": "GET"
  }],
  "path": "/helloworld",
  "method": "POST"
}

以上是典型的配置:将业务网关的「/helloworld」POST 请求路由至下游业务域 open.mall 的「/helloworld」GET。其中下游业务域的路由地址通过 Name Server(「大禹」使用的是哔哩哔哩开源的 Discovery)异步的进行本地缓存,关于异步更新和本地缓存 Name Server 节点的步骤作为相对成熟的方案此处就不再多赘述。来看一个稍微复杂但贴合业务场景的配置:


{
  "parallel": [{
    "host": "open.mall",
    "path": "/helloword1",
    "method": "GET"
  }, {
    "host": "open.trade",
    "path": "/helloword2",
    "method": "POST",
    "required": [{
      "host": "open.marketing",
      "path": "/helloword3",
      "method": "GET"
    }]
  }],
  "path": "/helloworld",
  "method": "POST"
}

以上配置表示首先会并行发起两个请求,待业务域 open.trade 完成后再并行发起业务域 open.marketing 的请求,最后将三个响应报文聚合(此处仅为展示使用JSON配置的灵活性,并没有描述完整的聚合配置)。 既配既生效,既关既下线,一直是研发赋能中一个非常重要的话题。「大禹」会将所有业务服务接口配置(Schema)加载至内存中,并采用推拉结合的方式感知配置变更,做到最大限度上实时配置。
技术图片

「大禹」会在打开服务端口(接流)前一次性从数据库加载当前所有可用的服务接口Schema至内存中。如果随后接口管理者通过控制台(Admin)新增/更新配置至DB,则通过 Alibaba Canal 监听 Binlog 的方式推送实时变更至任意一台业务网关,而后由该台业务网关再通过内部接口的形式转发扩散至其他节点。

同时所有节点均内置低频 Job 通过时间戳作为参数,实时访问数据库最新变更。由于推拉过程由不同线程触发,难免导致同一时刻同时触发造成的更新不一致,同步 Schema 时需要通过比较最后更新时间是否大于当前版本(相当于版本号)。

Ognl,动态配置与报文重写

业务网关的配置灵活性,动态化是研发赋能的重要途径之一。

如上文所述,「大禹」的业务服务接口配置会以 Schema 的形式存在于内存中,而 Schema 是针对业务服务接口级别的静态描述,并无法为特定类型的业务请求做异化处理(如请求 Header 中 Agent!=H5 时需要额外访问特定业务服务或裁减响应报文)。

「大禹」使用 Ognl 表达式作为 Schema 与动态化的桥梁,允许在 Schema 中的特定部分(其实也是大多数配置)诸如 Header 处理,请求参数判断,动态请求路由,响应报文重写等配置项编写并预编译 Ognl 表达式。同时会为每个业务请求封装 Ognl 上下文并传递给 Scheam 进行表达式回调从而达到动态配置的效果(由于 Ognl 上下文中包含了原始请求数据,响应报文暂存区,用于存储临时计算结果的缓存区,几乎可以动态化所有配置和重写任意格式的响应报文)。
技术图片

正因为动态化的支持,为研发赋能提供了更大的操作空间。比如「大禹」请求路由的动态化配置如下:


{
  "parallel": [{
    "check": "$请求报文.a == 1",
    "host": "open.mall1",
    "path": "/helloword1",
    "method": "GET"
  }, {
    "check": "$请求报文.a != 1",
    "host": "open.mall2",
    "path": "/helloword1",
    "method": "GET"
  }],
  "path": "/helloworld",
  "method": "GET"
}

其中参数 Check 是基于 Ognl 预编译的表达式,在解析请求路由时会将包含原始请求的 Ognl 上下文传递给 Schema 进行参数 Check 判断。如果满足表达式(比如返回 True)则执行该请求路由。

{
  ...
  "project": [{
    "express": "$响应报文.a != null ? $响应报文.a.b = ‘b‘ : null"
  }, {
    "express": "$响应报文.c = #{‘c‘ : new java.util.Date()}"
  }, {
    "express": "$请求报文.agent == ‘Android‘ ? ‘‘ : ‘‘",
    "path": "$响应报文.from"
  }]
  ...
}

又或者如上配置:是对响应报文重写。同样是通过在 Schema 中预编译的 Ognl 表达式,通过对包含响应报文的 Ognl 上下文进行响应报文动态调整,诸如 Case1:为已有哈希表对象增加属性,Case2:新增属性,Case3:通过请求报文类型新增属性,并写入响应报文指定位置。

这种动态化能力极大改善了研发面对多端多视图时的窘境,并诸如 A/B Test,业务服务接口重构提供了原生支持。

业务服务接口缓存(大颗粒缓存)

缓存作为架构设计中的重要环节,一直是研发过程中既恨又爱的保留项目。同样是使用缓存,优良的设计和不佳的设计间相差了十万八千里。业务网关作为研发赋能的重要途径之一,自然应该提供相应的解决方案和灵活的配置方式以缓解研发过程中的不安定因素。
技术图片

「大禹」在 Pipeline 中为缓存处理内置两个处理器。CacheGetPipeline 用于在实际请求路由前查询缓存,而 CacheSetPiepline 则用于更新响应报文至缓存。同之前的设计方式类似:Schema 会回调被 Ognl 上下文包装后的请求报文和响应报文,用以动态的计算缓存 KEY,比如:


{
  ...
  "cache": {
    "key": "$请求报文.itemsId",
    "expired": 5000,
    "segment": 5,
    "compress": {
      "tool": "snappy"
    }
  }
  ...
}

以上配置利用 Ognl 预编译表达式,从请求报文中获取 itemsId 属性作为缓存KEY,进行缓存查询。

同时指定该业务服务接口的缓存KEY自动切分为 5 片(比如A_1,A_2,A_x)以防止热 KEY。当 CacheGetPipeline 动态计算 KEY 并成功从 Redis 获取到数据后(同时需要根据配置决定是否需要并解压)将直接返回业务服务接口级别的大颗粒缓存,如果数据获取失败则继续进行后续 Pipeline 处理器,直到 CacheSetPipeline 处理器将响应报文以多切片的形式更新至 Redis(同样根据配置指定有效期和压缩方式,并发更新锁和 Redis Pipeline 更新细节不在赘述)。

通过即时生效的多样化配置项和动态化能力,业务网关可以利用自身完备的通用缓存逻辑为研发赋能,让业务开发专注于业务逻辑,而不是被这类公式化的缓存策略搞的业务域支离破碎(当然这并不是说业务系统就不再需要关注缓存,而是在某些场景下业务网关可以承担缓存的责任从而减少业务系统研发过程中的压力)。

同样的,业务网关由于其不同与常规业务系统的架构设计,在处理一些特定业务问题上会来的更得心应手:比如业务缓存毛刺。

业务缓存毛刺顾名思义是指在缓存失效瞬间有大量请求穿透缓存而直接访问到了下游业务服务从而造成流量监控上的明显毛刺。这类缓存毛刺危害较大,在没有限流的情况下可能瞬时击垮下游业务服务。业务系统一般会采用限流来抵御这类突发的流量激增,但代价也是显而易见的:大量的流量被拒绝随之而来的是极差的用户体验。

而业务网关可以为业务服务提供通用的配置化解决方案从而减少甚至避免这类问题。以「大禹」为例:在业务网关层内置了缓存更新的 Circle 机制来缓解业务研发对缓存毛刺的恐惧。
技术图片

CacheGetPipeline 处理器读取缓存时会利用 Redis Pipeline 同时读取缓存 TTL(Time To Live),并将 TTL 加入至 Ognl 上下文后交由 Schema 进行 Circle 配置的触发判断。

Circle 配置通过回调 Ongl 上下文,检查当前 TTL 是否处于 Circle 时效范围内(Head-Tail 是相对于缓存超时时间的上下限,比如缓存失效时间 = 5s,Head = 3s,Tail = 1s,那么如果 TTL = 2s 则会被判定处于时效内,实际上「大禹」是使用百分比计算 Head-Tail,此处仅为解释通俗),如果处于 Circle 时效范围内则将当前请求传递至 Circle 异步线程池,同时后续 Pipeline 处理器按常规流程继续执行(通常为直接返回缓存)。

而在 Circle 异步线程中,首先会检查当前是否存在同样 URL 的 Circle 请求正在执行或等待被执行(保证同一时刻同一业务网关节点仅一个 Circle 请求会被传递至下游业务服务,保证下游业务服务的稳定),如果检查通过,则当前请求会被完整的复制一份(Netty Buffer Copy)并标记为 Circle 请求后重新投递至 Pipeline 处理器执行。

再次执行时 CacheGetPipeline 处理器识别请求的 Circle 标记后将主动跳过(此时缓存可能尚未失效),从而将 Circle 请求发送至下游业务服务,在获取响应报文后通过 CacheSetPipeline 处理器更新至 Redis(利用 Circle 配置在业务服务接口流量持续高峰时提前更新缓存,防止缓存突然失效,从而降低毛刺出现的概率)。

以上机制同样仅需使用配置化就可以应用到指定的业务服务接口,并允许随时开启或关闭,真正做到了为业务场景而生。


{
  ...
    "circle": {
      // 默认Head-Tail为60%-30%
      "circle": true
    }
  ...
  }

可以做的,还有很多

如果把话题继续发散下去,可能一时半会都收不了尾。以上仅仅例举了哔哩哔哩「会员购」在业务网关实践中很小的一部分场景,其他诸如「更多的业务服务聚合场景」,「业务服务接口的限流与整流」等均是通过不同功能的 Pipeline 处理器或是 Ognl 表达式实现的,原理就不再多赘述了,读者们应该自己也能想到,只要场景需要,网关就能扩展。

但是业务网关始终是一种脱胎于业务场景的技术工具,本质上需要为之对应的业务需求服务,他可以不具备五花八门的「杂技」,但必须身持为业务保驾护航的三板斧。有句话说的好:「从业务中来,到业务中去,谨记」。

「大禹」业务网关一些不完全压测指标(使用哔哩哔哩内部压测平台)

部署 8C8G * 4 台,下游业务服务接口(聚合后)平均耗时 60ms,接口响应报文 6.4K,业务网关 CPU 平均水位 70%,压测时长 15m,QPS 稳定 8w 左右,平均错误率低于千分之一,无超时。

求贤:

「会员购」是哔哩哔哩的二次元电商平台。我们是B站多元化商业收入的重要航道之一,同时也是B站增长最快的潜力业务。我们寻找志同道合的伙伴一起寻找属于我们的One Piece。可以通过以下链接或点击阅读原文联系。

本文首发于B站专栏:
https://www.bilibili.com/read/cv6168589

参考阅读:

  • 一次K8S容器内存占用居高不下的排查案例
  • 类型化消息的一种设计模式
  • Go 新版泛型使用:80余行代码构建一个哈希表
  • 哔哩哔哩「会员购」在流量回放上的探索
  • 为什么我放弃使用 Kotlin 中的协程?
  • 基准测试表明, Async Python 远不如同步方式

技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。

高可用架构

改变互联网的构建方式

技术图片

哔哩哔哩「会员购」业务网关的研发赋能实践

标签:细分领域   设计理念   完全   项目   产生   变化   graph   copy   agent   

原文地址:https://blog.51cto.com/14977574/2546107

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!