网关介绍以及搭建

约定不等于承诺〃 2023-10-11 20:44 142阅读 0赞

网关


学习目标:

1、网关的作用

2、独立搭建Zuul网关

3、会用zuul网关路由功能

4、了解Zuul的过滤器

此山是我开,此树是我栽,要想此路过,留下买路财。

奈何桥是中国民间神话观念中是送人转世投胎必经的地点,在奈何桥边会有一名称作[孟婆]的年长女性神祇,给予每个鬼魂一碗[孟婆汤]以遗忘前世记忆,好投胎到下一世。

引言


大家都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?

fd81ca61195b4c318057584f9fdb6def.png

这样的架构,会存在着诸多的问题:

  • 每个业务都会需要鉴权、限流、权限校验、跨域等逻辑,如果每个业务各自为战,自己造轮子实现一遍,会很蛋疼,完全可以抽出来,放到一个统一的地方去做。

  • 如果业务量比较简单的话,这种方式前期不会有什么问题,但随着业务越来越复杂,比如淘宝、亚马逊打开一个页面可能会涉及到数百个微服务协同工作,如果每一个微服务都分配一个域名的话,一方面客户端代码会很难维护,涉及到数百个域名,另一方面是连接数的瓶颈,想象一下你打开一个APP,通过抓包发现涉及到了数百个远程调用,这在移动端下会显得非常低效。

  • 后期如果需要对微服务进行重构的话,也会变的非常麻烦,需要客户端配合你一起进行改造,比如商品服务,随着业务变的越来越复杂,后期需要进行拆分成多个微服务,这个时候对外提供的服务也需要拆分成多个,同时需要客户端配合你进行改造,非常蛋疼。

上面的这些问题可以借助API网关来解决

24753627bcaa415f9aaf6f302a3630e2.png

所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等。

它是一个路由网关组件,通过前面的学习,使用Spring Cloud实现微服务的架构基本成型,大致是这样的:

0dfc78fa9ee6493db8b175bf9527351d.png

简介


官网:https://github.com/Netflix/zuul

Zuul加入后的架构


6ea20e201d014d5498edcdb33695c691.png

不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。

快速入门


新建工程

pom文件导入

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
  4. </dependency>

编写配置

  1. server:
  2. port: 8380 #服务端口
  3. spring:
  4. application:
  5. name: zuul-server #指定服务名

编写引导类

通过@EnableZuulProxy注解开启Zuul的功能:

  1. @SpringBootApplication
  2. @EnableZuulProxy // 开启网关功能
  3. public class ZuulServerApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(ZuulServerApplication.class, args);
  6. }
  7. }

编写路由规则

我们需要用Zuul来代理spring-provider服务

  • ip为:127.0.0.1

  • 端口为:8180

