SpringCloud系列之服务网关Zuul(五)

本篇文章来学习微服务当中非常重要的一个知识点-服务网关(API GateWay),什么叫服务网关呢?为了方便理解,先举个简单的例子,比如假如先在要实现业务功能A,而功能A可能需要调用服务1,服务2,,服务3...,那么如何调用呢?看下面的图


这样调用的方式是最常用,当然肯定也是能完成功能的,那么有没有问题呢?问题肯定是有的,因为既然是微服务,说明服务的拆分粒度肯定是很细的,可能一个功能需要调用几十个服务甚至上百个可能更多,如果是这种情况会存在哪些问题呢?

1 对于上面的功能A,它需要知道每个服务的IP和端口号,这对于它来说是个灾难(因为要完全通过硬编码写入,一旦一个变化,修改起来,不敢想象)

2 对于上的功能A,它可能还会做一些其他的非业务相关的功能,比如在调用某个服务的时候,需要做用户权限的验证,访问的流量监控,如果这些都是它来做,那也是一个不可想象的灾难

3 对于上面的功能A,可能先需要查询Service1和Servcie2获取到一个结果,然后再把Service3的结果进行聚合,从而达到最终的结果,也就是说要做服务的聚合,如果每个聚合都需要它自身完成,这个业务也太复杂了,维护起来也很困难

4 对于上面的功能A,可能需要在不同的载体中进行展示,比如有的需要在PC端,有的需要在移动端,还有的需要在APP端,如果都要它来区分的话,它就是太重了

上面随便分析了直接调用服务存在的一些弊端,既然知道了问题,那么肯定就需要解决方案?其实也很容易想出来,不管怎么解决,其实思想就是进行职责的拆分,尽量让功能A所做都是核心业务的事情,非核心业务的事情就不需要它做,让个其他人做不就可以了吗?

演变一下调用上面的调用方式图:


其实对比之下就是多了一个黄色标注的的载体,不错那就是我们所说的服务网关,把一些事情就分配给它来做,看一下它的定义

API网关是一个服务器,是系统的唯一入口。从面向对象设计的角度看,它与外观模式类似。API网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理。

API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关也是提供REST/HTTP的访问API。服务端通过API-GW注册和管理服务。

其实在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个API网关根据请求的url,路由到相应的服务。当添加API网关后,在第三方调用端和服务提供方之间就创建了一面墙,这面墙直接与调用方通信进行权限控制,后将请求均衡分发给后台服务端,下面简单总结一下它的好处:

服务网关的好处:

1 屏蔽了客户端调用服务端的复杂性

》客户端不需要具体指导每个服务的存在具体信息,比如服务的域名 ,端口号 是否可访问等,这些都由网关来做

2 数据的聚合和适当的剪枝

》网关自身来进行部分服务数据的部分聚合,或者根据适当的场景进行数据的剪枝

3 数据的多客户端或者多版本的支持

》网关可以根据不同的展示载体,比如上面说到的PC端,移动端,APP端定制不同的展示,当然也可以支持多版本等

4 实现用户权限的验证,流量限制,日志拦截等

》这些功能本身就并非是业务的核心,给网关来做非常的合适

OK,那么网关的作用很大,如果是自己开发一个这样的组件,那是很庞大的工程啊,放心开源的世界已经为我们提供了多种解决方案:

Tyk:Tyk是一个开放源码的API网关,它是快速、可扩展和现代的。Tyk提供了一个API管理平台,其中包括API网关、API分析、开发人员门户和API管理面板。Try 是一个基于Go实现的网关服务。

Kong:Kong是一个可扩展的开放源码API Layer(也称为API网关或API中间件)。Kong 在任何RESTful API的前面运行,通过插件扩展,它提供了超越核心平台的额外功能和服务。

Orange:和Kong类似也是基于OpenResty的一个API网关程序,是由国人开发的,学姐也是贡献者之一。

Netflix zuul:Zuul是一种提供动态路由、监视、弹性、安全性等功能的边缘服务。Zuul是Netflix出品的一个基于JVM路由和服务端的负载均衡器。

apiaxle: Nodejs 实现的一个 API 网关。

api-umbrella: Ruby 实现的一个 API 网关。

方案很多种,目前业界最常用的就是nginx+lua的这种方式,如果使用springcloud推荐使用zuul,也是接下来要学习的组件,下面主要介绍zuul相关知识:

& zuul的各种配置使用举例

继续在之前的项目建立一个新模块叫server_zuul:,不要写多少代码,只需要写一个main方法的类,看一下项目结构,后续的功能都会这个上进行扩展


看一下pom.xml的文件内容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-cloud</artifactId>
        <groupId>com.suning.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>server_zuul</artifactId>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--集成ZUUL网关API-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zuul</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
    </dependencies>
</project>

看一下main方法的类

package com.server.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

/**
 * @Author 18011618
 * @Description
 * @Date 14:17 2018/7/12
 * @Modify By
 */
@SpringBootApplication
@EnableZuulProxy
public class ServerZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServerZuulApplication.class,args);
    }
}
 
 

要使用zuul功能,只需要在应用类上加一个新的注解即可,OK 看一下配置文件

