Nacos学习笔记

2023-05-11

仓库:https://gitee.com/sensationhhw/nacos-discovery-demo.git

0、部署

  1. 下载

    官网: https://nacos.io/zh-cn/docs/quick-start.html

    根据需求下载对应版本,这里以release最新版为例

  2. 创建数据库

​ 为nacos创建数据库,在数据库中执行/conf中的数据库脚本文件mysql-schema.sql

  1. 修改配置文件

    1. 配置数据库地址

      ### Connect URL of DB:
      db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
      db.user.0=root
      db.password.0=12345678
      
    2. 配置秘钥nacos.core.auth.plugin.nacos.token.secret.key

      这里使用官方的默认值(2.2.0.1后无默认值,请自行配置),正式使用时请自行生成

      ### The default token (Base64 String):
      nacos.core.auth.plugin.nacos.token.secret.key=SecretKey012345678901234567890123456789012345678901234567890123456789
      
  2. 启动

    1. 在/bin目录中,修改startup.sh文件,以单机模式启动(实际我们应该使用集群模式):

      export MODE="standalone"
      
    2. 执行启动命令

      sh ./startup.sh
      

      可在/Users/hhw/Documents/java_study/nacos/nacos-release-2.1.1/nacos/logs/start.out中查询日志文件,显示单机模式启动成功:

      2023-04-06 08:39:24,456 INFO Nacos started successfully in stand alone mode. use embedded storage
      
    3. 默认访问地址:http://localhost:8848/nacos

  3. 集群

    【略】

1、动态配置

1.1新建配置

新建配置时可以指定:

  • Data ID:相当于一个配置文件,比如相当于application.properties,或者application-dev.properties,不过要注意的是,我们在某个项目中使用application.properties,那个application表示的就是当前应用,当我们在nacos进行配置的时候,就要尽可能取一些有含义的Data ID,比如user.properties(表示用户相关的配置)、redis.properties(表示redis相关的配置)

  • Group:在nacos中,一个或多个Data ID可以归类到同一个Group中,Group的作用就是用来区分Data ID相同的情况,不同的应用或者中间件使用了相同的Data ID时就可以使用Group来区分。默认的Group是DEFAULT_GROUP

  • 配置内容:写具体的配置项,支持多种常见格式,如:yaml、properties、text等

1.2拉取配置

Java SDK

真正开发时,一般不会使用Java SDK的方式来拉取nacos的配置,这里仅供了解即可

引入依赖
<!-- https://mvnrepository.com/artifact/com.alibaba.nacos/nacos-client -->
<dependency>
    <groupId>com.alibaba.nacos</groupId>
    <artifactId>nacos-client</artifactId>
    <version>2.1.1</version>
</dependency>
使用Java SDK获取nacos配置内容
String serverAddr = "localhost:8848";
String dataId = "user-dev.yml";
String group = "DEFAULT_GROUP";
try {
  Properties properties = new Properties();
  properties.put("serverAddr", serverAddr);
  ConfigService configService = NacosFactory.createConfigService(properties);
 	// 获取配置
  String config = configService.getConfig(dataId, group, 5000);
  // 输出文本类型
  System.out.println(config);
} catch (NacosException e) {
  e.printStackTrace();
}
监听配置

当nacos中的配置发生修改时,将会调用receiveConfigInfo方法,全量返回最新的配置内容

String serverAddr = "localhost:8848";
String dataId = "user-dev.yml";
String group = "DEFAULT_GROUP";
// 监听配置
configService.addListener(dataId, group, new Listener() {
  @Override
  public Executor getExecutor() {
    return null;
  }

  @Override
  public void receiveConfigInfo(String configInfo) {
    // nacos发生变化时的回调
    System.out.println("receiveConfigInfo");
    System.out.println(configInfo);
  }
});
//阻塞主线程 
System.in.read();

修改前输出:

user:
    name: huanghwh
    age: 18

修改后输出:

user:
    name: huanghwh
    age: 18
user2:
    name: huang2
    age: 99

提供发布配置与删除配置的api

