nacos

nacos

一、nacos概述

1.1 nacos概述

官方:一个更易于构建云原生应用的动态服务发现(NacosDiscovery)、服务配置(NacosConfig)和服务管理平台。

通俗的讲:nacos具备:注册中心,配置中心,服务管理(对注册到注册中心的服务进行管理)三个功能。

nacos_第1张图片

阿里为 SpringCloud 贡献了一个子项目,叫做 SpringCloud Alibaba,其中包括了微服务开发中的几个基础组件,Nacos 就是此项目中的一项技术。

官网:https://spring.io/projects/spring-cloud-alibaba

Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案,Nacos 作为其核心组件之一,可以作为注册中心和配置中心使用;快速实现动态服务发现、服务配置、服务元数据及流量管理。

1.2 常见注册中心的比较

nacos_第2张图片

nacos_第3张图片

二、注册中心

2.1 项目环境准备

说明:nacos的相关介绍,我们创建新的工程来进行测试。准备mall-product-nacos,mall-order-nacos,之前eureka的注册中心的服务端需要我们自己搭建,使用nacos之后,不需要我们自己搭建了。

2.2.1 mall-product-nacos

复制mall-product工程,修改名称为mall-product-nacos,将工程中eureka相关的配置全部删除。

需要修改以下两处文件:

nacos_第4张图片

nacos_第5张图片

2.2.2 mall-order-nacos

同上面的商品工程修改的方式一致。在order工程中删除Eureka之后负载均衡的代码会报错,暂时不用管,后面把nacos的依赖添加进来就可以了。

2.2 安装nacos

2.2.1 版本说明

https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E

SpringCloud 与 SpringCloud alibaba 及 springBoot 及相关组件的版本关系,可以参考GitHub上 cloudalibaba的相关说明。

2.2.2 下载安装

1)下载

下载地址:https://github.com/alibaba/nacos/releases

2)安装

解压安装包,直接运行bin目录下的startup.cmd -m standalone

说明:如果启动的时候不添加参数,则默认是集群方式启动

如果不想以命令行的方式启动,则需要修改startup.cmd中的启动方式为standalone,这样就可以双击启动了

nacos_第6张图片

运行成功后,访问http://localhost:8848/nacos可以查看Nacos的主页,默认账号密码都是nacos

nacos_第7张图片

nacos_第8张图片

2.3 服务注册到nacos

2.3.1 nacos服务端

nacos服务端我们已经安装好了,接着就是将服务注册到nacos的服务端就可以了。

2.3.2 nacos客户端

1)添加依赖

在父工程中引入alibabaCloud的父工程依赖

    com.alibaba.cloud    spring-cloud-alibaba-dependencies    ${alibaba-cloud-version}    pom    import

在mall-product-nacos及mall-order-nacos中引入nacos客户端依赖

    com.alibaba.cloud    spring-cloud-starter-alibaba-nacos-discovery

2)修改配置文件

cloud:  nacos:    discovery:      # 添加nacos服务端的地址      server-addr: http://localhost:8848      # 用户名      username: nacos      # 密码      password: nacos

3)启动测试

nacos_第9张图片

nacos_第10张图片

5)通过feign远程调用

  1. 添加依赖
    org.springframework.cloud    spring-cloud-starter-openfeign
  1. 添加注解
@SpringBootApplication@MapperScan(basePackages = "com.woniu.order.mapper")// 添加启动feign的注解@EnableFeignClientspublic class OrderApplication {
  1. 编写feign客户端接口
@FeignClient(value = "product-server-nacos", path = "/product")public interface ProductFeignClient {    @GetMapping("/{id}")    public Result findById(@PathVariable Integer id);}
  1. 启动测试

nacos_第11张图片

2.4 nacos多实例

注册中心解决的其中一个问题就是负载均衡的问题,订单微服务调用了商品微服务,如果商品微服务有多台服务器,那么,就需要在订单微服务中进行一个客户端的负载均衡。之前我们用的eureka中默认集成了ribbon客户端负载均衡,nacos中也默认集成了ribbon负载均衡。

