本文由去哪儿网技术团队田文琦分享,本文有修订和改动。
1、引言
本文针对去哪儿网酒店业务网关的吞吐率下降、响应时间上升等问题,进行全流程异步化、服务编排方案等措施,进行了高性能网关的技术优化实践。
技术交流:
- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》
- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK(备用地址点此)
(本文已同步发布于:http://www.52im.net/thread-4618-1-1.html)
2、作者介绍
田文琦:2021年9月加入去哪儿网机票目的地事业群,担任软件研发工程师,现负责国内酒店主站技术团队。主要关注高并发、高性能、高可用相关技术和系统架构。主导的酒店业务网关优化项目,荣获22年去哪儿网技术中心TC项目三等奖。
3、专题目录
本文是专题系列文章的第9篇,总目录如下:
- 《长连接网关技术专题(一):京东京麦的生产级TCP网关技术实践总结》
- 《长连接网关技术专题(二):知乎千万级并发的高性能长连接网关技术实践》
- 《长连接网关技术专题(三):手淘亿级移动端接入层网关的技术演进之路》
- 《长连接网关技术专题(四):爱奇艺WebSocket实时推送网关技术实践》
- 《长连接网关技术专题(五):喜马拉雅自研亿级API网关技术实践》
- 《长连接网关技术专题(六):石墨文档单机50万WebSocket长连接架构实践》
- 《长连接网关技术专题(七):小米小爱单机120万长连接接入层的架构演进》
- 《长连接网关技术专题(八):B站基于微服务的API网关从0到1的演进之路》
- 《长连接网关技术专题(九):去哪儿网酒店高性能业务网关技术实践》(* 本文)
4、技术背景
近来,Qunar 酒店的整体技术架构在基于 DDD 指导思想下,一直在进行调整。其中最主要的一个调整就是包含核心领域的团队交出各自的“应用层”,统一交给下游网关团队,组成统一的应用层。
这种由多个网关合并成大前台(酒店业务网关)的融合,带来的好处是核心系统边界清晰了,但是对酒店业务网关来说,也带来了不小的困扰。
系统面临的压力主要来自两方面:
- 1)首先,一次性新增了几十万行大量硬编码、临时兼容、聚合业务规则的复杂代码且代码风格迥异,有些甚至是跨语言的代码迁移;
- 2)其次,后续的复杂多变的应用层业务需求,之前分散在各个子网关中,现在在源源不断地汇总叠加到酒店业务网关。
这就导致了一系列的问题:
- 1)业务网关吞吐性能变差:应对流量尖峰时期的单机最大吞吐量与合并之前相比,下降了20%
- 2)内部业务逻辑处理速度变差:主流程业务逻辑的处理时间与合并之前相比,上涨了10%。
- 3)代码难以维护、开发效率低:主站内部各个模块之间严重耦合,边界不清,修改扩散问题非常明显,给后续的迭代增加了维护成本,开发新需求的效率也不高。
酒店业务网关作为直接面对用户的系统,出现任何问题都会被放大百倍,上述这些问题亟待解决。
5、吞吐量下降问题分析
现有系统虽然业务处理部分是异步化的,但是并不是全链路异步化(如下图所示)。
同步 servlet 容器,servlet 线程与业务逻辑线程是同一个,高峰期流量上涨或者尤其是遇到流量尖峰的时候,servlet 容器线程被阻塞的时候,我们服务的吞吐量就会明显下降。
业务处理虽然使用了线程池确实能实现异步调用的效果,也能压缩同步等待的时间,但是也有一些缺陷。
比如:
- 1)CPU 资源大量浪费在阻塞等待上,导致 CPU 资源利用率低;
- 2)为了增加并发度,会引入更多额外的线程池,随着 CPU 调度线程数的增加,会导致更严重的资源争用,上下文切换占用 CPU 资源;
- 3)线程池中的线程都是阻塞的,硬件资源无法充分利用,系统吞吐量容易达到瓶颈。
6、响应时间上涨问题分析
前期为了快速落地酒店 DDD 架构,合并大前台的重构中,并没有做到一步到位的设计。
为了保证项目质量,将整个过程切分为了迁移+重构两个步骤。迁移之后,整个酒店业务网关的内部代码结构是割裂、混乱的。
总结如下:
我们最核心的一个接口会调用70多个上游接口,上述问题:边界不清、不内聚、各种重复调用、依赖阻塞等问题导致了核心接口的响应时间有明显上涨。
7、 解决方案Part1:全流程异步化提升吞吐量
全流程异步化方案,我们主要采用的是 Spring WebFlux。
7.1选择的理由
1)响应式编程模型:Spring WebFlux 基于响应式编程模型,使用异步非阻塞式 I/O,可以更高效地处理并发请求,提高应用程序的吞吐量和响应速度。同时,响应式编程模型能够更好地处理高负载情况下的请求,降低系统的资源消耗。
2)高性能:Spring WebFlux 使用 Reactor 库实现响应式编程模型,可以处理大量的并发请求,具有出色的性能表现。与传统的 Spring MVC 框架相比,Spring WebFlux 可以更好地利用多核 CPU 和内存资源,以实现更高的性能和吞吐量。
3)可扩展性:Spring WebFlux 不仅可以使用 Tomcat、Jetty 等常规 Web 服务器,还可以使用 Netty 或 Undertow 等基于 NIO 的 Web 服务器实现,与其它非阻塞式 I/O 的框架结合使用,可以更容易地构建可扩展的应用程序。
4)支持函数式编程:Spring WebFlux 支持函数式编程,使用函数式编程可以更好地处理复杂的业务逻辑,并提高代码的可读性和可维护性。
5)50与 Spring 生态系统无缝集成:Spring WebFlux 可以与 Spring Boot、Spring Security、Spring Data 等 Spring 生态系统的组件无缝集成,提供了完整的 Web 应用程序开发体验。
7.2实现原理和异步化过程
上图中从下到上每个组件的作用:
- 1)Web Server:适配各种 Web 服务, 监听客户端请求,并将其转发到 HttpHandler 处理;
- 2)HttpHandler:以非阻塞的方式处理响应式 http 请求的最底层处理器,不同的处理器处理的请求都会归一到 httpHandler 来处理,并返回响应;
- 3)DispatcherHandler:调度程序处理程序用于异步处理 HTTP 请求和响应,封装了HandlerMapping、HandlerAdapter、HandlerResultHandler 的调用,实际实现了HttpHandler的处理逻辑;
- 4)HandlerMapping:根据路由处理函数 (RouterFunction) 将 http 请求路由到相应的handler。WebFlux 中可以有多个 handler,每个 handler 都有自己的路由;
- 5)HandlerAdapter:使用给定的 handler 处理 http 请求,必要时还包括使用异常处理handler 处理异常;
- 6)HandlerResultHandler:处理返回结果,将 response 写到输出流中;
- 7)Reactive Streams:Reactive Streams 是一个规范,用于处理异步数据流。Spring WebFlux 实现了 Reactor 库,该库基于响应式流规范,处理异步数据流。
在整个过程中 Spring WebFlux 实现了响应式编程模型,构建了高吞吐量、高并发的 Web 应用程序,同时也具有响应快速、可扩展性好、资源利用率高等优点。
下面我们来看下 webFlux 是如何将 Servlet 请求异步化的:
1)ServletHttpHandlerAdapter 展示了使用 Servlet 异步支持和 Servlet 3.1非阻塞I/O,将 HttpHandler 适配为 HttpServlet。
2)第10行:request.startAsync()开启异步模式,然后将原始 request 和 response 封装成 ServletServerHttpRequest 和 ServletServerHttpResponse。
3)第36行:httpHandler.handle(httpRequest, httpResponse) 返回一个 Mono 对象(即Publisher),对 Request 和 Response 的所有具体处理都在 Mono 对象中定义。
所有的操作只有在 subscribe 订阅的那一刻才开始进行,HandlerResultSubscriber 是 Reactive Streams 规范中标准的 subscriber,在它的 onComplete 事件触发时,会结束 servlet 的异步模式。
对 Servlet 返回结果的异步写入,以 DispatcherHandler 为例说明:
1)第2行:exchange 是对 ServletServerHttpRequest 和 ServletServerHttpResponse 的封装。
2)第10-15行:在系统预加载的 handlerMappings 中根据 exchange 找到对应的 handler,然后利用 handler 处理 exchange 执行相关业务逻辑,最终结果由 result 将 ServletServerHttpResponse 写入到输出流中。
最后:除了 Servlet 的异步化,作为业务网关,要实现全链路异步化还需要在远程调用方面要支持异步化。在 RPC 调用方式下,我们采用的异步 Dubbo,在 HTTP 调用方式下,我们采用的是 WebClient。
WebClient 默认使用的是 Netty 的 IO 线程进行发送请求,调用线程通过订阅一些事件例如:doOnRequest、doOnResponse 等进行回调处理。异步化的客户端,避免了业务线程池的阻塞,提高了系统的吞吐量。
在使用 WebClient 这种异步 http 客户端的时候,我们也遇到了一些问题:
1)首先:为了避免默认的 NettyIO 线程池可能会执行比较耗时的 IO 操作导致 Channel 阻塞,建议替换成其他线程池,替换方法是 Mono.publishOn(reactor.core.scheduler.Schedulers.newParallel("biz_scheduler", 300))。
2)其次:因为线程发生了切换,无法兼容 Qtracer (Qunar内部的分布式全链路跟踪系统),所以在初始化 WebClient 客户端的时候,需要在 filter 里插入对 Request 的修改,记录前一个线程保存的 Qtracer 的上下文。WebClient.Builder wcb = WebClient.builder().filter(new QTraceRequestFilter())。
8、解决方案Part2:服务编排降低响应时间
Spring WebFlux 并不是银弹,它并不能保证一定能降低接口响应时间,除了全流程异步化,我们还利用 Spring WebFlux 提供的响应式编程模型,对业务流程进行服务编排,降低依赖之间的阻塞。
8.1服务编排解决方案
在介绍服务编排之前,我们先来了解一下 Spring WebFlux 提供的响应式编程模型 Reactor。
它有最重要的两个响应式类 Flux 和 Mono:
- 1)一个 Flux 对象表明一个包含0..N 个元素的响应式序列;
- 2)一个 Mono 对象表明一个包含零或者一个(0..1)元素的结果。
不管是 Flux 还是 Mono,它的处理过程分三步:
- 1)首先声明整个执行过程(operator);
- 2)然后连通主过程,触发执行;
- 3)最后执行主过程,触发并执行子过程、生成结果。
每个执行过程连通输入流和输出流,子过程之间可以是并行的,也可以是串行的这个取决于实际的业务逻辑。我们的服务编排就是完成输入和输出流的编排,即在第一步声明执行过程(包括子过程),第二步和第三步完全交给 Reactor。
下面是我们服务编排的总体设计:
如上图所示:
1)service:是最小的业务编排单元,对 invoker 和 handler 进行了封装,并将结果写回到上下文中。主流程中,一般是由多个 service 进行并行/串行地编排。
2)Invoker:是对第三方的异步非阻塞调用,对返回结果作 format,不包含业务逻辑。相当于子过程,一个 service 内部根据实际业务场景可以编排0个或多个 Invoker。
3)handler:纯内存计算,封装共用和内聚的业务逻辑。在实际的业务开发过程中,对上下文中的任一变量,只有一个 handler 有写权限,避免了修改扩散问题。也相当于子过程,根据实际需要编排进 service 中。
4)上下文:为每个接口都设计了独立的请求/处理/响应上下文,方便监控定位每个模块的处理正确性。
上下文设计举例:
在复杂的 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")将多个流合并成为一个流输出。
在 controller 层就负责处理一件事,即对 service 进行编排(如下图所示)。
我们利用 flatMap 方法可以方便地将多个 service 按照业务逻辑要求,进行多次地并行/串行编排。
1)并行编排示例:第12、14行是两个并行处理的输入流 afterAdapterValidMono、preRankSecMono ,二者并行执行各自 service 的处理。
2)并行处理后的流合并:第16行,搜索结果流 rankMono 和不依赖搜索的其他结果流preRankAsyncMono,使用 Mono.zip 操作将两者合并为一个输出流 afterRankMergeMono。
3)串行编排举例:第16、20、22行,afterRankMergeMono 结果流作为输入流执行 service14 后转换成 resultAdaptMono,又串行执行 service15 后,输出流 cacheResolveMono。
以上是酒店业务网关的整体服务编排设计。
8.2编排示例
下面来介绍一下,我们是如何进行流程编排,发挥网关优势,在系统内和系统间达到响应时间全局最优的。
8.2.1)系统内:
上图示例中的左侧方案总耗时是300ms。
这300ms 来自最长路径 Service1的200ms 加上 Service3 的100ms:
- 1)Service1 包含2个并行 invoker 分别耗时100ms、200ms,最长路径200ms;
- 2)Service3 包含2个并行invoker 分别耗时50ms、100ms,最长路径100ms。
而右图是将 Service1 的200ms 的 invoker 迁移至与 Service1 并行的 Service0 里。
此时,整个处理的最长路径就变成了200ms:
- 1)Service0 的最长路径是200ms;
- 2)Service1+service3 的最长路径是100ms+100ms=200ms。
通过系统内 invoker 的最优编排,整体接口的响应时间就会从300ms 降低到200ms。
8.2.2)系统间:
举例来说:优化前业务网关会并行调用 UGC 点评(接口耗时100ms)和 HCS 住客秀(接口耗时50ms)两个接口,在 UGC 点评系统内部还会串行重复调用 HCS 住客秀接口(接口耗时50ms)。
发挥业务网关优势,UGC 无需再串行调用 HCS 接口,所需业务聚合处理(这里的业务聚合处理是纯内存操作,耗时可以忽略)移至业务网关中操作,这样 UGC 接口的耗时就会降下来。对全局来说,整体接口的耗时就会从原来的100ms 降为50ms。
还有一种情况:假设业务网关是串行调用 UGC 点评接口和 HCS 住客秀接口的话,那么也可以在业务网关调用 HCS 住客秀接口后,将结果通过入参在调用 UGC 点评接口的时候传递过去,也可以省去 UGC 点评调用 HCS 住客秀接口的耗时。
基于对整个酒店主流程业务调用链路充分且清晰的了解基础之上,我们才能找到系统间的最优解决方案。
9、优化后的效果
9.1页面打开速度明显加快
优化后最直接的效果就是在用户体感上,页面的打开速度明显加快了。
以详情页为例:
9.2接口响应时间下降50%
列表、详情、订单等主流程各个核心接口的P50响应时间都有明显的降幅,平均下降了50%。
以详情页的 A、B 两个接口为例,A接口在优化前的 P50 为366ms:
A 接口优化后的 P50 为36ms:
B 接口的 P50 响应时间,从660ms 降到了410ms:
9.3单机吞吐量性能上限提升100%,资源成本下降一半
单机可支持 QPS 上限从100提升至200,吞吐量性能上限提升100%,平稳应对七节两月等常规流量高峰。
在考试、演出、临时政策变化、竞对故障等异常突发事件情况下,会产生瞬时的流量尖峰。在某次实战的情况下,瞬时流量高峰达到过二十万 QPS 以上,酒店业务网关系统经受住了考验,能够轻松应对。
单机性能的提升,我们的机器资源成本也下降了一半。
9.4圈复杂度降低38%,研发效率提升30%
具体就是:
- 1)优化后酒店业务网关的有效代码行数减少了6万行;
- 2)代码圈复杂度从19518减少至12084,降低了38%;
- 3)网关优化后,业务模块更加内聚、边界清晰,日常需求的开发、联调时间均有明显减少,研发效率也提升了30%。
10、本文小结与下一步规划
1)通过采用 Spring WebFlux 架构和系统内/系统间的服务编排,本次酒店业务网关的优化取得了不错的效果,单机吞吐量提升了100%,整体接口的响应时间下降了50%,为同类型业务网关提供一套行之有效的优化方案。
2)在此基础上,为了保持优化后的效果,我们除了建立监控日常做好预警外,还开发了接口响应时长变化的归因工具,自动分析变化的原因,可以高效排查问题作好持续优化。
3)当前我们在服务编排的时候,只能根据上游接口在稳定期的响应时间,来做到最优编排。当某些上游接口响应时间存在波动较大的情况时,目前的编排功能还无法做到动态自动最优,这部分是我们未来需要优化的方向。
11、相关文章
[4] 以网游服务端的网络接入层设计为例,理解实时通信的技术挑战
[9] 深入操作系统,彻底理解同步与异步
[10] 通俗易懂,高性能服务器到底是如何实现的
[13] 百度基于金融场景构建高实时、高可用的分布式数据传输系统的技术实践
(本文已同步发布于:http://www.52im.net/thread-4618-1-1.html)