服务网关实践

概述

微服务架构由很多个微小的服务(微服务)组成系统,每个微服务有自己的主机和端口号。如果直接让客户端与各个微服务通信,带来的问题有:

  • 客户端需要维护多个请求地址(主机和端口号)
  • 每个微服务都需要独立认证
  • 某些场景存在跨域问题

服务网关实践_第1张图片

在客户端和各个微服务之间添加微服务网关可以解决如上问题,客户端的请求都先经过微服务网关,客户端就只需要知道网关的请求地址即可,添加网关的优点有:

  • 统一入口,只需要维护一个请求地址(主机和端口号)
  • 易于监控
  • 统一认证

服务网关实践_第2张图片

本文介绍的微服务网关技术是Spring Cloud GateWay。该项目提供了一个用于在 Spring WebFlux 或 Spring WebMVC 之上构建 API 网关的库。 Spring Cloud Gateway 旨在提供一种简单而有效的方法来路由到 API 并为其提供横切关注点,例如:安全性、监控。

本文的操作是在服务熔断保护实践--Sentinal的基础上进行。

环境说明

jdk1.8

maven3.6.3

mysql8

spring cloud2021.0.8

spring boot2.7.12

idea2022

步骤

创建子模块

在父工程下,创建子模块:api_gateway_server

添加依赖


    
        org.springframework.cloud
        spring-cloud-starter-gateway
    

启动类

package org.example.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

路由配置

配置文件application.yml

server:
  port: 8080