映射规则:

  1. zuul:
  2. routes:
  3. spring-provider: # 这里是路由id,随意写
  4. path: / spring-provider/** # 这里是映射路径
  5. url: http://127.0.0.1:8180 # 映射路径对应的实际url地址

我们将符合path 规则的一切请求,都代理到 url参数指定的地址

本例中,我们将 /provider/**开头的请求,代理到http://127.0.0.1:8180

启动测试

访问的路径中需要加上配置规则的映射路径,我们访问:http://127.0.0.1:8380/spring-provider/provider/1

078b3c7661c242e99478ca1cf9f38c3f.png

面向服务的路由


在刚才的路由规则中,我们把路径对应的服务地址写死了!如果同一服务有多个实例的话,这样做显然就不合理了。我们应该根据服务的名称,去Eureka注册中心查找 服务对应的所有实例列表,然后进行动态路由才对!

对spring-zuul工程修改优化:

添加Eureka客户端依赖

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  4. </dependency>

添加Eureka配置,获取服务信息

  1. eureka:
  2. client:
  3. service-url:
  4. defaultZone: http://127.0.0.1:8081/eureka

开启Eureka客户端发现功能

  1. @SpringBootApplication
  2. @EnableZuulProxy // 开启Zuul的网关功能
  3. @EnableDiscoveryClient
  4. public class ZuulServerApplication {
  5. public static void main(String[] args) {
  6. SpringApplication.run(ZuulServerApplication.class, args);
  7. }
  8. }

修改映射配置,通过服务名称获取

因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。

  1. zuul:
  2. routes:
  3. spring-provider: # 这里是路由id,路由名,随意写,不能写中文
  4. path: /spring-provider/** # 这里是映射路径
  5. serviceId: spring-provider # 指定服务名称

启动测试

再次启动,这次Zuul进行代理时,会利用Ribbon进行负载均衡访问:

6fae53c655c941eb8995c16dcd41ee6f.png

再次访问

01f76f482d0849fb809128a85710665a.png

简化的路由配置


在刚才的配置中,我们的规则是这样的:

  • zuul.routes..path=/xxx/**: 来指定映射路径。是自定义的路由名

  • zuul.routes..serviceId=spring-provider:来指定服务名。

而大多数情况下,我们的路由名称往往和服务名会写成一样的。因此Zuul就提供了一种简化的配置语法:zuul.routes.=

比方说上面我们关于spring-provider的配置可以简化为一条:

  1. zuul:
  2. routes:
  3. spring-provider: /spring-provider/** # 这里是映射路径 左边是一个服务名

http://localhost:8380/spring-provider/provider/2

注意:下面配置方式所有的请求都会访问到spring-provider服务

  1. zuul:
  2. routes:
  3. spring-provider: /** # 这里是映射路径 左边是一个服务名

http://localhost:8380/spring-provider/provider/2

http://localhost:8380/provider/2

默认的路由规则


在使用Zuul的过程中,上面讲述的规则已经大大的简化了配置项。但是当服务较多时,配置也是比较繁琐的。因此Zuul就指定了默认的路由规则:

  • 默认情况下,一切服务的映射路径就是服务名本身。例如服务名为:spring-provider,则默认的映射路径就 是:/spring-provider/**

也就是说,刚才的映射规则我们完全可以不用配置。

  1. #zuul:
  2. # routes:
  3. # spring-provider: /spring-provider/** # 这里是映射路径 左边是一个服务名

路由参数配置


  • 路由前缀prefix

配置示例:

  1. zuul:
  2. routes:
  3. spring-provider: /spring-provider/**
  4. spring-consumer: /spring-consumer/**
  5. prefix: /api # 添加路由前缀,当然最终默认/api/spring-provider或者/api/spring-consumer帮我们截掉

我们通过zuul.prefix=/api来指定了路由的前缀,这样在发起请求时,路径就要以/api开头。

43d9549b3ccc43e1a159364e0b5abb12.png

  • 不去除前缀 strip-prefix:strip-prefix 默认值为true,表示除去前缀,strip-prefix = false 表示不除去前缀

配置示例:

  1. zuul:
  2. routes:
  3. spring-provider:
  4. path: /spring-provider/**
  5. #url: http://localhost:8081 或者用下面的serviceId
  6. serviceId: user-service ##服务名
  7. strip-prefix: true

说明:如果采用第三种(服务名:路径)配置方式,则 strip-prefix不会生效,例如:

  1. zuul:
  2. routes:
  3. spring-provider: /spring-provider/**
  4. spring-consumer: /spring-consumer/**
  5. strip-prefix: false #采用这个方式配置路由strip-prefix不会生效
  • ignored-services忽略使用默认的路由规则

配置示例:

  1. zuul:
  2. routes:
  3. b-service: /xx/** ##以/xx开头的,转发给b-service微服务
  4. ignored-services: c-service ##如果有多个,用逗号隔开

1、我们发起请求 localhost:8380/xx/test1,则会执行b-service的路径test1映射请求

2、我们发起请求:localhost:8380/c-service/test2时,由于c-service没配置,则直接会使用默认的路径规则,转发到c-service的路径test2的请求。

3、但是配置了ignored-services: c-service ,表示不允许c-service走默认的路径规则,即我们输入的localhost:8380/c-service/test2,zuul不会认为c-service是服务名。

如果所有的请求都不允许使用默认的路由规则,就是什么都不配,地址栏直接写服务名,就可以在配置文件中加入下列内容,即可关闭所有默认的路由规则,那么需要在配置文件中逐个为需要路由的服务添加映射规则。

  1. zuul:
  2. ignored-services: '*' ##忽略掉所有直接在地址栏写服务名的请求























通配符

说明

举例

?

匹配任意单个字符

/xxx/?

匹配任意数量的字符

/xxx/

匹配任意数量的字符, 包括多级目录

/xxx/

  • ignored-patterns:zuul 还提供了一个忽略表达式参数 zuul.ignored-patterns,该参数用来设置不被网关进行路由的 Url 表达式

    zuul:
    routes:

    1. b-service: /xx/**
    2. c-service: /yy/**

    ignored-patterns: /xx

则我们在浏览器中输入:http://localhost:8380/xx/test1,不会帮我们转发

过滤器


Zuul作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的。

ZuulFilter

ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法:

  1. public abstract ZuulFilter implements IZuulFilter{
  2. abstract public String filterType();
  3. abstract public int filterOrder();
  4. boolean shouldFilter();// 来自IZuulFilter
  5. Object run() throws ZuulException;// IZuulFilter
  6. }
  • shouldFilter:返回一个Boolean值,判断该过滤器是否需要执行。返回true执行,返回false不执行。

  • run:过滤器的具体业务逻辑。

  • filterType:返回字符串,代表过滤器的类型。包含以下4种:

  • pre:请求在被路由之前执行

  • route:在路由请求时调用

  • post:在route和errror过滤器之后调用

  • error:处理请求时发生错误调用

  • filterOrder:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。

过滤器执行生命周期

这张是Zuul官网提供的请求生命周期图,清晰的表现了一个请求在各个过滤器的执行顺序。

eb2426d08f094fc3880d7620bc3e56c1.png

正常流程:

  • 请求到达首先会经过pre类型过滤器,而后到达route类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。

异常流程:4种情况

  • 整个过程中,如果pre或者route过滤器出现异常,都会直接进入error过滤器,在error处理完毕后,会将请求交给POST过滤器,最后返回给用户。(2种)

  • 如果是error过滤器自己出现异常,最终也会进入POST过滤器,将最终结果返回给请求客户端。

  • 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和route不同的是,请求不会再到达POST过滤器了,而是直接响应用户

所有内置过滤器列表:

e7c48c4a7b25437a845b9bebeb8f5706.png

使用场景

场景非常多

  • 请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了

  • 异常处理:一般会在error类型和post类型过滤器中结合来处理。

  • 服务调用时长统计:pre和post结合使用。

自定义过滤器


接下来我们来自定义一个过滤器,模拟一个登录的校验。基本逻辑:如果请求中有access-token参数,则认为请求有效,放行。

定义过滤器类

内容:

  1. @Component
  2. public class LoginFilter extends ZuulFilter {
  3. /**
  4. * 过滤器类型,前置过滤器
  5. * @return
  6. */
  7. @Override
  8. public String filterType() {
  9. return "pre";
  10. }
  11. /**
  12. * 过滤器的执行顺序,这个顺序只是控制同一种类型过滤器的先后顺序,如果是不同类型的,则还是按照先执行pre,route,post的顺序执行
  13. * @return
  14. */
  15. @Override
  16. public int filterOrder() {
  17. return 10;
  18. }
  19. /**
  20. * 该过滤器是否生效
  21. * @return
  22. */
  23. @Override //返回true,表示执行下面方法的run,返回false表示不执行run,但是依然会执行后面的所有过滤器.
  24. //另外是否路由到目标微服务,主要看 “route”类型过滤器,如果route返回false,则不会执行run()方法,会路由到目标微服务, //如果”route“的该方法返回true,则执行run()方法,如果run()方法执行context.setSendZuulResponse(false),则不会路由
  25. public boolean shouldFilter() {
  26. return true;
  27. }
  28. /**
  29. * 登陆校验逻辑
  30. * @return
  31. * @throws ZuulException
  32. */
  33. @Override
  34. public Object run() throws ZuulException {
  35. // 获取zuul提供的上下文对象
  36. RequestContext context = RequestContext.getCurrentContext();
  37. // 从上下文对象中获取请求对象
  38. HttpServletRequest request = context.getRequest();
  39. // 获取token信息
  40. String token = request.getParameter("access-token");
  41. HttpServletResponse response = context.getResponse();
  42. response.setCharacterEncoding("UTF-8");
  43. response.setHeader("Content-type","text/html;charset=UTF-8");
  44. // 判断
  45. if (StringUtils.isBlank(token)) {
  46. // 过滤该请求,不对其进行路由,注意这个设置只是zuul不对其路由到目标微服务,但是后面的过滤器还是会正常执行
  47. context.setSendZuulResponse(false);
  48. // 设置响应状态码,401
  49. context.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
  50. // 设置响应信息
  51. response.getWriter().write("{\"status\":\"401\", \"text\":\"token无效!\"}");
  52. //context.setResponseBody("{\"status\":\"401\", \"text\":\"request error!\"}");
  53. }
  54. // 校验通过,把登陆信息放入上下文信息,继续向后执行
  55. context.set("token", token);
  56. return null;
  57. }
  58. }