nacos_第12张图片

2.4.1 启动多个商品微服务

nacos_第13张图片

nacos_第14张图片

2.4.2 实现负载均衡

因为nacos中默认集成了ribbon,不需要进行额外的配置及可以实现负载均衡的效果,默认的是轮询的机制。

也可以通过配置切换其它方式的负载均衡

# 指定product-server-nacos服务的负载均衡策略product-server:  ribbon:    # 执行ribbon的负载均衡为随机的策略    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

注意:此种方式只能指定product-server-nacos服务的负载均衡策略,如果要指定所有的服务的负载均衡策略,那么则不能使用该种配置

// 指定所有服务的负载均衡策略,如果配置文件中也配置了,以该配置为准@Beanpublic IRule rule(){   return new RoundRobinRule();// 轮询策略}

2.5 nacos细节及源码分析

2.5.1 nacos细节

nacos_第15张图片

1)服务注册

nacos-client(如订单服务,商品服务)启动时,就会去注册中心进行服务注册,其实就是通过HTTP请求调用nacos-server,当nacos-server端接收到客户端的注册请求时,会将客户端的实例数据(包括ip、端口、微服务名等)保存到server端的注册表中(内存),如果订单服务是集群部署,那么同一个微服务名就会有多个实例数据形成一个实例列表
文档:https://nacos.io/zh-cn/docs/open-api.html

nacos_第16张图片

2)心跳续约

客户端心跳续约:(对于临时实例)

nacos-client进行服务注册时(具体时机是调用nacos-server注册接口之前),会开启心跳任务,默认每5秒(可通过元数据参数preserved.heart.beat.interval进行设置)向nacos-server发送心跳,告诉服务端我还活着,不要将我剔除。

服务端心跳检查:(对于永久实例)

对于永久实例的的监看检查,Nacos 采用的是注册中心探测机制,注册中心会在永久服务初始化时根据客户端选择的协议类型注册探活的定时任务。永久实例会在被主动删除前⼀直存在于注册中心,那么我们健康检查并不会去删除实例,所以我们只需要在负责的节点永久实例健康状态变更的时候通知到其余的节点即可。

3)服务剔除

nacos-server接收到client端的服务注册请求后,将注册的实例数据写到注册表之前,会首先开启一个健康检查的定时任务(首次启动会延迟5秒执行,之后每5秒执行一次),其实主要就是处理nacos-client的心跳信息的,如果客户端实例超过15秒还没有发送心跳过来,则将实例健康状态改成false;如果客户端实例超过30秒还没有发送心跳过来,则剔除该实例。

4)服务发现

客户端拉取:

在进行服务注册之后,该服务就会去这个服务注册中心里面拉取全部的微服务实例,会将全部的实例存在本地的缓存里面,并且同时会去开启一个定时任务,每隔5秒就会去拉取一次最新的微服务实例。

服务端推送:

服务端采取的是基于push的方式向客户端通知,由于服务端和服务提供者(各个微服务provider)建立了心跳机制,一旦某个服务出现故障,服务端察觉出后,会发送一个push消息给Nacos客户端,也就是我们的消费者。

5)服务下线

当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给nacos Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务从注册表中剔除。

2.5.2 nacos源码分析

服务注册
  • 保存服务的结构
  • 最终应该发送一个post请求给nacos服务端,并且传入相关参数

服务注册到Nacos以后,会保存在一个本地注册表中,其结构如下(一个双层map):

nacos_第17张图片

首先最外层是一个Map,结构为:Map>:

key:是namespace_id,起到环境隔离的作用。namespace下可以有多个groupvalue:又是一个Map,代表分组及组内的服务。一个组内可以有多个服务    key:代表group分组,不过作为key时格式是group_name:service_name    value:分组下的某一个服务,例如userservice,用户服务。类型为Service,内部也包含一个Map,一个服务下可以有多个集群        key:集群名称        value:Cluster类型,包含集群的具体信息。一个集群中可能包含多个实例,也就是具体的节点信息,其中包含一个Set,就是该集群下的实例的集合            Instance:实例信息,包含实例的IP、Port、健康状态、权重等等信息