spring:
  application:
    name: api-gateway-server
  cloud:
    gateway:
      routes:
        - id: product-service
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/product/**

配置的作用:通过网关访问路径符合/product/开头时,网关会将请求转发到uri(http://127.0.0.1:9001)的对应的路径下。例如:访问localhost:8080/product/1,网关会将请求转发到http://127.0.0.1:9001/product/1,其中127.0.0.1localhost可以互相替换。

启动网关服务报错如下

Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway.

Action:

Please set spring.main.web-application-type=reactive or remove spring-boot-starter-web dependency.

启动报错原因:

SpringCloud Gateway使用的web框架为webflux,和SpringMVC不兼容,所以gateway不需要引入web依赖,否则造成依赖冲突。

服务网关实践_第3张图片

解决启动报错:

父工程不全局引入web依赖,需要的子工程单独引入web依赖,不需要则不引入(例如:gateway工程不需要则不引入)

父工程,删除web依赖

服务网关实践_第4张图片

在需要的子工程(order-service、order-service-feign_hystrix、product-service)里添加Web依赖

		
            org.springframework.boot
            spring-boot-starter-web
        

刷新依赖

测试

启动eureka服务、启动product服务(确认product服务的端口号为9001)

浏览器访问

直接访问微服务

http://localhost:9001/product/1

 通过网关访问

http://localhost:8080/product/1

服务网关实践_第5张图片

可以看到通过网关访问和直接访问微服务效果一样。

路由规则

Spring Cloud Gateway 的功能很强大,前面只是使用了 Predicates(断言) 进行了简单的条件匹配,其实Spring Cloud Gataway 帮我们内置了很多 Predicates 功能。在 Spring Cloud Gateway 中 Spring 利用Predicate 的特性实现了各种路由匹配规则,如:通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。

路由匹配示例

spring:
  cloud:
    gateway:
      routes:
        - id: product-service
          uri: https://xxxx.com
          predicates:
            # 路由断言之前匹配
            - Before=xxx
            # 路由断言之后匹配
            - After=xxx
            # 路由断言之间匹配
            - Between=xxx,xxx
            # 路由断言Cookie匹配,此predicate匹配给定名称(chocolate)和正则表达式(ch.p)
            - Cookie=chocolate, ch.p
            # 路由断言Header匹配,header名称匹配X-Request-Id,且正则表达式匹配\d+
            - Header=X-Request-Id, \d+
            # 路由断言匹配Host匹配,匹配下面Host主机列表,**代表可变参数
            - Host=**.somehost.org, **.anotherhost.org
            # 路由断言Method匹配,匹配的是请求的HTTP方法
            - Methos=GET
            # 路由断言访问路径匹配,{segment}为可变参数
            - Path=/foo/{segment},/bar/{segment}
            # 路由断言请求参数匹配,将请求的参数param(baz)进行匹配
            - Query=baz
            # 路由断言请求参数匹配,可以进行regexp正则表达式匹配 (参数包含foo,并且foo的值匹配ba.)
            - Query=foo,ba.
            # 路由断言RemoteAddr匹配,将匹配192.168.1.1~192.168.1.254之间的ip地址,其中24为子网掩码位  数即255.255.255.0
            - RemoteAddr=192.168.1.1/24

动态路由(面向服务的路由)

之前我们配置路由uri是直接配置了微服务的主机和端口号:uri: http://127.0.0.1:9001

如果微服务地址有变化,需要修改网关配置uri,耦合度高。

Spring Cloud GateWay支持动态路由:即自动的从注册中心中获取服务列表并访问。

api_gateway_server服务(网关服务)引入eureka依赖

        
            org.springframework.cloud
            spring-cloud-starter-netflix-eureka-client
        

修改网关服务配置文件application.yml

  • 路由uri的值改为 lb://service-product

       lb代表从注册中心获取服务,同时具备赋值均衡功能,//后面接的是服务名称

  • 添加配置eureka的注册中心相关信息
server:
  port: 8080
spring:
  application:
    name: api-gateway-server
  cloud:
    gateway:
      routes:
        - id: product-service
          uri: lb://service-product
          predicates:
            - Path=/product/**
# 配置eureka
eureka:
  client:
    service-url:
      defaultZone: http://localhost:9000/eureka/
  instance:
    prefer-ip-address: true

配置的作用:通过网关访问,如果路径包含/product开头,会转发到uri(lb://service-product) 对应的微服务接口。例如:访问localhost:8080/product/1符合断言路径的条件,网关将请求会转发到service-product微服务,微服务名称映射的主机和端口号从eureka注册中心得到,转发对应的接口为localhost:9001/product/1

测试

重启gateway服务

查看eureka服务

服务网关实践_第6张图片

访问正常

http://localhost:8080/product/1

过滤器重写请求路径

为了让更好识别访问路径将转发到哪个服务,在网关访问路径中带有具有识别作用的特定路径,例如:用带有product-service的路径表示请求将转发到product服务。

完整的访问路径如下

http://localhost:8080/product-service/product/1

响应错误页面

服务网关实践_第7张图片

要访问成功,可以使用过滤器重写请求路径,

application.yml配置如下:

          predicates:
            #- Path=/product/**
            - Path=/product-service/**
          filters:
            - RewritePath=/product-service/(?),/$\{segment} #路径重写的过滤器,把/product-service/xxx的路径重写为/xxx

服务网关实践_第8张图片

重启网关服务

再次访问

http://localhost:8080/product-service/product/1

成功访问到数据

过滤器

过滤器类型:局部过滤器(GatewayFilter)和全局过滤器(GlobalFilter)。

局部过滤器(GatewayFilter):应用到单个路由或者一个分组的路由上

全局过滤器(GlobalFilter):应用到所有路由上。

局部过滤器

部分局部过滤器如下

过滤器工厂 作用 参数
AddRequestHeader 为原始请求添加Header Header的名称及值
AddRequestParameter 为原始请求添加请求参数 参数名称及值
AddResponseHeader 为原始响应添加Header Header的名称及值
DedupeResponseHeader 剔除响应头中重复的值 需要去重的Header名称及去重策略
Hystrix 为路由引入Hystrix的断路器保护 HystrixCommand 的名称
FallbackHeaders 为fallbackUri的请求头中添加具体的异常信息 Header的名称
PrefixPath 为原始请求路径添加前缀 前缀路径
PreserveHostHeader 为请求添加一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host
RequestRateLimiter 用于对请求限流,限流算法为令牌桶 keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus
RedirectTo 将原始请求重定向到指定的URL http状态码及重定向的url
RemoveHopByHopHeadersFilter 为原始请求删除IETF组织规定的一系列Header 默认就会启用,可以通过配置指定仅删除哪些Header
RemoveRequestHeader 为原始请求删除某个Header Header名称
RemoveResponseHeader 为原始响应删除某个Header Header名称
RewritePath 重写原始的请求路径 原始路径正则表达式以及重写后路径的正则表达式
RewriteResponseHeader 重写原始响应中的某个Header Header名称,值的正则表达式,重写后的值
SaveSession 在转发请求之前,强制执行WebSession::save 操作
secureHeaders 为原始响应添加一系列起安全作用的响应头 无,支持修改这些安全响应头的值
SetPath 修改原始的请求路径 修改后的路径
SetResponseHeader 修改原始响应中某个Header的值 Header名称,修改后的值
SetStatus 修改原始响应的状态码 HTTP 状态码,可以是数字,也可以是字符串
StripPrefix 用于截断原始请求的路径 使用数字表示要截断的路径的数量
Retry 针对不同的响应进行重试 retries、statuses、methods、series
RequestSize 设置允许接收最大请求包的大 小。如果请求包大小超过设置的值,则返回 413 Payload TooLarge 请求包大小,单位为字节,默认值为5M
ModifyRequestBody 在转发请求之前修改原始请求体内容 修改后的请求体内容
ModifyResponseBody 修改原始响应体的内容 修改后的响应体内容

每个过滤器工厂都对应一个实现类,并且这些类的名称必须以GatewayFilterFactory 结尾,这是Spring Cloud Gateway的一个约定,例如 AddRequestHeader 对应的实现类为AddRequestHeaderGatewayFilterFactory

服务网关实践_第9张图片

全局过滤器

全局过滤器(GlobalFilter)作用于所有路由。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。

GloabalFilter接口源码如下:

过滤器应用
网关统一鉴权

编写一个全局过滤器

package org.example.gateway.filter;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 自定义全局过滤器
 */