测试

没有token参数时,访问失败:

965054a202414a92915e17f9c6926a94.png

添加token参数后:

2b954b2b67e246fcba4694eafa4fa473.png

Zuul 与 Hystrix 结合实现熔断


Zuul 和 Hystrix 结合使用实现熔断功能时,需要完成 FallbackProvider 接口。该接口提供了 2 个方法:

  • .getRoute 方法:用于指定为哪个服务提供 fallback 功能。

  • .fallbackResponse 方法:用于执行回退操作的具体逻辑。

例如,我们为 service-id 为 eureka-client-department 的微服务(在 zuul 网关处)提供熔断 fallback 功能。

  • 实现 FallbackProvider 接口,并托管给 Spring IoC 容器

1、Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置:

  1. ribbon:
  2. ReadTimeout: 12000
  3. ConnectTimeout: 10000
  4. spring-provider: #在网关上配置 请求某个服务名的负载均衡测试 服务名小写
  5. ribbon:
  6. NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

1.网关转发给A服务,A服务通过openfeign请求B服务,并且在A服务配置了openfeign的重试功能,假如A服务第一次请求B服务以及重试总时长是12s((1+5)*2)),那么网关的 ribbon.ReadTimeout应该等于12s,12s之后立刻降级.
2.网关降级之后,如果定义了post过滤器,则走post过滤器,此时post过滤器收到的状态值就是zuul网关降级的状态值,
@Override
publicHttpStatusgetStatusCode() throwsIOException {
returnHttpStatus.OK; //或者其他的状态值
}
然后post把处理结果响应给浏览器,如果post过滤器没有对status进行处理,最终响应结果还是网关降级的结果,这并不意味着post过滤器不执行,也就是说执行降级后,依然会执行post过滤器
3.如果ReadTimeout超时时间小于微服务请求的总时长,如ReadTimeout=3000,那么3s网关就降级了,然后执行post过滤器,此时post过滤器收到的响应状态值依然是200.只不过不要这样配置,否则网关都给客户响应了,而微服务通过feign还在不断的重试
4.如果ReadTimeout超时时间大于微服务请求的总时长,如ReadTimeout=20000,也就是20s后才降级,那么在12s后,A服务不再重试B服务了,请求失败,直接抛出状态码为500的异常,而此时zuul还没有降级,所以post过滤器收到的响应状态值是500,post过滤器可以对500的状态码进行处理,然后响应
5.如果A服务通过feign调用了B服务失败,A服务自己实现了feign的降级功能,那么站在zuul的角度来看,A服务是正常响应,此时zuul不会执行降级,最终只执行post类型过滤器,过滤器收到A服务的响应状态码就是200,你可以注掉feign的降级实现类。

