前言
在分布式系统中,注册中心充当着重要角色,是服务发现、客户端负载均衡中不可缺少的一员。注册中心除了能够实现基本的功能外,他的稳定性、可用性和健壮性对整个分布式系统的流畅运行影响重大。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作为注册中心。