什么是服务网关

image-20240118205720365

API Gateway(API网关)是一个用于管理、调度和保护微服务架构中的API(Application Programming Interface)的服务器。API网关充当客户端和后端服务之间的中介,提供一种集中式的入口点,用于处理从客户端到各个微服务的请求和响应。以下是API网关的主要功能和特点:

  1. 路由和调度: API网关负责将客户端请求路由到相应的后端服务。它可以基于路径、域名或其他规则进行路由,并将请求分发给相应的微服务。
  2. 负载均衡: API网关可以实施负载均衡策略,确保后端服务的请求得到平均分配,提高系统的可伸缩性和性能。
  3. 安全性: API网关提供了安全性的特性,包括身份验证、授权、加密和防火墙等。它可以验证请求的身份,并确保只有授权的用户或系统可以访问后端服务。
  4. 请求转换: API网关可以处理和转换客户端请求和后端服务的响应,以适应不同的协议、数据格式或版本。
  5. 监控和分析: API网关提供了监控和分析功能,用于跟踪请求和响应的性能、错误率和其他指标。这对于系统的健康和故障排除非常重要。
  6. 缓存: API网关可以实施缓存策略,缓存常见的请求和响应,减轻后端服务的压力,提高响应速度。
  7. 限流和配额: API网关可以实施限流和配额控制,防止滥用和过度使用后端服务资源。
  8. API文档和发现: API网关可以提供API文档和发现服务,使开发者能够了解可用的API和如何使用它们。
  9. 服务发现: 在微服务架构中,API网关可以与服务发现工具集成,动态地发现和管理后端服务的实例。

一些流行的API网关包括NGINX, Kong, Apigee, AWS API Gateway等。API网关在微服务架构中起到了重要的作用,简化了前端和后端之间的通信和协作,提高了系统的可维护性、扩展性和安全性。

举个实际的例子,我们为什么要使用网关?

服务网关解决了什么问题?

image-20240118211346181

什么是Zuul

Zuul是从设备和网站到应用程序后端的所有请求的前置。作为边缘服务应用程序,它主要用于处理微服务架构中的请求路由、负载均衡、服务过滤和服务发现等功能。Zuul最初是为Netflix的基于云的架构而设计的,但现在已经成为一个开源项目,广泛应用于各种微服务架构中。

以下是Zuul的一些主要特点和功能:

  1. 请求路由: Zuul可以根据请求的URL将流量路由到相应的微服务。这使得微服务架构中的各个服务可以通过一个中心入口点提供服务。
  2. 负载均衡: Zuul支持负载均衡(Ribbon),可以将请求分发到多个相同功能的微服务实例中,以确保高可用性和性能。
  3. 服务过滤: Zuul允许定义一系列过滤器,可以在请求和响应的不同阶段执行,实现例如身份验证、日志记录、性能监控等功能。
  4. 服务降级: (Hystrix/Sentinel)当后端微服务不可用时,Zuul可以提供一些默认的响应或转发到备用服务,以防止整个系统的故障。
  5. 动态路由: Zuul支持动态路由配置(2.x版本才支持),可以根据运行时的条件动态调整路由规则,而不需要重启Zuul服务。
  6. 服务发现: Zuul可以集成服务注册与发现机制,与服务发现工具(如Eureka)协同工作,以自动获取可用的微服务实例。
  7. 安全性: Zuul可以通过配置安全规则,例如基于路径的访问控制、HTTPS支持等,来保护系统的安全。
  8. 易于扩展: Zuul是一个可插拔的架构,可以通过定义自定义过滤器和路由规则来扩展其功能。

在微服务架构中,Zuul扮演着一个重要的角色,帮助开发者实现了一些常见的跨服务的功能,同时提供了一些重要的运维和安全性特性。值得注意的是,Zuul 1.x版本是基于Servlet技术的,而Zuul 2.x及之后版本是基于Netty的异步非阻塞架构。

Zuul实现API网关入门案例

首先引入zuul的spring依赖

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

一个注解开启zuul

@EnableZuulProxy
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(DemoApplication.class)
                .listeners((ApplicationListener<ApplicationEnvironmentPreparedEvent>) event -> {
                }).run(args);
    }
}

路由配置参数


