API服务网关

文章内容来自 《springcloud微服务架构开发实战》 董超 胡炽维

API服务网关Zuul:微服务访问的统一入口,负责服务请求路由、组合及协议转换等处理。

当API服务网关成为微服务访问的唯一入口后,就可以实现对系统内部架构的封装,将一些与业务无关的公共逻辑抽象到API服务网关中实现.

API服务网关经常会通过编排多个微服务来处理一个服务请求。

利用API服务网关为不同类型的客户端提供定制的API。

缺点:首先,需要为API服务网关开发一个具有高可用的组件,此外,为了能够将微服务的端点接入API服务网关,开发人员需要及时地将微服务相关端点更新/注册到API服务网关中。还有,API服务网关需要知道与之通信的每个微服务服务器地址和端口信息,特别是在API服务网关中启用了负载均衡时。

Zuul是一个基于JVM路由和服务端的负载均衡器,其参考GOF设计模式中的外观(Facade)模式。Zuul组件可以用于反向代理功能,通过路由寻址将请求转发到后端的服务上,并增加一些通用逻辑处理。Zuul对请求提供了路由和过滤器两个功能

通过Zuul组件,可以完成以下功能:

动态路由;Zuul提供一系列的路由规则配置,可以针对生产中的实际情况进行配置,实现微服务路由的灵活控制。

监控与审查;·监控与审查:通过对一些特定的接口设置访问白名单、访问次数、访问频率等各类设置,可以在不影响微服务实现的情况下,对访问实施监控和审查处理。

身份认证与安全:可以统一在服务网关层增加一个额外的保护层来防止恶意攻击,如果客户端直连微服务的话,则每个暴露的微服务都需要面临这个安全问题。

压力测试:过滤器功能可以逐渐增加对某一服务集群的流量,以了解服务性能,从而及早对服务运维架构做出调优。

金丝雀、A/B测试:

服务迁移:通过Zuul代理可以处理来自旧端点的客户端上的所有流量,将一些请求重定向到新的端点,从而慢慢地用不同的实现来替换旧端点。

负载剪裁/限流:为每一个负载类型分配对应的容量,对超过限定值的请求弃用,这样可以防止站点不被未知的大流量冲跨。通常,可以利用API服务网关配置一个阀值,当请求数超过该阀值时会直接返回错误。


在构建API访问网关的时候需要把握以下两个原则:KISS原则和stateless原则。

KISS原则是指Keep it Simple and Stupid,也就是说要保持API服务网关的简单和轻量。服务网关只不过是服务调用过程中的一个检查点,不应该用来处理业务的复杂性,也不应将其用于解决架构的难点上,这些应该是微服务需要处理的事情。

stateless原则是指在Zuul服务网关中不应该、也不可以保存有关服务调用过程中的状态数据。对于无状态,相信大家都深有体会,如果你的代码是状态相关的,那么在进行水平扩展的时候势必难以进行,所以需要在各服务器上直接进行状态的复制、同步等处理后才能进行扩展。而对于Zuul路由服务也是一样的,要始终保持无状态。

Hystrix容错与监控测试

Zuul天生就拥有线程隔离和服务容错的自我保护能力,以及对服务调用的客户端负载均衡功能。

但是需要注意,当使用path与url的映射关系来配置路由规则时,对于路由转发的请求则不会采用Hystrix Command来包装,所以这类路由请求就没有线程隔离和服务容错保护功能,并且也不会有负载均衡的能力。因此在使用Zuul的时候尽量使用path和serviceId的组合进行配置,这样不仅可以保证API网关的健壮和稳定,也能用到Ribbon的客户端负载均衡功能。


路由配置规则

Zuul提供了多种机制对请求路由进行映射:

·与Eureka服务器整合自动根据微服务的ID进行映射,这个是默认机制,也是之前示例中所使用的机制。

·结合微服务ID通过自定义方式进行路由映射。

·直接使用静态URL路径的方式对微服务进行路由映射。

·添加全局路由映射。

·通过自定义路由转换器,实现更灵活的路由映射。

1 服务路由默认规则

http://[Zuul路由服务器地址]/[serviceId]/[具体服务的端点];

2 自定义微服务访问路径

在Zuul路由服务器配置文件中通过增加格式为“zuul.routes.微服务Id=指定路径”的属性配置方式进行配置,对访问路径进行控制,

