学习微服务系列(七):nacos原理分析

我们前两篇文章介绍了nacos在服务注册发现和分布式配置方面的作用。在实际生产中使用nacos你就会体会到nacos是多么的方便,基于nacos的服务注册能力可以做优雅停服功能,从此我们发版上线就不必非要等到半夜才能发布。只要随时找个业务低峰发布对应的服务集群即可。接下来我们看一下nacos的原理。

Nacos 服务注册与发现原理分析

nacos的功能之一就是作为服务注册发现模块也就是我们常说的注册中心,nacos支持所有主流类型服务框架的注册配置和管理,微服务我们最开始接触的中间件就是Dubbo,很多人对dubbo很熟悉那么下面这个图就不会陌生:
在这里插入图片描述
其实nacos与dubbo都是阿里开源出来的,所以对于设计思路基本是一样的。咱们就拿着上面的图来说明,先描述一下各个节点的含义。

节点说明
Provider暴露服务的服务提供方
Consumer调用远程服务的服务消费方
Registry服务注册与发现的注册中心
Monitor统计服务的调用次数和调用时间的监控中心
Container服务运行容器

我们此时的nacos就充当 Registry 节点的含义。

微服务是如何注册的

服务注册,这个是个动作,那么注册到哪里了呢,那就需要在注册中心有个容器来存储注册上来的服务。这个容器可能是map,数组,持久化的mysql,mongodb。有了容器存储之后那我们就想那注册上来的是啥呢?也就是存在容器中的内容是什么呢?其实放在容器中的主要信息就是微服务各个节点所在的ip+端口。之后调用端才能知道你服务提供方在哪提供服务,这样调用端才能顺着这条线去调用服务。
在这里插入图片描述
如上图有一个服务提供者服务1,这个服务有2个实例,如果不好了解你可以理解成这个服务1放在了2个服务器上并且都启动把自己服务所在ip和端口通知给了注册中心也就是nacos。
在这里插入图片描述
注册中心通过维护serviceHolder去处理每一个服务,为每一个服务通过实例空间。当各个服务启动并注册到注册中心之后就通过心跳的模式保证注册中心可以知晓各个服务的存活状态。如果发现有无心跳的服务那么注册中心就会及时提出失效的服务实例,发送心跳的周期默认是 5 秒,Nacos 服务端会在 15 秒没收到心跳后将实例设置为不健康,在 30 秒没收到心跳时将这个临时实例摘除。
在这里插入图片描述

微服务是如何发现的

服务注册到注册中心后,服务的消费者就可以向注册中心订阅某个服务,并提交一个监听器,当注册中心中服务发生变更时,监听器会收到通知,这时消费者更新本地的服务实例列表,以保证所有的服务均是可用的。
在这里插入图片描述
如上图如果消费者订阅了服务,那么会在本地基于内存维护一个服务信息列表,之后进行服务调用是直接从本地列表获取对应的服务实例进行调用,否则去主从中心获取服务实例。

二者之间是怎么调用的

服务提供者与服务消费者之间是通过feign+ribbon进行配合调用的,feign提供http请求的封装以及调用,ribbon提供负载均衡。负载均衡有很多中实现方式,包括轮询法,随机方法法,对请求ip做hash后取模等等。
Nacos 的客户端在获取到服务的完整实例列表后,会在客户端进行负载均衡算法来获取一个可用的实例,默认使用的是随机获取的方式。
在这里插入图片描述

Nacos Config实现原理分析

对于nacosconfig来讲其实就是提供了一系列的crud的访问接口使应用可以完成配置的增删改查的操作。对于nacos的config来讲终究就是怎么进行存储的,数据是如何持久化的。我们画个图看一下:
在这里插入图片描述
如上图其本质就是这样的。那么当config配置进行更新变化的时候就需要相关的应用进行跟着相关变化,这里就有个问题了那客户端怎么知道配置变化了呢,客户端是什么时候进行更新的呢?涉及更新方式就有2种,推和拉;

  • 客户端主动从服务端定时拉取配置,如果有变化则进行替换。
  • 服务端主动把变化的内容发送给客户端。

两种方式各有利弊,比如对于推的模式来讲,就需要服务端与客户端进行长连接,那么这种就会出现服务端需要耗费大量资源维护这个链接,并且还得加入心跳机制来维护连接有效性。而对于拉的模式则需要客户端定时去服务端访问,那么就会存在时间间隔,也就保证不了数据的实时性。那nacos采用那种模式呢?nacos是采用了拉模式是一种特殊的拉模式,也就是我们通常听的长轮询机制。

  1. 如果服务端配置发生了变化
    在这里插入图片描述
  2. 如果服务端配置与客户端一直没有变化

