微服务:nacos 配置中心

357 阅读6分钟

概述

在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件

quick start

  • 引入依赖

<dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
   <version>2.2.5.RELEASE</version>
</dependency>

  • nacos 录入配置

image.png

nameSpace : 命名空间

data id : 配置文件id,默认applicat.server.name{applicat.server.name}-{active}.${extFile}

group id: 组id

  • 添加配置(bootStrap.yml,多文件配置)

spring:
  application:
    name: spring.cloud.web01
  cloud:
    nacos:
      config:
        server-addr: yserver:8848
        group: spring.cloud.web
        file-extension: yml
        namespace: spring-cloud-web
        extension-configs:
          - data-id: application.yml
            group: spring.cloud.web
            refresh: true
          - data-id: biz.yml
            group: spring.cloud.web
            refresh: true

extension-configs : 配置列表,参考yml 配置list

  • 测试类

@Configuration
@RefreshScope
@Data
public class BizConfig {

    @Value("${switchOff}")
    private int switchOff;
}
@Configuration
@RefreshScope
@Data
public class RedisConfig {

    @Value("${spring.redis.database}")
    private int database;

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;


//    @Bean(name = "redisProperties")
//    RedisProperties redisPropertiesConfig() {
//        RedisProperties redisProperties = new RedisProperties();
//        redisProperties.setDatabase(database);
//        redisProperties.setHost(host);
//        redisProperties.setPort(port);
//        return redisProperties;
//    }


}

@RestController
@RequestMapping("/config")
public class TestNacosController {

    @Autowired
    private BizConfig bizConfig;

    @Autowired
    private RedisConfig redisConfig;

    @RequestMapping("/getSwitch")
    public int get() {
        return bizConfig.getSwitchOff();
    }


    @RequestMapping("/getRedisHost")
    public String getRedisHost(){
        return redisConfig.getHost();
    }
}

spring cloud nacos 接入方式

  • 原生注解方式

@RefreshScope

@Value("${key}")

@Configuration
@RefreshScope
@Data
public class BizConfig {

    @Value("${switchOff}")
    private int switchOff;
}
  • 监听器方式

@Slf4j
@Component
public class Case1AlicloudNacosConfigBean implements InitializingBean {

    @Autowired
    private NacosConfigManager nacosConfigManager;

    @Override
    public void afterPropertiesSet() {
        try {
            ConfigService configService = nacosConfigManager.getConfigService();
            addListener("price-admin", "basic", configService);
            addListener("price-admin", "botConfig", configService);
            addListener("price-admin", "smsConfig", configService);
        } catch (NacosException e) {
            e.printStackTrace();
        }
    }

    private void addListener(String group, String dataId, ConfigService configService) throws NacosException {
        //todo  反序列化
        //添加监听
        configService.addListener(dataId, group, getListener(dataId));
    }

    private Listener getListener(String dataId) {
        return new Listener() {
            @Override
            public void receiveConfigInfo(String value) {
                log.info("recieve: {}", dataId);
                //todo  反序列化
            }

            @Override
            public Executor getExecutor() {
                return null;
            }
        };
    }

}

nacos 原理

image.png

  • 服务管理:实现服务CRUD,域名CRUD,服务健康状态检查,服务权重管理等功能
  • 配置管理:实现配置管CRUD,版本管理,灰度管理,监听管理,推送轨迹,聚合数据等功能
  • 元数据管理:提供元数据CURD 和打标能力
  • 插件机制:实现三个模块可分可合能力,实现扩展点SPI机制
  • 事件机制:实现异步化事件通知,sdk数据变化异步通知等逻辑
  • 日志模块:管理日志分类,日志级别,日志可移植性(尤其避免冲突),日志格式,异常码+帮助文档
  • 回调机制:sdk通知数据,通过统一的模式回调用户处理。接口和数据结构需要具备可扩展性
  • 寻址模式:解决ip,域名,nameserver、广播等多种寻址模式,需要可扩展
  • 推送通道:解决server与存储、server间、server与sdk间推送性能问题
  • 容量管理:管理每个租户,分组下的容量,防止存储被写爆,影响服务可用性
  • 流量管理:按照租户,分组等多个维度对请求频率,长链接个数,报文大小,请求流控进行控制
  • 缓存机制:容灾目录,本地缓存,server缓存机制。容灾目录使用需要工具
  • 启动模式:按照单机模式,配置模式,服务模式,dns模式,或者all模式,启动不同的程序+UI
  • 一致性协议:解决不同数据,不同一致性要求情况下,不同一致性机制
  • 存储模块:解决数据持久化、非持久化存储,解决数据分片问题
  • Nameserver:解决namespace到clusterid的路由问题,解决用户环境与nacos物理环境映射问题
  • CMDB:解决元数据存储,与三方cmdb系统对接问题,解决应用,人,资源关系
  • Metrics:暴露标准metrics数据,方便与三方监控系统打通
  • Trace:暴露标准trace,方便与SLA系统打通,日志白平化,推送轨迹等能力,并且可以和计量计费系统打通
  • 接入管理:相当于阿里云开通服务,分配身份、容量、权限过程
  • 用户管理:解决用户管理,登录,sso等问题
  • 权限管理:解决身份识别,访问控制,角色管理等问题
  • 审计系统:扩展接口方便与不同公司审计系统打通
  • 通知系统:核心数据变更,或者操作,方便通过SMS系统打通,通知到对应人数据变更
  • OpenAPI:暴露标准Rest风格HTTP接口,简单易用,方便多语言集成
  • Console:易用控制台,做服务管理、配置管理等操作
  • SDK:多语言sdk
  • Agent:dns-f类似模式,或者与mesh等方案集成
  • CLI:命令行对产品进行轻量化管理,像git一样好用

