作者介绍

田文琦,2021年9月加入去哪儿网机票目的地事业群,担任软件研制工程师,现担任国内酒店东站技能团队。首要关注高并发、高功能、高可用相关技能和体系架构。主导的酒店事务网关优化项目,荣获22年去哪儿网技能中心TC项目三等奖。

一、背景

近来,Qunar 酒店的全体技能架构在依据 DDD 指导思想下,一直在进行调整。其中最首要的一个调整便是包括核心范畴的团队交出各自的“应用层”,统一交给下流网关团队,组成统一的应用层。这种由多个网关兼并成大前台(酒店事务网关)的融合,带来的优点是核心体系鸿沟明晰了,可是对酒店事务网关来说,也带来了不小的困扰,体系面临的压力首要来自两方面:首要,一次性新增了几十万行很多硬编码、暂时兼容、聚合事务规矩的杂乱代码且代码风格迥异,有些乃至是跨言语的代码搬迁。其次,后续的杂乱多变的应用层事务需求,之前涣散在各个子网关中,现在在源源不断地汇总叠加到酒店事务网关。这就导致了一系列的问题:

  • 事务网关吞吐功能变差

应对流量尖峰时期的单机最大吞吐量与兼并之前比较,下降了20%

  • 内部事务逻辑处理速度变差

主流程事务逻辑的处理时刻与兼并之前比较,上涨了10%

  • 代码难以维护、开发功率低

主站内部各个模块之间严峻耦合,鸿沟不清,修正扩散问题非常显着,给后续的迭代添加了维护本钱,开发新需求的功率也不高。

酒店事务网关作为直接面临用户的体系,呈现任何问题都会被扩大百倍,上述这些问题亟待处理。

二、现状剖析

1、吞吐量下降剖析

现有体系虽然事务处理部分是异步化的,可是并不是全链路异步化,如图所示:

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

同步 servlet 容器,servlet 线程与事务逻辑线程是同一个,顶峰期流量上涨或许尤其是遇到流量尖峰的时分,servlet 容器线程被堵塞的时分,咱们服务的吞吐量就会显着下降。

事务处理虽然运用了线程池的确能完结异步调用的效果,也能紧缩同步等待的时刻,可是也有一些缺点:

  • CPU 资源很多糟蹋在堵塞等待上,导致 CPU 资源运用率低。
  • 为了添加并发度,会引入更多额定的线程池,跟着 CPU 调度线程数的添加,会导致更严峻的资源争用,上下文切换占用 CPU 资源。
  • 线程池中的线程都是堵塞的,硬件资源无法充分运用,体系吞吐量简单到达瓶颈。

2、呼应时刻上涨剖析

前期为了快速落地酒店 DDD 架构,兼并大前台的重构中,并没有做到一步到位的规划。为了确保项目质量,将整个进程切分为了搬迁+重构两个步骤。搬迁之后,整个酒店事务网关的内部代码结构是割裂、混乱的。总结如下:

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

咱们最核心的一个接口会调用70多个上游接口,上述问题:鸿沟不清、不内聚、各种重复调用、依靠堵塞等问题导致了核心接口的呼应时刻有显着上涨。

三、处理计划

1、全流程异步化进步吞吐量

全流程异步化计划,咱们首要选用的是 Spring WebFlux。

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

1.1 选择的理由

呼应式编程模型:Spring WebFlux 依据呼应式编程模型,运用异步非堵塞式 I/O,能够更高效地处理并发恳求,进步应用程序的吞吐量和呼应速度。一起,呼应式编程模型能够更好地处理高负载情况下的恳求,下降体系的资源消耗。

高功能:Spring WebFlux 运用 Reactor 库完结呼应式编程模型,能够处理很多的并发恳求,具有超卓的功能体现。与传统的 Spring MVC 结构比较,Spring WebFlux 能够更好地运用多核 CPU 和内存资源,以完结更高的功能和吞吐量。

可扩展性:Spring WebFlux 不只能够运用 Tomcat、Jetty 等惯例 Web 服务器,还能够运用 Netty 或 Undertow 等依据 NIO 的 Web 服务器完结,与其它非堵塞式 I/O 的结构结合运用,能够更简单地构建可扩展的应用程序。