// 还提供发布配置与删除配置的api
configService.publishConfig(//....);
configService.removeConfig(//....);

SpringBoot

引入依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  <version>2.5.4</version>
</dependency>
<!-- nacos-springboot -->
<dependency>
  <groupId>com.alibaba.boot</groupId>
  <artifactId>nacos-config-spring-boot-starter</artifactId>
  <version>0.2.12</version>
</dependency>

编写配置

server:
  port: 8085

# nacos
nacos:
  config: 
    server-addr: 127.0.0.1:8848
    username: nacos
    password: nacos
    data-id: user-dev.yml
    type: yaml # 配置文件类型,必须配置,否则引起空指针
    auto-refresh: true # 开启自动刷新
    bootstrap:
      enable: true  # springboot必须开启这个

测试类

@RestController
public class NacosController {
    
    /** springboot中,需要使用NacosValue,且指定autoRefreshed=true,才能达到自动刷新效果 */
    @NacosValue(value = "${user2.name}", autoRefreshed = true)
    private String username;
    
    @GetMapping("/test")
    public String test() {
        return "输出:"+username;
    }   
}

可能引发的问题

  • 启动失败

    exception is java.lang.NoClassDefFoundError: org/springframework/boot/context/properties/ConfigurationBeanFactoryMetadata
    	at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:224) ~[spring-beans-5.3.15.jar:5.3.15]
    	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:117) ~[spring-beans-5.3.15.jar:5.3.15]
    	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:311) ~[spring-beans-5.3.15.jar:5.3.15]
    	... 19 common frames omitted
    Caused by: java.lang.NoClassDefFoundError: org/springframework/boot/context/properties/ConfigurationBeanFactoryMetadata
    	at com.alibaba.boot.nacos.config.binder.NacosBootConfigurationPropertiesBinder.<init>(NacosBootConfigurationPropertiesBinder.java:51) ~[nacos-config-spring-boot-autoconfigure-0.2.5.jar:0.2.5]
      
    

    原因:springboot版本与nacos-config-spring-boot-starter 版本不一致,该测试demo使用的版本为:

    springboot:2.5.4 nacos-config-spring-boot-starter:0.2.12

SpringCloud

引入依赖

注意:要求springboot版本与SpringCloud-nacos版本强一致,参考官网

**坑:建议不要使用2020.0.x以后的Spring Cloud,nacos读不到bootstrap.yml了,导致找不到placeholder中的内容**
Spring Cloud Alibaba Version Spring Cloud Version Spring Boot Version
2.2.10-RC1* Spring Cloud Hoxton.SR12 2.3.12.RELEASE
2.2.9.RELEASE Spring Cloud Hoxton.SR12 2.3.12.RELEASE
2.2.8.RELEASE Spring Cloud Hoxton.SR12 2.3.12.RELEASE
2.2.7.RELEASE Spring Cloud Hoxton.SR12 2.3.12.RELEASE
2.2.6.RELEASE Spring Cloud Hoxton.SR9 2.3.2.RELEASE
2.2.1.RELEASE Spring Cloud Hoxton.SR3 2.2.5.RELEASE
2.2.0.RELEASE Spring Cloud Hoxton.RELEASE 2.2.X.RELEASE
2.1.4.RELEASE Spring Cloud Greenwich.SR6 2.1.13.RELEASE
2.1.2.RELEASE Spring Cloud Greenwich 2.1.X.RELEASE
2.0.4.RELEASE(停止维护,建议升级) Spring Cloud Finchley 2.0.X.RELEASE
1.5.1.RELEASE(停止维护,建议升级) Spring Cloud Edgware 1.5.X.RELEASE
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  <version>2.3.2.RELEASE</version>
</dependency>
<!-- nacos-springcloud-config 配置中心jar包 -->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  <version>2.2.6.RELEASE</version>
</dependency>

配置文件bootstrap.properties

使用nacos-config时,nacos会优先读取bootstrap.properties中的内容(实测配置在application中没有效果)

使用bootstrap.properties配置相关参数

spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.application.name=example
spring.cloud.nacos.config.file-extension=properties
  • Data ID的完整格式:${prefix}-${spring.profiles.active}.${file-extension}

​ 当${spring.profiles.active}为空时,则为 ${prefix}.${file-extension}

​ 如上图代码块,对应的DataId:example.properties image-20230409172952116

  • demo2

    # demo2
    spring.application.name=user
    spring.profiles.active=dev
    spring.cloud.nacos.config.file-extension=yml
    # 该代码块对应的DataId:user-dev.yml
    
  • 拉取多个配置

    • 需要注意的是:主配置自动刷新默认开启;非主配置自动刷新默认是关闭的
    # demo2 对应的DataId:user-dev.yml 【主配置自动刷新默认开启】
    spring.application.name=user
    spring.profiles.active=dev
    spring.cloud.nacos.config.file-extension=yml
       
    # 拉取其他【公用】配置文件 对应DataId:common.yml
    spring.cloud.nacos.config.shared-configs[0].data-id=common.yml  
    #【非主配置】自动刷新默认是关闭的,需要手动开启
    spring.cloud.nacos.config.shared-configs[0].refresh=true
      
    spring.cloud.nacos.config.shared-configs[1].data-id=common2.yml
      
    # 拉取【扩展】配置文件
    spring.cloud.nacos.config.extension-configs[0].data-id=test.yml
      
    
    	-  shared-configs与extension-configs功能相同,只是代表的含义不同,share表示与其他服务共享的配置,extension一般是非共享的
    	-  优先级:主配置 > extension > share
    

