使用API Gateway处理微服务请求转发、合并

前面两篇Docker微服务的服务发现以及Docker微服务的服务间通信机制。主要介绍了如何解决微服务的服务发现和通信问题。
在微服务的架构体系中,为了减少服务间的耦合,在划分服务间的限界上下文的时候。会尽量减少微服务之间的 调用。在实际的需求场景中,往往要同时请求多个微服务资源。

解耦微服务的调用

如下面一个场景,”用户订单列表“的一个聚合页面,需要请求”用户服务“获取基础用户信息,以及”订单服务“获取订单信息,再通过请求”商品服务“获取订单列表的商品聚合订单上的商品信息。如下图所示的场景 :
微服务Docker-GatewayAPI

如果让客户端发出多个请求来解决多个信息聚合,则会增加客户端的复杂度。比较合理的方式就是增加API Gateway层。API Gateway跟微服务一样,也可以部署、运行在Docker容器中,也是一个Springboot应用。如下,通过Gateway API进行转发后:
Docker微服务-GatewayApi

所有的请求的信息,由Gateway进行聚合,Gateway也是进入系统的唯一节点。并且Gateway和所有微服务,以及提供给客户端的也是Restful风格API。Gateway层的引入可以很好的解决信息的聚合问题。而且可以更好得适配不同的客户端的请求,比如H5的页面不需要展示用户信息,而iOS客户端需要展示用户信息,则只需要添加一个Gateway API请求资源即可,微服务层的资源不需要进行变更。

API Gateway的特点

API gateway除了可以进行请求的合并、转发。还需要有其他的特点,才能成为一个完整的Gateway。

响应式编程

Gateway是所有客户端请求的入口。就像Facade模式。为了提高请求的性能,最好选择一套非阻塞I/O的框架。在一些多客户端的场景下,前文举例的“用户订单列表”。对于每个微服务请求不一定需要同步的。比如获取用户信息,以及获取订单列表,就是两个独立请求。然后,获取订单的商品详情,需要等订单信息返回之后,根据订单的商品id列表再去请求商品微服务。为了减少整个请求的响应时间,需要Gateway能够并发处理相互独立的请求。一种解决方案就是采用响应式编程。
目前使用Java技术栈的响应式编程方式有,Java8的CompletableFuture,以及ReactiveX提供的基于JVM的实现-RxJava。

ReactiveX是一个使用可观察数据流进行异步编程的编程接口,ReactiveX结合了观察者模式、迭代器模式和函数式编程的精华。除了RxJava还有RxJS,RX.NET等多语言的实现。

对于Gateway来说,RxJava提供的Observable可以很好的解决并行的独立I/O请求,并且如果微服务项目中使用Java8,那对RxJava的函数学习吸收会更快,基于Lambda风格的响应式编程,可以使代码更加简洁。中文文档可以阅读RxJava文档和教程
通过响应式编程的Observable模式,可以很简洁、方便得创建事件流、数据流,以及用简洁的函数进行数据的组合和转换,同时可以订阅任何可观察的数据流并执行操作。
如下图所示的“用户订单列表”的资源请求时序图:
RxJava并行流处理-用户订单列表时序图

响应式编程可以更好的处理各种线程同步、并发请求,通过Observables和Schedulers提供了透明的数据流、事件流的线程处理。对于敏捷模式下,响应式编程的代码更加简洁,更好维护。作为为了适配不同客户端以及会频繁变更的Gateway来说,是一个很好的解决方案。

鉴权

通过Gateway作为系统的唯一入口,那么所有的鉴权,都可以围绕Gateway去做。在Springboot工程中,基础的授权可以使用Spring Security(Spring Security也可以集成在Spring MVC项目中)。Spring Security主要使用AOP对资源请求进行拦截,内部维护了一个角色的Filter Chain。因为微服务都是通过Gateway请求的,所以微服务的@Secured可以根据Gateway的资源请求鉴权级别进行设置。对于各不同客户端的Token信息的加密、存储以及验证,需要应用自己完成。经过Gateway鉴权之后,解析后的token信息可以直接传递给微服务层。
如果应用需要授权(对资源请求需要管理不同的角色、权限),也只要在Gateway的Rest API基础上基于AOP来做即可。统一管理鉴权和授权,这也是使用类似Facade模式的Gateway API的好处之一。

负载均衡

API Gateway跟Microservice一样,作为Springboot应用,提供Rest api。所以同样运行在Docker容器中。Gateway和微服务之间的服务发现还是可以采用前篇所述的客户端发现模式,或者服务端发现模式。
在集群环境下,API Gateway 可以暴露统一的端口,其实例会运行在不同IP的服务器上。因为我们是使用阿里云的ECS作为容器的基础设施,所以在集群环境的负载均衡也是使用阿里云的负载均衡SLB,域名解析也使用AliyunDNS。下图是一个简单的网络请求的示意:

Gateway 微服务服务集群

为了不暴露服务的端口和资源地址,也可以在Gateway之上再部署Nginx服务,外部的负载均衡设施比如SLB可以将请求转发到Nginx服务器,请求通过Nginx再转发给Gateway端口。如果是自建机房的集群,就需要搭建高可用的负载均衡中心。为了跨机器请求,最好使用Consul,Consul(Consul Template)+Registor+Haproxy来做服务发现和负载均衡中心。