支撑函数式编程:Spring WebFlux 支撑函数式编程,运用函数式编程能够更好地处理杂乱的事务逻辑,并进步代码的可读性和可维护性。

与 Spring 生态体系无缝集成:Spring WebFlux 能够与 Spring Boot、Spring Security、Spring Data 等 Spring 生态体系的组件无缝集成,提供了完整的 Web 应用程序开发体会。

1.2 完结原理和异步化进程

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

上图中从下到上每个组件的作用:

Web Server:适配各种 Web 服务, 监听客户端恳求,并将其转发到 HttpHandler 处理。

HttpHandler:以非堵塞的办法处理呼应式 http 恳求的最底层处理器,不同的处理器处理的恳求都会归一到 httpHandler 来处理,并回来呼应。

DispatcherHandler:调度程序处理程序用于异步处理 HTTP 恳求和呼应,封装了HandlerMapping、HandlerAdapter、HandlerResultHandler 的调用,实践完结了HttpHandler的处理逻辑。

HandlerMapping:依据路由处理函数 (RouterFunction) 将 http 恳求路由到相应的handler。WebFlux 中能够有多个 handler,每个 handler 都有自己的路由。

HandlerAdapter:运用给定的 handler 处理 http 恳求,必要时还包括运用反常处理handler 处理反常。

HandlerResultHandler:处理回来成果,将 response 写到输出流中。

Reactive Streams:Reactive Streams 是一个标准,用于处理异步数据流。Spring WebFlux 完结了 Reactor 库,该库依据呼应式流标准,处理异步数据流。

在整个进程中 Spring WebFlux 完结了呼应式编程模型,构建了高吞吐量、高并发的 Web 应用程序,一起也具有呼应快速、可扩展性好、资源运用率高级优点。

下面咱们来看下 webFlux 是如何将 Servlet 恳求异步化的:

ServletHttpHandlerAdapter 展现了运用 Servlet 异步支撑和 Servlet 3.1非堵塞I/O,将 HttpHandler 适配为 HttpServlet。

第10行,request.startAsync()敞开异步形式,然后将原始 request 和 response 封装成 ServletServerHttpRequest 和 ServletServerHttpResponse。

第36行,httpHandler.handle(httpRequest, httpResponse) 回来一个 Mono 对象(即Publisher),对 Request 和 Response 的一切详细处理都在 Mono 对象中定义。

一切的操作只有在 subscribe 订阅的那一刻才开始进行,HandlerResultSubscriber 是 Reactive Streams 标准中标准的 subscriber,在它的 onComplete 事情触发时,会结束 servlet 的异步形式。

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

对 Servlet 回来成果的异步写入,以 DispatcherHandler 为例阐明:

第2行,exchange 是对 ServletServerHttpRequest 和 ServletServerHttpResponse 的封装。

第10-15行,在体系预加载的 handlerMappings 中依据 exchange 找到对应的 handler,然后运用 handler 处理 exchange 履行相关事务逻辑,终究成果由 result 将 ServletServerHttpResponse 写入到输出流中。

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

最终,除了 Servlet 的异步化,作为事务网关,要完结全链路异步化还需要在长途调用方面要支撑异步化。在 RPC 调用办法下,咱们选用的异步 Dubbo,在 HTTP 调用办法下,咱们选用的是 WebClient。

WebClient 默许运用的是 Netty 的 IO 线程进行发送恳求,调用线程经过订阅一些事情例如:doOnRequest、doOnResponse 等进行回调处理。异步化的客户端,防止了事务线程池的堵塞,进步了体系的吞吐量。

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

在运用 WebClient 这种异步 http 客户端的时分,咱们也遇到了一些问题:首要,为了防止默许的 NettyIO 线程池可能会履行比较耗时的 IO 操作导致 Channel 堵塞,建议替换成其他线程池,替换办法是 Mono.publishOn(reactor.core.scheduler.Schedulers.newParallel(“biz_scheduler”, 300))。

