本文由去哪儿网技术团队田文琦分享,本文有修订和改动。
本文针对去哪儿网酒店业务网关的吞吐率下降、响应时间上升等问题,进行全流程异步化、服务编排方案等措施,进行了高性能网关的技术优化实践。
技术交流:
- 移动端im开发入门文章:《》
- 开源im框架源码:()
(本文已同步发布于:)
田文琦:2021年9月加入去哪儿网机票目的地事业群,担任软件研发工程师,现负责国内酒店主站技术团队。主要关注高并发、高性能、高可用相关技术和系统架构。主导的酒店业务网关优化项目,荣获22年去哪儿网技术中心tc项目三等奖。
本文是专题系列文章的第9篇,总目录如下:
- 《》
- 《》
- 《》
- 《》
- 《》
- 《》
- 《》
- 《》
- 《》(* 本文)
近来,qunar 酒店的整体技术架构在基于 ddd 指导思想下,一直在进行调整。其中最主要的一个调整就是包含核心领域的团队交出各自的“应用层”,统一交给下游网关团队,组成统一的应用层。
这种由多个网关合并成大前台(酒店业务网关)的融合,带来的好处是核心系统边界清晰了,但是对酒店业务网关来说,也带来了不小的困扰。
系统面临的压力主要来自两方面:
- 1)首先,一次性新增了几十万行大量硬编码、临时兼容、聚合业务规则的复杂代码且代码风格迥异,有些甚至是跨语言的代码迁移;
- 2)其次,后续的复杂多变的应用层业务需求,之前分散在各个子网关中,现在在源源不断地汇总叠加到酒店业务网关。
这就导致了一系列的问题:
- 1)业务网关吞吐性能变差:应对流量尖峰时期的单机最大吞吐量与合并之前相比,下降了20%
- 2)内部业务逻辑处理速度变差:主流程业务逻辑的处理时间与合并之前相比,上涨了10%。
- 3)代码难以维护、开发效率低:主站内部各个模块之间严重耦合,边界不清,修改扩散问题非常明显,给后续的迭代增加了维护成本,开发新需求的效率也不高。
酒店业务网关作为直接面对用户的系统,出现任何问题都会被放大百倍,上述这些问题亟待解决。
现有系统虽然业务处理部分是异步化的,但是并不是全链路异步化(如下图所示)。
同步 servlet 容器,servlet 线程与业务逻辑线程是同一个,高峰期流量上涨或者尤其是遇到流量尖峰的时候,servlet 容器线程被阻塞的时候,我们服务的吞吐量就会明显下降。
业务处理虽然使用了线程池确实能实现异步调用的效果,也能压缩同步等待的时间,但是也有一些缺陷。
比如:
- 1)cpu 资源大量浪费在阻塞等待上,导致 cpu 资源利用率低;
- 2)为了增加并发度,会引入更多额外的线程池,随着 cpu 调度线程数的增加,会导致更严重的资源争用,上下文切换占用 cpu 资源;
- 3)线程池中的线程都是阻塞的,硬件资源无法充分利用,系统吞吐量容易达到瓶颈。
前期为了快速落地酒店 ddd 架构,合并大前台的重构中,并没有做到一步到位的设计。
为了保证项目质量,将整个过程切分为了迁移 重构两个步骤。迁移之后,整个酒店业务网关的内部代码结构是割裂、混乱的。
总结如下:
我们最核心的一个接口会调用70多个上游接口,上述问题:边界不清、不内聚、各种重复调用、依赖阻塞等问题导致了核心接口的响应时间有明显上涨。
全流程异步化方案,我们主要采用的是 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())。
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)最后执行主过程,触发并执行子过程、生成结果。
每个执行过程连通输入流和输出流,子过程之间可以是并行的,也可以是串行的这个取决于实际的业务逻辑。凯发k8网页登录的服务编排就是完成输入和输出流的编排,即在第一步声明执行过程(包括子过程),第二步和第三步完全交给 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.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%。
1)通过采用 spring webflux 架构和系统内/系统间的服务编排,本次酒店业务网关的优化取得了不错的效果,单机吞吐量提升了100%,整体接口的响应时间下降了50%,为同类型业务网关提供一套行之有效的优化方案。
2)在此基础上,为了保持优化后的效果,我们除了建立监控日常做好预警外,还开发了接口响应时长变化的归因工具,自动分析变化的原因,可以高效排查问题作好持续优化。
3)当前我们在服务编排的时候,只能根据上游接口在稳定期的响应时间,来做到最优编排。当某些上游接口响应时间存在波动较大的情况时,目前的编排功能还无法做到动态自动最优,这部分是我们未来需要优化的方向。
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]
[10]
[11]
[12]
[13]
(本文已同步发布于:)