微服务之apollo获取配置原理源码分析

代码边缘

如果网络的边缘是设备那么代码的边缘可能是调用api的地方
最近有使用到携程的微服务配置服务apollo,根据介绍在客户端使用的是client来获取配置代码如下:

		Config config = ConfigService.getAppConfig();
        config.addChangeListener(configChangeEvent -> {
            Set<String> changedKeys = configChangeEvent.changedKeys();
            for (String key : changedKeys) {
                ConfigChange change = configChangeEvent.getChange(key);
                System.out.println("key " + key + ",oldValue: " + change.getOldValue() + ",newValue: "
                        + change.getNewValue() + ",changeType: " + change.getChangeType().name());
            }
        });

进一步分析

首先是通过config类获取配置,然后再通过添加监听器可以来监听配置的修改和日志输出。接下来从ConfigService.getAppConfig()开始进去分析

	
	// 默认使用"application" 作为namespace
	public static Config getAppConfig() {
    	return getConfig(ConfigConsts.NAMESPACE_APPLICATION);
  	}

	// 进入getConfig 
	public static Config getConfig(String namespace) {
    	return s_instance.getManager().getConfig(namespace);
  	}

	// 	DefaultConfigManager -》 getConfig
	private Map<String, Config> m_configs = Maps.newConcurrentMap();
	@Override
  	public Config getConfig(String namespace) {
	    Config config = m_configs.get(namespace);
	
	    if (config == null) {
	      synchronized (this) {
	        config = m_configs.get(namespace);
	
	        if (config == null) {
	          ConfigFactory factory = m_factoryManager.getFactory(namespace);
	
	          config = factory.create(namespace);
	          m_configs.put(namespace, config);
	        }
	      }
	    }
	    return config;
	  }

这里首先从本地的map里获取配置信息,如果没有再同步地从本地的map里获取一次,其实这里是怕并发多次获取导致配置的不一致性避免重复获取远程的配置。相当于一个线程安全的单例模式。接下来获取完配置就设置到本地了供下一次获取。接下来进入factory.create()

此处使用的配置工厂方法创建了一个配置类config

	// calss: DefaultConfigFactory 
 	 public Config create(String namespace) {
	    ConfigFileFormat format = determineFileFormat(namespace);
	    //此处判断是否是.yml文件 代码如下
	    if (ConfigFileFormat.isPropertiesCompatible(format)) { 
	      return new DefaultConfig(namespace, createPropertiesCompatibleFileConfigRepository(namespace, format));
	    }
	    // 此时进入这里创建配置文件
	    return new DefaultConfig(namespace, createLocalConfigRepository(namespace));
	  }
	  
	  // ConfigFileFormat.isPropertiesCompatible
 	 public static boolean isPropertiesCompatible(ConfigFileFormat format) {
        return format == YAML || format == YML;
     }

接下来进入创建配置文件的里面,首先会创建个本地的配置仓库作为缓存。

LocalFileConfigRepository createLocalConfigRepository(String namespace) {
	// 本地模式的话直接返回 此处可以在配置文件配置env
    if (m_configUtil.isInLocalMode()) {
      logger.warn(
          "==== Apollo is in local mode! Won't pull configs from remote server for namespace {} ! ====",
          namespace);
      return new LocalFileConfigRepository(namespace);
    }
    // 否则进入下面创建和远程相连的配置文件
    return new LocalFileConfigRepository(namespace, createRemoteConfigRepository(namespace));
  }

 // 这里主要看从远程创建的情况 createRemoteConfigRepository()
  RemoteConfigRepository createRemoteConfigRepository(String namespace) {
    return new RemoteConfigRepository(namespace);
  }
 // 接下来是一个构造器 忽略一些无关的代码
 public RemoteConfigRepository(String namespace) {
    m_namespace = namespace;
    ........
    this.trySync(); // 首先进行一次同步
    this.schedulePeriodicRefresh(); // 在配置一个定时的刷新也就是客户端从远程拉去新配置
    this.scheduleLongPollingRefresh(); // 再来一个长轮训用来接收远端的配置信息
  }

以上 RemoteConfigRepository构造方法里的几个方法可以结合apollo的wiki图理解
在这里插入图片描述

方法细节

sync

本方法主要就是从远端拉去最新的配置信息同步到本地

// 此处进入同步
protected boolean trySync() {
    try {
      sync();
      return true;
    } catch (Throwable ex) {
      Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));
      logger
          .warn("Sync config failed, will retry. Repository {}, reason: {}", this.getClass(), ExceptionUtil
              .getDetailMessage(ex));
    }
    return false;
  }
  
