肝了6个小时的Nacos 2.0

Nacos

一. nacos 配置中心

引言:nacos配置中心主要采用的模式是客户端pull服务端模式利用长连接,而nacos对于配置的crud是根据监听配置的改变来做的。

简单看下nacos config在crud之前的init

  • 主要是通过api里面的configservice来实现的,而configservice又是通过nacos工厂创建的我们简单看下。
import java.util.Properties;

/**
 * Nacos Factory.
 *
 * @author Nacos
 */
public class NacosFactory {
    
    /**
     * Create config service.
     * # 这个方法是nacos工厂来初始化configservice的就是配置中心的api
     * @param properties init param 初始化参数
     * @return config 配置
     * @throws NacosException Exception
     */
    public static ConfigService createConfigService(Properties properties) throws NacosException {
        return ConfigFactory.createConfigService(properties);
    }
    
    /**
     * Create naming service.
     * # 这个方法是nacos工厂来初始化configservice的就是注册中心的api
     * @param properties init param 初始化参数
     * @return Naming 配置
     * @throws NacosException Exception
     */
    public static NamingService createNamingService(Properties properties) throws NacosException {
        return NamingFactory.createNamingService(properties);
    }
}

  • 先简单看下 createConfigService怎么初始化configservice
    /**
     * Create Config.
     * #看起来非常简单的代码,其实就是通过反射去创建对象的。
     * @param properties init param
     * @return ConfigService
     * @throws NacosException Exception
     */
    public static ConfigService createConfigService(Properties properties) throws NacosException {
        try {
            #反射获取service
            Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
            Constructor constructor = driverImplClass.getConstructor(Properties.class);
            #创建对象
            ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties);
            return vendorImpl;
        } catch (Throwable e) {
            throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
        }
    }
  • 其实从这里就看到我们configservice这个已经初始化完了,那我们来看下configservice里面做了什么,看源码进到这个类里先看他的构造方法做了什么事情,因为在创建对象的时候会执行他的构造方法,可以更好的理解init初始化的过程
    public NacosConfigService(Properties properties) throws NacosException {        
        #利用正则校验contextPath格式
        ValidatorUtils.checkInitParam(properties);
        #初始化命名空间  -----停止 我们看下他的命名空间怎么init
        initNamespace(properties);
        this.configFilterChainManager = new ConfigFilterChainManager(properties);
        ServerListManager serverListManager = new ServerListManager(properties);
        serverListManager.start();

        this.worker = new ClientWorker(this.configFilterChainManager, serverListManager, properties);
        // will be deleted in 2.0 later versions
        agent = new ServerHttpAgent(serverListManager);
    }
