当前位置: 首页 > 科技观察

高性能Java应用层网关设计实践

时间:2023-03-22 15:46:46 科技观察

前言我们简单的讲解了接入层网关的实现原理。很多人对Java网关的实现也很感兴趣,那么本文就简单说说Java的应用。网关设计,本文将从以下几个方面来讲解Java应用层网关的设计Java应用层网关的必要性核心网关技术选择嵌入式网关设计Java应用层网关的必要性我们的Java网关分为应用层有两种部分,网关和业务嵌入式网关。架构图如下。在这里插入图片来描述Java网关。Java网关分为核心网关和业务嵌入式网关服务两部分。主要工作原理如下。经过流水线处理(风控、路由协议转换、流控、降级等操作)后,发起通用调用,进入业务层网关。业务层网关也会经过一系列的流水线(接口验证、签名验证、session验证等)进入到最终的业务逻辑中,然后调用相关的dubbo服务,最终完成对这个Java请求的响应。核心网关和嵌入式业务网关的功能如下。在这里插入一张图片来描述嵌入式网关。嵌入式网关以jar包的形式集成到业务项目中。这种设计的具体原因后面会详细介绍。首先我们来看一下Java网关为什么分为核心网关和嵌入式服务网关两部分。从接入层直接连接服务网关不是更方便吗?.这里有三个原因。核心网关主要起到风控、鉴权、路由协议转换、流量控制、降级、管理统计(请求错误上报等)等作用。这些功能对每个层请求都是通用的。统一将这些功能分离出来,放在核心网关上实现是比较合理的。当然,第一点描述的功能可以在接入层实现,但是这样会让接入层看起来很臃肿。另外第一点还有一个很重要的功能,路由协议转换(将http转换为dubbo),由于我们的接入层使用的是OpenResty,所以不支持这种协议转换,除非基于OpenResty进行二次开发耗时又费力,又没有必要,所以抽出一个Java核心网关来承担第一点所描述的功能似乎更合理。计算机界不是有句话吗:计算机界的任何问题,加一个中间层就可以解决。添加Java核心网关符合单一职责、分层的设计理念。增加核心网关确实是多了一层,多了一个损失,但是核心网关并没有处理具体的逻辑,主要起到流量转发的作用,下面我们可以看到,它使用了webflux的reactive造成的损失与引入它所带来的优势相比,编程框架是微不足道的。下面简单说一下核心网关和服务网关的设计思路。核心网关技术选择同步阻塞VS异步非阻塞上一节的介绍可以看出,Java核心网关负责所有的流量入口,会调用大量的业务接口(进入业务网关),所以IO操作会很频繁。类型上有要求,先看传统的SpringMVC(servlet3.0之前),很明显是同步阻塞的,一个请求需要一个ServletThread来处理,当有DB,网络IO,这个线程会阻塞,可想而知,采用这种方案,线程很快就会被占满,导致系统不可用。显然我们应该采用异步非阻塞的编程模型,它是如何工作的,如下图所示,工作原理如下:只有一个请求线程负责接受所有的请求,每个请求都有一个Eventhandler和callback,并且请求线程收到请求后,请求发出后,会在EventLoop中为这个请求注册一个回调函数,然后立即将请求抛给线程池中的一个线程进行处理,然后请求线程会立即返回,其他请求可以立即处理。线程池中的线程处理完请求的EventHandler(DB、网络IO等逻辑)后,会调用之前注册的回调函数返回请求结果。从上面的工作原理可以看出,负责处理请求的请求线程只需要一个,线程数大大减少!更少的线程意味着更高的内存利用率,也意味着更少的线程切换开销!所以显然应该使用这种编程模型。打个简单的比方,相信大家都有去酒店吃饭的经历。对于酒店来说,如何才能最大程度地提高接客效率呢?一种方法是为每位客人安排一名接待员。该接待员负责客人的接待、就座、服务等所有流程。显然,如果做出这样的安排,就有多少客人就有多少接待员。第二种方法是只安排一名接待员。接待客人入座后,接待员立即回到门口迎接客人,剩下的交给服务服务员(线程池工作)。这样的话,接待人员的数量就大大减少了,可以大大提高效率。最终,我们选择了SpringWebFlux,一个反应式、事件驱动、异步和非阻塞的框架。反应式编程和SpringWebFlux反应式编程简介反应式编程是一种基于数据流和变化传播的声明式编程范式。它是一种编程思想,可以根据数据流中的事件(变化)进行相关的反应处理。举个简单的例子:在语句a=b+c中,要得到a的值,如果使用传统的编程模型,每次b或c发生变化,都需要重新计算得到a,而在响应式编程中,我们把b、c看成一个数据流,a会实时响应b、c的变化。响应式编程具有以下特点1.事件驱动在事件驱动程序中,组件是通过松散耦合的生产者(也称为订阅者,即Publisher)和订阅者模式(Subscriber)来实现的。事件以异步和非阻塞方式接收和发送。事件驱动编程有什么好处?简单的说,它依赖于push模式而不是pull模式,也就是说只有producers有消息(change)才会通知consumer响应,也就是说consumer不需要轮询或者等待数据。2.实时响应以我们的网关为例。请求线程收到请求后,迅速返回存储结果的上下文,并将具体执行交给线程池中的线程(可以认为是后台线程)。处理完成后,异步调用将结果封装到结果的上下文中。可见这个过程是完全异步的,也就是说实时响应必须通过异步编程来实现。在Java8中,CompletableFuture对象可以在发起调用后快速返回。3.弹性机制事件驱动的松散耦合,提供组件在发生故障时可以捕获完全隔离的上下文场景,封装为消息,发送给其他组件时,可以检查是否收到等错误,以及接受的指令在具体编程时是否可用。执行等,决定如何应对。响应式编程的主要工作流程如下:订阅者主动向订阅者推送数据,当订阅者异步或完成异常时触发另外两个方法,触发onError,所有推送无异常完成,最后执行onSuccess方法这里有个问题,如果Publisher发送消息速度过快,超过了Subscriber的处理速度怎么办,所以不得不提一下背压(BackPressure)的概念。我知道网友ThrowingLine对这个概念解释的很好:背压是从工程学上的概念推导出来的:在管道输送中,由于管道的突然变窄和急弯,气体流动或液体流动引起反压从下游到上游某处。这种情况称为“背压”,对应的在响应式编程中,当数据流从上游生产者传输到下游消费者时,上游生产速度大于下游消费速度,导致下游Buffer溢出。这种现象称为背压。这里的重点是“BufferOverflow”,为什么需要buffer,因为Publisher的生产速度大于Subscriber的消费速度,所以需要Buffer,因为外部条件,显然Buffer是有上限的,如果生产速度超过buffer,那么就会产生背压,如果超过buffer,唯一的选择就是丢弃新的事件。这就好比你的服务器只能承受5000~6000个请求,如果你设置buffer为5000,一旦请求数超过5000,就会产生反压,多余的请求会被丢弃,这样就保证了本机不会被源源不断的Publisher生产事件淹没,有效提高了网关的可用性。SpringWebFlux简介为了更好的推动反应式编程的应用,在Java平台上,Netflix(开发了RxJava)、TypeSafe(开发了Scala、Akka)、Pivatol(开发了Spring、Reactor)共同开发了一个名为TheReactiveStreams的框架项目(specification)用于制定与反应式编程相关的规范和接口。Reactor基于ReactiveStream定制了一个反应式编程框架,而WebFlux则是基于Reactor实现了一个Web领域的反应式编程框架。由于反应式编程的异步和非阻塞特性,WebFlux运行在Netty、Undertow等之上,异步编程模型的服务端也可以运行在支持Servlet3.1的服务端容器上(Servlet3.1开始支持异步)。如图,左边是传统的SpringMVC结构,右边是webflux组件。为了让大家更好的使用webflux编程,Spring紧密兼容webflux中@Controller等SpringMVC注解的使用,让用户更好的过渡到webflux编程,但是在底层实现上,不同于SpringMVC实现的请求InputStream不同于响应OutputStream。webflux实现了一组响应式请求(ServerHttpRequest)和响应(ServerHttpResponse)。这两个类以Flux的形式暴露了请求体和响应体(下面会简单介绍Flux)同时webflux底层还实现了基于Flux的JSON、XML序列化和反序列化、HTML实景渲染和服务器发送事件。通过介绍我们可以看出webflux支持从请求到响应、渲染、事件发送等一整套响应式事件,是的,要最大化webflux的性能,中间的所有事件都应该响应Mono或者Flux底层WebFlux的实现其实是基于Reactor实现的。在Reactor的核心类中,下面两个类代表发布者Mono:PublisherFlux代表0到1个元素:代表0到N个元素的发布者如何使用,如下图@RequestMapping("/demo")@RestControllerpublicclassDemoController{@RequestMapping(value="/foobar")publicMonofoobar(){returnMono.just(newFoobar());}}本来是想返回foobar对象,结果最后以Mono(或Flux),从而在响应式编程中构建一个生产者(Publisher),然后调用subscribe完成对生产者Consumption的监听。在我们的网关设计中,当收到请求时,Mono被用作发布者。如果中间出现问题,会调用onError,最后成功后会调用onSuccess。以下是网关实现采用的整体框架。图中的mono.empty代表创建一个不包含任何元素,只发布消息的队列。消息发送后,网关的slot会在线程池中处理,处理成功后会调用onSuccess方法,处理失败会调用onError。在下一节中,我们将看到如何处理这些网关插槽。网关的责任链设计无论是核心网关还是嵌入式网关,我们都采用了责任链的模式来实现网关的核心处理流程,将每一个处理逻辑都看成一个槽,每个槽依次执行按照预先设定的顺序。与开源kong、zuul等类似,我们也采用PRPE模式(Pre、Routing、Post、Error)Pre阶段:initParamsSlot初始化组装请求上下文参数sentinelSlot流控组件引入,集群当前使用riskSlot风控limit,downgrade,circuitbreakerProcesstheRoute阶段:dubboSlot将dubbo泛化调用转为dubbo协议进行远程调用POSTSlots:后处理APMMonitorSlotAPM监控处理,请求错误等打点监控采用这种设计方式,每个slot自己执行职责,并且有更好的可扩展性,如果你想添加任何槽,只需定义槽函数并指定它在调用链中的位置。需要注意的是,有些Slots的请求结果依赖于前面Slots的执行结果。在这种情况下,需要将之前的执行事件以Mono的形式进行封装,让这些slot一个一个的构成一个响应式的事件流,保证这些Slot是异步执行的,不会阻塞主线程。另外注意高亮的dubboSlot阶段。在dubbo2.7之前,dubbo底层返回的是Future(会一直占用一个线程轮询结果),对异步编程不友好。2.7之后返回CompleteFuture,与webflux的异步编程模型完美结合(发起调用嵌入式网关立即返回,调用完成后执行,真正意义上的异步)。嵌入式网关设计首先,我们需要了解为什么需要嵌入式网关。主要有以下三个原因。目前有H5、小程序、app终端,每个终端的session存储不一样。需要找到请求的每个终端对应的session。uid,这个操作显然应该在网关层面进行,放在嵌入式网关中比较合理。每个请求进入业务层后,我们需要验证其时间戳、app签名、小程序签名等,这些验证对于每一端的请求都是必须的,所以很明显有些业务应该在网关上做。有些业务需要在业务执行前后进行扩展,比如需要在执行前后进行分析等,网关也应该支持扩展的实现。嵌入式网关如何实现嗯,业务服务以dubbo服务的形式存在,dubbo中有一个过滤机制,专门用来拦截服务提供者和服务消费者的调用过程。每执行一次远程方法,就会执行拦截。这为开发者提供了非常方便的扩展性,所以嵌入式网关的主要设计思路是自定义dubbo的filter,然后在这个filter中执行相关的扩展逻辑。伪代码如下:这样,通过自定义过滤器这样,我们就解决了扩展性的问题。注意我们使用了Activate注解,这样dubbo会自动加载注解后的Filter作为Dubbo的原生Filter,而不需要显式配置provider或者consumer的filter,从而避免了代码的侵入性。这里执行前后业务逻辑的展开,也是通过责任链模式,将槽一个一个执行。我们先定义时间戳验证、签名验证、Sessiontoid等slot,然后在xml中指定这些slot的执行每个业务都有一个gateway.xml文件,里面可以配置H5、app、applets的slot需要执行。以app请求配置需要执行的pre-slot和post-processingslot为例,伪代码如下,只要在需要支持的网关(ImportResource)的xml文件中引入即可启动函数,配置的bean会生效,然后在filter中,bizChannel对应的slotBizList(request必传,代表业务端的标识,如biz_h5,biz_app,biz_applet)可以在业务逻辑之前和之后执行。这样就有效的指定了业务逻辑执行前后需要执行的slot。如果每个业务都想在业务逻辑执行前后进行扩展,只需要定义自己的槽逻辑,并在xml文件中指定槽的位置即可。生效。嵌入式网关按照上述思路实现后,通过jar包分发到各个业务系统中。优点是:稳定性提高,每个业务集成一个稳定版本的网关Jar。当某个业务系统升级网关Jar时,其他业务系统不会受到干扰。小结本文详细介绍了网关的实用思路。对编程、dubbofilter等应该有一定的了解。首先,Java核心网关作为承载所有流量的入口,对其性能要求很高,采用响应式编程的异步非阻塞编程模型可以很好的满足我们。(如果对响应式编程的介绍不了解,可以阅读文末参考链接,介绍的清楚明白),其次,不同的业务需要在执行前后做各种扩展业务逻辑,所以我们使用自定义过滤器实现这个需求,在嵌入式网关中实现显然更合理,嵌入式网关以jar包的形式嵌入到业务服务中,从而实现非入侵到业务层,也具有很强的可扩展性。本文转载自微信公众号“码海”,可通过以下二维码关注。转载本文请联系码海公众号。