网关介绍以及搭建

网关


学习目标:

1、网关的作用

2、独立搭建Zuul网关

3、会用zuul网关路由功能

4、了解Zuul的过滤器

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

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

引言


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

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

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

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

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

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

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

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

简介


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

Zuul加入后的架构


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

快速入门


新建工程

pom文件导入

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

编写配置

server:
  port: 8380 #服务端口
spring:
  application:
    name: zuul-server #指定服务名

编写引导类

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

@SpringBootApplication
@EnableZuulProxy // 开启网关功能
public class ZuulServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulServerApplication.class, args);
    }
}

编写路由规则

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

  • ip为:127.0.0.1

  • 端口为:8180

映射规则:

zuul:
  routes:
    spring-provider: # 这里是路由id,随意写
      path: / spring-provider/** # 这里是映射路径
      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

面向服务的路由


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

对spring-zuul工程修改优化:

添加Eureka客户端依赖

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

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

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8081/eureka

开启Eureka客户端发现功能

@SpringBootApplication
@EnableZuulProxy // 开启Zuul的网关功能
@EnableDiscoveryClient
public class ZuulServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulServerApplication.class, args);
    }
}

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

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

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

启动测试

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

再次访问

简化的路由配置


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

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

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

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

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

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

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

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

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

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

http://localhost:8380/provider/2

默认的路由规则


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

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

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

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

路由参数配置


  • 路由前缀prefix

配置示例:

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

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

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

配置示例:

zuul:
  routes:
    spring-provider:
      path: /spring-provider/**
      #url:  http://localhost:8081  或者用下面的serviceId
      serviceId: user-service ##服务名
      strip-prefix: true

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

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

配置示例:

zuul:
  routes:
    b-service: /xx/**  ##以/xx开头的,转发给b-service微服务
  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是服务名。

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

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

通配符

说明

举例

?

匹配任意单个字符

/xxx/?

*

匹配任意数量的字符

/xxx/*

**

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

/xxx/**

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

zuul:
  routes:
    b-service: /xx/**
    c-service: /yy/**
  ignored-patterns: /xx

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

过滤器


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

ZuulFilter

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

public abstract ZuulFilter implements IZuulFilter{

    abstract public String filterType();  

    abstract public int filterOrder();
    
    boolean shouldFilter();// 来自IZuulFilter

    Object run() throws ZuulException;// IZuulFilter
}
  • shouldFilter:返回一个Boolean值,判断该过滤器是否需要执行。返回true执行,返回false不执行。

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

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

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

  • route:在路由请求时调用

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

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

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

过滤器执行生命周期

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

正常流程:

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

异常流程:4种情况

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

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

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

所有内置过滤器列表:

使用场景

场景非常多

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

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

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

自定义过滤器


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

定义过滤器类

内容:

@Component
public class LoginFilter extends ZuulFilter {
    /**
     * 过滤器类型,前置过滤器
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 过滤器的执行顺序,这个顺序只是控制同一种类型过滤器的先后顺序,如果是不同类型的,则还是按照先执行pre,route,post的顺序执行
     * @return
     */
    @Override
    public int filterOrder() {
        return 10;  
    }

    /**
     * 该过滤器是否生效
     * @return
     */
    @Override   //返回true,表示执行下面方法的run,返回false表示不执行run,但是依然会执行后面的所有过滤器.
    //另外是否路由到目标微服务,主要看 “route”类型过滤器,如果route返回false,则不会执行run()方法,会路由到目标微服务,           //如果”route“的该方法返回true,则执行run()方法,如果run()方法执行context.setSendZuulResponse(false),则不会路由
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 登陆校验逻辑
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        // 获取zuul提供的上下文对象
        RequestContext context = RequestContext.getCurrentContext();
        // 从上下文对象中获取请求对象
        HttpServletRequest request = context.getRequest();
        // 获取token信息
        String token = request.getParameter("access-token");
        
        HttpServletResponse response = context.getResponse();
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-type","text/html;charset=UTF-8");
        
        // 判断
        if (StringUtils.isBlank(token)) {
            // 过滤该请求,不对其进行路由,注意这个设置只是zuul不对其路由到目标微服务,但是后面的过滤器还是会正常执行
            context.setSendZuulResponse(false);
            // 设置响应状态码,401
            context.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
            // 设置响应信息
            response.getWriter().write("{\"status\":\"401\", \"text\":\"token无效!\"}");
            //context.setResponseBody("{\"status\":\"401\", \"text\":\"request error!\"}");
        }
        // 校验通过,把登陆信息放入上下文信息,继续向后执行
        context.set("token", token);
        return null;
    }
}

