Nacos-Client如何实现高可用

本文介绍了Nacos在Java中的配置管理,包括配置模型、查询和监听。配置由命名空间、组和数据构成唯一标识,查询优先本地文件。监听通过长轮询实现,但存在性能问题。新版本采用推送方式,性能更优,客户端与服务端兼容,升级需参考手册。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引入

本期文章暂时以 nacos-client 1.4.x 版本进行介绍,后期有时间再写一篇关于 2.x 加入 Grpc 版本的,文章较长,谢谢你的观看。

使用案例

public class ConfigExample {

    public static void main(String[] args) throws NacosException, InterruptedException {
        String serverAddr = "localhost";
        String dataId = "test";
        String group = "DEFAULT_GROUP";
        Properties properties = new Properties();
        // 指定server地址
        properties.put("serverAddr", serverAddr);
        ConfigService configService = NacosFactory.createConfigService(properties);
        // 查询配置
        String content = configService.getConfig(dataId, group, 5000);
        System.out.println(content);
        // 监听配置
        configService.addListener(dataId, group, new Listener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                System.out.println("receive:" + configInfo);
            }

            @Override
            public Executor getExecutor() {
                return null;
            }
        });
		// 发布配置
        boolean isPublishOk = configService.publishConfig(dataId, group, "content");
        System.out.println(isPublishOk);
		
        Thread.sleep(3000);
        // 查询配置
        content = configService.getConfig(dataId, group, 5000);
        System.out.println(content);
		
        // 删除配置
        boolean isRemoveOk = configService.removeConfig(dataId, group);
        System.out.println(isRemoveOk);
        Thread.sleep(3000);
		
        // 查询配置
        content = configService.getConfig(dataId, group, 5000);
        System.out.println(content);
        Thread.sleep(300000);

    }
}

Nacos 配置模型

在这里插入图片描述

  • NamspaceTenant),命名空间(租户),默认命名空间是 public一个命名空间可以包含多个 Group,在 Nacos 源码里有些变量是 tenant 租户;
  • Group:组,默认分组是 DEFAULT_GROUP一个组可以包含多个 dataId
  • dataId:译为数据id,在 nacosDataId 代表一整个配置文件,是配置的最小单位;

以上值共同构成一个配置或者一个服务的唯一标识。

ConfigService

  • ConfigServiceNacos 暴露给客户端的配置服务接口,一个 Nacos 配置中心 + 一个 Namespace = 一个 ConfigService 实例
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
properties.put("namespace", namespace);
ConfigService configService = NacosFactory.createConfigService(properties);
  • ConfigService 通过 ConfigFactory 获取,每次 create 出来的都是不同的实例,内部没有做任何缓存
    • 主要是为了将 nacos-api 层单独拆分出来
public class NacosFactory {
    /**
     * Create config service.
     */
    public static ConfigService createConfigService(Properties properties) throws NacosException {
        return ConfigFactory.createConfigService(properties);
    }
}
public class ConfigFactory {
    /**
     * Create Config.
     */
    public static ConfigService createConfigService(Properties properties) throws NacosException {
      Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
      Constructor constructor = driverImplClass.getConstructor(Properties.class);
      ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties);
      return vendorImpl;
    }
}
  • ConfigService 包括定义了配置的增删查改,以及监听操作
public interface ConfigService {
    String getConfig(String dataId, String group, long timeoutMs) throws NacosException;
    String getConfigAndSignListener(String dataId, String group, long timeoutMs, Listener listener) throws NacosException;
    boolean publishConfig(String dataId, String group, String content) throws NacosException;
    boolean publishConfig(String dataId, String group, String content, String type) throws NacosException;
    boolean removeConfig(String dataId, String group) throws NacosException;
    void addListener(String dataId, String group, Listener listener) throws NacosException;
    void removeListener(String dataId, String group, Listener listener);
    String getServerStatus();
    void shutDown() throws NacosException;
}
配置查询

Nacos 配置来源

  • 首先 getConfig() 会本地 Failover 文件查询,Failover 文件存在的情况下,Nacos-client 不会向远程发起查询
  • Failover 文件不存在时,就会通过 ClientWorker 向远程发起查询获取配置,并将查询结果保存在 snapshot 文件中;
  • 远程服务器不可达时,就会直接采用本地 snapshot 文件