nacos client

@RefreshScope

spring 中bean 的作用域

scope=singleton 单例

scope=prototype 多例

scope=request

scope=session

scope= global session

org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean方法如下

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

   /**
    * @see Scope#proxyMode()
    * @return proxy mode
    */
   ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}
...
   // Create bean instance.
   //单例
   if (mbd.isSingleton()) {
      sharedInstance = getSingleton(beanName, () -> {
         try {
            return createBean(beanName, mbd, args);
         }
         catch (BeansException ex) {
            // Explicitly remove instance from singleton cache: It might have been put there
            // eagerly by the creation process, to allow for circular reference resolution.
            // Also remove any beans that received a temporary reference to the bean.
            destroySingleton(beanName);
            throw ex;
         }
      });
      beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
   }
   //多例
   else if (mbd.isPrototype()) {
      // It's a prototype -> create a new instance.
      Object prototypeInstance = null;
      try {
         beforePrototypeCreation(beanName);
         prototypeInstance = createBean(beanName, mbd, args);
      }
      finally {
         afterPrototypeCreation(beanName);
      }
      beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
   }
    //其他
   else {
      String scopeName = mbd.getScope();
      if (!StringUtils.hasLength(scopeName)) {
         throw new IllegalStateException("No scope name defined for bean '" + beanName + "'");
      }
      Scope scope = this.scopes.get(scopeName);
      if (scope == null) {
         throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
      }
      try {
         Object scopedInstance = scope.get(beanName, () -> {
            beforePrototypeCreation(beanName);
            try {
               return createBean(beanName, mbd, args);
            }
            finally {
               afterPrototypeCreation(beanName);
            }
         });
         beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
      }
      catch (IllegalStateException ex) {
         throw new ScopeNotActiveException(beanName, scopeName, ex);
      }
   }
}

...

RefreshScope注解中清楚说明,它是@Scope("refresh"),就是上面代码的的第三种状况。

动态刷新配置缓存

  • spring boot spi 注入几个核心类

image.png

  • NacosContextRefresher 引入spring-cloud-starter-alibaba-nacos-config依赖,spring-cloud-starter-alibaba-nacos-config spring.factories 中找com.alibaba.cloud.nacos.NacosConfigAutoConfiguration,如下图

image.png

image.png

image.png

image.png

NacosContextRefresher 实现了 ApplicationListener, ApplicationContextAware接口 ApplicationReadyEvent :Application 启动成功事件

下面来看com.alibaba.cloud.nacos.refresh.NacosContextRefresher 几个核心方法

//监听事件
public void onApplicationEvent(ApplicationReadyEvent event) {
    if (this.ready.compareAndSet(false, true)) {
        this.registerNacosListenersForApplications();
    }

}

private void registerNacosListenersForApplications() {
    if (this.isRefreshEnabled()) {
        Iterator var1 = NacosPropertySourceRepository.getAll().iterator();

        while(var1.hasNext()) {
            NacosPropertySource propertySource = (NacosPropertySource)var1.next();
            if (propertySource.isRefreshable()) {
                String dataId = propertySource.getDataId();
                this.registerNacosListener(propertySource.getGroup(), dataId);
            }
        }
    }

}