其次,由于线程发生了切换,无法兼容 Qtracer (Qunar内部的分布式全链路盯梢体系),所以在初始化 WebClient 客户端的时分,需要在 filter 里插入对 Request 的修正,记录前一个线程保存的 Qtracer 的上下文。WebClient.Builder wcb = WebClient.builder().filter(new QTraceRequestFilter());

2、服务编列下降呼应时刻

Spring WebFlux 并不是银弹,它并不能确保一定能下降接口呼应时刻,除了全流程异步化,咱们还运用 Spring WebFlux 提供的呼应式编程模型,对事务流程进行服务编列,下降依靠之间的堵塞。

2.1 服务编列处理计划

在介绍服务编列之前,咱们先来了解一下 Spring WebFlux 提供的呼应式编程模型 Reactor。它有最重要的两个呼应式类 Flux 和 Mono,一个 Flux 对象标明一个包括0..N 个元素的呼应式序列,而一个 Mono 对象标明一个包括零或许一个(0..1)元素的成果。

不管是 Flux 还是 Mono,它的处理进程分三步:首要声明整个履行进程(operator);然后连通主进程,触发履行;最终履行主进程,触发并履行子进程、生成成果。

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

每个履行进程连通输入流和输出流,子进程之间能够是并行的,也能够是串行的这个取决于实践的事务逻辑。咱们的服务编列便是完结输入和输出流的编列,即在第一步声明履行进程(包括子进程),第二步和第三步完全交给 Reactor。下面是咱们服务编列的总体规划:

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

service:是最小的事务编列单元,对 invoker 和 handler 进行了封装,并将成果写回到上下文中。主流程中,一般是由多个 service 进行并行/串行地编列。

Invoker:是对第三方的异步非堵塞调用,对回来成果作 format,不包括事务逻辑。相当于子进程,一个 service 内部依据实践事务场景能够编列0个或多个 Invoker。

handler:纯内存计算,封装共用和内聚的事务逻辑。在实践的事务开发进程中,对上下文中的任一变量,只有一个 handler 有写权限,防止了修正扩散问题。也相当于子进程,依据实践需要编列进 service 中。

上下文:为每个接口都规划了独立的恳求/处理/呼应上下文,方便监控定位每个模块的处理正确性。

上下文规划举例:

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

在杂乱的 service 中咱们会依据实践事务需求拼装 invoker 和 handler,例如:日历房售卖信息展现 service 拼装了酒店报价、辅营权益等第三方调用 invoker,优惠明细计算、过滤报价规矩等共用的逻辑处理 handler。

在实践优化进程中咱们抽象了100多个 service,180多个 invoker,120多个 handler。他们都是小而独立的类,一般都不会超越200行,减轻了开发同学尤其是新同学对代码的认知负担。鸿沟明晰,逻辑内聚,代码的不可知问题也得到了处理。

每个 service 都是由一个或多个 Invoker、handler 拼装编列的事务单元,内部处理都是全异步并行处理的。如下图所示,ListPreAsyncReqService 中编列了多个 invoker,在基类 MonoGroupInvokeService 中,会经过 Mono.zip(list, s -> this.getClass() + ” succ”)将多个流兼并成为一个流输出。

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

在 controller 层就担任处理一件事,即对 service 进行编列。如下图所示,

咱们运用 flatMap 办法能够方便地将多个 service 按照事务逻辑要求,进行多次地并行/串行编列。

并行编列示例:第12、14行是两个并行处理的输入流 afterAdapterValidMono、preRankSecMono ,二者并行履行各自 service 的处理。

并行处理后的流兼并:第16行,搜索成果流 rankMono 和不依靠搜索的其他成果流preRankAsyncMono,运用 Mono.zip 操作将两者兼并为一个输出流 afterRankMergeMono。

串行编列举例:第16、20、22行,afterRankMergeMono 成果流作为输入流履行 service14 后转换成 resultAdaptMono,又串行履行 service15 后,输出流 cacheResolveMono。

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

以上是酒店事务网关的全体服务编列规划。

2.2 编列示例