Failover 文件在 Nacos 里是优先级最高的,如果 Failover 文件存在则不会使用 nacos 服务端的配置,永远会使用 Failover 文件,即使服务端的配置发生了变化.

NacosFailover 文件内容没有更新的入口,也就是说这个文件只能在文件系统中修改生效,生效时机在长轮询过程中

在这里插入图片描述

源码定义

Nacos 客户端获取配置的入口方法是 NacosConfigService#getConfigInner

private final ClientWorker worker;
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
    // group默认设置为DEFAULT_GROUP
    group = null2defaultGroup(group); 
    ConfigResponse cr = new ConfigResponse();
    cr.setDataId(dataId);
    cr.setTenant(tenant);
    cr.setGroup(group);

    // LEVEL1 : 使用本地文件系统的failover配置
    String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
    if (content != null) {
        cr.setContent(content);
        content = cr.getContent();
        return content;
    }

    // LEVEL2 : 读取config-server实时配置,并将snapshot保存到本地文件系统
    try {
        String[] ct = worker.getServerConfig(dataId, group, tenant, timeoutMs);
        cr.setContent(ct[0]);
        content = cr.getContent();
        return content;
    } catch (NacosException ioe) {
        if (NacosException.NO_RIGHT == ioe.getErrCode()) {
            throw ioe;
        }
        // 非403错误进入LEVEL3
        LOGGER.warn(...);
    }

    // LEVEL3 : 如果读取config-server发生非403Forbidden错误,使用本地snapshot
    content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
    cr.setContent(content);
    content = cr.getContent();
    return content;
}

FailoverSnapshot 文件存储位置

${user.home) 下,以 _nacos 结尾的目录下,以 config-data-{namespace} 的就是 Failover 文件目录,以 snapshoy-{namespace} 的就是快照文件目录。

配置监听

组件调用图

在这里插入图片描述

  • ClientWorker 内维护着一张 Map<GroupKey, CacheData>。一个唯一的配置 GroupKeygroupKeynamespacegroupdataId 共同组成
  • CacheData 是一个配置缓存的抽象,它可能来源于 Failover 文件或 Snapshot 文件,在创建的时候,只需要指定组和数据 id,它会默认去加载 Snapshot 文件,它维护着一个 Listener 列表,在配置发生变更时,可以对 Listener 进行通知
  • ClientWorker 内的长轮询任务由两个线程池共同完成
    • 第一个线程池只有一个线程,10ms 触发一次,负责去监听 Map<GroupKey, CacheData> 的数量变化判断需要的长轮询任务数默认情况下认为一个长轮询任务负责 3000CacheData 的监听
    • 第二个线程池拥有和机器相同核数的线程数由第一个线程池触发任务,真正进行轮询校验和通知的线程
  • 外部程序调用 API 层添加对某个配置的监听器,本质上也是ClientWorker 增加一个 CacheData,在长轮询任务被检测而已
  • 长轮询任务中去检测服务端的配置是否更新采用的是 Open API 的方式,长轮询任务不是死循环的,但是在任务提交后,就会马上提交下一次轮询,所以本质上也类似死循环;
    • 如果长轮询任务出现异常,那么会延迟 2s 才会提交下一次轮询,而不会马上提交;
  • 每个长轮询任务 LongPollingRunnable 都有一个唯一的 taskIdCacheData 也会有一个 taskId,用来对整体的 CacheData 进行分组,一个长轮询任务负责一组(一组最多允许 3000);

ClientWorker 的属性

public class ClientWorker implements Closeable {
	// 检测是否需要提交longPolling任务到executorService,如果需要则提交
    // 这个线程池只有一个线程
    final ScheduledExecutorService executor;
    
    // 执行长轮询,一般情况下执行listener回调也是在这个线程里
    // 这个线程池有和机器核数一样多的线程
    final ScheduledExecutorService executorService;
    
    // groupKey -> cacheData
    private final ConcurrentHashMap<String, CacheData> cacheMap = new ConcurrentHashMap<String, CacheData>();
    
    // httpClient, 用来向Server发起请求的
    private final HttpAgent agent;
    