2、zuul网关的降级,实现ClientHttpResponse接口

  1. package com.woniu.fallback;
  2. import org.springframework.http.HttpHeaders;
  3. import org.springframework.http.HttpStatus;
  4. import org.springframework.http.MediaType;
  5. import org.springframework.http.client.ClientHttpResponse;
  6. import java.io.ByteArrayInputStream;
  7. import java.nio.charset.StandardCharsets;
  8. public class ClientHttpResponseImpl implements ClientHttpResponse {
  9. /**
  10. * 设置一个状态码,如果有post过滤器,则post过滤器得到的状态码就是该值
  11. */
  12. @Override
  13. public HttpStatus getStatusCode() throws IOException {
  14. return HttpStatus.OK;
  15. }
  16. /**
  17. * 状态值
  18. */
  19. @Override
  20. public int getRawStatusCode() throws IOException {
  21. return this.getStatusCode().value();
  22. }
  23. @Override
  24. public String getStatusText() throws IOException {
  25. return this.getStatusCode().getReasonPhrase();
  26. }
  27. @Override
  28. public void close() {
  29. }
  30. /**
  31. * 最终要响应给浏览器的内容
  32. * @return
  33. * @throws IOException
  34. */
  35. @Override
  36. public InputStream getBody() throws IOException {
  37. String str = "{xxx:请注意,msg:服务器正忙,请稍后再试}";
  38. return new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
  39. }
  40. /**
  41. * 响应头 MediaType
  42. * @return
  43. */
  44. @Override
  45. public HttpHeaders getHeaders() {
  46. HttpHeaders header = new HttpHeaders();
  47. MediaType type = new MediaType("application","json", StandardCharsets.UTF_8);
  48. header.setContentType(type);
  49. return header;
  50. }
  51. }

