• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

JAVA的微服务网关实操【二】:SCG + Nacos 实现动态的感知上下线

武飞扬头像
juejin
帮助453

前言

一、JAVA的微服务网关实操【一】:SCG 和 APISIX 到底应该怎么选择?

二、JAVA的微服务网关实操【二】:SCG Nacos 实现动态的感知上下线

三、JAVA的微服务网关实操【三】:很详细介绍并理解 SCG 路由和断言与过滤器

上一篇简单讲解了微服务网关实战对于网关中间件的选型,本篇将给大家详细讲解网关的搭建,整体上会以一个网关和两个微服务作为演示,加上 Nacos 提供注册中心和配置中心的能力,并且解决搭建网关会遇到的第一个问题:网关感知服务动态上下线。

先赞后看,养成习惯,相信我的文章一定会让大家有所收获,废话不多说,开始了~

1. Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台,并且它支持一些主流的开源生态,它是我们做微服务系统必不可少的一个基础中间件,下面是它的架构图(图片来自官网):

从图中可以看到我们比较常用的三个生态:Spring Cloud、Dubbo、K8S,Nacos都提供了支持,这对我们的开发和运维也提供了很大的方便。

Nacos 除了对于开源生态的支持之外,它还可以集群部署的方式来提供高可用、并且配备了易于操控的可视化控制台,就这两点而言就已经比它的一些老前辈高出一大截,所以我这次的网关实战我选择它为我们的微服务提供注册中心 配置中心。

接下来我们需要将它启动起来,直接去 GitHub 下载它的稳定版本,这里我选择比较新的 2.1.0 版本,下载压缩包之后你可以在 target 目录下找到这个 jar 包:

然后就是再熟悉不过的 java -jar了,不过 Nacos 在新版本默认使用集群模式启动,集群模式需要连接数据库作为持久化数据源,如果你不想这么麻烦可以使用单机模式启动,单机模式下数据存储在内存中:

java -jar -Dnacos.standalone=true nacos-server.jar

接着你就可以在控制台中看到你的 Nacos 地址了:

点击网址进去,输入账号 nacos 密码 nacos,就可以登陆了。

注: Nacos 在 Windows 环境下对中文目录不友好,你可以放在 Linux 虚拟机中安装,或者使用英文目录,安装教程也官网也有。

2. 项目搭建

简单介绍一下 Nacos 之后,我们就可以着手我们的项目搭建了:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.1</version>
        <relativePath/>
    </parent>

一般我对于自己重新做项目都比较喜欢使用新版本的依赖,这样可以有效利用上新版本的特性,所以我这次选择了最新的 Spring Boot 2.7

接下来就是 Spring Cloud 的选择,选择现在已经是2022年了,但是 Spring Cloud 的版本号还是以2021开头,其中按照官网上的描述对于Spring Boot2.7版本的支持,Spring Cloud是从 2021.0.3 才开始支持,目前的最新版是 2021.0.4,所以我就选择 2021.0.3 版本了,这个版本下 Spring Cloud Gateway 对应的版本是 3.1.3。

然后就是 Spring Cloud Alibaba 的选择,Nacos 与 Spring Cloud 相关整合是放在 Spring Cloud Alibaba 家族当中的:

这里我一如既往的选择次新版本,但是请注意它的版本号有一点问题,2021.0.x是 2022 年的,这个版本是最新的,反而看上去要高一点的 2021.1.x 是 2021年的,而且它还排在前面,所以大家在选择版本的时候要注意清楚:

<properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-cloud.version>2021.0.3</spring-cloud.version>
        <alibaba-cloud.version>2021.0.1.0</alibaba-cloud.version>
    </properties>

主要的版本号都搞清楚之后,就可以引入依赖了,虽然我本机是 jdk17 了,但是为了普适性我这里选择了 jdk1.8,接下来就是依赖选择:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>nacos-client</artifactId>
                    <groupId>com.alibaba.nacos</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>nacos-client</artifactId>
                    <groupId>com.alibaba.nacos</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <artifactId>nacos-client</artifactId>
            <groupId>com.alibaba.nacos</groupId>
            <version>2.1.0</version>
        </dependency>
    </dependencies>