//注册监听
private void registerNacosListener(final String groupKey, final String dataKey) {
    String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
    Listener listener = (Listener)this.listenerMap.computeIfAbsent(key, (lst) -> {
        return new AbstractSharedListener() {
            public void innerReceive(String dataId, String group, String configInfo) {
                NacosContextRefresher.refreshCountIncrement();
                NacosContextRefresher.this.nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
                //发布RefreshEvent事件
                NacosContextRefresher.this.applicationContext.publishEvent(new RefreshEvent(this, (Object)null, "Refresh Nacos config"));
                if (NacosContextRefresher.log.isDebugEnabled()) {
                    NacosContextRefresher.log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s", group, dataId, configInfo));
                }

            }
        };
    });

    try {
        //添加监听
        this.configService.addListener(dataKey, groupKey, listener);
    } catch (NacosException var6) {
        log.warn(String.format("register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey, groupKey), var6);
    }

}

  • org.springframework.cloud.endpoint.event.RefreshEventListener监听
public class RefreshEventListener implements SmartApplicationListener {

   private static Log log = LogFactory.getLog(RefreshEventListener.class);

   private ContextRefresher refresh;

   private AtomicBoolean ready = new AtomicBoolean(false);

   public RefreshEventListener(ContextRefresher refresh) {
      this.refresh = refresh;
   }

   @Override
   public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
      return ApplicationReadyEvent.class.isAssignableFrom(eventType)
            || RefreshEvent.class.isAssignableFrom(eventType);
   }

    //监听
   @Override
   public void onApplicationEvent(ApplicationEvent event) {
      if (event instanceof ApplicationReadyEvent) {
         handle((ApplicationReadyEvent) event);
      }
      else if (event instanceof RefreshEvent) {
         //处理 RefreshEvent 事件
         handle((RefreshEvent) event);
      }
   }

   //设置ready=true,aqs
   public void handle(ApplicationReadyEvent event) {
      this.ready.compareAndSet(false, true);
   }

    //处理 RefreshEvent 事件
   public void handle(RefreshEvent event) {
      if (this.ready.get()) { // don't handle events before app is ready
         log.debug("Event received " + event.getEventDesc());
         Set<String> keys = this.refresh.refresh();
         log.info("Refresh keys changed: " + keys);
      }
   }

}
  • org.springframework.cloud.context.refresh.ContextRefresher refresh 方法
public synchronized Set<String> refresh() {
    // refresh environment
   Set<String> keys = refreshEnvironment();
   // refresh all
   this.scope.refreshAll();
   return keys;
}



public synchronized Set<String> refreshEnvironment() {
   Map<String, Object> before = extract(
         this.context.getEnvironment().getPropertySources());
   addConfigFilesToEnvironment();
   //获取有变化的key
   Set<String> keys = changes(before,
         extract(this.context.getEnvironment().getPropertySources())).keySet();
         
   //发布事件
   this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
   return keys;
}



  • org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder 监听
@Component
@ManagedResource
public class ConfigurationPropertiesRebinder
      implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {

   private ConfigurationPropertiesBeans beans;

   private ApplicationContext applicationContext;

   private Map<String, Exception> errors = new ConcurrentHashMap<>();

   public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
      this.beans = beans;
   }

   @Override
   public void setApplicationContext(ApplicationContext applicationContext)
         throws BeansException {
      this.applicationContext = applicationContext;
   }

   /**
    * A map of bean name to errors when instantiating the bean.
    * @return The errors accumulated since the latest destroy.
    */
   public Map<String, Exception> getErrors() {
      return this.errors;
   }

   @ManagedOperation
   public void rebind() {
      this.errors.clear();
      for (String name : this.beans.getBeanNames()) {
         rebind(name);
      }
   }

   @ManagedOperation
   public boolean rebind(String name) {
      if (!this.beans.getBeanNames().contains(name)) {
         return false;
      }
      if (this.applicationContext != null) {
         try {
            Object bean = this.applicationContext.getBean(name);
            if (AopUtils.isAopProxy(bean)) {
               bean = ProxyUtils.getTargetObject(bean);
            }
            if (bean != null) {
               // TODO: determine a more general approach to fix this.
               // see https://github.com/spring-cloud/spring-cloud-commons/issues/571
               if (getNeverRefreshable().contains(bean.getClass().getName())) {
                  return false; // ignore
               }
               this.applicationContext.getAutowireCapableBeanFactory()
                     .destroyBean(bean);
               this.applicationContext.getAutowireCapableBeanFactory()
                     .initializeBean(bean, name);
               return true;
            }
         }
         catch (RuntimeException e) {
            this.errors.put(name, e);
            throw e;
         }
         catch (Exception e) {
            this.errors.put(name, e);
            throw new IllegalStateException("Cannot rebind to " + name, e);
         }
      }
      return false;
   }

   @ManagedAttribute
   public Set<String> getNeverRefreshable() {
      String neverRefresh = this.applicationContext.getEnvironment().getProperty(
            "spring.cloud.refresh.never-refreshable",
            "com.zaxxer.hikari.HikariDataSource");
      return StringUtils.commaDelimitedListToSet(neverRefresh);
   }

   @ManagedAttribute
   public Set<String> getBeanNames() {
      return new HashSet<>(this.beans.getBeanNames());
   }

   @Override
   public void onApplicationEvent(EnvironmentChangeEvent event) {
      if (this.applicationContext.equals(event.getSource())
            // Backwards compatible
            || event.getKeys().equals(event.getSource())) {
         rebind();
      }
   }

}

nacos通过发布RefreshEvent事件通知spring-cloud进行刷新操作,spring-cloud监听到事件后做两件事:

  • 刷新属性源–属性源相对应的属性bean从旧的换成新的
  • 触发scope的refreshAll操作,针对RefreshScope来说就是清空了他所管理的缓存bean,待再次调用时重新创建,创建过程就会注入新的属性源

长轮训

nacos server