如果客户端拉取发现客户端与服务端配置是一致的(其实是通过MD5判断的)那么服务端会先拿住这个请求不返回,直到这段时间内配置有变化了才把刚才拿住的请求返回。他的步骤是nacos服务端收到请求后检查配置是否发生变化,如果没有则开启定时任务,延迟29.5s执行。同时把当前客户端的连接请求放入队列。那么此时服务端并没有将结果返回给客户端,当有一下2种情况的时候才触发返回。

  • 就是等待29.5s后触发自动检查
  • 在29.5s内有配置进行了更改

经过这2种情况才完成这次的pull操作。这种的好处就是保证了客户端的配置能及时变化更新,也减少了轮询给服务端带来的压力。所以之前文章我们说过这个长链接回话超时时间默认是30s。
在这里插入图片描述

基于nacos优雅停服的小坑

在采用金丝雀灰度发布的过程中发现服务已经把原来的注册的服务进行下线操作了,但是仍然有流量请求进来,这样就违背了官方宣称的秒级上下线特点。其中我们服务的负载均衡使用的ribbon这个负载均衡组件。我们通过源码看一下:

注册中心更新核心机制

public String update(HttpServletRequest request) throws Exception {
        String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
        String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);

        String agent = request.getHeader("Client-Version");
        if (StringUtils.isBlank(agent)) {
            agent = request.getHeader("User-Agent");
        }

        ClientInfo clientInfo = new ClientInfo(agent);

        if (clientInfo.type == ClientInfo.ClientType.JAVA &&
                clientInfo.version.compareTo(VersionUtil.parseVersion("1.0.0")) >= 0) {
            serviceManager.updateInstance(namespaceId, serviceName, parseInstance(request));
      n "ok";
}

上边这段代码说明的是nacos进行上线下线的逻辑,可以看到核心逻辑是parseInstance()方法,里面的的实例信息就是从request中获取的。再看一下updateInstance方法:

public void updateInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {

	Service service = getService(namespaceId, serviceName);

	if (service == null) {
		throw new NacosException(NacosException.INVALID_PARAM, "service not found, namespace: " + namespaceId + ", service: " + serviceName);
	}

	if (!service.allIPs().contains(instance)) {
		throw new NacosException(NacosException.INVALID_PARAM, "instance not exist: " + instance);
	}

	addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}

public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips) throws NacosException {

	String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);

	Service service = getService(namespaceId, serviceName);

	List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);

	Instances instances = new Instances();
	instances.setInstanceList(instanceList);

	consistencyService.put(key, instances);
}

可以看到实例下线,是立马更新server中的实例信息数据的。

Ribbon负载均衡更新机制

NacosServerList继承了AbstractServerList最终是在DynamicServerListLoadBalancer这类中进行负载所有的server的

public class NacosServerList extends AbstractServerList<NacosServer> {

	private NacosDiscoveryProperties discoveryProperties;

	private String serviceId;

	public NacosServerList(NacosDiscoveryProperties discoveryProperties) {
		this.discoveryProperties = discoveryProperties;
	}

	@Override
	public List<NacosServer> getInitialListOfServers() {
		return getServers();
	}

	@Override
	public List<NacosServer> getUpdatedListOfServers() {
		return getServers();
	}

	private List<NacosServer> getServers() {
		try {
			List<Instance> instances = discoveryProperties.namingServiceInstance()
					.selectInstances(serviceId, true);
			return instancesToServerList(instances);
		}
		catch (Exception e) {
			throw new IllegalStateException(
					"Can not get service instances from nacos, serviceId=" + serviceId,
					e);
		}
	}

	private List<NacosServer> instancesToServerList(List<Instance> instances) {
		List<NacosServer> result = new ArrayList<>();
		if (null == instances) {
			return result;
		}
		for (Instance instance : instances) {
			result.add(new NacosServer(instance));
		}

		return result;
	}

	public String getServiceId() {
		return serviceId;
	}

	@Override
	public void initWithNiwsConfig(IClientConfig iClientConfig) {
		this.serviceId = iClientConfig.getClientName();
	}
}
protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
        @Override
        public void doUpdate() {
            updateListOfServers();
        }
    };