为了文章的简洁性,像 SpringBootTest 这种依赖我就不展示了,具体可以稍后查看我的源码,我这里主要讲讲我都依赖了什么:

  1. spring-boot-starter-actuator:这个是为了方便观测网关。
  2. spring-cloud-starter-gateway:网关。
  3. spring-boot-starter-data-redis-reactive:异步非阻塞的 Redis 依赖,在网关中所有的 IO 操作都应该是异步非阻塞的,由于后文中的限流器需要用到它,所以我这里直接引入了。
  4. spring-cloud-starter-loadbalancer:网关分发所用到的负载均衡,老版本可能还在用 ribbon,新版本已经不用了。
  5. spring-cloud-starter-bootstrap:分布式配置中心相关依赖,没有它无法读取配置中心配置。
  6. spring-cloud-starter-alibaba-nacos-discovery:Nacos 注册中心客户端依赖。
  7. spring-cloud-starter-alibaba-nacos-config:Nacos 配置中心客户端依赖。

比较细心的小伙伴可能已经发现了,我把 Nacos 的两个依赖中的 nacos-client 排除了,并且自己引入了一个新的 2.1.0 版本的 nacos-client,这是因为原先依赖自带的 1.4 版本并不能完全发挥我们的 2.X 版本的 Nacos 的优势,所以我这里选择了和我们部署的 Nacos 实例相同版本的客户端依赖。

3. 项目配置

依赖引入之后,就该做一些必要的配置了,因为我们引入了 Nacos 和 Redis,所以配置这里主要是这两个东西,我一般习惯在建立两个配置文件,一个 bootstrap.yaml 作为公共配置使用,比如优雅关机、应用名称、redis 线程池大小之类的共有配置,然后再建立一个带有环境变量的多环境配置 bootstrap-test.yaml,这里面主要就是放一些连接相关的配置,比如测试环境的连接和生产环境的连接肯定是不一样的,就在这种配置文件里面区分。

先来看看公共配置 bootstrap.yaml:

server:
  shutdown: graceful
​
spring:
  application:
    name: gateway-api
  lifecycle:
    timeout-per-shutdown-phase: 120s
  redis:
    lettuce:
      pool:
        enabled: true
        max-active: 16
        min-idle: 1
      cluster:
        refresh:
          adaptive: true
          period: 60
​
  cloud:
    gateway:
      discovery:
        locator:
          # 开启服务发现动态路由
          enabled: true
          # 是否将服务名称小写
          lower-case-service-id: true
      httpclient:
        pool:
          max-idle-time: 12000
    nacos:
      discovery:
        group: public
      config:
        group: public
        extension-configs:
          - data-id: gateway-routes
            group: public
            refresh: true
  profiles:
    active: test

公共配置主要配置了一下应用名字和优雅关机,其中配置了一下 nacos 的服务发现,在注册中心和配置中心处都配置了一个group=public,其实这个是默认值配置不配置都可以的,然后配置了一下要监听的配置文件,这个在下一篇文章会用到。

在 Nacos 的规则下,默认是监听和应用名同名的配置文件,比如我们的应用名是 gateway-api,它就会监听这个配置文件,所以需要要先同时监听其他配置文件就需要指定,我这里就指定了 public 下的名为 gateway-routes 的配置文件。

最后,我设置了默认环境为测试环境,这会将 bootstrap-test.yaml 也加载进来:

spring:
  redis:
    host: 172.22.104.105
    database: 0
    port: 6379
    connect-timeout: 5000
    password: root
  cloud:
    nacos:
      discovery:
        server-addr: http://172.22.104.105:8848
        namespace: public
      config:
        server-addr: http://172.22.104.105:8848
#        file-extension: yaml
        namespace: public
​
management:
  endpoints:
    web:
      exposure:
        include: gateway

测试环境配置就比较简单了,配置了一下 redis 地址和 nacos 的两个地址,其中 nacos 有个 namespace 需要注意,它默认也是 public,它是用来将 nacos 进行环境切分的,测试环境和生产环境可以用同一台 nacos 实例的不同 namespace。

4. 启动网关

上面的一切都做好之后就可以把我们的网关服务 run 起来了,run 起来之后可以通过我们前面提到过的 nacos 控制台查看服务列表:

不出意外的话,在这里你可以看到你的服务,这就代表你的服务已经被注册到 nacos 上面了,出了意外的话,就解决一下吧。

网关起来之后,为了模仿微服务场景,我们还需要再起两个服务作为演示,那就新建一个订单服务和用户服务吧,新建完成后将他们启动起来就可以看到如下效果:

这样我们本次所演示需要的三个服务就集齐了,接着为两个服务各编写一个示例 Controller:

@RestController
public class OrderController {
​
    @GetMapping("/order/test")
    public String test() {
        return "order-api";
    }
}
​
@RestController
public class UserController {
​
    @GetMapping("/user/test")
    public String test() {
        return "user-api";
    }
}

这样我们就凑齐了一个网关 两个微服务的示例,且这两个服务各有一个接口,麻雀虽小五脏俱全,我们生产环境的结构是和这个一模一样的,无非就是后面跟的服务不同罢了。