每一个服务去注册到Nacos时,就会把信息组织并存入这个Map中。

1)自动装配

NacosServiceRegistryAutoConfiguration

nacos_第18张图片

在NacosServiceRegistryAutoConfiguration这个类中,包含一个跟自动注册有关的Bean:

@Bean@ConditionalOnBean({AutoServiceRegistrationProperties.class})
public NacosAutoServiceRegistration nacosAutoServiceRegistration(NacosServiceRegistry registry, AutoServiceRegistrationProperties autoServiceRegistrationProperties, NacosRegistration registration) {    
    return new NacosAutoServiceRegistration(registry, autoServiceRegistrationProperties, registration);
}

可以看到在初始化时,其父类AbstractAutoServiceRegistration也被初始化了,实现了ApplicationListener接口,监听Spring容器启动过程中的事件。在监听到WebServerInitializedEvent(web服务初始化完成)的事件后,执行了bind 方法。

public void onApplicationEvent(WebServerInitializedEvent event) {
    this.bind(event);
}

在bind方法中启动服务注册流程

@Deprecatedpublic void bind(WebServerInitializedEvent event) {
    ApplicationContext context = event.getApplicationContext();
    if (!(context instanceof ConfigurableWebServerApplicationContext) || !"management".equals(((ConfigurableWebServerApplicationContext)context).getServerNamespace())) {
        this.port.compareAndSet(0, event.getWebServer().getPort());
        // 启动服务注册流程       
        this.start();
    }
}

进行服务注册操作,其中最关键的register()方法就是完成服务注册

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(false, true);
        }    
    }
}
protected void register() {
    this.serviceRegistry.register(this.getRegistration());
}

nacos_第19张图片

实现类使用的是NacosServiceRegistry,代码如下:

public void register(Registration registration) {
    // 判断serviceID是否为空,即spring.application.name是否为空,nacos一定要配置服务名称
    if (StringUtils.isEmpty(registration.getServiceId())) {
        log.warn("No service to register for nacos client...");
    } else {
        NamingService namingService = this.namingService();
        String serviceId = registration.getServiceId();
        String group = this.nacosDiscoveryProperties.getGroup();
        // 封装服务实例的基本信息
        Instance instance = this.getNacosInstanceFromRegistration(registration);
        try {
            // 服务注册的方法
            namingService.registerInstance(serviceId, group, instance);
            log.info("nacos registry, {} {} {}:{} register finished", new Object[]{group, serviceId, instance.getIp(), instance.getPort()});
        } catch (Exception var7) {
            log.error("nacos registry, {} register failed...{},", new Object[]{serviceId, registration.toString(), var7}); 
            ReflectionUtils.rethrowRuntimeException(var7);
        }
    
    }}

可以看到方法中最终是调用NamingService的registerInstance方法实现注册的,而NamingService接口的默认实现就是NacosNamingService

public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    // 检查超时参数是否异常
    NamingUtils.checkInstanceIsLegal(instance);
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    // 判断是否为临时实例
    if (instance.isEphemeral()) {
        // 如果是临时实例,则开启定时任务,定时的向nacos发送心跳
        BeatInfo beatInfo = this.beatReactor.buildBeatInfo(groupedServiceName, instance);        // 添加心跳任务
        this.beatReactor.addBeatInfo(groupedServiceName, beatInfo);
    }
    // 注册服务
    this.serverProxy.registerService(groupedServiceName, groupName, instance);
}

最终,由NacosProxy的registerService方法,完成服务注册。代码如下:

public void registerService(
    String serviceName, String groupName, Instance instance) throws NacosException {
    LogUtils.NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", new Object[]{this.namespaceId, serviceName, instance});    
    Map<String, String> params = new HashMap(16);
    params.put("namespaceId", this.namespaceId);
    params.put("serviceName", serviceName);
    params.put("groupName", groupName);
    params.put("clusterName", instance.getClusterName());
    params.put("ip", instance.getIp());
    params.put("port", String.valueOf(instance.getPort()));
    params.put("weight", String.valueOf(instance.getWeight()));
    params.put("enable", String.valueOf(instance.isEnabled()));
    params.put("healthy", String.valueOf(instance.isHealthy()));
    params.put("ephemeral", String.valueOf(instance.isEphemeral()));
    params.put("metadata", JacksonUtils.toJson(instance.getMetadata())); 
    this.reqApi(UtilAndComs.nacosUrlInstance, params, "POST");
}

这里提交的信息就是Nacos服务注册接口需要的完整参数。至此,服务注册完成。服务注册的大致流程图。

nacos_第20张图片

服务发现

我们讲到一个类NacosNamingService,这个类不仅仅提供了服务注册功能,同样提供了服务发现的功能,最终通过getAllInstances方法从nacos拉取服务:

public List<Instance> getAllInstances(
    String serviceName, String groupName, List<String> clusters, boolean subscribe) throws NacosException {
    ServiceInfo serviceInfo;
    // 判断是否是订阅服务(默认值为true)
    if (subscribe) {
        serviceInfo = this.hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ","));
    } else {
        // 去nacos拉取服务
        serviceInfo = this.hostReactor.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ","));
    }
    // 从服务信息中获取实例并返回
    List list;
    return (List)(serviceInfo != null && !CollectionUtils.isEmpty(list = serviceInfo.getHosts()) ? list : new ArrayList());
}

订阅服务消息,这里是由HostReactor类的getServiceInfo()方法来实现的:

public ServiceInfo getServiceInfo(String serviceName, String clusters) {
    LogUtils.NAMING_LOGGER.debug("failover-mode: " + this.failoverReactor.isFailoverSwitch());
    // 拼接key
    String key = ServiceInfo.getKey(serviceName, clusters);
    if (this.failoverReactor.isFailoverSwitch()) {
        return this.failoverReactor.getService(key);
    } else {
        // 读取本地服务列表的缓存,缓存是一个map:Map
        ServiceInfo serviceObj = this.getServiceInfo0(serviceName, clusters);
        // 如果本地缓存没有,则从nacos中拉取
        if (null == serviceObj) {
            // 创建一个空的ServiceInfo
            serviceObj = new ServiceInfo(serviceName, clusters);
            // 放入缓存里面
            this.serviceInfoMap.put(serviceObj.getKey(), serviceObj);
            // 放入带更新的列表中 
            this.updatingMap.put(serviceName, new Object());
            // 立即更新服务列表
            this.updateServiceNow(serviceName, clusters);
            // 从带更新列表中删除
            this.updatingMap.remove(serviceName);
            // 缓存中有,但是需要更新
        } else if (this.updatingMap.containsKey(serviceName)) { 
            synchronized(serviceObj) {
                try {
                    serviceObj.wait(5000L);                
                } catch (InterruptedException var8) {
                    LogUtils.NAMING_LOGGER.error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, var8);
                }
            }
        }
        // 开启定时更新服务列表的功能
        this.scheduleUpdateIfAbsent(serviceName, clusters);
        // 返回缓存中的服务信息
        return (ServiceInfo)this.serviceInfoMap.get(serviceObj.getKey());
    }
}

不管是立即更新服务列表,还是定时更新服务列表,最终都会执行HostReactor中的updateService()方法:

public void updateService(String serviceName, String clusters) throws NacosException {
    ServiceInfo oldService = this.getServiceInfo0(serviceName, clusters);
    boolean var12 = false;
    try {
        var12 = true;
        String result = this.serverProxy.queryList(serviceName, clusters, this.pushReceiver.getUdpPort(), false);
        if (StringUtils.isNotEmpty(result)) {
            this.processServiceJson(result);
            var12 = false;
        } else {
            var12 = false;
        }
    } finally {
        if (var12) {
            if (oldService != null) {
                synchronized(oldService) {
                    oldService.notifyAll();
                }
            }
        }
    }
    if (oldService != null) {
        synchronized(oldService) {
            oldService.notifyAll();
        }
    }
}