protected synchronized void sync() {
    Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");

    try {
    // 获取远程和本地的配置
      ApolloConfig previous = m_configCache.get();
      ApolloConfig current = loadApolloConfig();

      //在返回http码为304时候表示相等 有修改的时候就设置本地的cache
      if (previous != current) {
        logger.debug("Remote Config refreshed!");
        m_configCache.set(current);
        // 调用监听器有修改本地仓库配置的 也有打印日志的
        this.fireRepositoryChange(m_namespace, this.getConfig());
      }
      if (current != null) {
        // 输出日志;
      }
      transaction.setStatus(Transaction.SUCCESS);
    } catch (Throwable ex) {
      transaction.setStatus(ex);
      throw ex;
    } finally {
      transaction.complete();
    }
  }

schedulePeriodicRefresh

定时远程获取 默认的单位是 5 * 分钟 也就是5分钟刷新一次 防止adminServer宕机未通知到位。

	private void schedulePeriodicRefresh() {
	    logger.debug("Schedule periodic refresh with interval: {} {}",
	        m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit());
	   	
	    m_executorService.scheduleAtFixedRate(
	        new Runnable() {
	          @Override
	          public void run() {
	            Tracer.logEvent("Apollo.ConfigService", String.format("periodicRefresh: %s", m_namespace));
	            logger.debug("refresh config for namespace: {}", m_namespace);
	            // 其实就是个定时的同步调用sync 此处的代码已经讲解
	            trySync();
	            Tracer.logEvent("Apollo.Client.Version", Apollo.VERSION);
	          }
	        }, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(),
	        m_configUtil.getRefreshIntervalTimeUnit());
	  }

scheduleLongPollingRefresh

这里又到了核心的代码,功能主要是对配置服务进行长轮训,如果配置服务更新会第一时间通知到客户端。这里的时间默认是1s

	// 先追到最底层
	private void scheduleLongPollingRefresh() {
	  remoteConfigLongPollService.submit(m_namespace, this);
	}
	public boolean submit(String namespace, RemoteConfigRepository remoteConfigRepository) {
	    boolean added = m_longPollNamespaces.put(namespace, remoteConfigRepository);
	    m_notifications.putIfAbsent(namespace, INIT_NOTIFICATION_ID);
	    if (!m_longPollStarted.get()) {
	      startLongPolling(); // 此处开始对远端的长轮训
	    }
	    return added;
	}

	private void startLongPolling() {
		// CAS的方式查看是否已经开始轮训
	    if (!m_longPollStarted.compareAndSet(false, true)) {
	      //already started
	      return;
	    }
	    try {
	    	// 加载项目的配置信息
	      final String appId = m_configUtil.getAppId();
	      final String cluster = m_configUtil.getCluster();
	      final String dataCenter = m_configUtil.getDataCenter();
	      final String secret = m_configUtil.getAccessKeySecret();
	      final long longPollingInitialDelayInMills = m_configUtil.getLongPollingInitialDelayInMills();
	      m_longPollingService.submit(new Runnable() {
	        @Override
	        public void run() {
	        	// 判断是否延迟进行长轮训
	          if (longPollingInitialDelayInMills > 0) {
	            try {
	              logger.debug("Long polling will start in {} ms.", longPollingInitialDelayInMills);
	              TimeUnit.MILLISECONDS.sleep(longPollingInitialDelayInMills);
	            } catch (InterruptedException e) {
	              //ignore
	            }
	          }
	          doLongPollingRefresh(appId, cluster, dataCenter, secret);  // 接下来进入这里
	        }
	      });
	    } // 忽略catch后面部分
	  }

接下去是doLongPollingRefresh的代码

在这里插入图片描述
apollo首先会获取配置服务的地址列表进行随机选择一个,在请求成功也就是返回200的时候进行通知,也就是1处。此时出现200的返回码说明发生了配置文件的修改。
在末尾也就是2处当配置文件一直没有修改时会随机的设置lastServiceDto为null,而lastServiceDto为null意味着需要重新去寻找一个配置服务地址,就有可能获取新的配置服务地址。如果有配置服务上下线也会进行更新。

结尾

那是不是不去获取配置就不会链接上配置服务了呢?那肯定不是的。通过以上说明我去掉了这个获取配置的方法,在项目启动时通过实现Spring的BeanFactoryPostProcessor接口实现了初始化的功能。
在这里插入图片描述
结束
L&P

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值