截止到现在虽然网关已经成功启动了,但是网关和服务之间还是没有建立联系的,在我们有了网关之后,所有的流量都会经过网关,再通过网关进行转发到不同的服务,接下来我们就要进行流量转发。

5. 流量转发

在 Spring Cloud Gateway 中,有两个概念特别重要:路由过滤器

路由是表示请求流量该以什么规则往哪进行转发,过滤器则是对经过它的请求进行一些处理,比如限流、添加请求头等。

所以我们既然需要用到了流量转发,那就不可避免的要配置路由,在 Spring Cloud Gateway 中配置路由有两种方式:配置文件 和 Bean 配置,由于官网文档中的配置和网上大部分配置都是配置文件配置,所以我这里就采用 Bean 配置,不过不用担心和其他资料脱节,在后续的章节里面这两个方式都不会使用,而是通过 Nacos 进行配置。

我们在配置类中声明如下 Bean:

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
    return builder.routes()
            .route((r) -> r.path("/user/**").uri("lb://user-api"))
            .route((r) -> r.path("/order/**").uri("lb://order-api"))
            .build();
}

这个 Bean 是一个路由的构建器,通过代码可以看出一个构建了两个路由,参数几乎一致,都是一个 path 和 一个 uri

Path 是代表路由的断言规则,所谓断言就是给出一个输入,断言处理后返回一个 true 或者 false,Path 断言是 SpringCloudGateway 的内置断言规则之一,它就是我们最常见的路径匹配,支持 Ant 操作符,所以你看到我配置了一个以 user 开头的请求和一个以 order 开头的请求这样一个规则,当请求经过断言判断符合后就会进行转发。

Uri 则代表请求转发的服务,并且要填上以什么协议进行转发,由于我们是在内网转发一般都是都是 lb://,lb 代表负载均衡协议,后面跟的是一个服务名,这个服务名必须和 Nacos 上面注册的服务名一直才能转发过去,如果你自己测试的话也可以配置一个 http 请求的转发,比如 www.swvq.com 也是可以的,只不过协议和服务不同而已。

除了这两点之外,我还需要进行一点说明,Path 并不会进行 url 重写,比如你发起一个 http://localhost:8080/user/test 请求,那么转发到 user-api 之后,它收到的就是 /user/test 请求,并不是 /test 请求。

除了 Path 断言之外,SpringCloudGateway 还提供了一堆各种各样的断言,比如根据IP地址、根据时间、根据请求头、根据域名等等,它们还可以组合起来使用,这点我们后文细说。

添加这么一层配置之后我们重启服务,然后发起一个 http://localhost:8080/user/test 请求,看到如下效果就证明请求转发成功了:

image-20220927233745638

6. 服务动态感知上下线

网关连通之后,只能说它通了,但是仍旧还有一些问题需要自己去解决,比如:服务上下线的动态感知。

我们知道网关后面往往是一堆服务,每个服务往往又是一个集群几个实例,就像这样:

image-20220928204441955

网关会将请求分发到具体的某个服务上,再通过负载均衡分发到具体的实例上,比如上面例子中的请求就会分发到 user 服务中,但是如果 user 服务有两个实例,那么你必然需要知道这两个实例的相关信息,才能将请求转发到对应的机器上去。

一般来讲为了性能考虑,网关都会把服务对应的实例列表信息缓存到自己的内存中,这样每次请求来了就可以直接通过内存中保存的实例列表去选择具体分发到哪个实例上,如果没有缓存那每次请求来了都要去请求一遍目前最新的实例列表那就太消耗时间了。

这种做法虽然性能很高,但是也会出现一个问题,拿 user 服务举例来说,如果其中一个 user 实例忽然挂掉了,网关并不会知道,而且会依然给这个实例分发请求,这就会导致很多请求被拒绝,影响正常的业务。

我们可以通过 IDEA 来模拟这个场景,前文我已经新建了一个 user 模块,现在只需要将它多开,就可以生成两个实例了,

image-20220928205520665

你需要对运行配置进行编辑,然后选择 Build and run 旁边的 Modify Option 就会出现上图一样的列表,接着将 Operating System 中的第一项 Allow multiple instances 打勾即可,然后将 user 服务配置文件中的端口号修改一下,不修改的话会和已经启动的那个实例的端口冲突,接着运行 UserApiApplication 即可得到两个实例了:

image-20220928205810625

此时 user 服务就有两个实例了,如果此时你关闭其中一个实例,再对 http://localhost:8080/user/test 发起两次请求,你会发现有一个请求会报错,查看网关控制台之后可以看到如下信息:

2022-09-28 20:59:33.829 ERROR 26956 --- [ctor-http-nio-7] a.w.r.e.AbstractErrorWebExceptionHandler : [d1e36856-7]  500 Server Error for HTTP GET "/user/test"

io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: no further information: /192.168.199.107:8083
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	*__checkpoint ⇢ org.springframework.web.cors.reactive.CorsWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ HTTP GET "/user/test" [ExceptionHandlingWebHandler]

这就复现了我在本节开头说的那种情况,虽然集群中有另一个实例已经被杀死了,但是网关并不知道这回事,根据负载均衡缓存中的信息依然会读取到那台已死实例的地址,依然会进行转发,所以我们需要一个手段在实例上下线的时候及时的告诉网关,让网关将原来的负载均衡缓存失效掉。

这里我们有两个方案可以选择:

  1. 缩短负载均衡缓存的失效时间(spring.cloud.loadbalancer.cache.ttl)。
  2. 监听 Nacos 实例刷新事件,一旦出现实例发生变化马上删除缓存。

第一个方案改配置就行了,但是它依然还会存在一点时间差,所以我们采用第二种方案进行处理,在我们之前引入的 Nacos 依赖之中,已经包含了 Nacos 客户端的相关事件,所以我们只需要监听 Nacos 事件在做一点自定义逻辑即可,代码如下:

@Component
    public static class NacosInstancesChangeEventListener extends Subscriber<InstancesChangeEvent> {
        private static final Logger logger = LoggerFactory.getLogger(NacosInstancesChangeEventListener.class);

        @Resource
        private CacheManager defaultLoadBalancerCacheManager;

        @Override
        public void onEvent(InstancesChangeEvent event) {
            logger.info("Spring Gateway 接收实例刷新事件:{}, 开始刷新缓存", JacksonUtils.toJson(event));
            Cache cache = defaultLoadBalancerCacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
            if (cache != null) {
                cache.evict(event.getServiceName());
            }
            logger.info("Spring Gateway 实例刷新完成");

        }

        @Override
        public Class<? extends com.alibaba.nacos.common.notify.Event> subscribeType() {
            return InstancesChangeEvent.class;
        }
    }

这里我通过继承的方式监听 Nacos 的 InstancesChangeEvent,在 onEvent 接收到实例刷新的信息后直接删除对应服务的负载均衡缓存,缓存的名字是定义在 Spring Gateway 的相关代码中的,直接引入即可,Cache 则是继承自 Spring Cache 接口,负载均衡缓存也继承了 Cache 接口,有了 Cache 接口就可以直接使用其接口定义的 evict 方法即可,而缓存的 key 名就则就是服务名,在 InstancesChangeEvent 中,通过 getServiceName 就可以得到服务名,这里给大家看一个事件的打印示例:

2022-10-02 15:16:55.195  INFO 8136 --- [ncesChangeEvent] stener$NacosInstancesChangeEventListener : Spring Gateway 接收实例刷新事件:{"serviceName":"user-api","groupName":"public","clusters":"","hosts":[{"ip":"192.168.0.103","port":8082,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"DEFAULT","serviceName":"public@@user-api","metadata":{"preserved.register.source":"SPRING_CLOUD"},"ipDeleteTimeout":30000,"instanceHeartBeatInterval":5000,"instanceHeartBeatTimeOut":15000}]}, 开始刷新缓存.

在你删除负载均衡缓存后,SpringCloudGateway 在处理请求时发现没有缓存会重新拉取一遍服务列表,这样之后都是用的是最新的服务列表了,也就达到了我们动态感知上下线的目的。


经朋友提醒之后,这里把另一种更简单的方式也分享给大家,只需要一个配置即可:

spring:
cloud:
loadbalancer:
nacos:
enabled: true

打开 NacoLoadbalancer 配置之后,在取实例列表的时候会直接取 NacosReactiveDiscoveryClient / NacosDiscoveryClient 内部的缓存数据,这个缓存也会及时响应 Nacos 的实例上下线通知,达到动态感知的目的。

但是经过我的一番测试,发现了此方式的一点点缺陷,在未开启此配置时,SCG 使用的负载均衡实现类是 RoundRobinLoadBalancer,这个类使用的轮询的方式进行负载均衡,也就是先请求实例A,再请求实例B。

而 NacoLoadbalancer 是使用随机 权重算法进行负载均衡,我在多轮测试中都发现了它经常会出现先请求几次实例A,再请求几次实例B的情况,哪怕是请求实例A的请求已经报错(500)了,它依然会继续请求实例A,这在某些场景下可能会导致用户体验不好。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanegcb
系列文章
更多 icon
同类精品
更多 icon
继续加载