zoukankan      html  css  js  c++  java
  • spring cloud项目07:网关(Gateway)(2)

    JAVA 8

    spring boot 2.5.2

    spring cloud 2020.0.3

    ---

    授人以渔:

    1、Spring Cloud PDF版本

    最新版本,下载下来,以便查阅。

    更多版本的官方文档:

    https://docs.spring.io/spring-cloud/docs/

    2、Spring Cloud Gateway

    没有PDF版本,把网页保存下来。

    前文:spring cloud项目06:网关(Gateway)(1)

    本文使用的项目:

    主要路径:前端请求经过 external.gateway 转发到 adapter.web。在此过程中,会做一些试验。

    external.gateway  网关服务 端口 25001
    adapter.web web适配层应用 端口 21001
    data.user user数据层应用 端口 20001
    eureka.server Eureka注册中心 端口 10001

    目录

    1、网关服务化

    2、限流

    使用RequestRateLimiterGatewayFilterFactory限流

    3、编程配置路由

    参考文档

    1、网关服务化

    前文 中,路由配置中的uri使用的是 http://localhost:21001,硬编码,而且uri无法配置多个(试验失败),实现不了负载均衡(LB,Load Balance),需要改进。

    在S.C.微服务系统中,所有服务都可以注册,那么,网关服务是否可以注册呢?当然可以!

    网关服务注册之后,即可使用注册中心的服务信息来访问 已注册的服务。

    添加依赖包:

    <!-- 注册到注册中心 -->
    <dependency>
    	<groupId>org.springframework.cloud</groupId>
    	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    添加配置(同其它微服务):

    # Eureka客户端配置
    eureka:
      instance.prefer-ip-address: true
      lease-renewal-interval-in-seconds: 15
      lease-expiration-duration-in-seconds: 30
      client:
        service-url:
          defaultZone: http://localhost:10001/eureka/
        registry-fetch-interval-seconds: 20
    
    # 提升日志级别,避免输出太多注册相关的正常日志
    logging:
      level:
        com.netflix.discovery.DiscoveryClient: warn
    
    spring:
      application:
        # 服务名
        name: external.gateway

    检查注册中心:网关服务已注册成功

     

    检查 /actuator/gateway/globalfilters端点:来自博客园

    和前文对比,多了一个 ReactiveLoadBalancerClientFilter,看起来是做 LB 的。

    返回信息
    {
        "org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@68b9834c": -2147482648,
        "org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@7da39774": 10000,
        "org.springframework.cloud.gateway.filter.NettyRoutingFilter@7d7cac8": 2147483647,
        "org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter@306f6f1d": 10150,
        "org.springframework.cloud.gateway.filter.ForwardRoutingFilter@441b8382": 2147483647,
        "org.lib.external.gateway.filters.TokenGlobalFilter@6f76c2cc": 0,
        "org.springframework.cloud.gateway.filter.ForwardPathFilter@1df1ced0": 0,
        "org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@5349b246": 2147483646,
        "org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@6fc6deb7": -1,
        "org.springframework.cloud.gateway.filter.GatewayMetricsFilter@32b0876c": 0,
        "org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@367f0121": -2147483648
    }

     在上面的配置后,还是只能使用之前配置的路由。来自博客园

    进一步:添加下面的配置后,可以无需手动添加路由即可访问注册中心 所有服务(不建议 生产环境使用)

    # spring.cloud.gateway.discovery.locator.enabled=true 默认是 false
        # 路由配置
        gateway:
          discovery:
            locator:
              enabled: true
              lowerCaseServiceId: true

    配置后,可以使用下面的链接访问 已注册的 adapter.web、data.user 服务的端点:

    http://localhost:25001/adapter.web/user/get?id=1

    http://localhost:25001/data.user/user/get?id=1

    注,红色部分是 小写了的服务名。

    此时,/actuator/gateway/routes 也发生了很大的变化,多了很多路由:来自博客园

    响应
    [
        {
            "predicate": "Paths: [/adapter.web/**], match trailing slash: true",
            "metadata": {
                "jmx.port": "59178",
                "management.port": "21001"
            },
            "route_id": "ReactiveCompositeDiscoveryClient_ADAPTER.WEB",
            "filters": [
                "[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
                "[[RewritePath /adapter.web/?(?<remaining>.*) = '/${remaining}'], order = 1]",
                "[[RequestTime logEnabled=true], order = 2]"
            ],
            "uri": "lb://ADAPTER.WEB",
            "order": 0
        },
        {
            "predicate": "Paths: [/data.user/**], match trailing slash: true",
            "metadata": {
                "jmx.port": "59158",
                "management.port": "20001"
            },
            "route_id": "ReactiveCompositeDiscoveryClient_DATA.USER",
            "filters": [
                "[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
                "[[RewritePath /data.user/?(?<remaining>.*) = '/${remaining}'], order = 1]",
                "[[RequestTime logEnabled=true], order = 2]"
            ],
            "uri": "lb://DATA.USER",
            "order": 0
        },
        {
            "predicate": "Paths: [/external.gateway/**], match trailing slash: true",
            "metadata": {
                "jmx.port": "53359",
                "management.port": "25001"
            },
            "route_id": "ReactiveCompositeDiscoveryClient_EXTERNAL.GATEWAY",
            "filters": [
                "[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
                "[[RewritePath /external.gateway/?(?<remaining>.*) = '/${remaining}'], order = 1]",
                "[[RequestTime logEnabled=true], order = 2]"
            ],
            "uri": "lb://EXTERNAL.GATEWAY",
            "order": 0
        },
        {
            "predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Paths: [/web/**], match trailing slash: true)",
            "route_id": "route1",
            "filters": [
                "[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
                "[[AddRequestHeader addHead = 'abc'], order = 1]",
                "[[RequestTime logEnabled=true], order = 2]",
                "[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]"
            ],
            "uri": "http://localhost:21001",
            "order": 0
        }
    ]

    甚至通过 其自身(/external.gateway/**) 来访问——不会死循环吗?!

    还好,spring.cloud.gateway.discovery.locator.* 下还有很多配置(可以在 官文 查到):

    负载均衡配置:

    开启3个adapter.web服务,端口分别为:21001、21002、21003。来自博客园

    去掉前面的spring.cloud.gateway.discovery.locator.* 的配置。

    修改路由中的uri为下面的:lb://adapter.web

          - id: route1
            # 1)服务
    #        uri: http://localhost:21001
            # 负载均衡访问服务 adapter.web
            uri: lb://adapter.web

    访问 /web/user/get?id=1,检查 3个adapter.web服务 是否均衡地收到并处理了请求:成功,均衡地处理了请求。

    小结,

    网关服务化后,可以很方便地实现负载均衡地访问代理服务。来自博客园

    spring.cloud.gateway.discovery.locator.* 的最佳实践还有待探索,比如,根据前缀只允许访问服务中的部分请求,这就需要开发不同的断言、过滤器了吧。

    2、限流

    限流,限制进入系统的流量。

    限流的作用:1)防止流量突发使服务器过载;2)防止流量攻击。

    常见限流维度:IP限流、请求URL限流、用户访问频次限流。(注:在使用微信公众平台接口时,还可以限制每个账号每小时、每天的调用次数等)

    限流发生的位置:1)网关层(Nginx、Zuul、S.C.Gateway等),2)应用层。

    本文介绍在S.C.Gateway中实现限流

    搜索:常见限流算法——计数器算法、漏桶算法、令牌桶算法

    自定义pre类型的过滤器,可以实现需要的限流算法。来自博客园

    在S.C.Gateway中,已经提供了一个 RequestRateLimiterGatewayFilterFactory,其使用 Redis和Lua脚本实现令牌桶算法进行限流。

    @ConfigurationProperties("spring.cloud.gateway.filter.request-rate-limiter")
    public class RequestRateLimiterGatewayFilterFactory
    		extends AbstractGatewayFilterFactory<RequestRateLimiterGatewayFilterFactory.Config> {
            
        // ...
        
    	private final RateLimiter defaultRateLimiter;
    
    	private final KeyResolver defaultKeyResolver;
        
        // ...
    }

    Lua脚本位置:

    注,官文中的 The RequestRateLimiter GatewayFilter Factory 一节有它详细的介绍。来自博客园

    6.10. The RequestRateLimiter GatewayFilter Factory
    
    The RequestRateLimiter GatewayFilter factory uses a RateLimiter implementation to determine if the
    current request is allowed to proceed. If it is not, a status of HTTP 429 - Too Many Requests (by
    default) is returned.
    
    This filter takes an optional keyResolver parameter and parameters specific to the rate limiter
    (described later in this section).

    使用RequestRateLimiterGatewayFilterFactory限流

    实现根据远程主机地址限流。

    由于S.C.Gateway基于Netty,因此,需要引入reactive版本的redis:

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency>

    配置Redis:来自博客园

    spring:
      # Redis配置-限流使用
      redis:
        host: mylinux
        port: 6379

    建立KeyResolver类并注册到Spring容器:

    # HostAddrKeyResolver.java
    public class HostAddrKeyResolver implements KeyResolver {
    
    	@Override
    	public Mono<String> resolve(ServerWebExchange exchange) {
    		return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    	}
    
    }
    
    # AppConfig.java
    @Configuration
    public class APPConfig {
    
    	/**
    	 * 限流的键解析器
    	 * @author ben
    	 * @date 2021-09-13 16:48:26 CST
    	 * @return
    	 */
    	@Bean
    	public HostAddrKeyResolver hostAddrKeyResolver() {
    		return new HostAddrKeyResolver();
    	}
        
    }
    

    配置路由使用RequestRateLimiterGatewayFilterFactory:

    配置方式和其它的不太一样,具体需要看看源码。

    配置参数已在下面的注释中有说明(SpEL真的很重要,使用Spring时键值无处不在啊)!

            # 过滤器配置
            filters:
            # 限流过滤器
            - name: RequestRateLimiter
              args:
                # 用于限流的键的解析器的Bean对象的名称——SpEL表达式
                # 默认有一个 PrincipalNameKeyResolver类,下面的hostAddrKeyResolver 需要自行实现
                key-resolver: '#{@hostAddrKeyResolver}'
                # 令牌桶每秒的平均填充速率
                redis-rate-limiter.replenishRate: 1
                # 令牌桶总量
                redis-rate-limiter.burstCapacity: 3

    测试限流效果:来自博客园

    1)Postman:快速点击(要足够快),以此触发限流机制

    测试期间发现响应的状态为:429 Too Many Requests,此时触发了限流规则。

    2)Apache JMeter:配置多个线程快速访问

    在Redis中,限流的数据是怎么保存的呢?检查下。来自博客园

    redis-cli检查
    # 多了两个key
    127.0.0.1:6379> keys *
    1) "xacxedx00x05tx00x04set1"
    2) "request_rate_limiter.{0:0:0:0:0:0:0:1}.tokens"
    3) "xacxedx00x05tx00x05test3"
    4) "request_rate_limiter.{0:0:0:0:0:0:0:1}.timestamp"
    5) "xacxedx00x05tx00x05test1"
    127.0.0.1:6379>
    
    # 生存期很短
    127.0.0.1:6379> ttl request_rate_limiter.{0:0:0:0:0:0:0:1}.tokens
    (integer) 4
    127.0.0.1:6379> get request_rate_limiter.{0:0:0:0:0:0:0:1}.tokens
    "2"
    
    127.0.0.1:6379> get request_rate_limiter.{0:0:0:0:0:0:0:1}.timestamp
    "1631535157"
    127.0.0.1:6379> ttl request_rate_limiter.{0:0:0:0:0:0:0:1}.timestamp
    (integer) 4
    

    小结,

    就这样,在S.C.Gateway中把 限流 用起来了。

    上面的用法很简单,真实的限流则有各种各样的规则,比如,服务器弹性部署时,网关怎么弹性更改配置呢?更改配置文件吗?这个时候就需要编程来实现了。

    Gateway中有限流,底层应用是否也要有限流呢?两者如何互补?

    Gateway中的令牌桶限流算法的实现原理是怎样的?那个Lua脚本是怎么写的?都需要继续探索的。

    先读一遍官文才好。

    3、编程配置路由

    在前面的示例中,网关的配置都是在 配置文件中完成的。

    是否可以通过编程来实现路由配置呢?

    配置文件配置 和 编程配置,两种方式的优缺点分别是什么?来自博客园

    编程配置路由方式:使用Spring容器中routeLocatorBuilder Bean生成一个RouteLocator Bean即可

    默认下,已经有 routeDefinitionRouteLocator、cachedCompositeRouteLocator 两个Bean了,是用来做什么的呢?

    示例代码:用各种方式,配置了 3个路由

    package org.lib.external.gateway.routes;
    
    import java.util.function.Function;
    
    import org.springframework.cloud.gateway.route.Route;
    import org.springframework.cloud.gateway.route.RouteLocator;
    import org.springframework.cloud.gateway.route.builder.Buildable;
    import org.springframework.cloud.gateway.route.builder.PredicateSpec;
    import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * 应用路由配置(编程方式)
     * @author ben
     * @date 2021-09-13 20:47:31 CST
     */
    @Configuration
    public class AppRoutesConfig {
    
    	@Bean
    	public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    		return builder
    				.routes()
    				// 路由1
    				.route("routeP1", new Function<PredicateSpec, Buildable<Route>>() {
    					
    					@Override
    					public Buildable<Route> apply(PredicateSpec t) {
    						return t.order(3)
    								.path("/user/**")
    								.filters(f->f.addResponseHeader("program-header", "routeP1"))
    								.uri("lb://adapter.web");
    					}
    				})
    				// 路由2
    				.route("routeP2", r->r.order(2)
    						.path("/routeP2/**")
    						.filters(f->f.addResponseHeader("program-header", "routeP2")
    								.retry(3)
    								.rewritePath("/routeP2/(?<segment>.*)", "/$\{segment}"))
    						.uri("lb://adapter.web")
    				)
    				// 路由3
    				.route(r->r.order(-10)
    						.path("/routeP3/**")
    						.filters(f->f.addResponseHeader("program-header", "routeP3")
    								.rewritePath("/routeP3/(?<segment>.*)", "/$\{segment}"))
    						.uri("lb://adapter.web")
    				)
    				.build();
    	}
    	
    }
    

    访问/actuator/gateway/routes:按优先级 从高到低 展示了系统中的路由,其中,第二的route1 是 配置文件中的,看来可以共存。

    [
        {
            "predicate": "Paths: [/routeP3/**], match trailing slash: true",
            "route_id": "c48fc06f-380c-405d-abf4-791df2008e37",
            "filters": [
                "[[RewritePath /routeP3/(?<segment>.*) = '/${segment}'], order = 0]"
            ],
            "uri": "lb://adapter.web",
            "order": -10
        },
        {
            "predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Paths: [/web/**], match trailing slash: true)",
            "route_id": "route1",
            "filters": [
                "[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
                "[[AddRequestHeader addHead = 'abc'], order = 1]",
                "[[RequestTime logEnabled=true], order = 2]",
                "[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]",
                "[org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory$$Lambda$978/889546737@2cad0ced, order = 3]"
            ],
            "uri": "lb://adapter.web",
            "order": 0
        },
        {
            "predicate": "Paths: [/routeP2/**], match trailing slash: true",
            "route_id": "routeP2",
            "filters": [
                "[[AddRequestHeader program-header = '210913'], order = 0]",
                "[[Retry routeId = 'routeP2', retries = 3, series = list[SERVER_ERROR], statuses = list[[empty]], methods = list[GET], exceptions = list[IOException, TimeoutException]], order = 0]",
                "[[RewritePath /routeP2/(?<segment>.*) = '/${segment}'], order = 0]"
            ],
            "uri": "lb://adapter.web",
            "order": 2
        },
        {
            "predicate": "Paths: [/user/**], match trailing slash: true",
            "route_id": "routeP1",
            "filters": [
                "[[AddRequestHeader program-header = '210913'], order = 0]"
            ],
            "uri": "lb://adapter.web",
            "order": 3
        }
    ]

    测试使用4个路由访问 web适配层应用:都能成功获取数据

    http://localhost:25001/user/get?id=1
    http://localhost:25001/web/user/get?id=1
    http://localhost:25001/routeP2/user/get?id=1
    http://localhost:25001/routeP3/user/get?id=1

    小结:

    编程添加路由,策略有变化,需要重启服务:确定。来自博客园

    配置文件中添加路由。策略有变化,是否不需要重启服务?更新配置即可?TODO

    对了,上面说的配置文件更新,是指存放于外部的配置文件(S.C.Config)更改后,是否可以更新到 正在运行的 网关服务?

    哪些路由需要使用 编程添加,哪些通过 配置文件添加?

    还是再看看官文吧,最权威的。来自博客园

    》》》全文完《《《

    参考文档

    1、《深入理解Spring Cloud与微服务构建》

    2019年9月第2版,作者:方志朋

    2、

  • 相关阅读:
    使用Sharepoint Designer 无法打开站点提示错误403 forbidden
    英文Windows系统打开带中文TXT出现乱码
    Linux查看MegaSAS raid卡缓存策略
    PostgreSQL基础CRUD
    PostgreSQL安装(on Windows 10)
    Node.js安装(on Windows 10)
    Spring Data JPA:建立实体类
    Java:类加载
    Spring Data JPA:关联关系(外键)
    MySQL系统化知识概要
  • 原文地址:https://www.cnblogs.com/luo630/p/15262499.html
Copyright © 2011-2022 走看看