SpringCloud学习之四:服务网关之Zuul

服务网关之Zuul

Spring Cloud版本:Hoxton.SR5

1. 简介

之前的文章已经搭建服务注册中心、服务提供者、服务消费者以及Turbine聚合监控。随着微服务越来越大,服务调用也将越来越麻烦。因为一个正常的业务处理可能涉及到多个服务调用,那么客户端就需要多次调用不同的微服务。这样就存在如下问题:

  • 客户端需要多次调用http请求。多次创建/销毁http连接,效率低
  • 每个微服务都需要实现一套认证功能。若认证方式不同,客户端的逻辑就更加复杂
  • 跨域问题。每个微服务都需要进行跨域处理
  • 客户端可能需要维护多个ip、端口。若服务进行迁移,客户端改动大

所以可以通过增加一个服务网关解决这些问题。所有的外部请求都先通过服务网关,在网关中进行统一的认证,认证通过后转发到相应的微服务中。这样客户端就仅与服务网关进行交互,隐藏了各个微服务。

graph LR A[客户端] --统一认证--> B[服务网关] B -.抓取注册信息.-> C[服务注册中心] D[微服务A] -.注册/续约.-> C E[微服务...] -.注册/续约.-> C F[微服务N] -.注册/续约.-> C B --转发--> D B --转发--> E B --转发--> F

2. Zuul网关

2.1 实现

  • 创建一个Spring Boot项目,引入如下依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        <exclusions>
            <exclusion>
                <groupId>com.fasterxml.jackson.dataformat</groupId>
                <artifactId>jackson-dataformat-xml</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    </dependency>
    
  • 在启动类上添加@EnableZuulProxy注解,声明一个Zuul代理。

    @EnableZuulProxy
    @SpringBootApplication
    public class SclZuulApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SclZuulApplication.class, args);
        }
    
    }
    
  • 编写配置文件application.yml

    server:
      port: 8500
    spring:
      application:
        name: scl-zuul # 服务名称
    eureka:
      client:
        service-url:
          defaultZone: http://root:123456@test1:8100/eureka-server1/eureka,http://root:123456@test2:8200/eureka-server2/eureka,http://root:123456@test3:8300/eureka-server3/eureka # 服务注册中心地址
    management:
      endpoint:
        health:
          show-details: always # 展示详细的健康检查信息
      endpoints:
        web:
          exposure:
            include: info,health,routes,hystrix.stream # 暴露actuator端点
    
  • 启动项目,请求url http://localhost:8500/微服务名称/url,zuul会自动转发到对应的微服务上。
    在这里插入图片描述