缓存

对于一些高QPS的请求,可以在API Gateway做多级缓存。分布式的缓存可以使用Mongo,Redis,Memcached等。如果是一些对实时性要求不高的,变化频率不高但是高QPS的页面级请求,也可以在Gateway层做本地缓存。而且Gateway可以让缓存方案更灵活和通用。

API Gateway的错误处理

在Gateway的具体实现过程中,错误处理也是一个很重要的事情。对于Gateway的错误处理,可以使用Hystrix来处理请求的熔断。并且RxJava自带的onErrorReturn回调也可以方便得处理错误信息的返回。对于熔断机制,主要需要处理以下几个方面:

  • 服务请求的容错处理
    作为一个合理的Gateway,其应该只负责处理数据流、事件流,而不应该处理业务逻辑。在
    处理多个微服务的请求时,会出现微服务请求的超时、不可用的情况。在一些特定的场景下,
    需要能够合理得处理部分失败。比如上例中的“用户订单列表”,当“User”微服务出现错误时,不应该影响“Order”数据的请求。最好的处理方式就是给当时错误的用户信息一个默认的返回,比如显示一个默认头像,默认用户昵称。然后对于正确的订单,以及商品信息给与正确的数据返回。如果这种场景下,当“Order”领域的微服务异常时,则应该给客户端一个错误码,以及合理的错误提示信息。这样的处理可以尽量在部分系统不可用时提升用户体验。使用RxJava时,具体的实现方式就是针对不同的客户端请求的情况,写好onErrorReturn,做好错误数据兼容即可。
  • 异常的捕捉和记录
    Gateway主要是做请求的转发、合并。为了能清楚得排查问题,定位到具体哪个服务、甚至是哪个Docker容器的问题,需要Gateway能对不同类型的异常、业务错误进行捕捉和记录。如果使用FeignClinet来请求微服务资源,可以通过对ErrorDecoder接口的实现,来针对Response结果进行进一步的过滤处理,以及在日志中记录下所有请求信息。如果是使用Spring Rest Template,则可以通过定义一个定制化的RestTempate,并对返回的ResponseEntity进行解析。在返回序列化之后的结果对象之前,对错误信息进行日志记录。
  • 超时机制
    Gateway线程中大多是IO线程,为了防止因为某一微服务请求阻塞,导致Gateway过多的等待线程,耗尽线程池、队列等系统资源。需要Gateway中提供超时机制,对超时接口能进行优雅的服务降级。
    在SpringCloud的Feign项目中集成了HystrixHystrix提供了比较全面的超时处理的熔断机制。默认情况下,超时机制是开启的。除了可以配置超时相关的参数,Netflix还提供了基于Hytrix的实时监控Netflix -Dashboard,并且集群服务只需再附加部署Netflix-Turbine。通用的Hytrix的配置项可以参考Hystrix-Configuration
    如果是使用RxJava的Observable的响应式编程,想对不同的请求设置不同的超时时间,可以直接在Observable的timeout()方法的参数进行设置回调的方法以及超时时间等。
  • 重试机制
    对于一些关键的业务,在请求超时时,为了保证正确的数据返回,需要Gateway能提供重试机制。如果使用SpringCloudFeign,则其内置的Ribbon,会提供的默认的重试配置,可以通过设置spring.cloud.loadbalancer.retry.enabled=false将其关闭。Ribbon提供的重试机制会在请求超时或者socket read timeout触发,除了设置重试,也可以定制重试的时间阀值以及重试次数等。
    对于除了使用Feign,也使用Spring RestTemplate的应用,可以通过自定义的RestTemplate,对于返回的ResponseEntity对象进行结果解析,如果请求需要重试(比如某个固定格式的error-code的方式识别重试策略),则通过Interceptor进行请求拦截,以及回调的方式invoke多次请求。

小结

对于微服务的架构,通过一个独立的API Gateway,可以进行统一的请求转发、合并以及协议转换。可以更灵活得适配不同客户端的请求数据。而且对于不同客户端的版本兼容的请求可以很好地进行屏蔽,让微服务更加纯粹。微服务只要关注内部的领域服务的设计,事件的处理。
API gateway还可以对微服务的请求进行一定的容错、服务降级。使用响应式编程来实现API gateway可以使线程同步、并发的代码更简洁,更易于维护。在对于微服务的请求可以统一通过FeignClint。代码也会很有层次。如下图,示例
Gateway rxJava userorders

  • Clint负责服务发现、负载均衡以及发出请求,并获取ResponseEntity对象。
  • Translator将ResponseEntity转换成Observable对象,以及对异常进行统一日志采集,类似于DDD中防腐层的概念。
  • Adapter调用各个Translator,使用Observable函数,对请求的数据流进行合并。如果存在多个数据的组装,可以增加一层Assembler专门处理DTO->Model的转换。
  • Controller,提供Restful资源的管理,每个Controller只请求唯一的Adapter。

在下一篇文章中,会继续介绍一下基于Docker的微服务的持续集成方案以及实践

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