3、实现FallbackProvider接口

  1. @Component
  2. public class UserProviderFallBack implements FallbackProvider {
  3. /**
  4. * 要降级的 服务名
  5. * @return
  6. */
  7. @Override
  8. public String getRoute() {
  9. return "*"; // * 所有服务的降级
  10. //return “服务名 (服务名小写)”
  11. }
  12. /**
  13. * 网关降级响应方法
  14. * @param route
  15. * @param cause
  16. * @return
  17. */
  18. @Override
  19. public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
  20. ClientHttpResponse response = new ClientHttpResponseImpl();
  21. return response;
  22. }
  23. }

在启动类添加@EnableCircuitBreaker注解

Zuul 中的 Eager Load 配置


zuul 的路由转发也是由通过 Ribbon 实现负载均衡的。默认情况下,客户端相关的 Bean 会延迟加载,在第一次调用微服务时,才会初始化这些对象。所以 zuul 无法在第一时间加载到 Ribbon 的负载均衡。

如果想提前加载 Ribbon 客户端,就可以在配置文件中开启饥饿加载(即,立即加载):

  1. zuul:
  2. ribbon:
  3. eager-load:
  4. enabled: true

注意 eager-load 配置对于默认路由不起作用。因此,通常它都是结合 zuul.ignored-services=* (即忽略所有的默认路由) 一起使用的,以达到 zuul 启动时就默认已经初始化各个路由所要转发的负载均衡对象。

禁用 zuul 过滤器


Spring Cloud 默认为 zuul 编写并启动了一些过滤器,这些过滤器都放在 org.springframework.cloud.netflix.zuul.filters 包下。

如果需要禁用某个过滤器,只需要设置 zuul...disabled=true,就能禁用名为 的过滤器。例如:

  1. zuul:
  2. JwtFilter:
  3. pre:
  4. disable: true

上述配置就禁用掉了我们自定义的 JwtFilter 。

发表评论

表情:
评论列表 (有 0 条评论,142人围观)

还没有评论,来说两句吧...

相关阅读

    相关 nginx服务器

    一、业务背景分析 前一段时间,需要开发一套业务系统,此系统需要对外统一提供api服务,但这些服务在内部是由多个业务子系统分别提供。 经过分析,此业务系统需要具有以下这么