@Component
public class LoginFilter implements GlobalFilter, Ordered {
    /**
     * 过滤器逻辑
     */
    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("执行了自定义的全局过滤器");
        return chain.filter(exchange);//放行,继续向下执行
    }

    /**
     * 指定过滤器执行顺序
     *  返回值越小,优先级越高
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

重启网关服务

 访问

http://localhost:8080/product-service/product/1

看到数据如下

查看网关服务IDEA控制台的输出,看到执行了自定义的全局过滤器。

说明过滤器正常工作了。

鉴权逻辑:

  • 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)

  • 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证

  • 以后每次请求,客户端都携带认证的token

  • 服务端对token进行解密,判断是否有效

修改LoginFilter的filter方法

/**
 * 过滤器逻辑
 *  对请求参数中的access-token进行判断
 *  如果存在此参数:代表已经认证成功
 *  如果不存在此参数:代表认证失败
 * ServerWebExchange:相当于请求和响应的上下文(类似于zuul中的RequestContext)
 */
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    System.out.println("执行了自定义的全局过滤器");
    //1.获取请求参数,例如:access-token
    String token = exchange.getRequest().getQueryParams().getFirst("access-token");
    //2.判断是否存在,不存在则认证失败,存在则放行
    if(token == null){
        System.out.println("没有登录");
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();//请求结束
    }
    return chain.filter(exchange);//放行,继续向下执行
}

重启网关服务

测试

  • 未携带access-token测试

    http://localhost:8080/product-service/product/1

服务网关实践_第10张图片

IDEA控制台提示没有登录  

服务网关实践_第11张图片

  • 携带access-token测试

    http://localhost:8080/product-service/product/1?access-token=1

网关限流

可以在网关阶段对服务调用进行限流,达到保护服务的作用。

限流算法

常见的限流算法有:计数器限流算法、滑动窗口算法、漏桶算法、令牌桶算法。

每种算法原理、优缺点、使用场景也各不一样,具体可参考 常见限流算法

限流后的处理方式
  • 拒绝服务

  • 排队等待

  • 服务降级

基于filter的限流

Spring Cloud Gateway官方就提供了基于令牌桶的限流支持。基于其内置的过滤器工厂RequestRateLimiterGatewayFilterFactory 实现。在过滤器工厂中是通过redis和lua脚本结合的方式进行流量控制。

安装好redis

启动redis服务端

启动redis客户端,使用monitor命令监控redis数据变化信息

127.0.0.1:6379> monitor

服务网关实践_第12张图片