测试&@RefreshScope原理

  • 实际开发过程中,可以将所有的配置项写到统一的一个文件CommonConfig,其他类使用get/set方法获取value,这样就只用写一个@RefreshScope

    @RestController
    @RefreshScope // 必须使用,保证配置文件可以刷新
    public class NacosController {
      
      @Value("${user2.name}")
      private String username;
      
      @GetMapping("/test")
      public String test() {
        return "输出:"+username;
      }
    }
    
  • @RefreshScope原理

    • 其内部如下,实际上是标识bean作用域=refresh,其作用是每当配置文件更新时,该bean会重新生成,从而保证配置文件的值是最新的。

      @Target({ElementType.TYPE, ElementType.METHOD})
      @Retention(RetentionPolicy.RUNTIME)
      @Scope("refresh")
      @Documented
      public @interface RefreshScope {
          ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
      }
      

1.3历史版本回滚

  • 配置文件可以回滚到某个历史版本

    image-20230411211652545

1.4监听查询

  • 表示当前有哪台机器在使用该配置

    image-20230411211820480

2、注册中心

DEMO:https://gitee.com/sensationhhw/nacos-discovery-demo.git

使用SpringCloud进行服务注册和发现

引入nacos注册中心jar包

<properties>
  <spring-boot.version>2.3.2.RELEASE</spring-boot.version>
  <spring-cloud-nacos-dicovery.version>2.2.6.RELEASE</spring-cloud-nacos-dicovery.version>
  <httpclient.version>4.5</httpclient.version>
</properties>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>${spring-boot.version}</version>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>${spring-boot.version}</version>
    <scope>test</scope>
  </dependency>

  <!-- nacos注册中心,服务注册与发现 -->
  <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <version>${spring-cloud-nacos-dicovery.version}</version>
  </dependency>

  <!-- nacos依赖httpclient,需手动引入  -->
  <dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>${httpclient.version}</version>
  </dependency>
</dependencies>

2.1服务注册

  • 在项目启动类中加入 @EnableDiscoveryClient 注解(实测不加也能成功注册)

  • 服务001配置文件

    server:
      port: 7001
        
    spring:
      cloud:
        nacos:
          server-addr: 127.0.0.1:8848
          discovery:
            weight: 100  # 权重
      application:
        name: nacosClient001
    
  • 服务002配置文件

    server:
      port: 7002
        
    spring:
      cloud:
        nacos:
          server-addr: 127.0.0.1:8848
      application:
        name: nacosClient002
      
    
  • 启动后可在nacos中查看到2台服务信息

    image-20230413222133559

2.2服务发现

模拟服务001向服务002发送请求

  • 由于这里是通过restTemplate发送请求,因此先试用bean将restTemplate进行管理

    @Configuration
    public class RestTemplateConfig {
      
        @LoadBalanced /*必须开启负载均衡,否则无法根据服务名找到对应的服务*/
        @Bean
        public RestTemplate restTemplate() {
            return new RestTemplate();
        }
          
    }
    
  • 创建Controller用于测试

    • 服务001,使用服务002的服务名发送请求,调用服务002的hello接口

      @RestController
      public class TestController {
          
        @Autowired
        private RestTemplate restTemplate;
          
        @GetMapping("/hello")
        public String hello() {
          return "这里是nacosClient001再向nacosClient002发送请求,请求结果:" 
            + restTemplate.getForObject("http://nacosClient002/hello", String.class);
        }
      }
      
    • 服务002,接收请求

      @RestController
      public class TestController {
              
          @GetMapping("/hello")
          public String hello() {
              return "你请求到了====nacosClient002";
          }   
      }
      
    • 请求服务001,获取返回结果:GET http://localhost:7001/hello

      成功返回:这里是nacosClient001再向nacosClient002发送请求,请求结果:你请求到了====nacosClient002