2.2 路由配置详解

  • 自定义指定微服务的访问路径

    zuul.routes.微服务名称 = 指定路径

    zuul:
      routes:
        scl-eureka-client-consumer: /secc/**
    

    此时访问 http://localhost:8500/scl-eureka-client-consumer/consumer/infohttp://localhost:8500/secc/consumer/info 的结果一致。都会转发到scl-eureka-client-consumer对应的微服务上。

  • 忽略指定微服务

    zuul.ignored-services.需要忽略的微服务名称

    zuul:
      ignored-services: scl-eureka-client-provider
    

    此时访问 http://localhost:8500/scl-eureka-client-provider/consumer/info将无法进行转发,返回404

  • 忽略所有微服务,只路由指定微服务

    zuul:
      ignored-services: '*' # 使用'*'可忽略所有微服务
      routes:
        scl-eureka-client-consumer: /secc/**
    
  • 同时指定path和URL

    zuul:
      routes:
        # user-route为路由名称,可以任意起名
        user-route:
          url: http://localhost:8090 # 指定的url
          path: /user/**             # url对应的路径
    

    此时访问 http://localhost:8500/user/** 将会转发到 http://localhost:8090/**

    这种方式配置的路由不会作为HystrixCommand执行,也无法使用Ribbon进行负载均衡多个URL。

  • 同时指定path和URL,并且不破坏Zuul的Hystrix和Ribbon特性

    zuul:
      routes:
        # user-route为路由名称,可以任意起名
        user-route:
          path: /user/**              # url对应的路径
          service-id: 'a'        
    ribbon:
      eureka:
        enabled: false                # 为Ribbon禁用Eureka
    a:
      ribbon:
        listOfServers: localhost:8080,localhost:8090
    
    • 若指定的service-id已存在服务注册中心里,则后续的不需要配置,结果与示例1相同
    • 若指定的service-id不存在服务注册中心里,则后续的必须配置

2.3 Zuul过滤器

Zuul中定义了4中标准过滤器

  • PRE:这种过滤器在请求被路由之前调用。可利用这种过滤器实现身份验证等
  • ROUTING:这种过滤器建将请求路由到微服务。这种过滤器用于构建发送给微服务的请求
  • POST:这种过滤器在路由到微服务之后执行。可为响应添加Header、收集统计信息和指标等
  • ERROR:在其他阶段发生错误时执行该过滤器

本文可实现一个简单的Zuul过滤器

  • 实现一个自定义的Zuul过滤器

    public class PreRequestFilter extends ZuulFilter {
    
        private static final Logger log = LoggerFactory.getLogger(PreRequestFilter.class);
    
        @Override
        public String filterType() {
            // 过滤器类型,可返回pre、routing、post、error
            return "pre";
        }
    
        @Override
        public int filterOrder() {
            // 过滤器的执行顺序
            return 1;
        }
    
        @Override
        public boolean shouldFilter() {
            // 可通过一些业务逻辑判断是否要执行该过滤器。true执行,false不执行
            log.info("该请求将经过过滤器...");
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
            // 过滤器的具体逻辑
            log.info("进行身份认证等处理...");
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();
            log.info("该请求信息为: " + request.getMethod() + " " + request.getRequestURL());
            return null;
        }
    }
    
  • 在启动类中添加如下内容

    @Bean
    public PreRequestFilter preRequestFilter() {
        return new PreRequestFilter();
    }
    
  • 启动项目,访问http://localhost:8500/微服务名称/url,会在控制台看见如下输出内容。表明自定义的Zuul过滤器被执行了。
    在这里插入图片描述

2.4 为Zuul添加回退

  • 实现一个Zuul的回退类

    @Component
    public class ProviderFallback implements FallbackProvider {
        @Override
        public String getRoute() {
            // 指定为哪个微服务提供回退
            return "scl-eureka-client-provider";
        }
    
        @Override
        public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
            return new ClientHttpResponse() {
                @Override
                public HttpStatus getStatusCode() throws IOException {
                    // 回退的状态码
                    return HttpStatus.OK;
                }
    
                @Override
                public int getRawStatusCode() throws IOException {
                    // 数字类型的状态码
                    return this.getStatusCode().value();
                }
    
                @Override
                public String getStatusText() throws IOException {
                    // 状态文本
                    return this.getStatusCode().getReasonPhrase();
                }
    
                @Override
                public void close() {
    
                }
    
                @Override
                public InputStream getBody() throws IOException {
                    // 响应体
                    return new ByteArrayInputStream("用户微服务不可用,请稍后再试。".getBytes());
                }
    
                @Override
                public HttpHeaders getHeaders() {
                    // 响应头
                    HttpHeaders headers = new HttpHeaders();
                    MediaType mt = new MediaType("application", "json", StandardCharsets.UTF_8);
                    headers.setContentType(mt);
                    return headers;
                }
            };
        }
    }
    
  • 启动项目,并停止scl-eureka-client-provider对应的服务。然后访问http://localhost:8500/scl-eureka-client-consumer/consumer/info
    在这里插入图片描述

2.5 使用Zuul聚合微服务

客户端的一个操作可能需要请求多次微服务,如果只是通过Zuul进行转发,那么网络开销、耗费时长依然会很长。所以我们可以使用Zuul聚合微服务请求。做到客户端只发送一个请求给Zuul,由Zuul请求其他的微服务,将数据组织好后返回。

本文利用RxJava结合Zuul实现微服务的聚合请求。

  • 需要利用OpenFeign请求其他的微服务,因此需要引入依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
  • 创建CustomerFeign接口,访问服务消费者接口

    @FeignClient(name = "scl-eureka-client-consumer", fallbackFactory = ConsumerFeignFallback.class)
    public interface ConsumerFeign {
    
        @GetMapping("/consumer/info")
        Map<String, String> get();
    
    }
    
    @Component
    class ConsumerFeignFallback implements FallbackFactory<ConsumerFeign> {
    
        @Override
        public ConsumerFeign create(Throwable throwable) {
            return () -> {
                Map<String, String> map = new HashMap<>();
                map.put("errcode", "500");
                map.put("errmsg", "Consumer异常,请稍后重试");
                return map;
            };
        }
    }
    
  • 创建ProviderFeign接口,访问服务提供者接口

    @FeignClient(name = "scl-eureka-client-provider", fallbackFactory = ProviderFeignFallback.class)
    public interface ProviderFeign {
    
        @GetMapping("/provider/info")
        Map<String, String> get();
    
    }
    
    @Component
    class ProviderFeignFallback implements FallbackFactory<ProviderFeign> {
    
        @Override
        public ProviderFeign create(Throwable throwable) {
            return () -> {
                Map<String, String> map = new HashMap<>();
                map.put("errcode", "500");
                map.put("errmsg", "Provider异常,请稍后重试");
                return map;
            };
        }
    }
    
  • 在启动类中添加@EnableHystrix@EnableFeignClients注解,支持Feign请求调用及回退

    @EnableHystrix
    @EnableZuulProxy
    @EnableFeignClients
    @SpringBootApplication
    public class SclZuulApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SclZuulApplication.class, args);
        }
    
        @Bean
        public PreRequestFilter preRequestFilter() {
            return new PreRequestFilter();
        }
    
    }
    
  • 修改配置文件application.yml,添加如下配置,使Feign支持Hystrix进行熔断回退

    feign:
      hystrix:
        enabled: true
    
  • 实现一个聚合服务类

    @Service
    public class AggregationService {
    
        private final ConsumerFeign consumerFeign;
        private final ProviderFeign providerFeign;
    
        @Autowired
        public AggregationService(ConsumerFeign consumerFeign, ProviderFeign providerFeign) {
            this.consumerFeign = consumerFeign;
            this.providerFeign = providerFeign;
        }
    
        public Observable<Map> getByConsumer() {
            return Observable.create(observer -> {
                Map<String, String> map = this.consumerFeign.get();
                observer.onNext(map);
                observer.onCompleted();
            });
        }
    
        public Observable<Map> getByProvider() {
            return Observable.create(observer -> {
                Map<String, String> map = this.providerFeign.get();
                observer.onNext(map);
                observer.onCompleted();
            });
        }
    }
    
  • 创建一个Controller,聚合多个请求。

    @RestController
    @RequestMapping("/aggregate")
    public class AggregationController {
    
        private static final Logger log = LoggerFactory.getLogger(AggregationController.class);
    
        private final AggregationService aggregationService;
    
        @Autowired
        public AggregationController(AggregationService aggregationService) {
            this.aggregationService = aggregationService;
        }
    
        @GetMapping
        public DeferredResult<Map> aggregate() {
            Observable<Map> result = this.aggregateObservable();
            return this.toDeferredResult(result);
        }
    
        private DeferredResult<Map> toDeferredResult(Observable<Map> result) {
            DeferredResult<Map> deferredResult = new DeferredResult<>();
            result.subscribe(new Observer<Map>() {
                @Override
                public void onCompleted() {
                    log.info("执行完成...");
                }
    
                @Override
                public void onError(Throwable e) {
                    log.info("执行失败:" + e.toString());
                }
    
                @Override
                public void onNext(Map map) {
                    log.info("onNext...");
                    deferredResult.setResult(map);
                }
            });
            return deferredResult;
        }
    
        private Observable<Map> aggregateObservable() {
            return Observable.zip(
                    this.aggregationService.getByConsumer(),
                    this.aggregationService.getByProvider(),
                    (consumer, provider) -> {
                        Map<String, Map<String, String>> map = new HashMap<>();
                        map.put("consumer", consumer);
                        map.put("provider", provider);
                        return map;
                    }
            );
        }
    }
    
  • 启动项目,访问http://localhost:8500/aggregate
    在这里插入图片描述

  • 停止服务提供者,再次访问http://localhost:8500/aggregate

    因为服务提供者停止,所以provider对应的数据在Zuul中进行熔断,返回Zuul中回退数据;服务消费者正常请求,在服务消费者请求服务提供者时熔断,返回服务消费者中回退数据。
    在这里插入图片描述

  • 由上述可知,服务聚合与Hystrix容错都正常

2.6 Turbine监控

之前已经利用Turbine+RabbitMQ+dashboard实现了聚合监控,因为也可将Zuul纳入监控中。只需将Zuul将Hystrix上报到RabbitMQ中接口。

  • 添加spring-cloud-netflix-hystrix-streamspring-cloud-starter-stream-rabbit依赖。完整依赖如下

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        <exclusions>
            <exclusion>
                <groupId>com.fasterxml.jackson.dataformat</groupId>
                <artifactId>jackson-dataformat-xml</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-netflix-hystrix-stream</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
    
    
  • 在配置文件中添加RabbitMQ的连接信息。完整配置如下

    server:
      port: 8500
    spring:
      application:
        name: scl-zuul # 服务名称
      rabbitmq:
        host: 127.0.0.1
        port: 5672
        username: guest
        password: guest
    eureka:
      client:
        service-url:
          defaultZone: http://root:123456@test1:8100/eureka-server1/eureka,http://root:123456@test2:8200/eureka-server2/eureka,http://root:123456@test3:8300/eureka-server3/eureka # 服务注册中心地址
    management:
      endpoint:
        health:
          show-details: always
      endpoints:
        web:
          exposure:
            include: info,health,routes,hystrix.stream
    zuul:
      routes:
        scl-eureka-client-consumer: /secc/**
    feign:
      hystrix:
        enabled: true
    
  • 重启项目,访问http://localhost:8401/hystrix,在输入框输入Turbine的监控端口地址http://localhost:8401,然后点击Monitor Stream按钮,即可看到监控数据
    在这里插入图片描述

posted @ 2020-07-01 15:45  禁忌夜色153  阅读(386)  评论(0编辑  收藏  举报