在api_gateway_server服务中引入redis相关依赖(基于reactive的redis依赖)

        
        
            org.springframework.boot
            spring-boot-starter-actuator
        
        
        
            org.springframework.boot
            spring-boot-starter-data-redis-reactive
        

 修改application.yml,配置redis和网关过滤器

server:
  port: 8080 #端口
spring:
  application:
    name: api-gateway-server #服务名称
  redis:
    host: localhost
    port: 6379
    database: 0
  cloud: #配置SpringCloudGateway的路由
    gateway:
      routes:
        - id: product-service
          uri: lb://service-product
          predicates:
            - Path=/product-service/**
          filters:
            - name: RequestRateLimiter
              args:
                # 使用SpEL从bean容器中获取对象
                key-resolver: '#{@pathKeyResolver}'
                # 令牌桶每秒填充平均速率
                redis-rate-limiter.replenishRate: 1
                # 令牌桶的上限
                redis-rate-limiter.burstCapacity: 3
            - RewritePath=/product-service/(?.*), /$\{segment}
#eureka注册中心
eureka:
  client:
    service-url:
      defaultZone: http://localhost:9000/eureka/
  instance:
    prefer-ip-address: true #使用ip地址注册

虽然args相关配置存在报错如下,但是经验证,不影响运行。

服务网关实践_第13张图片

org.example.gateway包下,新建config包,并在config包下编写KeyResolverConfiguration配置

package org.example.gateway.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;

@Configuration
public class KeyResolverConfiguration {
    /**
     * 基于请求路径的限流
     */
    @Bean
    public KeyResolver pathKeyResolver() {
        return exchange -> Mono.just(
                exchange.getRequest().getPath().toString()
        );
    }
}

启动eureka、product、gateway服务

浏览器访问

http://localhost:8080/product-service/product/1?access-token=1

连续多次访问,每秒超过3次访问后,响应如下:

服务网关实践_第14张图片

查看redis客户端监控的输出如下:

服务网关实践_第15张图片

以上实现了基于请求路径的限流,其他方式限流,参考代码如下:

package org.example.gateway.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;

@Configuration
public class KeyResolverConfiguration {
    /**
     * 基于请求路径的限流
     *  方法名称与yml中key-resolver的值对应
     */
    @Bean
    public KeyResolver pathKeyResolver() {
        return exchange -> Mono.just(
                exchange.getRequest().getPath().toString()
        );
    }
    
    // 如果是其他的方式限流,代码如下,同时注意修改yml的key-resolver的值。 注意只能有一个@Bean生效,否则启动出错

    /**
     * 基于请求ip地址的限流
     */
//    @Bean
//    public KeyResolver ipKeyResolver() {
//        return exchange -> Mono.just(
//                exchange.getRequest().getHeaders().getFirst("X-Forwarded-For")
//        );
//    }

    /**
     * 基于用户的限流(参数)
     */
//    @Bean
//    public KeyResolver userKeyResolver() {
//        return exchange -> Mono.just(
//                exchange.getRequest().getQueryParams().getFirst("user")
//        );
//    }
}

测试其他的限流,例如:基于用户参数的限流

注释其他Bean代码,让如下Bean生效

	/**
     * 基于请求参数的限流
     *  请求 ?userId=
     * @return
     */
    @Bean
    public KeyResolver userKeyResolver(){
        return exchange -> Mono.just(
                exchange.getRequest().getQueryParams().getFirst("userId")
        );
    }

修改application.yml

                key-resolver: '#{@userKeyResolver}'

测试

http://localhost:8080/product-service/product/1?userId=1&access-token=1

访问达不到限流条件时

访问达到限流条件时

服务网关实践_第16张图片

基于sentinel的限流

api_gateway_server服务添加如下依赖

		
        
            com.alibaba.csp
            sentinel-spring-cloud-gateway-adapter
        

添加配置类

package org.example.gateway.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayParamFlowItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.*;

@Configuration
public class GatewayConfiguration {

	private final List viewResolvers;

	private final ServerCodecConfigurer serverCodecConfigurer;

	public GatewayConfiguration(ObjectProvider> viewResolversProvider,
	                            ServerCodecConfigurer serverCodecConfigurer) {
		this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
		this.serverCodecConfigurer = serverCodecConfigurer;
	}