## 路径前缀
zuul.routes.demo-product.path=/dome-product/**
## 转发路由地址
zuul.routes.demo-product.url=http://localhost:8089
## 转发时是否去掉前缀
zuul.routes.demo-product.stripPrefix=false

## --------------------------------------------------
## 如果用到注册中心(如Eureka)可以指定这个参数
zuul.routes.demo-product.serviceId=demo-product
## 如果只是简单的实现上面的功能则所有的都无需配置即可默认支持(约定大于配置)

## --------------------------------------------------
## 移除内置的三个敏感参数"Cookie", "Set-Cookie", "Authorization"
zuul.sensitiveHeaders=
## 排除包含actuator、error的所有路径(禁止访问的意思)
zuul.ignored-patterns=/**/error/**,/**/actuator/**
## 排除服务(禁止访问的意思),如果是*代表所有
zuul.ignored-services=demo-product1
## 路由前缀,等于访问的路径前面加这一串如:
## 之前是http://localhost:8080/dome-product/ 
## 现在之前是http://localhost:8080/api/dome-product/
zuul.prefix=/api

## --------------------------------------------------
spring.application.name=demo-product
## eureka注册配置
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=${spring.application.name}:${server.port}
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

## --------------------------------------------------
#熔断超时时间配置
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=16000

#zuul超时配置,路由规则是url才生效
zuul.host.connect-timeout-millis=17000(连接超时)
zuul.host.socket-timeout-millis=60000(请求超时)

#负载均衡超时时间配置,路由规则是注册中心ID生效,此处生效
#总的超时时间 = (1 + MaxAutoRetries + MaxAutoRetriesNextServer) * ReadTimeout
ribbon.ReadTimeout=5000
ribbon.ConnectTimeout=5000

Zuul过滤器

  1. Filter(过滤器): 过滤器是Zuul中的基本构建块,用于执行特定类型的过滤逻辑。Zuul的请求处理过程中,会经过一系列的过滤器,包括前置过滤器、路由过滤器、后置过滤器等。

  2. Filter Type(过滤器类型): 过滤器类型定义了过滤器的执行时机,包括"pre"(前置过滤器)、“route”(路由过滤器)、“post”(后置过滤器)、“error”(错误过滤器)。

  3. Filter Order(过滤器顺序): 过滤器顺序决定了过滤器的执行顺序。具有相同类型的过滤器会根据过滤器顺序从小到大执行。

  4. Should Filter(是否执行过滤器): shouldFilter 方法返回一个布尔值,用于决定是否执行当前过滤器。如果返回 true,则执行过滤器的 run 方法;如果返回 false,则跳过该过滤器。

  5. Request Context(请求上下文): 请求上下文是Zuul中的一个对象,包含了当前请求的信息和状态。过滤器可以通过请求上下文获取请求和响应等信息,并对其进行修改。

  6. Filter Result(过滤器结果): 过滤器的 run 方法的返回值。用于控制是否继续执行后续的过滤器。返回 null 表示继续执行后续过滤器,返回 new FilterResult().setShouldFilter(true) 表示继续执行,返回 new FilterResult().setShouldFilter(false) 表示中止过滤器链。

image-20240118220005863

import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UrlPathHelper;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_DECORATION_FILTER_ORDER;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;

public class FtdZuulFilter extends AbstractRouteFilter {
    public FtdZuulFilter(RouteLocator routeLocator, UrlPathHelper urlPathHelper) {
        super(routeLocator, urlPathHelper);
    }

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

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

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

    @Override
    public Object run() throws ZuulException {
        // Zuul上下文
        RequestContext ctx = RequestContext.getCurrentContext();

        // 获取目标服务名
        Route route = route(ctx.getRequest());  // http://localhost:8080/lingxi/getUser
				String serviceId = route.getId(); //lingxi
        // 判断用户是否登录
        String currentUserName = UserInfoHolder.getUsername();
        if (currentUserName == null) {
            throw new ZuulException("未登录", 401, "未登录");
        }

        // 获取用户名信息
        if (StringUtils.equals(serviceId, "demo-product")) {
            ctx.addZuulRequestHeader("UserInfo", currentUserName);
            ctx.addZuulRequestHeader("Token", "saas");
            ctx.addZuulRequestHeader("Authorization", "Bearer 123123213" );
        }
        //如果 run() 方法返回 null,表示该过滤器逻辑执行成功,将继续执行后续的过滤器(如果有的话)。
        return null; // 或者 return new FilterResult().setShouldFilter(true);
    }
  
    protected Route route(HttpServletRequest request) {
        String requestURI = urlPathHelper.getPathWithinApplication(request);
        return routeLocator.getMatchingRoute(requestURI);
    }
}

Zuul Error过滤器

如果需要定义自己的异常返回,那使用下面的示例统一进行处理。

package com.demo.web.gateway.filter;

import com.netflix.zuul.context.RequestContext;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UrlPathHelper;

import java.io.IOException;
import java.io.PrintWriter;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SEND_ERROR_FILTER_ORDER;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.ERROR_TYPE;

@Component
public class ErrorZuulFilter extends AbstractRouteFilter {
    public ErrorZuulFilter(RouteLocator routeLocator, UrlPathHelper urlPathHelper) {
        super(routeLocator, urlPathHelper);
    }

    @Override
    public String filterType() {
        return ERROR_TYPE;
    }

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

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

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();

        ctx.setResponseStatusCode(HttpStatus.EXPECTATION_FAILED.value());

        PrintWriter writer = null;
        try {
            writer = ctx.getResponse().getWriter();
            writer.println("Is Cookie Error!");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(writer != null){
                writer.close();
            }
        }

        return null;
    }
}

另外:如果没有请求到自己的异常类则要加一下这个参数,排除掉Zuul自带的异常过滤器SendErrorFilter

zuul.SendErrorFilter.error.disable=true

Zuul请求生命周期

1、HTTP客户端发送请求到Zuul网关

2、Zuul网关首先经过Pre filter

3、验证通过后进入Routing filter,接着将请求转发给远端服务,远端服务执行后返回结果,如果出错则Error filter

4、继续执行Post filter

5、最后返回响应给HTTP客户端

image-20240122193524634

Zuul接入Sentinel限流

Zuul本身自带Hystrix限流器,我们优先接入Sentinel限流,MSF中已经带了雨Sentinel:msf-spring-boot-starter-sentinel依赖包,引入即可,对Zuul的Sentinel过滤器提交给Spring托管【Sentinel里面的默认实现】。

@Configuration
public class GatewayConfig {

    @Bean
    public ZuulFilter sentinelPreFilter() {
        return new com.alibaba.csp.sentinel.adapter.gateway.zuul.filters.SentinelZuulPreFilter();
    }

    @Bean
    public ZuulFilter sentinelPostFilter() {
        return new com.alibaba.csp.sentinel.adapter.gateway.zuul.filters.SentinelZuulPostFilter();
    }

    @Bean
    public ZuulFilter sentinelErrorFilter() {
        return new com.alibaba.csp.sentinel.adapter.gateway.zuul.filters.SentinelZuulErrorFilter();
    }
}

image-20240123145310655

image-20240123145253359

测试效果:

image-20240123145230584

image-20240123145209991

总结

总的来说,Zuul是一个强大而小巧灵活的API网关,为构建和管理微服务架构中的API提供了一套全面的解决方案。主要组件有路由器、过滤器和限流器。缺点是在 Zuul 1.x 版本中,动态添加新路由是相对困难的,因为在启动时就需要定义路由规则,而后期动态修改这些规则较为复杂,通常需要通过重启 Zuul 服务来应用新的配置。

然而,在 Zuul 2.x 及之后的版本中,支持动态路由的能力得到了显著改进。Zuul 2.x 使用了基于 Netty 的异步非阻塞架构,允许在运行时动态修改路由规则而无需重启整个服务。这使得可以更灵活地适应不同的需求,例如微服务的动态变化、新服务的添加等。

Zuul(Netflix开源的版本)并没有被维护的情况。然而,需要注意的是,Netflix已经在一段时间内将注意力转移到了新的网关项目Spring Cloud Gateway上。

Spring Cloud Gateway是Spring Cloud生态系统中的一个组件,用于构建基于Spring Boot的微服务架构的网关服务。相比于Zuul 1.x版本,Spring Cloud Gateway采用了Reactor和WebFlux,支持反应式编程,提供更好的性能和可伸缩性。由于Spring Cloud Gateway是Spring Cloud生态系统的一部分,因此与Spring Boot以及其他Spring Cloud组件更好地集成,具有更现代化的架构。

因此,一些团队和开发者可能更愿意选择Spring Cloud Gateway,而不是继续使用Zuul,特别是在构建新的微服务架构时。然而,这并不意味着Zuul就不再使用或维护了,而是取决于具体的项目需求和技术栈选择。

在选择API网关时,需要根据项目的需求、团队的经验以及生态系统的集成性等因素进行权衡。如果你正在考虑使用API网关,建议查看最新的技术趋势和生态系统的发展,以便做出符合项目需求的选择。

上一篇 下一篇