通常将 Nacos 面试题分为概念类、原理类和实战类,基于Nacos的面试经验,我整理了高频和深度的面试题,并提供一些实现思路的描述。
Nacos是一个动态服务发现、配置管理和服务管理平台。它的核心功能包括:
Nacos 的核心优势在于"一站式"解决方案,同时支持服务发现和配置管理,且支持 CP 和 AP 模式切换,适应不同业务场景。
sh # 切换CP curl -X PUT 'http://$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP' # 切换AP curl -X PUT 'http://$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=AP'
注意
| 特性维度 | AP模式(默认) | CP模式 |
|---|---|---|
| 核心协议 | Distro协议 | Raft协议 |
| 数据一致性 | 最终一致性,可能存在短暂不一致 | 强一致性,所有节点数据实时同步 |
| 可用性 | 高可用,部分节点故障仍可响应 | 可能牺牲可用性,网络分区时为保证一致性可能 |
| 实例类型 | 临时实例 | 持久化实例 |
| 适用场景 | 服务注册与发现 | 配置管理、关键服务(如支付、库存) |
Nacos 为 AP 和 CP 模式分别设计了不同的底层协议和数据处理方式。
AP模式与Distro协议
Nacos的AP模式基于自研的Distro协议。这是一个为临时实例数据设计的一致性协议,其核心设计思想包括:
通过以上机制,Distro 协议在保证高可用的同时,实现了数据的最终一致性。
Distro 协议
Distro 协议是 Nacos 社区自研的一种 AP 分布式协议,是面向临时实例设计的一种分布式协议,其保证了在某些 Nacos 节点宕机后,整个临时实例处理系统依旧可以正常工作。作为一种有状态的中间件应用的内嵌协议,Distro 保证了各个 Nacos 节点对于海量注册请求的统一协调和存储。
地址:https://nacos.io/docs/ebook/ktwggk.mdx/
Distro 协议的主要设计思想如下:
CP模式与Raft协议
当Nacos切换到CP模式时,其核心则基于 Raft协议(Nacos 1.0及之前)或增强的 JRaft协议(Nacos 1.0之后)。这是一种强一致性协议,其核心机制包括:
这种方式确保了数据的强一致性,但可能在网络分区或Leader选举时牺牲部分可用性。
提示
需要掌握 Raft 一致协议。
客户端启动:
应用启动时 NacosServiceRegistryAutoConfiguration 自动装配了 NacosAutoServiceRegistration Nacos 自动服务注册器实例对象,而起内部实现了监听器,并监听了 WebServerInitializedEvent 事件,启动时会触发 onApplication() 方法 ,并通过 NamingService.registerInstance() 进行服务注册
java // com.alibaba.nacos.client.naming.NamingService#registerInstance @Override public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException { NamingUtils.checkInstanceIsLegal(instance); clientProxy.registerService(serviceName, groupName, instance); }
选择客户端代理(GRPC vs HTTP)
java // com.alibaba.nacos.client.naming.NamingClientProxyDelegate#registerService @Override public void registerService(String serviceName, String groupName, Instance instance) throws NacosException { getExecuteClientProxy(instance).registerService(serviceName, groupName, instance); } private NamingClientProxy getExecuteClientProxy(Instance instance) { return instance.isEphemeral() ? grpcClientProxy : httpClientProxy; }
关键设计:
为什么这样设计?
Nacos默认使用临时实例(ephemeral=true),因为微服务通常是动态伸缩的,需要心跳维持状态。相比HTTP协议,GRPC比HTTP更高效,减少了网络开销和CPU消耗,适合高频心跳场景。
GRPC协议优势:
在服务注册完成时,发布了 ServiceEvent.ServiceChangedEvent 的事件,在 NamingSubscriberServiceV2Impl 订阅者的监听中,同时会添加到任务执行引擎中,等待通过 GRPC 调用 通知订阅者请求
java @Override public void onEvent(Event event) { // 服务变更事件 if (event instanceof ServiceEvent.ServiceChangedEvent) { // 服务发生变化,推送给所有订阅者 ServiceEvent.ServiceChangedEvent serviceChangedEvent = (ServiceEvent.ServiceChangedEvent) event; // 服务 Service service = serviceChangedEvent.getService(); // 添加延迟任务,500ms delayTaskEngine.addTask(service, new PushDelayTask(service, PushConfig.getInstance().getPushTaskDelay())); // 增加服务变更次数 MetricsMonitor.incrementServiceChangeCount(service.getNamespace(), service.getGroup(), service.getName()); } // 服务订阅事件 else if (event instanceof ServiceEvent.ServiceSubscribedEvent) { // 如果服务由一个客户端订阅,则只推送此客户端 ServiceEvent.ServiceSubscribedEvent subscribedEvent = (ServiceEvent.ServiceSubscribedEvent) event; Service service = subscribedEvent.getService(); // 添加延迟任务,默认 500ms 执行一次 delayTaskEngine.addTask(service, new PushDelayTask(service, PushConfig.getInstance().getPushTaskDelay(), subscribedEvent.getClientId())); } }
此逻辑是通知其他订阅的客户端,告知其状态的变更
java public class NamingPushRequestHandler implements ServerRequestHandler { private final ServiceInfoHolder serviceInfoHolder; public NamingPushRequestHandler(ServiceInfoHolder serviceInfoHolder) { this.serviceInfoHolder = serviceInfoHolder; } @Override public Response requestReply(Request request) { // 是订阅者通知的请求 if (request instanceof NotifySubscriberRequest) { // 请求 NotifySubscriberRequest notifyResponse = (NotifySubscriberRequest) request; // 处理服务信息 serviceInfoHolder.processServiceInfo(notifyResponse.getServiceInfo()); return new NotifySubscriberResponse(); } return null; } }
在应用启动的同时通过 ConnectionManager 开启检查,将不健康的实例进行剔除
java // ConnectionManager 实例构建完成后,调用 @PostConstruct 注解的方法 @PostConstruct public void start() { // Start UnHealthy Connection Expel Task. RpcScheduledExecutor.COMMON_SERVER_EXECUTOR.scheduleWithFixedDelay(() -> { try { // 发送心跳... } catch (Throwable e) { Loggers.REMOTE.error("Error occurs during connection check... ", e); } }, 1000L, 3000L, TimeUnit.MILLISECONDS); }
Nacos 在 2.0 版本选择转向 gRPC 协议,主要是为了解决 1.x 版本在性能和资源消耗上的核心痛点,并引入更现代的通信能力。为了让你快速了解版本间的核心差异,这里有一个简要的对比表格:
| 对比维度 | Nacos 1.x | Nacos 2.0 |
|---|---|---|
| 核心连接模型 | HTTP 短连接 + UDP | gRPC 长连接 |
| 服务发现/配置推送 | UDP(不可靠,需对账查询)或 HTTP 长轮询 | gRPC 双向流推送 |
| 连接开销 | 高(每次请求建立断开,TIME_WAIT多) | 低(单一长连接,多路复用) |
| 资源消耗 | 高(无效查询多,频繁GC) | 大幅降低 |
| 性能与延迟 | 相对较低,推送延迟秒级 | 性能提升数倍至十倍,推送延迟毫秒级 |
| 数据可靠性 | UDP推送可能丢失 | 可靠传输 |
Nacos 2.0 采用 gRPC 主要为了解决 1.x 版本通信模型的几个关键问题:
gRPC 基于 HTTP/2,为 Nacos 2.0 带来了显著改进:
单一长连接与多路复用:gRPC 在客户端和服务端间建立单一长连接。该连接支持 多路复用,允许同时处理多个请求,极大减少了连接管理开销,解决了 HTTP 短连接的 TIME_WAIT 问题。
双向流与实时推送:gRPC 支持双向流通信。配置变更或服务列表更新时,服务端可通过已建立的gRPC连接主动、实时推送给客户端,取代了不可靠的UDP推送和低效的HTTP长轮询。这使得配置推送延迟从秒级降至毫秒级。
更高的性能与更低延迟:
增强的可靠性:gRPC 建立在 HTTP/2 之上,提供了可靠的、有状态的连接,内置流量控制和错误处理机制。同时,gRPC 支持 TLS 加密,增强了数据传输的安全性
Nacos 2.0 转向 gRPC 是一次为解决 1.x 版本核心瓶颈的架构升级。它通过长连接替代短连接,可靠的双向流推送替代不可靠的UDP和低效的轮询,显著降低了资源消耗,提升了系统吞吐能力和实时性,为大规模微服务场景提供了更稳固的支撑。
提示
掌握 gRPC 协议与 UDP 协议
📡 服务端的长轮询与变更感知
在 Nacos 中,客户端与服务端保持一个 长轮询连接 来感知配置变更。 这就像是客户端在问服务端:“我关注的配置有变化吗?”如果此时没有变化,服务端不会立即回应,而是将这个请求“挂起”一段时间。 在此期间,一旦配置发生修改,服务端就能立刻发现并响应这个还在等待的连接,告知客户端:“有配置更新了。”
这种机制相比传统的频繁短轮询(频繁地问),能有效减少不必要的网络请求,提升效率,并能较快地感知到变更。
java # 源码位置在 ClientWorker 类中,以下为每5秒对阻塞队列出栈一次,然后执行配置监听 private final BlockingQueue<Object> listenExecutebell = new ArrayBlockingQueue<>(1); @Override public void startInternal() { // 定时任务 executor.schedule(() -> { // 线程池没有关闭 while (!executor.isShutdown() && !executor.isTerminated()) { try { listenExecutebell.poll(5L, TimeUnit.SECONDS); if (executor.isShutdown() || executor.isTerminated()) { continue; } // 执行配置监听 executeConfigListen(); } catch (Throwable e) { LOGGER.error("[ rpc listen execute ] [rpc listen] exception", e); } } }, 0L, TimeUnit.MILLISECONDS); }
Nacos 服务端判断配置是否变更,主要依赖 MD5 校验机制。 每次配置更新,服务端都会计算新配置的 MD5 值。只有当 MD5 值发生变化时,才会触发后续的更新流程。 这确保了只有真正的内容变更才会通知客户端,避免了不必要的刷新。
java # 源码位置在 ConfigChangeBatchListenRequestHandler 类中,接收客户端的配置监听请求,通过下列 isUptodate 方法中对比客户端缓存中的 MD5 与服务端 MD5 是否匹配 @Override @TpsControl(pointName = "ConfigListen") @Secured(action = ActionTypes.READ, signType = SignType.CONFIG) public ConfigChangeBatchListenResponse handle(ConfigBatchListenRequest configChangeListenRequest, RequestMeta meta) throws NacosException { // 获取连接ID,使用StringPool优化字符串存储 String connectionId = StringPool.get(meta.getConnectionId()); // 获取请求头中的VIPSERVER标签 String tag = configChangeListenRequest.getHeader(Constants.VIPSERVER_TAG); // 创建响应对象 ConfigChangeBatchListenResponse configChangeBatchListenResponse = new ConfigChangeBatchListenResponse(); // 遍历所有配置监听上下文 for (ConfigBatchListenRequest.ConfigListenContext listenContext : configChangeListenRequest .getConfigListenContexts()) { // 构造groupKey,用于唯一标识一个配置项 String groupKey = GroupKey2 .getKey(listenContext.getDataId(), listenContext.getGroup(), listenContext.getTenant()); groupKey = StringPool.get(groupKey); // 获取配置内容的MD5值,用于后续比较配置是否发生变化 String md5 = StringPool.get(listenContext.getMd5()); // 判断是添加监听还是移除监听 if (configChangeListenRequest.isListen()) { // 添加监听:将groupKey、md5和connectionId关联起来 configChangeListenContext.addListen(groupKey, md5, connectionId); // 检查配置是否为最新状态,如果不是最新则需要通知客户端更新 boolean isUptoDate = ConfigCacheService.isUptodate(groupKey, md5, meta.getClientIp(), tag); if (!isUptoDate) { // 配置已变更,添加到变更列表中返回给客户端 configChangeBatchListenResponse.addChangeConfig(listenContext.getDataId(), listenContext.getGroup(), listenContext.getTenant()); } } else { // 移除监听:从监听上下文中删除对应的监听关系 configChangeListenContext.removeListen(groupKey, connectionId); } } // 返回处理结果 return configChangeBatchListenResponse; }
📱 客户端的处理与配置应用
客户端在收到服务端的配置变更通知后,并不会立即盲目应用。它会重新拉取最新的配置内容,并计算其 MD5 值,与本地缓存的旧配置 MD5 值进行比对。 这可以看作是一次客户端的“二次校验”,确保需要处理的确实是发生了变化的配置。
java 在上述执行配置监听 executeConfigListen() 的方法中,有下列一段代码,调用 refreshContentAndCheck 方法查询最新配置进行 MD5 检查 @Override public void executeConfigListen() { // 获取指定taskId的RPC客户端 RpcClient rpcClient = ensureRpcClient(taskId); // 向服务器发送批量监听请求 ConfigChangeBatchListenResponse configChangeBatchListenResponse = (ConfigChangeBatchListenResponse) requestProxy( rpcClient, configChangeListenRequest); // 如果服务器响应成功 if (configChangeBatchListenResponse.isSuccess()) { // 创建变更键集合,存储发生变更的配置key Set<String> changeKeys = new HashSet<>(); // 处理发生变更的配置项 if (!CollectionUtils.isEmpty(configChangeBatchListenResponse.getChangedConfigs())) { // 标记存在变更的配置 hasChangedKeys = true; // 遍历所有发生变更的配置 for (ConfigChangeBatchListenResponse.ConfigContext changeConfig : configChangeBatchListenResponse .getChangedConfigs()) { // 构造变更配置的唯一标识key String changeKey = GroupKey .getKeyTenant(changeConfig.getDataId(), changeConfig.getGroup(), changeConfig.getTenant()); // 将变更key加入集合 changeKeys.add(changeKey); // 检查该配置是否处于初始化状态 boolean isInitializing = cacheMap.get().get(changeKey).isInitializing(); // 刷新变更配置的内容并检查MD5 refreshContentAndCheck(changeKey, !isInitializing); } } } } private void refreshContentAndCheck(CacheData cacheData, boolean notify) { try { // 获取服务端配置 ConfigResponse response = getServerConfig(cacheData.dataId, cacheData.group, cacheData.tenant, 3000L, notify); cacheData.setEncryptedDataKey(response.getEncryptedDataKey()); cacheData.setContent(response.getContent()); if (null != response.getConfigType()) { cacheData.setType(response.getConfigType()); } if (notify) { LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}", agent.getName(), cacheData.dataId, cacheData.group, cacheData.tenant, cacheData.getMd5(), ContentUtils.truncateContent(response.getContent()), response.getConfigType()); } // 检查监听器MD5 cacheData.checkListenerMd5(); } catch (Exception e) { LOGGER.error("refresh content and check md5 fail ,dataId={},group={},tenant={} ", cacheData.dataId, cacheData.group, cacheData.tenant, e); } }
MD5 校验通过后,客户端会发布一个 RefreshEvent 事件。 在 Spring Cloud 环境中,这个事件会被 RefreshEventListener 捕获,继而触发 Spring Cloud 自身的配置刷新机制。
获取变更后的配置信息
在 RefreshEventListener 监听器中,会提取本地缓存中的 Environment 环境信息进行变更对比,将本地环境中发生变更的属性,通过发布 EnvironmentChangeEvent 事件从而对 @RefreshScope 注解的 Bean 实例进行重新绑定与初始化。
对于 Spring 应用,配置的动态更新主要借助 @RefreshScope 注解。 被它标记的 Bean(比如使用了 @Value 注入配置的类),在配置刷新事件触发后,会被特殊处理:Spring 容器会销毁这些 Bean 的实例,当下次请求到来时,再重新创建。在新实例的创建过程中,@Value 等注解会重新解析,从而注入最新的配置值。 这就实现了应用级别的热更新,无需重启服务。
java // 在 ConfigurationPropertiesRebinder 中监听到 `EnvironmentChangeEvent` 事件,开始处理重新绑定 @ManagedOperation public boolean rebind(String name) { // 检查要重新绑定的bean是否在配置属性bean列表中 if (!this.beans.getBeanNames().contains(name)) { return false; } // 确保应用上下文不为空 if (this.applicationContext != null) { try { // 从应用上下文中获取指定名称的bean实例 Object bean = this.applicationContext.getBean(name); // 如果该bean是AOP代理对象,则获取其原始目标对象 if (AopUtils.isAopProxy(bean)) { bean = ProxyUtils.getTargetObject(bean); } // 确保bean对象不为null if (bean != null) { // TODO: determine a more general approach to fix this. // see https://github.com/spring-cloud/spring-cloud-commons/issues/571 // 检查该bean类型是否在永不刷新列表中,如果是则跳过刷新 if (getNeverRefreshable().contains(bean.getClass().getName())) { return false; // ignore } // 销毁该bean实例,释放相关资源 this.applicationContext.getAutowireCapableBeanFactory().destroyBean(bean); // 重新初始化该bean实例,应用最新的配置属性,也就是重新填充数据 this.applicationContext.getAutowireCapableBeanFactory().initializeBean(bean, name); // 重新绑定成功,返回true return true; } } // 捕获运行时异常,记录错误并重新抛出 catch (RuntimeException e) { this.errors.put(name, e); throw e; } // 捕获其他异常,记录错误并封装为IllegalStateException抛出 catch (Exception e) { this.errors.put(name, e); throw new IllegalStateException("Cannot rebind to " + name, e); } } // 应用上下文为空或其他条件不满足时,返回false表示重新绑定失败 return false; }
🛡️ 确保可靠性与一致性
任何系统都不能保证百分之百无故障,Nacos 的配置刷新机制也考虑了这一点。
java // 执行配置监听 `executeConfigListen` 方法中有下面这么一段,如果5分钟都没有从服务端拉取过配置,则进行一次全量同步 @Override public void executeConfigListen() { // ... // 获取当前时间戳,用于判断是否需要进行全量同步 long now = System.currentTimeMillis(); // 判断是否需要全量同步:距离上次全量同步时间超过设定间隔(5分钟) boolean needAllSync = now - lastAllSyncTime >= ALL_SYNC_INTERNAL; // ... }
⚠️ 注意事项
@RefreshScope:需要动态刷生的 Bean 应使用 @RefreshScope 注解。 注意,刷新时这些 Bean 会被重建,考虑其状态和性能影响。Nacos 采用多租户隔离机制,通过其命名空间和作用域的设计,为微服务架构下的配置管理和服务发现提供了清晰的多环境隔离与逻辑管理能力。
其核心设计思想可以概括为:命名空间(Namespace)实现了顶层的环境或租户强制隔离,而分组(Group)则在命名空间内部提供了灵活的逻辑分组能力。
| 概念维度 | 层级 | 核心作用 | 类比 |
|---|---|---|---|
| 命名空间 (Namespace) | 最高层 | 强隔离:隔离不同环境(如开发、生产)或不同租户的数据。 | 公司里的独立办公室,彼此完全隔离。 |
| 分组 (Group) | 命名空间内 | 逻辑区分:在同一个环境中,对服务或配置进行业务分类(如按应用、配置类型)。 | 办公室里的不同文件柜,用于分类存放资料。 |
| 服务 (Service)/配置集 (Config) | 分组内 | 实体单元:具体的服务实体或配置文件。 | 文件柜里的具体文件。 |
| 集群 (Cluster) | 服务下 | 物理/逻辑划分:对服务实例的虚拟划分,常用于容灾或流量调度。 | 无直接类比,可理解为文件的不同副本或版本。 |
应用场景
基于上述设计,Nacos的命名空间和分组能有效支撑以下场景:
本文作者:张豪
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!