	/**
	 * 配置限流的异常处理器:SentinelGatewayBlockExceptionHandler
	 */
	@Bean
	@Order(Ordered.HIGHEST_PRECEDENCE)
	public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
		return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
	}

	/**
	 * 配置限流过滤器
	 */
	@Bean
	@Order(Ordered.HIGHEST_PRECEDENCE)
	public GlobalFilter sentinelGatewayFilter() {
		return new SentinelGatewayFilter();
	}

	/**
	 * 配置初始化的限流参数
	 */
	@PostConstruct
	public void initGatewayRules() {
		Set rules = new HashSet<>();
//		rules.add(
//			new GatewayFlowRule("order-service") //资源名称
//					.setCount(1) // 限流阈值
//					.setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒
//		);
//		rules.add(new GatewayFlowRule("order-service")
//				.setCount(1)
//				.setIntervalSec(1)
//				.setParamItem(new GatewayParamFlowItem()
//						.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM).setFieldName("id")
//				)
//		);
		//限流规则对访问路径匹配product-service开头的路径生效
		rules.add(new GatewayFlowRule("product-service")
				.setCount(1)
				.setIntervalSec(1)
		);
//		rules.add(new GatewayFlowRule("product_api")
//				.setCount(1)
//				.setIntervalSec(1)
//		);
//		rules.add(new GatewayFlowRule("order_api")
//				.setCount(1)
//				.setIntervalSec(1)
//		);
		GatewayRuleManager.loadRules(rules);
	}

//	@PostConstruct
//	public void initBlockHandlers() {
//		BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
//			public Mono handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
//				Map map = new HashMap<>();
//				map.put("code", 001);
//				map.put("message", "对不起,接口限流了");
//				return ServerResponse.status(HttpStatus.OK).
//						contentType(MediaType.APPLICATION_JSON_UTF8).
//						body(BodyInserters.fromObject(map));
//			}
//		};
//		GatewayCallbackManager.setBlockHandler(blockRequestHandler);
//	}

//	@PostConstruct
//	private void initCustomizedApis() {
//		Set definitions = new HashSet<>();
//		ApiDefinition api1 = new ApiDefinition("product_api")
//				.setPredicateItems(new HashSet() {{
//					add(new ApiPathPredicateItem().setPattern("/product-service/product/**").
//							setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
//				}});
//		ApiDefinition api2 = new ApiDefinition("order_api")
//				.setPredicateItems(new HashSet() {{
//					add(new ApiPathPredicateItem().setPattern("/order-service/order"));
//				}});
//		definitions.add(api1);
//		definitions.add(api2);
//		GatewayApiDefinitionManager.loadApiDefinitions(definitions);
//	}
}

以上配置,根据路由(访问路径)进行限流,限流规则对访问路径匹配product-service开头的路径生效。

修改application.yml

  • 注释RequestRateLimiter filter相关配置
          filters:
#            - name: RequestRateLimiter
#              args:
#                # 以下上个配置项目报错,不能解析,但不影响使用
#                # 使用SpEL从容器中获取对象
#                #key-resolver: '#{@pathKeyResolver}'
#                key-resolver: '#{@userKeyResolver}'
#                # 令牌桶每秒填充平均速率
#                redis-rate-limiter.replenishRate: 1
#                # 令牌桶的上限
#                redis-rate-limiter.burstCapacity: 3
            - RewritePath=/product-service/(?.*), /$\{segment}
  •  注释掉redis相关配置
#  redis:
#    host: localhost
#    port: 6379
#    database: 0

为了防止启动服务报redis连接拒绝,注释掉redis依赖,同时注意刷新依赖生效

        



测试

启动eureka、product、gateway服务

访问

http://localhost:8080/product-service/product/1?access-token=1

访问超过阈值后,响应

Blocked by Sentinel: ParamFlowException

因为路径中包含product-service开头,符合限流规则,所以当访问超出哦阈值后,执行限流操作,看到如上默认的响应信息。

为了更加友好的限流提示,可以自定义响应信息,如下:

取消initBlockHandlers方法的注释

	/**
	 * 自定义限流提示
	 */
	@PostConstruct
	public void initBlockHandlers() {
		BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
			public Mono handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
				Map map = new HashMap<>();
				map.put("code", 001);
				map.put("message", "对不起,接口限流了");
				return ServerResponse.status(HttpStatus.OK).
						contentType(MediaType.APPLICATION_JSON_UTF8).
						body(BodyInserters.fromObject(map));
			}
		};
		GatewayCallbackManager.setBlockHandler(blockRequestHandler);
	}

重启gateway服务

测试,访问超过限流阈值时,响应如下

以上是基于路由的限流规则,这个路由下的所有资源都受到这个限流规则限制,sentinel也支持自定义API限流规则,去掉initCustomizedApis()方法注释

/**
 * 自定义API限流规则
 *        1.定义分组,例如:product_api和order_api
 *        2.在initGatewayRules方法对小组配置限流规则
 */
@PostConstruct
private void initCustomizedApis() {
   Set definitions = new HashSet<>();
   ApiDefinition api1 = new ApiDefinition("product_api")
         .setPredicateItems(new HashSet() {{
            add(new ApiPathPredicateItem().setPattern("/product-service/product/**").
                  setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
         }});
   ApiDefinition api2 = new ApiDefinition("order_api")
         .setPredicateItems(new HashSet() {{
            add(new ApiPathPredicateItem().setPattern("/order-service/order"));
         }});
   definitions.add(api1);
   definitions.add(api2);
   GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}

在initGatewayRules方法对小组配置限流规则

@PostConstruct
	public void initGatewayRules() {
		Set rules = new HashSet<>();

		rules.add(new GatewayFlowRule("product_api")
				.setCount(1)
				.setIntervalSec(1)
		);
//		rules.add(new GatewayFlowRule("order_api")
//				.setCount(1)
//				.setIntervalSec(1)
//		);
		GatewayRuleManager.loadRules(rules);
	}

重启gateway服务

测试

访问超过阈值后,同样能够起到限流效果

网关高可用

所有请求都需要先经过网关才到具体的微服务,网关只有一个节点会存在单点故障,解决办法是使用多个网关服务构成网关集群,实现网关高可用,当一个网关服务不可用,还有其他网关服务可用。实现架构如下图所示:

服务网关实践_第17张图片

为了方便测试,取消网关限流配置,注释@Configuration注解

//@Configuration
public class GatewayConfiguration {

重启网关服务(8080端口)

复制配置得到另一个网关服务

修改api_gateway_server网关服务的配置文件 application.yml,将端口号改为8081,通过选中运行中的GatewayServerApplication,右键-->Copy Configuration复制配置得到另一个网关服务运行实例

服务网关实践_第18张图片

运行复制得到的网关服务(8081端口)

访问8080和8081均能访问到服务

http://localhost:8080/product-service/product/1?access-token=1

http://localhost:8081/product-service/product/1?access-token=1

官网下载并解压nginx安装包

服务网关实践_第19张图片

修改nginx配置,进入conf目录,修改nginx.conf文件

服务网关实践_第20张图片

注释掉原来location /的配置,添加如下配置

http {
   upstream gateway{
        server 127.0.0.1:8080;
        server 127.0.0.1:8081;
    }
    server {
        ...
	    location / {
            	proxy_pass http://gateway; //proxy_pass请求转发
        }
        ...
    }  
}

配置后,如图 

服务网关实践_第21张图片

启动nginx

服务网关实践_第22张图片

浏览器访问

http://localhost/product-service/product/1?access-token=1

如果需要改策略,可以修改nginx配置,配置案例如下

IP哈希(IP Hash)策略

upstream gateway {
        ip_hash;
        server 127.0.0.1:8080;
        server 127.0.0.1:8081;
    }

重启nginx,同一机器访问服务三次,均是8080网关提供服务

服务网关实践_第23张图片

服务网关实践_第24张图片

最少连接(Least Connections)策略

upstream gateway {
        least_conn;
        server 127.0.0.1:8080;
        server 127.0.0.1:8081;
    }

加权轮询(Weighted Round Robin)策略

upstream gateway {
        server 127.0.0.1:8080 weight=3;
        server 127.0.0.1:8081 weight=1;
    }

模拟故障,停掉一个网关服务

服务网关实践_第25张图片

依然能访问,实现了网关的高可用。

完成!enjoy it!

你可能感兴趣的:(微服务,IDEA,微服务,gateway)