    // 钩子管理器, 内部含有多个Filter,用来对ConfigResponse进行处理加工的
    private final ConfigFilterChainManager configFilterChainManager;
    
    // nacos服务端是否健康
    private boolean isHealthServer = true;
    
    // 长轮询超时时间 默认30s
    private long timeout;
    
    // 当前长轮询任务数量
    private double currentLongingTaskCount = 0;
    
    // 长轮询发生异常,默认延迟2s进行下次长轮询
    private int taskPenaltyTime;
    
    // 是否在添加监听器时,主动获取最新配置
    private boolean enableRemoteSyncConfig = false;
}

CacheData 的属性

public class CacheData {
    // agentName
    private final String name;
    
    // dataId
    public final String dataId;
    
    // group
    public final String group;
    
    // namespace
    public final String tenant;
    
    // 注册在这个配置上的监听器
    private final CopyOnWriteArrayList<ManagerListenerWrap> listeners;
    
    // 配置的md5
    private volatile String md5;
    
    // 是否使用failover配置文件
    private volatile boolean isUseLocalConfig = false;
    
    // failover配置文件的上次更新时间戳
    private volatile long localConfigLastModified;
    
    // 配置
    private volatile String content;
    
    // 所属长轮询任务id
    private int taskId;
    
    // 是否正在初始化
    private volatile boolean isInitializing = true;
    
    // 配置文件类型 如:TEXT、JSON、YAML
    private String type;
    
    // 对查询配置的请求和响应提供钩子处理
    private final ConfigFilterChainManager configFilterChainManager;
    
    public CacheData(ConfigFilterChainManager configFilterChainManager, 
                     String name, String dataId, String group,
        String tenant) {
        if (null == dataId || null == group) {
            throw new IllegalArgumentException("dataId=" + dataId + ", group=" + group);
        }
        this.name = name;
        this.configFilterChainManager = configFilterChainManager;
        this.dataId = dataId;
        this.group = group;
        this.tenant = tenant;
        listeners = new CopyOnWriteArrayList<ManagerListenerWrap>();
        this.isInitializing = true;
        // 这里会从本地文件系统加载配置内容,failover > snapshot
        this.content = loadCacheContentFromDiskLocal(name, dataId, group, tenant);
        this.md5 = getMd5String(content);
    }

}

ClientWorker 长轮询线程池任务

public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,
        final Properties properties) {
     // 以下是第一个线程池, 只有一个线程, 负责去监听Map<GroupKey, CacheData>
    this.executor.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            try {
                checkConfigInfo();
            } catch (Throwable e) {
                LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
            }
        }
    }, 1L, 10L, TimeUnit.MILLISECONDS);
}

public void checkConfigInfo() {
    // cacheMap大小
    int listenerSize = cacheMap.size();
    // (cacheMap大小 / 3000)向上取整, 一个长轮询任务负责监听3000个配置
    int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
    // 如果算出来任务数大于当前已经创建的任务数, 那么就加到标准为止
    if (longingTaskCount > currentLongingTaskCount) {
        for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
            // 开启新的长轮询任务
            executorService.execute(new LongPollingRunnable(i));
        }
        currentLongingTaskCount = longingTaskCount;
    }
}

LongPollingRunnable 长轮询任务