肝了6个小时的Nacos 2.0
  • initNamespace 初始化命名空间
    #默认命名空间
    public static final String EMPTY = "";
    
    
    private void initNamespace(Properties properties) {
        #这里是初始化命名空间的调用了下面的方法
        namespace = ParamUtil.parseNamespace(properties);
        properties.put(PropertyKeyConst.NAMESPACE, namespace);
    }


  public static String parseNamespace(Properties properties) {
        String namespaceTmp = null;
        
        String isUseCloudNamespaceParsing = properties.getProperty(PropertyKeyConst.IS_USE_CLOUD_NAMESPACE_PARSING,
                System.getProperty(SystemPropertyKeyConst.IS_USE_CLOUD_NAMESPACE_PARSING,
                        String.valueOf(Constants.DEFAULT_USE_CLOUD_NAMESPACE_PARSING)));
        
        if (Boolean.parseBoolean(isUseCloudNamespaceParsing)) {
            namespaceTmp = TenantUtil.getUserTenantForAcm();
            #重点其实就是在这里 尝试去获取阿里云的命名空间 
            #看是否是在云环境中的namespcace
            namespaceTmp = TemplateUtils.stringBlankAndThenExecute(namespaceTmp, () -> {
                String namespace = System.getenv(PropertyKeyConst.SystemEnv.ALIBABA_ALIWARE_NAMESPACE);
                return StringUtils.isNotBlank(namespace) ? namespace : StringUtils.EMPTY;
            });
        }
        
        #如果不是的话,那么就读取properties中指定的namespace
        if (StringUtils.isBlank(namespaceTmp)) {
            namespaceTmp = properties.getProperty(PropertyKeyConst.NAMESPACE);
        
        #都没有读到的话就走默认命名空间就是null
        return StringUtils.isNotBlank(namespaceTmp) ? namespaceTmp.trim() : StringUtils.EMPTY;
    }

初始化命名空间结束,我们继续看构造方法

    public NacosConfigService(Properties properties) throws NacosException {        
        #利用正则校验contextPath格式
        ValidatorUtils.checkInitParam(properties);
        #初始化命名空间 
        initNamespace(properties);
        this.configFilterChainManager = new ConfigFilterChainManager(properties);
        #服务器列表的管理
        ServerListManager serverListManager = new ServerListManager(properties);
        serverListManager.start();
        #检查配置 动态刷新 客户端pull服务端配置  -----来看下ClientWorker
        this.worker = new ClientWorker(this.configFilterChainManager, serverListManager, properties);
        // will be deleted in 2.0 later versions
        agent = new ServerHttpAgent(serverListManager);
    }
  • ClientWorker构造方法
    public ClientWorker(final ConfigFilterChainManager configFilterChainManager, ServerListManager serverListManager,
            final Properties properties) throws NacosException {
        this.configFilterChainManager = configFilterChainManager;
        
        init(properties);
        #好好看 这里先初始化了一个线程池
        agent = new ConfigRpcTransportClient(properties, serverListManager);
        int count = ThreadUtils.getSuitableThreadCount(THREAD_MULTIPLE);
        ScheduledExecutorService executorService = Executors
                .newScheduledThreadPool(Math.max(count, MIN_THREAD_NUM), r -> {
                    Thread t = new Thread(r);
                    t.setName("com.alibaba.nacos.client.Worker");
                    t.setDaemon(true);
                    return t;
                });
        agent.setExecutor(executorService);
        #主要看这里 方法在下面
        agent.start();
    }
    
    
        /**
     * base start client.
     */
    public void start() throws NacosException {
        
        if (securityProxy.isEnabled()) {
            securityProxy.login(serverListManager.getServerUrls());
              
            #登录操作
            this.executor.scheduleWithFixedDelay(new Runnable() {
                @Override
                public void run() {
                    securityProxy.login(serverListManager.getServerUrls());
                }
            }, 0, this.securityInfoRefreshIntervalMills, TimeUnit.MILLISECONDS);
            
        }
        
        #注意看这里配置监听
        startInternal();
    }

  • startInternal
        #我们单独拎出来看
        @Override
        public void startInternal() throws NacosException {
            executor.schedule(() -> {
                #一直循环,只要线程没有停止
                while (!executor.isShutdown() && !executor.isTerminated()) {
                    try {
                        listenExecutebell.poll(5L, TimeUnit.SECONDS);
                        if (executor.isShutdown() || executor.isTerminated()) {
                            continue;
                        }
                        #执行配置监听 里面大概意思就是对配置监听有修改就改掉之后持久化
                        executeConfigListen();
                    } catch (Exception e) {
                        LOGGER.error("[ rpc listen execute ] [rpc listen] exception", e);
                    }
                }
            }, 0L, TimeUnit.MILLISECONDS);
            
        }

大概到这里配置中心就完了,整体就是通过nacos工厂来创建configservice这个api,通过configservice来实现对配置增删改查,然后通过客户端长连接服务端建立长连接的方式来对配置实现动态刷新。

  • 这里我们看的是2.0以后的源码所以使用的是长连接,解决了长轮询出现的配置中心频繁GC的问题,长连接的意思大概是:http客户端服务端建立连接之后并不会立即断开而是选择性的断开连接,如果没有断开那么下次服务端再有改动会直接推送到与之对应的客户端,并不需要在经历TCP三次握手等建立连接的行为

  • 2.0以前使用的是长轮询,简述下长轮询的意思,我们知道http请求正常流程是,客户端连接服务端修改数据之后断开,长轮询的概念就是,客户端连接服务端然后保持连接一段时间,这段时间服务端有数据更改就直接写到客户端,时间到了断开连接之后在重新建立连接,继续保持一段时间,如此往复就是长轮询

二. nacos 注册中心

  • nacos的注册中心
import java.util.Properties;

/**
 * Nacos Factory.
 *
 * @author Nacos
 */
public class NacosFactory {
    
    /**
     * Create config service.
     * # 这个方法是nacos工厂来初始化configservice的就是配置中心的api
     * @param properties init param 初始化参数
     * @return config 配置
     * @throws NacosException Exception
     */
    public static ConfigService createConfigService(Properties properties) throws NacosException {
        return ConfigFactory.createConfigService(properties);
    }
    
    /**
     * Create naming service.
     * # 这个方法是nacos工厂来初始化configservice的就是注册中心的api
     * @param properties init param 初始化参数
     * @return Naming 配置
     * @throws NacosException Exception
     */
    public static NamingService createNamingService(Properties properties) throws NacosException {
        return NamingFactory.createNamingService(properties);
    }
}

反过头来我们看NamingService

  • 还是对构造方法简单看下
    public NacosNamingService(Properties properties) throws NacosException {
        #调用init
        init(properties);
    }
    
    private void init(Properties properties) throws NacosException {
        ValidatorUtils.checkInitParam(properties);
        #获取注册中心命名空间
        this.namespace = InitUtils.initNamespaceForNaming(properties);
        InitUtils.initSerialization();
        InitUtils.initWebRootContext(properties);
        initLogName(properties);
        
        this.changeNotifier = new InstancesChangeNotifier();
        NotifyCenter.registerToPublisher(InstancesChangeEvent.class, 16384);
        NotifyCenter.registerSubscriber(changeNotifier);
        this.serviceInfoHolder = new ServiceInfoHolder(namespace, properties);
        #注册中心客户端
        this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, properties, changeNotifier);
    }
 */
