一场事故告诉你zookeeper和nacos谁更适合做注册中心

前言

        在分布式系统中,注册中心充当着重要角色,是服务发现、客户端负载均衡中不可缺少的一员。注册中心除了能够实现基本的功能外,他的稳定性、可用性和健壮性对整个分布式系统的流畅运行影响重大。dubbo作为国内一款主流的分布式系统,支持的注册中心有zookeeper、nacos和redis等第三方中间件,同时也支持Simple和Multicast的方式。zk和nacos可能是最常使用的方式,到底谁更胜一筹呢?以下的事故现场便有答案。

在分布式系统中,服务往往由提供方来定义,并给出服务定义的sdk包。消费方通过引入提供方的sdk包,进行服务的发现。但是当一个子系统需要依赖成千上百个子系统的服务,那么需要依赖成千上百个子系统的sdk包,显然有些不友好,那么有什么方式可以不引依赖呢,dubbo提供了泛化调用方式。泛化调用虽然解决了依赖引用的问题,但是也存在一些使用不当引发的致命问题,通过如下一个泛化服务定义未缓存的demo案例来揭穿。

案例复现

        pom.xml文件中引入2.5.7版本的dubbo,0.11版本的zkClient依赖,采用zk作为注册中心。

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>dubbo</artifactId>
			<version>2.5.7</version>
		</dependency>

		<dependency>
			<groupId>com.101tec</groupId>
			<artifactId>zkclient</artifactId>
			<version>0.11</version>
		</dependency>

        通过如下的代码来模拟泛化调用,helloService()方法中进行泛化服务的定义,并返回泛化服务。然后在sayHello()方法中进行服务泛化调用,sayHello方法总通过一个死循环一直进行服务获取,真到发生异常。

import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ReferenceConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.rpc.service.GenericService;
import org.springframework.stereotype.Service;

@Service
public class HelloGenericService {

    private GenericService helloService() {
        ReferenceConfig<GenericService> config = new ReferenceConfig<>();
        config.setInterface("com.qiao.hao.ting.service.HelloService");
        config.setGeneric(true);
        config.setProtocol("dubbo");
        config.setCheck(false);
        //采用zk作为注册中心
        config.setRegistry(new RegistryConfig("zookeeper://127.0.0.1:2181"));
        //config.setRegistry(new RegistryConfig("nacos://127.0.0.1:8848"));
        config.setTimeout(1000);
        config.setApplication(new ApplicationConfig("general"));
        GenericService service = config.get();
        return service;
    }

    public Object sayHello() {
        while (true) {
            try {
                GenericService genericService = helloService();
                //rpc调用
                //genericService.$invoke("syaHello", new String[]{}, new Object[]{});
            } catch (Exception e) {
                e.printStackTrace();
                break;
            }
        }
        return "success";
    }
}

        触发sayHello调用之后,来看zk节点的信息。通过zkCli客户端窗口查看dubbo注册节点信息,helloService每被调用一次,则会向zk的/dubbo/对应接口/consumers目录写入一个消费节点。

        程序一直运行下去,消费者节点个数会直接溢出ls命令能够接受的数组大小 。

         同时zk的data目录下的文件大小在不断地增加,那么一个最直观的问题就是磁盘随着时间推移一定会被打满。

         同时,通过dubbo-admin查看服务注册信息,可以看到com.qiao.hao.ting.service.HelloService服务节点个数不止一个,随着helloService的一直运行,那么节点个数就会一直增加。

        现在把注册中心改为nacos,注册客户端采用0.0.1版本的dubbo-registry-nacos。

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>dubbo</artifactId>
			<version>2.5.7</version>
		</dependency>

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>dubbo-registry-nacos</artifactId>
			<version>0.0.1</version>
		</dependency>

        把泛化服务定义的registry设置为nacos。

   private GenericService helloService() {
        ReferenceConfig<GenericService> config = new ReferenceConfig<>();
        config.setInterface("com.qiao.hao.ting.service.HelloService");
        config.setGeneric(true);
        config.setProtocol("dubbo");
        config.setCheck(false);
        //config.setRegistry(new RegistryConfig("zookeeper://127.0.0.1:2181"));
        //采用nacos作为注册中心
        config.setRegistry(new RegistryConfig("nacos://127.0.0.1:8848"));
        config.setTimeout(1000);
        config.setApplication(new ApplicationConfig("general"));
        GenericService service = config.get();
        return service;
    }

        触发sayHello方法,通过nacos的管理界面可知,无论程序怎么跑,com.qiao.hao.ting.service.HelloService消费者注册信息只有一条。

        为了对比的一致性,都通过dubbo-admin进行对比。dubbo-admin默认是通过zk进行注册的,这里需要对dubbo-admin进行小改造,通过以下两步把dubbo-admin切换到nacos。第一,下载对应dubbo-admin版本的源码(本案例是2.5.7版本),然后引入0.0.1版本的dubbo-registry-nacos的依赖。

        第二,把dubbo.registry.address的地址改为nacos://127.0.0.1:8848。

        然后重新构建dubbo-admin,运行。最后查看服务列表,可知,在nacos作为注册中心下,该com.qiao.hao.ting.service.HelloService服务也只会存在一条注册信息。 

问题分析

        由于没有把泛化服务进行缓存,每次调用的时候都会进行一次服务注册,服务注册请求发送到zk,zk就会进行一个节点的写入;nacos中的一致性不是像zk通过节点数据进行维护,并不会出现服务无限重复注册的情况(两者具体的原理不在这里进行说明,敬请期待)。

GenericService service = config.get();

        当然实际代码中几乎不可能出现死循环调用注册的,但是在高并发,或者长时间维持一定量的请求,那么还是会导致zk的磁盘耗尽、io读写异常、导致zk不可用,从而导致整个集群的服务注册发型能力不可用。

        能不能在测试阶段发现这种问题。如果测试人员比较厉害的,还可能关注服务注册这块。但是一般不可能,服务注册一般不在测试范围,在功能测试,就算算上单元、冒烟、整体及回归测试,也不可能会出现zk的不可用。压力测试,一般比较短暂,短暂时间内的磁盘写入量,机器应该是能够抗住的,除非测试环境也做了监控,但一般也不可能。

解决方案

        如果使用zk作为注册中心了,那么如何预防和解决这样的问题呢。

1、对服务进行缓存,比如改为如下代码。

@Service
public class HelloGenericService {
    
    private GenericService genericService;
    
    private Object lockObject = new Object();

    private GenericService helloService() {
        if(genericService != null) {
            return genericService;
        }
        synchronized (lockObject) {
            if(genericService != null) {
                return genericService;
            }
            ReferenceConfig<GenericService> config = new ReferenceConfig<>();
            config.setInterface("com.qiao.hao.ting.service.HelloService");
            config.setGeneric(true);
            config.setProtocol("dubbo");
            config.setCheck(false);
            //config.setRegistry(new RegistryConfig("zookeeper://127.0.0.1:2181"));
            config.setRegistry(new RegistryConfig("nacos://127.0.0.1:8848"));
            config.setTimeout(1000);
            config.setApplication(new ApplicationConfig("general"));
            genericService = config.get();
        }
        return genericService;
    }

}

2、加强代码审核

3、对zk节点进行监控,比如磁盘、cpu、io等物理监控,注册服务请求的网络监控。

结论

        建议选择nacos作为注册中心。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>