如zuul.routes.userservice=/user/**配置的路径可以通过指定正则表达式进行路径匹配

可以通过/user/来访问userservice提供的服务

通过Postman访问http://localhost:8280/routes返回两个服务,

假如你的微服务实例尚未注册到Eureka服务器中,如果通过routes端点来查看,是看不到相应路由映射的;但如果是通过自定义的方式进行路由映射配置,那么不论你的微服务是否已经注册到Eureka服务器中,routes端点都会返回该路由映射。如果在相应微服务没有启动的情况下去访问,很不幸,Zuul会返回一个500的错误。

3 忽略指定微服务

Zuul路由服务器配置中提供了一个属性zuul.ignored-services,通过设置该属性可以指定在默认映射中所要忽略的微服务。

参数的值可以设置多个服务的ID,如果需要忽略多个服务,那么服务ID之间需使用逗号隔开即可。

如果想让Zuul忽略所有服务的路由映射,并全部采用自定义方式,zuul.ignored-services的值设置为“*”

4 设置路由前缀

Zuul提供了zuul.prefix属性可为所有的路由映射增加统一前缀

默认情况下,Zuul代理会在转发到具体服务实例时自动剥离这个前缀。如果需要在转发时带上该前缀,可以将zuul.stripPrefix属性的值设置为false来关闭这个默认行为。需要说明的是,zuul.stripPrefix只会对zuul.prefix的前缀起作用,而对于path指定的前缀不会起作用。

5 通过静态URL路径配置路由映射

对于没有注册到Eureka服务器中的微服务,可以通过在Zuul路由服务器中配置静态的URL,来进行服务的路由映射

zuul.routes.python-servicece.path=/pythonservice/**

zuul.routes.python-servicece.url=http://pythonserver:8686

通过这种配置,Zuul路由服务器就可以将对pythonservice路径下的所有访问转发到http://pythonserver:8686,而不需要访问Eureka服务器。

但这样就会有一个问题,如果没有经过Eureka服务器,那自然就得不到Ribbon的负载均衡功能了。针对这个问题,Zuul已经帮开发者想到了解决方案,在这种情况下开发者只需要禁用Ribbon与Eureka的自动集成设置,采用手工设置方式开启即可,配置如下:

ribbon.eureka.enabled=false

python-service.ribbon.listOfServices=http://pythonserver:8686,http://pythonserver:8687,http://pythonserver:8688

这时的访问不但支持Ribbon的负载均衡,同时所有转发到该服务的请求将会使用HystrixCommand来执行,服务降级、容错保护等功能也会起效,所以在配置时尽量使用serviceId进行配置,避免直接设置静态URL的方式.

6 路由配置顺序

使用yaml格式的配置文件(properties文件格式,则会丢失配置顺序)

7 自定义路由规则

将形如servicename-vx的服务名称映射为/vx/servicename的访问路径:

public PatternServiceRouteMapper serviceRouteMapper{

return new PatternServiceRouteMapper ("? ^.+)-(? v.+ ) " ,  " )", " )","{version}/${name}")}

这样,服务ID为users-v1的服务,就会被映射到路由为/v1/users/的路径上。但是对于所定义的命名组必须包括servicePattern和routePattern两部分。如果servicePattern没有匹配一个serviceId,那就会使用默认的。在上例中,一个服务实例的ID为users的服务,将会被映射到路由/users/中(不带版本信息)。这个特性默认是关闭的,而且只适用于已经发现的服务。


Zuul路由其他设置

反向代理是通过代理服务器来接受客户端的连接请求,然后将这些请求转发给内部的服务,并将从内部服务上得到的结果返回给请求客户端。

1 Header设置

如果你的微服务依赖其他第三方的服务,这时候或者不想Headers中的一些敏感信息随着HTTP请求转发而泄露出去,那么就需要在路由配置中设置一个忽略Header的清单。

默认情况下,Zuul在请求路由时,会过滤HTTP请求头信息中的一些敏感信息。默认这些敏感头信息通过zuul.sensitiveHeaders定义,包括Cookie、Set-Cookie和Authorization等,若配置多个sensitiveHeaders时可以用逗号分隔。

    # 对指定路由开启自定义敏感头
    zuul.routes.[route].customSensitiveHeaders=true
    zuul.routes.[route].sensitiveHeaders=[这里设置要过滤的敏感头]

全局设置可以使用:

zuul.sensitiveHeaders=[这里设置要过滤的敏感头]

2.忽略Header设置

如果每一个路由都需要配置一些额外的敏感Header时,那么可以通过zuul.ignored Headers来统一设置需要忽略的Header。

zuul.ignored Headers=[这里设置要忽略的Header]

在默认情况下是没有这个配置的,如果项目中引入了Spring Security,那么Spring Security会自动加上这个配置,默认值为Pragma、Cache-Control、X-Frame-Options、X-Content-Type-Options、X-XSS-Protection和Expries。

此时,如果还需要使用下游微服务的Spring Security的Header时,可以增加下面的设置:

zuul.ignoreSecurityHeaders=false

6.5.2 HttpClient配置

Zuul的HTTP客户端支持Apache Http、Ribbon的RestClient和OkHttpClient,默认使用Apache HTTP客户端。可以通过下面的方式启用相应的客户端:

启用Ribbon的RestClient
ribbon.restclient.enabled=true

启用OkHttpClient

​ ribbon.okhttp.enabled=true

如果使用OkHttpClient,需要确保项目中已经包含com.squareup.okhttp3相关包的引用。

6.5.3 路由配置的动态加载

涉及Spring Cloud的另外一个子项目Config(见第七章)

Spring Cloud Config子项目提供了配置文件的统一管理,我们需要将原来存放在src/main/resources目录下的配置文件application.properties或yml文件抽出来统一存放在版本管理服务器上,比如Git中,然后将Zuul路由服务器的配置从统一配置服务器中进行加载。当需要修改路由映射规则时,就需要将修改后的配置文件提交到Git仓库中,然后在Zuul路由服务器中使用/refresh端点重新加载配置


6.6 Zuul容错与回退

查看Hystrix的监控界面,Zuul路由服务在与Hystrix整合的时候其监控的颗粒度是微服务,而不是微服务中具体的某个方法,再次访问方法会报错;

6.6.1 实现Zuul的回退

Zuul提供了一个ZuulFallbackProvider接口,通过实现该接口就可以为Zuul实现回退功能。

@Componentpublic class UserServiceFallbackProvider implements ZuulFallbackProvider {@Overridepublic String getRoute() {// 注意: 这里是route的名称,而不是服务的名称
            // 如果这里写成大写USERSERVICE,则将无法起到回退作用
            return "userservice";}@Overridepublic ClientHttpResponse fallbackResponse() {// 创建一个Fallback响应
            return new ClientHttpResponse() {// 实现响应的状态、状态码的定义
              @Overridepublic HttpStatus getStatusCode() throws IOException {return HttpStatus.OK;}@Overridepublic int getRawStatusCode() throws IOException {return 200;}@Overridepublic String getStatusText() throws IOException {return "OK";}@Overridepublic void close() {}// 针对回退,构建了一个Fake用户信息
              @Overridepublic InputStream getBody() throws IOException {
                  String mockUserJson = "{\n" +"  \"id\": -3, \n" +"  \"nickname\": \"fakeUser\", \n" +"  \"avatar\": \"/users/avatar/user.png\"\n" +"}";return new ByteArrayInputStream(mockUserJson.getBytes());}@Overridepublic HttpHeaders getHeaders() {// 需要将返回的格式设置为JSON
                  HttpHeaders headers = new HttpHeaders();
                  headers.setContentType(MediaType.APPLICATION_JSON_UTF8);return headers;}};}}

针对上面的代码需要说明的是:·getRoute()方法返回的是要为哪个微服务提供回退功能。这里需要注意其返回的值是route的名称,而不是微服务的名称,所以不能写为USERSERVICE,否则该回退将不起作用。

·fallbackResponse()方法返回ClientHttpResponse对象,作为我们的回退响应。这里实现非常简单,仅仅是返回一个假的用户对象。

重启Zuul-Server进行postman实验,如果没有起作用,可以仔细检查一下getRoute()方法的返回是否正确。

6.6.2 服务超时

当Zuul路由服务将客户端请求转发到具体服务时,Zuul会使用HystrixCommand来包装这些执行过程,所以之前我们在Hystrix章节中所讲到的配置及服务容错机制,对于Zuul的请求执行都是适用的,也会影响到API服务网关的行为。

在默认情况下,当Zuul请求执行一个服务时间超过1秒时,则会中断执行并返回一个500的错误,通过hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds属性重新设置这个值。

也可以针对某一个服务独立设置

hystrix.command.userservicet.execution.isolation.thread.timeoutInMilliseconds=5000

另外,Ribbon在服务执行时也有一个超时时间配置,默认为5秒钟,我们可以通过servicename. ribbon.ReadTimeout属性设置最大超时时间。

需要强调的一点是这里需要同时设置Hytrix和Ribbon的值,只设置其中一个是不行的。


6.7 Zuul过滤器

实际上,Zuul路由映射和请求转发这些功能都是由几个不同的过滤器组合完成的,所以说Zuul的过滤器才是核心所在。

6.7.1 过滤器特性当我们要实现一个Zuul过滤器时需要继承ZuulFilter,来看一下ZuulFilter的源码:核心关键点:

·Type:定义在请求执行过程中何时被执行;

·Execution Order:当存在多个过滤器时,用来指示执行的顺序,值越小就越早执行;

·Criteria:执行的条件,即该过滤器何时被触发;

·Action:该过滤器具体要执行的动作。

在实现一个自定义过滤器时需要实现的方法有以下几点:

·filterType()方法返回过滤器的类型;

·filterOrder()方法返回过滤器的执行顺序;

·shouldFilter()方法判断是否需要执行该过滤器;

·run()方法是该过滤器所要执行的具体过滤动作。

6.7.2 过滤器类型及生命周期

·PRE过滤器:在请求被路由之前调用,可用来实现身份验证、在集群中选择请求的微服务、记录调试信息等。

·ROUTING过滤器:在调用目标服务之前被调用,通常可以用来处理一些动态路由。比如,A/B测试,在这里可以随机让部分用户访问指定版本的服务,然后通过用户体验数据的采集和分析来决定哪个版本更好。另外,还可以结合PRE过滤器实现不同版本服务之间的处理

·POST过滤器:在目标微服务执行以后,所返回的结果在送回给客户端时被调用,我们可以利用该过滤器实现为响应添加标准的HTTP Header、数据采集、统计信息和指标、审计日志处理等。

·ERROR过滤器:该过滤器在处理请求过程中发生错误时被调用,可以使用该过滤器实现对异常、错误的统一处理,从而为客户端调用显示更加友好的界面。

此外,Zuul还允许我们创建自定义的过滤器类型。例如,可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应(PRE过滤器之后执行),而不将请求转发到后端的微服务上。


6.7.3 自定义Zuul过滤器

该过滤器是一个用户认证校验过滤器,同时是一个PRE类型的过滤器,通过对用户的请求进行拦截从而获取HTTP请求头部中的认证信息JWT令牌,并对令牌信息进行解析。如果令牌信息不存在或不正确,那么就会抛出错误,如果能够正确解析,将会把解析之后的数据存放到Zuul请求的头部中,所调用的下游服务就可以直接使用这些信息,而不用再次对JWT中的信息进行解析。(第十章)

6.7.4 禁用Zuul过滤器

zuul.[filter-name].[filter-type].disable=true

禁用FormBodyWrapperFilter中的PRE过滤器:

zuul.FormBodyWrapperFilter.pre.disable=true

5 关于Error过滤器的一点补充

如果在Zuul执行过程中抛出异常的话,那么Error过滤器就会被执行。而Zuul内部所定义的SendErrorFilter只有在RequestContext.getThrowable()不为空的时候才会执行,它将错误信息设置到请求的javax.servlet.error.*属性中,并转发Spring Boot的错误页面进行处理。

ZuulServlet Filter核心代码:

Override
        public void doFilter(ServletRequest servletRequest,
            ServletResponse servletResponse, FilterChain filterChain)throws IOException, ServletException {try {init((HttpServletRequest) servletRequest,(HttpServletResponse) servletResponse);try {preRouting();} catch (ZuulException e) {// 对于PRE类型的路由出错后会被error捕捉,并执行POST路由处理
                  error(e);postRouting();return;}// 只有当前响应还没有返回时才可以继续执行
              if (! RequestContext.getCurrentContext().sendZuulResponse()) {
                  filterChain.doFilter(servletRequest, servletResponse);return;}try {routing();} catch (ZuulException e) {// 对于ROUTING类型的路由出错后会被Error捕捉,并执行POST路由处理
                  error(e);postRouting();return;}try {postRouting();} catch (ZuulException e) {// POST类型的路由出错后只会被error捕捉
                  error(e);return;}} catch (Throwable e) {error(new ZuulException(e,500, "UNCAUGHT_EXCEPTION_FROM_FILTER_" +
                      e.getClass().getName()));} finally {
                  RequestContext.getCurrentContext().unset();}}




从以上代码中可以看出,error()方法可以在所有阶段捕获异常后执行,但如果是在post阶段中出现的异常被error()方法处理后,则不再继续回到post阶段执行了,而其他阶段则是在error()方法处理后转入到post阶段继续进行处理。也就是说我们的代码需要保证在post阶段不再有异常抛出,因为一旦有异常后就会造成该过滤器后面其他的post过滤器将不再被执行。

基于Zuul过滤器执行的逻辑,可以很方便地添加一个全局异常处理。添加的方法是:新增一个类型为Error的过滤器,在该过滤器中将错误信息写入RequestContext中,这样SendErrorFilter就可以获取该错误信息,并转发到Spring Boot中进行通用的错误处理。新建的Error类型的过滤器代码如下:


public class GlobalErrorFilter extends ZuulFilter {@Overridepublic String filterType() {// 类型为:Error
              return ERROR_TYPE;}@Overridepublic int filterOrder() {// 执行的顺序设置为10
              return 10;}@Overridepublic boolean shouldFilter() {// 默认对所有的请求都执行该过滤器
              return true;}@Overridepublic Object run() {// 这里处理的核心就是将错误信息设置到RequestContext中
              RequestContext context = RequestContext.getCurrentContext();
              Throwable throwable = context.getThrowable();this.logger.error("[ErrorFilter] error message: {}",
                  throwable.getCause().getMessage());// 将捕捉的错误设置到context中
              context.set("error.status_code",
                  HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
              context.set("error.exception", throwable.getCause());return null;}}

6.8 @EnableZuulServer与@EnableZuulProxy比较

@EnableZuulProxy注解包含@EnableZuulServer的所有功能,并且还加入了@EnableCircuitBreaker和@EnableDiscoveryClient。

当我们需要运行一个没有代理功能的Zuul服务,或者需要有选择地开/关部分代理功能时,需要使用 @EnableZuulServer替代@EnableZuulProxy。这时开发者添加的任何ZuulFilter类型实体类都会被自动加载

但@EnableZuulProxy不会自动加载任何代理过滤器。


6.8.1 EnableZuulServer注解的过滤器

默认所加载的过滤器有以下几个。

1.PRE类型过滤器

·ServletDetectionFilter:该过滤器是最先被执行的。其主要用来检查当前请求是通过Spring的DispatcherServlet处理运行的,还是通过ZuulServlet来处理运行的?判断结果会保存在isDispatcherServletRequest中,值类型为布尔型。

·FormBodyWrapperFilter:该过滤器的目的是将符合要求的请求体包装成FormBody RequestWrapper对象,以供后续处理使用。

·DebugFilter:当请求参数中设置了debug参数时,该过滤器将当前请求上下文中的debugRouting和debugRequest设置为true,这样后续的过滤器可以根据这两个参数信息定义一些debug信息,当生产环境出现问题时,可以通过增加该参数让后台打印出debug信息,以帮助开发者进行问题分析。对于请求中的debug参数的名称,可以通过zuul.debug.parameter属性进行自定义。

2.ROUTE类型过滤器

·SendForwardFilter:该过滤器只对请求上下文中存在forward.to (Filter Constants.FORWARD_TO_KEY)参数的请求进行处理,即处理之前我们路由规则中forward的本地跳转。

3.POST类型过滤器

·SendResponseFilter:该过滤器是对代理请求所返回的响应进行封装,然后作为本次请求的响应发送给请求者。

4.Error类型过滤器

·SendErrorFilter:该过滤器判断当前请求上下文中是否有异常信息,判断的标准是RequestContext.getThrowable()是否不为空,如果有,则转发到Spring Boot默认的错误页面:/error,也可以通过设置error.path来自定义错误页面。

6.8.2 EnableZuulProxy注解的过滤器

@EnableZuulProxy则在@EnableZuulServer的基础上增加了以下过滤器。

1.PRE类型过滤器

·PreDecorationFilter:该过滤器根据提供的RouteLocator确定路由到的地址,以及怎样去路由。该路由器也可为后端请求设置各种代理相关的header。

2.ROUTE类型过滤器

·RibbonRoutingFilter:该过滤器会针对上下文中存在serviceId(可通过RequestContext.getCurrentContext().get(“serviceId”)获取)的请求进行处理,使用Ribbon、Hystrix和可插拔的HTTP客户端发送请求,并将服务实例的请求结果返回。也就是之前所说的只有当我们使用serviceId配置路由规则时Ribbon和Hystrix方才生效。

·SimpleHostRoutingFilter:该过滤器检测到routeHost参数(可通过RequestContext.getRouteHost()获取)设置时,则会通过ApacheHttpClient向指定的URL发送请求。此时,该请求不会使用HystrixCommand进行包装,所以这类请求也就没有线程隔离和服务容错保护功能。

你可能感兴趣的:(springcloud)