public interface NamingService {
    
    #提供了很多的注册方法
    @Override
    public void registerInstance(String serviceName, String groupName, String ip, int port, String clusterName)
            throws NacosException {
            
        #其实就是对配置的一个初始化
        Instance instance = new Instance();
        #ip
        instance.setIp(ip);
        #port
        instance.setPort(port);
        #权重
        instance.setWeight(1.0);
        #集群名等
        instance.setClusterName(clusterName);
        #然后调用重载的方法或者可以直接指定重载的方法
        registerInstance(serviceName, groupName, instance);
    }
    
    @Override
    public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
        #这里是校验一下param
        NamingUtils.checkInstanceIsLegal(instance);
        #主要还是这里发送注册的Api 我们看下这个注册中心的Api看下图
        clientProxy.registerService(serviceName, groupName, instance);
    }

肝了6个小时的Nacos 2.0
  • 如图我们可以看到走的是不同的策略模式其实在2.0之后走的是grpc长连接的方式去维护的最终在InstanceController中处理请求
@RestController
@RequestMapping(UtilsAndCommons.NACOS_NAMING_CONTEXT + UtilsAndCommons.NACOS_NAMING_INSTANCE_CONTEXT)
public class InstanceController {
    
    @Autowired
    private SwitchDomain switchDomain;
    
    @Autowired
    private InstanceOperatorClientImpl instanceServiceV2;
    
    @Autowired
    private InstanceOperatorServiceImpl instanceServiceV1;
    
    @Autowired
    private UpgradeJudgement upgradeJudgement;
    
    
    #最后是在这里去处理请求注册服务 上面其实都只是发送了心跳包(请求)而已
    @CanDistro
    @PostMapping
    @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
    public String register(HttpServletRequest request) throws Exception {
        
        final String namespaceId = WebUtils
                .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
        final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
        NamingUtils.checkServiceNameFormat(serviceName);
        
        final Instance instance = HttpRequestInstanceBuilder.newBuilder()
                .setDefaultInstanceEphemeral(switchDomain.isDefaultInstanceEphemeral()).setRequest(request).build();
        
        getInstanceOperator().registerInstance(namespaceId, serviceName, instance);
        return "ok";
    }
  }