2.3临时与持久化实例

  • 默认情况下,注册到nacos的实例都是临时实例,临时实例通过与服务端之间的心跳检测来保活,默认情况下,客户端会每隔5s发送一次心跳。

  • 在服务端,如果超过15s没有收到客户端(实例)的心跳,那么就会把实例标记为不健康状态。

  • 在服务端,如果超过30s没有收到客户端心跳,那么就会删除实例。

  • 临时实例与持久化的实例区别在于,持久化的实例就算服务下线了,那么也不会删除该条服务记录,只会将该条实例标记为不健康,消费端仍旧能够获取到已下线的实例信息

  • 通过参数设置持久化,需要注意的是:已经注册为临时实例的服务,不允许再以持久化实例注册。

    spring:
      cloud:
        nacos:
          server-addr: 127.0.0.1:8848
          discovery:
            ephemeral: false #持久化
    
  • 注意:永久实例与nacos的健康监测是需要nacos集群之间采用的一致性协议是raft,但是测试环境nacos是单节点,不能采用raft协议,采用的是默认的Distro协议,只支持临时实例模式

  • 调试小技巧:通过Idea复制一份启动类,然后指定端口,可以实现再启动一份代码

    达到一个服务多个实例的效果

    image-20230418211040053

2.4保护阈值

  • 在使用过程中,阈值可以设置一个0~1的数值,表示如果服务的所有实例中,健康实例的比重低于这个值,就会触发保护机制,一旦触发保护机制,nacos就会把所有实例拉取下来,不管是否健康,这样就起到了保护作用,因为不健康的实例也会参与负载均衡。

  • 如果没有该机制,那么大量的请求仍旧打在少数的健康实例上,仅剩的几个健康实例也会被压垮,所以只要触发了保护,消费端就会拉取到所有实例,这样部分消费端就有机会访问到不健康的实例从而导致请求失败。

    image-20230418213407017

2.5权重

修改权重

image-20230420212203541

使用nacos负载均衡

在客户端注入nacos负载均衡的bean

/**
     * 使用Nacos的负载均衡策略,在nacos中配置的权重才会生效,否则默认走的是SpringCloud的@LoadBalanced负载均衡
     *
     * @return {@link IRule}
     */
@Bean
public IRule nacosRibbonRule() {
  return new NacosRule();
}

测试

使用客户端执行请求时,访问nacosClient002服务,该服务由2个实例,nacos会按实例的权重分配请求到具体哪个实例上。

@RestController
public class TestController {
    
    @Autowired
    private RestTemplate restTemplate;
    
    @GetMapping("/hello")
    public String hello() {
        return "这里是nacosClient001再向nacosClient002发送请求,请求结果:" 
                + restTemplate.getForObject("http://nacosClient002/hello", String.class);
    }
    
}

问题记录

  • Nacos修改权重报错caused: errCode: 500, errMsg: do metadata operation failed ;caused: com.alibaba.nacos.con

    解决:

    • 停掉nacos服务
    • 将nacos文件夹下data中的protocol文件夹删除
    • 重启nacos服务即可

2.6就近访问

可指定消费者和生产者的所在集群,相同集群(cluster_name)只能访问相同集群的服务,例如上海集群的消费者只能访问上海集群的生产者。

spring:
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      discovery:
        cluster-name: bj # 集群名称

如果消费者端没有指定cluster-name,那么会访问到所有集群

3、Feign服务调用

上面演示了使用restTemplate进行远程调用,那么接下来展示如何使用openFeign进行远程调用

3.1引入openFeign依赖

与SpringCloud版本号相同即可

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
  <version>${spring-cloud-nacos-dicovery.version}</version>
</dependency>

3.2启动类

在启动类加上注解,开启openFeign

@EnableFeignClients /*启动feign*/

3.3编写feign接口

使用interface编写feign的远程调用接口,消费者如下:

@FeignClient("nacosClient002")
public interface FeignService {

    /**
     * 使用feign进行远程调用
     */
    @GetMapping("/hello")
    String hello();
}

生产者如下:

@RestController
public class TestController {
    
    @GetMapping("/hello")
    public String hello() {
        System.out.println("收到请求");
        return "你请求到了====nacosClient002:7002:sh";
    }
    
}

3.4测试

在消费者端编写一个Controller用于测试feign客户端,与使用restTemplate的进行对比

@RestController
public class TestController {
    
    @Autowired
    private RestTemplate restTemplate;
  	// 直接注入feign
    @Autowired
    private FeignService feignService;

    /**
     * use restTemplate to request
     */
    @GetMapping("/hello")
    public String hello() {
        return "这里是nacosClient001使用restTemplate向nacosClient002发送请求,请求结果:" 
                + restTemplate.getForObject("http://nacosClient002/hello", String.class);
    }


    /**
     * use openFeign to request
     */
    @GetMapping("/hello2")
    public String hello2() {
        return "这里是nacosClient001使用feign向nacosClient002发送请求,请求结果:" + feignService.hello();
    }
}