class LongPollingRunnable implements Runnable {
    private final int taskId;
    public LongPollingRunnable(int taskId) {
        this.taskId = taskId;
    }
    @Override
    public void run() {
        // 当前长轮询任务负责的CacheData集合
        List<CacheData> cacheDatas = new ArrayList<CacheData>();
        // 正在初始化的CacheData 即刚构建的CacheData,内部的content仍然是snapshot版本
        List<String> inInitializingCacheList = new ArrayList<String>();
        try {
            // 1. 对于failover配置文件的处理
            for (CacheData cacheData : cacheMap.values()) {
                if (cacheData.getTaskId() == taskId) {
                    cacheDatas.add(cacheData);
                    try {
                        // 判断cacheData是否需要使用failover配置,设置isUseLocalConfigInfo
                        // 如果需要则更新内存中的配置
                        checkLocalConfig(cacheData);
                        // 使用failover配置则检测content内容是否发生变化,如果变化则通知监听器
                        if (cacheData.isUseLocalConfigInfo()) {
                            cacheData.checkListenerMd5();
                        }
                    } catch (Exception e) {
                        LOGGER.error("get local config info error", e);
                    }
                }
            }

            // 2. 对于所有非failover配置,执行长轮询,返回发生改变的groupKey
            List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);

            for (String groupKey : changedGroupKeys) {
                String[] key = GroupKey.parseKey(groupKey);
                String dataId = key[0];
                String group = key[1];
                String tenant = null;
                if (key.length == 3) {
                    tenant = key[2];
                }
                try {
                    // 3. 对于发生改变的配置,查询实时配置并保存snapshot
                    String[] ct = getServerConfig(dataId, group, tenant, 3000L);
                    // 4. 更新内存中的配置
                    CacheData cache = cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));
                    cache.setContent(ct[0]);
                    if (null != ct[1]) {
                        cache.setType(ct[1]);
                    }
                } catch (NacosException ioe) {
                    LOGGER.error(message, ioe);
                }
            }
            // 5. 对于非failover配置,触发监听器
            for (CacheData cacheData : cacheDatas) {
                // 排除failover文件
                if (!cacheData.isInitializing() || inInitializingCacheList
                        .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
                    // 校验md5是否发生变化,如果发生变化通知listener
                    cacheData.checkListenerMd5();
                    cacheData.setInitializing(false);
                }
            }
            inInitializingCacheList.clear();
            // 6-1. 都执行完成以后,再次提交长轮询任务
            executorService.execute(this);
        } catch (Throwable e) {
            // 6-2. 如果长轮询执行发生异常,延迟2s执行下一次长轮询
            executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
        }
    }
}

结尾

综上,就是 Nacos SDK1.4.X 对配置管理的高可用实现,但是实际使用过程中,因为长轮询导致性能问题。

其实 Server 端有配置变更推送机制,那能不能为何还需要客户端长轮询呢?是因为 Server 只能采用 UDP 方式,不可靠传输导致部分通知无法到达客户端,所以才客户端主动轮询进行补偿。

而在 2.X (目前是 Nacos 正在进行 3.0 版本建设)版本,使用 grpc 的方式,摒弃了长轮询,在性能上远远超过 1.4.x

虽然,通知的方式变化了,但是 1.4.x+ 的客户端是完全兼容 2.x 的服务端的,但是为了更好地发挥 Grpc 的性能优势,建议还是使用 2.x 的客户端和服务端,Nacos 支持从 1.4.x 升级到 2.1.x ,但是需要进行一定的配置,比如 2.1.x 默认关闭了双写,需要打开等,具体可参考官方网站的手册:Nacos手册


最后,文章较长,新手文手,谢谢你的耐心观看!