下面来介绍一下,咱们是如何进行流程编列,发挥网关优势,在体系内和体系间到达呼应时刻大局最优的。

  • 体系内:

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

上图示例中的左边计划总耗时是300ms。这300ms 来自最长途径 Service1的200ms 加上 Service3 的100ms:

Service1 包括2个并行 invoker 别离耗时100ms、200ms,最长途径200ms。

Service3 包括2个并行invoker 别离耗时50ms、100ms,最长途径100ms。

而右图是将 Service1 的200ms 的 invoker 搬迁至与 Service1 并行的 Service0 里。此刻,整个处理的最长途径就变成了200ms:

Service0 的最长途径是200ms

Service1+service3 的最长途径是100ms+100ms=200ms

经过体系内 invoker 的最优编列,全体接口的呼应时刻就会从300ms 下降到200ms

  • 体系间:

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

举例来说,优化前事务网关会并行调用 UGC 点评(接口耗时100ms)和 HCS 住客秀(接口耗时50ms)两个接口,在 UGC 点评体系内部还会串行重复调用 HCS 住客秀接口(接口耗时50ms)。

发挥事务网关优势,UGC 无需再串行调用 HCS 接口,所需事务聚合处理(这里的事务聚合处理是纯内存操作,耗时能够忽略)移至事务网关中操作,这样 UGC 接口的耗时就会降下来。对大局来说,全体接口的耗时就会从本来的100ms 降为50ms。

还有一种情况,假设事务网关是串行调用 UGC 点评接口和 HCS 住客秀接口的话,那么也能够在事务网关调用 HCS 住客秀接口后,将成果经过入参在调用 UGC 点评接口的时分传递曩昔,也能够省去 UGC 点评调用 HCS 住客秀接口的耗时。

依据对整个酒店东流程事务调用链路充分且明晰的了解基础之上,咱们才能找到体系间的最优处理计划。

四、优化效果

1、页面翻开速度显着加快

优化后最直接的效果便是在用户体感上,页面的翻开速度显着加快了(以详情页为例):

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

2、接口呼应时刻下降50%

列表、详情、订单等主流程各个核心接口的P50呼应时刻都有显着的降幅,均匀下降了50%:

以详情页的 A、B 两个接口为例,A接口在优化前的 P50 为366ms:

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

A 接口优化后的 P50 为36ms:

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

B 接口的 P50 呼应时刻,从660ms 降到了410ms:

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

3、单机吞吐量功能上限进步100%,资源本钱下降一半

单机可支撑 QPS 上限从100进步至200,吞吐量功能上限进步100%,平稳应对七节两月等惯例流量顶峰。

在考试、表演、暂时政策改变、竞对毛病等反常突发事情情况下,会产生瞬时的流量尖峰。在某次实战的情况下,瞬时流量顶峰到达过二十万 QPS 以上,酒店事务网关体系经受住了考验,能够轻松应对。

单机功能的进步,咱们的机器资源本钱也下降了一半。

单机吞吐提升100%,响应时间降低50%:去哪儿网酒店高性能业务网关优化实践

4、圈杂乱度下降38%,研制功率进步30%

优化后酒店事务网关的有效代码行数削减了6万行;代码圈杂乱度从19518削减至12084,下降了38%;网关优化后,事务模块愈加内聚、鸿沟明晰,日常需求的开发、联调时刻均有显着削减,研制功率也进步了30%。

五、小结与下一步规划

1、经过选用 Spring WebFlux 架构和体系内/体系间的服务编列,本次酒店事务网关的优化取得了不错的效果,单机吞吐量进步了100%,全体接口的呼应时刻下降了50%,为同类型事务网关提供一套行之有效的优化计划。

2、在此基础上,为了坚持优化后的效果,咱们除了建立监控日常做好预警外,还开发了接口呼应时长改变的归因东西,主动剖析改变的原因,能够高效排查问题作好持续优化。

3、当前咱们在服务编列的时分,只能依据上游接口在稳定期的呼应时刻,来做到最优编列。当某些上游接口呼应时刻存在波动较大的情况时,目前的编列功能还无法做到动态主动最优,这部分是咱们未来需要优化的方向。