但是我们在服务启动的时候好像就直接注册到nacos上了,为什么呢,哈哈我们可以简单看下

  • 使用nacos注册中心的时候是不是都会引入这个依赖
 <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  </dependency>
  • 我们只要引入了上面的依赖就会默认注册,入口在:AbstractAutoServiceRegistration里面的bind
    @Deprecated
    public void bind(WebServerInitializedEvent event) {
        ApplicationContext context = event.getApplicationContext();
        if (!(context instanceof ConfigurableWebServerApplicationContext) || !"management".equals(((ConfigurableWebServerApplicationContext)context).getServerNamespace())) {
            this.port.compareAndSet(0, event.getWebServer().getPort());
            #主要看的是start这个方法
            this.start();
        }
    }
    
    
        public void start() {
        if (!this.isEnabled()) {
            if (logger.isDebugEnabled()) {
                logger.debug("Discovery Lifecycle disabled. Not starting");
            }

        } else {
            if (!this.running.get()) {
                this.context.publishEvent(new InstancePreRegisteredEvent(this, this.getRegistration()));          
                #找到重点注册方法
                this.register();
                if (this.shouldRegisterManagement()) {
                    this.registerManagement();
                }

                this.context.publishEvent(new InstanceRegisteredEvent(this, this.getConfiguration()));
                this.running.compareAndSet(falsetrue);
            }

        }
    }
    
    
    
  #这个注册方法里面
  @Override
 public void register(Registration registration) {

  if (StringUtils.isEmpty(registration.getServiceId())) {
   log.warn("No service to register for nacos client...");
   return;
  }

  NamingService namingService = namingService();
  String serviceId = registration.getServiceId();
  String group = nacosDiscoveryProperties.getGroup();
  #是不是跟刚才我们看的创建的一样,构建心跳包
  Instance instance = getNacosInstanceFromRegistration(registration);

  try {
   #namingService这个对象熟悉不熟悉
   namingService.registerInstance(serviceId, group, instance);
   log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
     instance.getIp(), instance.getPort());
  }
  catch (Exception e) {
   log.error("nacos registry, {} register failed...{},", serviceId,
     registration.toString(), e);
   // rethrow a RuntimeException if the registration is failed.
   // issue : https://github.com/alibaba/spring-cloud-alibaba/issues/1132
   rethrowRuntimeException(e);
  }
 }

看到namingService调用registerInstance是不是就熟悉了,调用注册方法就回到了上面的注册逻辑,构建心跳包,发送API然后交给InstanceController里面去处理注册逻辑,没懂可以回去看下namingService

三. 自己总结面试题

  1. nacos动态配置是通过什么方式?
+ 2.0以前通过长轮询,客户端服务端建立一段时间的连接,在连接中主动去pull数据,但是一段时间会断开然后重新建立连接,2.0以后通过长连接,客户端服务端只要不主动断开就一直保持连接
  1. 为什么要把长轮询改成长连接
+ 避免TCP连接中握手等开销
+ grpc长连接的流式推送,比 UDP 更加可靠
+ 服务注册在2.0以前是通过心跳机制,每5秒发送一次心跳,2.0之后的长连接就不需要客户端一直想服务端发送心跳,重复 TPS 可以大幅降低
+ 解决配置模块的GC问题,之前默认每30秒断开一次连接,断开连接就会有上下文切换,然后造成GC

3.三.Nacos配置中心宕机了,我们的服务还可以读取到配置信息吗

+ 当然可以,nacos默认会先把配置写在我们的内存中,然后客户端会去主动拉去服务端变更的配置但是也是写在内存中的。

原文始发于微信公众号(闯sir9):肝了6个小时的Nacos 2.0

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/20678.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!