<think>好的,我现在遇到了一个Nacos连接问题,需要仔细分析一下。首先看错误信息,Caused by: com.alibaba.nacos.api.exception.NacosException: failed to req API:/nacos/v1/ns/instance after all servers([192.168.1.111:8848]) tried: java.net.ConnectException: Connection refused: connect。这说明客户端尝试连接Nacos服务器时被拒绝了,可能服务器没启动或者网络不通。 接下来看堆栈跟踪,NamingProxy在调用registerService时失败,最终是因为无法连接到192.168.1.111:8848。首先想到的是检查Nacos服务器是否正常运行。可能需要登录到那台机器,查看nacos的进程是否存在,或者用systemctl status nacos之类的命令检查服务状态。如果服务没启动,启动它应该能解决问题。 如果服务是运行的,那可能是客户端配置的地址不对。比如application.yml里配置的nacos地址是否正确,是否有拼写错误,或者是否应该是localhost而不是IP地址。有时候配置成hostname也可能有问题,特别是DNS解析不正确的时候。这时候可以尝试用curl或者telnet 192.168.1.111 8848来测试端口是否可达。如果连接被拒绝,说明端口没开放,可能是防火墙的问题。需要检查服务器端的防火墙设置,是否允许8848端口的入站流量。如果是云服务器,安全组规则也需要检查。 另外,Nacos的版本兼容性也可能导致问题。比如客户端和服务端版本不一致,可能会有兼容性问题。需要确认使用的nacos-client和服务器版本是否匹配。例如,客户端是1.4.1,服务器是否也是1.x版本,或者是否需要升级客户端。 还有可能是Nacos集群配置的问题,如果服务器是集群模式,是否所有节点都健康?客户端是否配置了所有正确的集群节点地址?有时候虽然配置了一个节点,但如果该节点不可用,又没有其他备用节点,也会导致连接失败。不过在这个错误里,只列出了一个服务器地址,可能客户端只配了一个,所以需要确认集群配置是否正确。 另外,查看客户端的日志是否有更详细的信息,比如是否有其他错误提示。有时候除了连接拒绝,可能还有SSL配置错误或者其他网络问题。比如是否启用了HTTPS而客户端配置的是HTTP,或者反之。检查Nacos服务器的配置文件,比如application.properties里的server.port和是否启用了鉴权。如果服务器开启了鉴权,客户端需要配置对应的用户名和密码。 如果是在Docker或Kubernetes环境中运行,可能涉及到网络命名空间或服务发现的问题。例如,容器内的应用无法访问宿主机的IP,需要配置正确的网络模式或者使用内部DNS名称。 最后,可以尝试重启客户端应用,有时候临时的网络故障可能导致连接问题,重启可能解决。如果所有步骤都检查过了还是不行,可能需要深入抓包分析,使用tcpdump或者Wireshark查看连接请求是否真的到达了服务器,或者是否有SYN包被拒绝,帮助确定是网络问题还是服务端问题。</think>这个错误表明您的应用程序在尝试连接Nacos服务器时遇到了网络连接问题。以下是逐步排查和解决问题的建议: ### 1. **检查Nacos服务器状态** - **确认Nacos服务是否运行** 登录服务器执行命令验证: ```bash curl http://192.168.1.111:8848/nacos/ # 预期返回Nacos登录页面 ps -ef | grep nacos # 查看Nacos进程是否存在 ``` - 如果未运行,启动Nacos: ```bash sh ${NACOS_HOME}/bin/startup.sh -m standalone # 单机模式启动 ``` ### 2. **验证网络连通性** - **从客户端测试连接** 在应用所在机器执行: ```bash telnet 192.168.1.111 8848 # 或使用nc命令 ``` - 若返回`Connection refused`,说明存在网络阻断 - **检查防火墙配置** 确保8848端口开放: ```bash # Linux查看防火墙规则 iptables -L -n | grep 8848 firewall-cmd --list-ports # (firewalld) ``` ### 3. **核对客户端配置** - **检查`application.yml`配置** 确认Nacos地址配置正确: ```yaml spring: cloud: nacos: discovery: server-addr: 192.168.1.111:8848 ``` - 注意避免多余空格或拼写错误 - 集群环境需配置多个节点:`192.168.1.111:8848,192.168.1.112:8848` ### 4. **检查Nacos服务端配置** - **查看`conf/application.properties`** 验证关键配置: ```properties server.port=8848 nacos.core.auth.enabled=false # 若启用鉴权需配置客户端账号 ``` - **集群模式需检查** `conf/cluster.conf`文件中的节点IP是否正确 ### 5. **版本兼容性验证** - **比对客户端与服务端版本** 参考官方兼容性矩阵: | 客户端版本 | 服务端兼容版本 | |------------|----------------| | 1.4.x | 1.4.x | | 2.x | 2.x | ### 6. **高级诊断** - **抓包分析(Linux环境)** ```bash tcpdump -i any host 192.168.1.111 and port 8848 -w nacos.pcap ``` 用Wireshark分析TCP握手是否完成 - **检查服务端日志** 查看`logs/nacos.log`是否有异常: ```log INFO Nacos is starting... INFO Nacos started successfully in stand alone mode ``` ### 7. **其他可能性** - **DNS解析问题** 尝试在客户端用IP代替域名 - **资源不足** 检查服务端内存和CPU使用: ```bash free -h top -p $(pgrep -f nacos) ``` **典型解决方案总结**: 1. 启动未运行的Nacos服务 2. 开放防火墙8848端口 3. 修正客户端配置中的错误IP/端口 4. 升级不兼容的客户端版本(如从1.x升级到2.x需同步升级服务端) 通过以上步骤排查,90%以上的类似连接问题都可以得到解决。如果问题依然存在,建议在Nacos GitHub提交issue并提供完整日志和配置信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值