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、

  • 相关阅读:
    Delphi公用函数单元
    Delphi XE5 for Android (十一)
    Delphi XE5 for Android (十)
    Delphi XE5 for Android (九)
    Delphi XE5 for Android (八)
    Delphi XE5 for Android (七)
    Delphi XE5 for Android (五)
    Delphi XE5 for Android (四)
    Delphi XE5 for Android (三)
    Delphi XE5 for Android (二)
  • 原文地址:https://www.cnblogs.com/luo630/p/15262499.html
Copyright © 2011-2022 走看看