本文由去哪儿网技术团队田文琦分享,本文有订正和改变。
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、相干文章
[1] 从 C10K 到 C10M 高性能网络应用的实践摸索
[2] 一文读懂高性能网络编程中的 I / O 模型
[3] 一文读懂高性能网络编程中的线程模型
[4] 以网游服务端的网络接入层设计为例,了解实时通信的技术挑战[5] 手淘亿级挪动端接入层网关的技术演进之路
[6] 喜马拉雅自研亿级 API 网关技术实际
[7] B 站基于微服务的 API 网关从 0 到 1 的演进之路
[8] 深刻操作系统,彻底了解 I / O 多路复用
[9] 深刻操作系统,彻底了解同步与异步
[10] 通俗易懂,高性能服务器到底是如何实现的
[11] 百度对立 socket 长连贯组件从 0 到 1 的技术实际
[12] 淘宝挪动端对立网络库的架构演进和弱网优化技术实际
[13] 百度基于金融场景构建高实时、高可用的分布式数据传输零碎的技术实际
(本文已同步公布于:http://www.52im.net/thread-4618-1-1.html)