spring:
  application:
    name: service-gatway-zuul
server:
  port: 9005
eureka:
  client:
    service-url:
      defaultZone:  http://admin:admin@localhost:8080/eureka
  instance:
    prefer-ip-address: true
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
  ConnectTimeout: 3000
  ReadTimeout: 60000
 
 

OK 启动应用类以及service_provider_user的应用,访问浏览器看看效果:http://localhost:9005/service-provider-user/findUser/1,意向不到的结果发生了:既然能真的访问到数据:

9005是zuul的端口号,而service-provider-user是服务提供者的名称,这就说明了zuul真的实现了服务转发调用的功能,但是貌似配置文件也没有配置啥?这是怎么实现的呢,原因很简单因为zuul提供了一个默认的配置规则:http://ZUUL_HOST:ZUUL_PORT/微服务在Eureka上的serviceId/**,也就是说zuul会自己去eureka上寻找注册服务的应用名称也就是serviceId(这个ID实际还可以自己指定,后面会讲到),上面因为使用的是默认配置,所以看起来比较长,能不能简化点呢,当然可以,实际使用中我们可能希望能根据业务类型来加访问区分字段,比如和用户有关的叫/user/**,和店铺有关的叫/shop/**,和商品有关的/product/**,和价格有关的叫/price/**,这个也是更加符合使用习惯,下面就修改配置文件实现这样的功能..,对于刚才的service-provider-user,希望通过/user/**这种方式来访问:在之前的配置文件中加一下这个配置即可,如下图所示

重启应用试试看:http://localhost:9005/user/findUser/2:也是可以正常访问的

,效果达到我们所想要的,当然可能有的一些服务,不需要使用这种自定义的访问方式,而是使用默认的话,这个也很简单只需要加一个这样的配置,比如我们希望service-provider-user2这个服务不能使用/user/**这种方式访问,这样配置

接下来再说中配置方法,就是为不同的微服务加上不同的path,看一下下面这个修改之后的配置:

这个配置的作用实现了,当要访问service-provider-user使用/user/**这种方式,而对于user-service-consumer-ribbon需要使用/user-ribbon/**这种方式,大家可以自行验证一下效果,接下来再说一种配置方式,假如一个服务没有注册到eureka上该怎么办,没有关系因为zuul也支持具体的url访问方式,如下所示

这样配置也是OK的。

,再看这样的一种配置,一个服务没有注册到eureka上,但是还要实现负载均衡,这种配置该怎么实现呢?看如下截图

一样的,可以看一下访问效果,如果要对访问前缀有需求的话,可以使用下面这样的配置

映射关系如下所示:http://127.0.0.1:8181/api/user/list -> http://192.168.1.100:8080/user/list,如果为false的话

映射关系如下所示:http://127.0.0.1:8181/api/user/list -> http://192.168.1.100:8080/api/user/list,如果为指定的url进行设置,可以按照这样配置:

到这里有关和zuul常用的配置就讲解的差不多了...,下面给一份完整的配置例子

spring:
  application:
    name: service-gatway-zuul
server:
  port: 9005
eureka:
  client:
    service-url:
      defaultZone:  http://admin:admin@localhost:8080/eureka
  instance:
    prefer-ip-address: true
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
  ConnectTimeout: 3000
  ReadTimeout: 60000

#为部分指定访问路径,其它服务使用默认访问路径
#zuul:
#  routes:
#    ignoredServices: service-provider-user2
#    service-provider-user: /user/**     指定service-provider-user使用/user/**访问,2服务不能使用

#为不同的服务指定不同的访问path
#zuul:
#  routes:
#    provider:  #ID 名称随便起 但是要保证不重复即唯一
#      path: /user/**
#      serviceId:  service-provider-user
#    consumer:  #ID 名称随便起 但是要保证不重复即唯一
#      path: /user-ribbon/**
#      serviceId:  user-service-consumer-ribbon

#服务没有注册到eureka
#zuul:
#  routes:
#    provider:
#      path: /user-url/**
#      url: http://localhost:9002/
#一个服务没有注册到eureka,但是还要实现负载均衡
#zuul:
#  routes:
#    user:
#      path: /user-url/**
#      service-id: service-provider-user
#ribbon:
#  eureka:
#    enabled: false  #禁用到eureka
#
#service-provider-user:     # 这边是ribbon要请求的微服务的serviceId
#  ribbon:
#    listOfServers: http://localhost:7900,http://localhost:7901 #指定当前服务提供者的具体地址


#http://127.0.0.1:8181/api/user/list -> http://192.168.1.100:8080/user/list
#zuul:
#  prefix: /api
#  strip-prefix: true

#http://127.0.0.1:8181/api/user/list -> http://192.168.1.100:8080/user/list
#zuul:
#  prefix: /api
#  strip-prefix: false

#为指定的映射路径 进行忽略
zuul:
  prefix: /findUser
  strip-prefix: false
在这里说一下通配符,上面我们都是使用/**,其实还有/?,/*,这种方式,这些方式有啥区别呢,看下面

》:/user/**可以匹配,/user/a/b,/user/a/b/c=多级目录

》:/user/*可以匹配,/user/aaa,/user/abcd=匹配多个字符

》:/user/?可以匹配,/user/a,/user/b=单一字符

多个服务先后顺序:

这里有个问题要注意,就是实际当中如果访问的服务是有顺序要求的话,切记不能使用properties来做配置文件,因为它不支持先后顺序,要使用yaml格式,它会按照你配置服务的先后顺序来保存。

& zuul的fallback机制

zuul已经集成了hystrix的功能,所以它也有回退,在调用失败的时候,可以获取到一些信息以及给客户端返回一些信息,比如下面要实现的一个功能就是,当服务者关闭的时候,向客户端显示调用信息,而非服务不可用,实现这个功能很简单自定一个类实现,看一下代码

package com.server.zuul.fallback;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
/**
* @Author 18011618
* @Date 16:45 2018/7/12
* @Function zuul调用失败了的回退的功能
*/
@Component
public class MyFallbackProvider implements ZuulFallbackProvider {
  @Override
  public String getRoute() {
    return "service-provider-user";
  }