2.6 nacos面板介绍

2.6.1 空间分组

nacos_第21张图片

命名空间及分组都是用来隔离服务的。

命名空间(namespace):一般用于区分不同的环境

分组(group):一般用于区分不同的项目

cloud:  nacos:    discovery:      # nacos服务端的地址      server-addr: http://localhost:8848      username: nacos      password: nacos      namespace: d03db0a2-3e7f-4d89-86a0-850eba6237d5      group: dev_group

nacos_第22张图片

说明:不同的命名空间之间不能相互调用。

2.6.2 保护阈值

  • 临时实例

    在nacos注册的实例分为临时实例和持久实例,主要体现在服务器对实例的处理上。

    临时实例向Nacos注册,Nacos不会对其进行持久化存储,只能通过心跳方式保活。默认模式是:客户端心跳上报Nacos实例健康状态,默认间隔5秒,Nacos在15秒内未收到该实例的心跳,则会设置为不健康状态,超过30秒则将实例删除。

  • 持久化实例向Nacos注册,Nacos会对其进行持久化处理。当该实例不存在时,Nacos只会将其健康状态设置为不健康,但并不会对将其从服务端删除。

    对于持久化实例,健康检查失败,会被标记成不健康状态。它的好处是运维可以实时看到实例的健康状态,便于后续的警告、扩容等一些列措施。

  • 除了上述场景之外,持久化实例还有另外一个场景用的到,那就是保护阈值。

Nacos中可以针对具体的实例设置一个保护阈值,值为0-1之间的浮点类型。本质上,保护阈值是⼀个⽐例值(当前服务健康实例数/当前服务总实例数)。

⼀般情况下,服务消费者要从Nacos获取可用实例有健康/不健康状态之分。Nacos在返回实例时,只会返回健康实例。

但在高并发、大流量场景会存在⼀定的问题。比如,nacos-user-service有10个实例,其中8个实例都处于不健康状态,如果Nacos只返回这两个健康实例的话。流量洪峰的到来可能会直接打垮这两个服务,进一步产生雪崩效应。

保护阈值存在的意义在于当某个服务健康实例数/总实例数 < 保护阈值时,说明健康的实例不多了,保护阈值会被触发(状态true)。

Nacos会把该服务所有的实例信息(健康的+不健康的)全部提供给消费者,消费者可能访问到不健康的实例,请求失败,避免雪崩,起到分流的作用。牺牲了⼀些请求,保证了整个系统的可⽤。

当某个服务健康实例数/总实例数 < 保护阈值时 ,保护阈值会被触发(状态true)

案例演示:

  • 商品微服务的2台实例都设置为永久实例(ephemeral: false)
  • nacos面板中设置商品微服务的保护阈值为0.9
  • 停掉其中的1台商品微服务的机器
    值(当前服务健康实例数/当前服务总实例数)。

⼀般情况下,服务消费者要从Nacos获取可用实例有健康/不健康状态之分。Nacos在返回实例时,只会返回健康实例。

但在高并发、大流量场景会存在⼀定的问题。比如,nacos-user-service有10个实例,其中8个实例都处于不健康状态,如果Nacos只返回这两个健康实例的话。流量洪峰的到来可能会直接打垮这两个服务,进一步产生雪崩效应。

保护阈值存在的意义在于当某个服务健康实例数/总实例数 < 保护阈值时,说明健康的实例不多了,保护阈值会被触发(状态true)。

Nacos会把该服务所有的实例信息(健康的+不健康的)全部提供给消费者,消费者可能访问到不健康的实例,请求失败,避免雪崩,起到分流的作用。牺牲了⼀些请求,保证了整个系统的可⽤。

当某个服务健康实例数/总实例数 < 保护阈值时 ,保护阈值会被触发(状态true)

案例演示:

  • 商品微服务的2台实例都设置为永久实例(ephemeral: false)
  • nacos面板中设置商品微服务的保护阈值为0.9
  • 停掉其中的1台商品微服务的机器
  • 通过order下单接口,查看效果

你可能感兴趣的:(spring,cloud)