public DynamicServerListLoadBalancer(IClientConfig clientConfig) {
	initWithNiwsConfig(clientConfig);
}
    
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
	try {
		super.initWithNiwsConfig(clientConfig);
		String niwsServerListClassName = clientConfig.getPropertyAsString( CommonClientConfigKey.NIWSServerListClassName, DefaultClientConfigImpl.DEFAULT_SEVER_LIST_CLASS);
		ServerList<T> niwsServerListImpl = (ServerList<T>) ClientFactory
                    .instantiateInstanceWithClientConfig(niwsServerListClassName, clientConfig);
                //得到所有的server实现
		this.serverListImpl = niwsServerListImpl;

                
		if (niwsServerListImpl instanceof AbstractServerList) {
			AbstractServerListFilter<T> niwsFilter = ((AbstractServerList) niwsServerListImpl)
                        .getFilterImpl(clientConfig);
			niwsFilter.setLoadBalancerStats(getLoadBalancerStats());
			this.filter = niwsFilter;
		}

    
		String serverListUpdaterClassName = clientConfig.getPropertyAsString( CommonClientConfigKey.ServerListUpdaterClassName, DefaultClientConfigImpl.DEFAULT_SERVER_LIST_UPDATER_CLASS);

               // 获取Updater对象
		this.serverListUpdater = (ServerListUpdater) ClientFactory.instantiateInstanceWithClientConfig(serverListUpdaterClassName, clientConfig);

  
		restOfInit(clientConfig);
	} catch (Exception e) {
		throw new RuntimeException(
                    "Exception while initializing NIWSDiscoveryLoadBalancer:"
                            + clientConfig.getClientName()
                            + ", niwsClientConfig:" + clientConfig, e);
	}
}

void restOfInit(IClientConfig clientConfig) {
	boolean primeConnection = this.isEnablePrimingConnections();
	// turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList()
	this.setEnablePrimingConnections(false);
        //采用定时任务进行定时刷新实例信息缓存
	enableAndInitLearnNewServersFeature();//最重要的点

        //进行一次实例拉取操作
	updateListOfServers();
	if (primeConnection && this.getPrimeConnections() != null) {
		this.getPrimeConnections() .primeConnections(getReachableServers());
	}
	this.setEnablePrimingConnections(primeConnection);
	LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
}

// 这里就是进行实例信息缓存更新的操作
@VisibleForTesting
public void updateListOfServers() {
	List<T> servers = new ArrayList<T>();
	if (serverListImpl != null) {
    // 调用拉取新实例信息的方法
		servers = serverListImpl.getUpdatedListOfServers();
		LOGGER.debug("List of Servers for {} obtained from Discovery client: {}", getIdentifier(), servers);

    // 用Filter对拉取的servers列表进行更新
		if (filter != null) {
			servers = filter.getFilteredListOfServers(servers);
			LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}", getIdentifier(), servers);
		}
	}
  // 更新实例列表
	updateAllServerList(servers);
}

之后我们看最重要的enableAndInitLearnNewServersFeature这个方法的操作

@Override
public synchronized void start(final UpdateAction updateAction) {
	if (isActive.compareAndSet(false, true)) {
		final Runnable wrapperRunnable = new Runnable() {
			@Override
			public void run() {
				if (!isActive.get()) {
					if (scheduledFuture != null) {
						scheduledFuture.cancel(true);
					}
					return;
				}
				try {
                   // 这里就是在DynamicServerListLoadBalancer中的Servers实现
					updateAction.doUpdate();
					lastUpdated = System.currentTimeMillis();
				} catch (Exception e) {
					logger.warn("Failed one update cycle", e);
				}
			}
		};

                // 默认定时任务执行时间间隔为30s
		scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                    wrapperRunnable,
                    initialDelayMs,
                    refreshIntervalMs,
                    TimeUnit.MILLISECONDS);
	} else {
		logger.info("Already active, no-op");
	}
}

最终虽然实现了秒级的实例上下线,但是由于在Spring Cloud中,负载组件rabbion的实例信息更新是采用了定时任务的形式,有可能这个任务上一秒刚刚执行完,下一秒你就执行实例上下线操作,那么ribbion要感知这个变化,就必须要等待refreshIntervalMs秒后才可以感知到。所以了解了根源那么我们才能从底层更好的去使用这些组件。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值