测试

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

添加token参数后:

Zuul 与 Hystrix 结合实现熔断


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

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

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

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

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

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

ribbon:
  ReadTimeout: 12000 
  ConnectTimeout: 10000 
  
spring-provider:  #在网关上配置 请求某个服务名的负载均衡测试  服务名小写
  ribbon:
    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接口

package com.woniu.fallback;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse; 

import java.io.ByteArrayInputStream; 
import java.nio.charset.StandardCharsets;

public class ClientHttpResponseImpl  implements ClientHttpResponse {

    /**
     * 设置一个状态码,如果有post过滤器,则post过滤器得到的状态码就是该值
     */
    @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() {
    }
    /**
     * 最终要响应给浏览器的内容
     * @return
     * @throws IOException
     */
    @Override
    public InputStream getBody() throws IOException {

        String str = "{xxx:请注意,msg:服务器正忙,请稍后再试}";
        return new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * 响应头     MediaType
     * @return
     */
    @Override
    public HttpHeaders getHeaders() {
        HttpHeaders header = new HttpHeaders();
        MediaType type = new MediaType("application","json", StandardCharsets.UTF_8);
        header.setContentType(type);
        return header;
    }
}

3、实现FallbackProvider接口

@Component
public class UserProviderFallBack implements FallbackProvider {
    /**
     * 要降级的 服务名
     * @return
     */
    @Override
    public String getRoute() {
        return "*";   // *  所有服务的降级
        //return “服务名 (服务名小写)”
    }
    /**
     * 网关降级响应方法
     * @param route
     * @param cause
     * @return
     */
    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        ClientHttpResponse response = new ClientHttpResponseImpl();
        return response;
    }
}

在启动类添加@EnableCircuitBreaker注解

Zuul 中的 Eager Load 配置


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

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

zuul:
  ribbon:
    eager-load:
      enabled: true

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

禁用 zuul 过滤器


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

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

zuul:
  JwtFilter:
    pre:
      disable: true

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

### 如何搭建网络网关或 API 网关 #### 搭建概述 API 网关作为系统统一入口,可以集中管理多个微服务的功能,例如请求路由、负载均衡、认证授权等[^4]。为了实现这些功能,通常可以选择开源工具或者框架来构建自己的 API 网关。 --- #### 使用 Kong 搭建 API 网关 Kong 是一种流行的开源 API 网关解决方案,支持插件扩展以及高性能的流量处理能力。以下是基于 Kong 的配置方法: 1. **安装 Kong** 需要在服务器环境中部署 Kong。可以通过 Docker 容器快速启动: ```bash docker run -d --name kong \ -e "KONG_DATABASE=off" \ -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \ -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" \ -e "KONG_PORT_MAPS=8000:8000" \ -p 8000:8000 \ -p 8443:8443 \ -p 8001:8001 \ kong:latest ``` 2. **定义服务和路由** 创建一个新的服务并为其设置路由: ```bash curl -X POST http://localhost:8001/services/ \ --data 'name=my-service' \ --data 'url=http://my-backend-server.com' curl -X POST http://localhost:8001/services/my-service/routes \ --data 'paths[]=/api/v1' ``` 3. **启用插件** 可以为特定的服务启用插件(如身份验证): ```bash curl -X POST http://localhost:8001/services/my-service/plugins \ --data "name=key-auth" ``` --- #### 使用 Nginx 和 OpenResty 构建自定义 API 网关 如果希望更灵活地控制逻辑,也可以通过 Nginx 或其增强版 OpenResty 来创建定制化的 API 网关。 1. **Nginx 基本配置** 下面是一个简单的 Nginx 配置文件示例,展示如何将不同路径映射到不同的后端服务: ```nginx server { listen 80; location /serviceA { proxy_pass http://backend-service-a; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /serviceB { proxy_pass http://backend-service-b; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } ``` 2. **集成 Lua 脚本** 如果需要动态行为(如限流),可以在 OpenResty 中嵌入 Lua 脚本来完成复杂业务逻辑。 --- #### 实现 API 文档聚合 除了基本的请求转发外,还可以利用网关层聚合 API 接口文档[^3]。例如,在 Kong 中可以通过 `deck` 工具同步本地 Swagger 文件至网关;而在 Spring Cloud Gateway 中,则可以直接引入第三方库生成完整的 API 列表页面。 --- #### 总结 无论是采用成熟的商业产品还是自行开发方案,都需要考虑实际需求场景下的性能指标与维护成本平衡关系。对于初学者来说推荐先尝试一些成熟度较高的开源项目比如 Kong/Zuul/Spring Cloud Gateway等等[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值