  @Override
  public ClientHttpResponse fallbackResponse() {
    return new ClientHttpResponse() {
      @Override
      public HttpStatus getStatusCode() throws IOException {
        return HttpStatus.BAD_REQUEST;
      }

      @Override
      public int getRawStatusCode() throws IOException {
        return HttpStatus.BAD_REQUEST.value();
      }

      @Override
      public String getStatusText() throws IOException {
        return HttpStatus.BAD_REQUEST.getReasonPhrase();
      }

      @Override
      public void close() {
      }

      /**
      * @Author 18011618
      * @Date 16:49 2018/7/12
      * @Function 这里可以向客户端展示信息
      */
      @Override
      public InputStream getBody() throws IOException {
        String result ="sorry,you get provider service is faild:";
        return new ByteArrayInputStream(("fallback:" + MyFallbackProvider.this.getRoute()).getBytes());
      }

      @Override
      public HttpHeaders getHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        return headers;
      }
    };
  }
}

直接看效果吧,重新启动应用,然后关闭服务提供者,看一下会显示啥

是不是很简单就实现了回退功能,实际环境中,这里可以写复杂的功能,这里只是为了演示,所以比较简单,当然它还支持过滤器的功能,下面就讲解这个

& zuul的filter各种场景分析

zuul可以实现各种与核心业务无关的一些功能,比如权限的验证,日志的拦截,流量限制,URL的改写,要实现这些功能,只需要定义自己的filter即可,下面举一个最简单的例子,在调用服务之前,打印对应的host信息。建立一个类继承即可,看一下主要代码

package com.server.zuul.filter;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
/**
* @Author 18011618
* @Date 16:59 2018/7/12
* @Function 日志拦截功能
*/
public class PreZuulFilter extends ZuulFilter {
  private static final Logger LOGGER = LoggerFactory.getLogger(PreZuulFilter.class);

  @Override
  public boolean shouldFilter() {
    return true;
  }

  @Override
  public Object run() {
    HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
    String host = request.getRemoteHost();
    PreZuulFilter.LOGGER.info("您当前请求的host:{}", host);
    return null;
  }

  @Override
  public String filterType() {
    return "pre";
  }

  @Override
  public int filterOrder() {
    return 1;
  }

}
 
 

详细分下一下里面的方法:

shouldFilter():一定是为true,不能是false,否则执行不了
run():实现具体业务逻辑的方法,比如权限验证,日志拦截,url的改写等 需要在这个方法来实现

filterType():返回方法执行的时机,pre:路由之前,routing:路由之时,post:路由之后,error:路由错误的时候
filterOrder():方法过滤的顺序

好了,重启应用,看看红色标注的日志拦截功能是否能够打印出来?


& zuul的pattern机制

你可以使用regexmapper提供serviceId和routes之间的绑定. 它使用正则表达式组来从serviceId提取变量, 然后注入到路由表达式中,在main的主类里面加一下这个代码

@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
    return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)", "${version}/${name}");
}

这表示serviceId “myusers-v1” 将会被映射到 “/v1/myusers/“.任何正则表达式都可以,但是所有的命名组都必须在servicePattern和routePattern中存在。如果servicePattern没有匹配到一个serviceId,默认的行为会被启用

&zuul的生成环境使用建议:

因为是网关,所以所有的请求都会优先进过zuul,那么也就是说在并发量很大情况下,有可能zuul就成为了瓶颈,所以可能也要考虑zuul的HA机制,还有可以根据自身业务需求对网关进行分类,不一定就一个网关,可以使用多重网关,比如有的网关是做权限验证以及日志拦截,还有的网关是做数据聚合的,这样对于zuul的压力也进行分摊,最后把上面的流程图再优化一下:


GateWay1:做权限验证,拦截,GateWay2:做数据聚合,剪枝等,至此和zuul相关的网关功能就介绍完了.

版权声明:本文为博主原创文章,未经博主允许不得转载:https://blog.csdn.net/qq_18603599/article/details/80941704





猜你喜欢

转载自blog.csdn.net/qq_18603599/article/details/80941704
今日推荐