通常将 Nacos 面试题分为概念类、原理类和实战类,基于Nacos的面试经验,我整理了高频和深度的面试题,并提供一些实现思路的描述。
Nacos是一个动态服务发现、配置管理和服务管理平台。它的核心功能包括:
服务注册与发现:服务提供者注册服务地址,消费者发现服务地址
配置管理:集中化管理应用配置,支持动态刷新
服务健康监测:通过心跳机制检测服务实例健康状态
动态DNS服务:支持权重路由,实现流量调度
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选举:集群中所有节点分为Leader、Follower和Candidate几种角色。所有写请求必须经由Leader节点处理,Leader负责将数据变更复制到多数派Follower节点。
日志复制与多数派确认:写请求到达Leader后,会作为日志条目先复制到多数派Follower节点。一旦得到多数派确认,Leader才会提交该条目并应用至状态机,随后通知客户端写入成功。
这种方式确保了数据的强一致性,但可能在网络分区或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()));
}
}
此逻辑是通知其他订阅的客户端,告知其状态的变更
javapublic 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 版本通信模型的几个关键问题:
HTTP短连接瓶颈:1.x 基于 HTTP 短连接,每次请求都需创建和断开连接,导致大量 TIME_WAIT 状态的连接,消耗大量系统资源。
UDP推送的不可靠性:1.x 使用 UDP 进行服务发现和配置变更推送。但UDP本身无连接、不保证可靠传输,可能丢包。为确保数据最终一致,客户端需定时轮询对账,产生大量无效查询。
配置长轮询的开销:1.x 配置模块使用 HTTP 长轮询模拟长连接,但本质仍是短连接,每30秒一次的请求重建和上下文切换会导致服务端频繁GC
gRPC 基于 HTTP/2,为 Nacos 2.0 带来了显著改进:
单一长连接与多路复用:gRPC 在客户端和服务端间建立单一长连接。该连接支持 多路复用,允许同时处理多个请求,极大减少了连接管理开销,解决了 HTTP 短连接的 TIME_WAIT 问题。
双向流与实时推送:gRPC 支持双向流通信。配置变更或服务列表更新时,服务端可通过已建立的gRPC连接主动、实时推送给客户端,取代了不可靠的UDP推送和低效的HTTP长轮询。这使得配置推送延迟从秒级降至毫秒级。
更高的性能与更低延迟:
得益于HTTP/2的二进制协议和更高效的序列化,数据传输量和解析耗时减少。
官方测试表明,Nacos 2.0 相比 1.x,注册、查询、注销性能总体提升数倍,部分场景甚至可达十倍。
增强的可靠性: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;
// ...
}
最终一致性保障:Nacos 服务端节点之间通过自研的通知协议来同步配置缓存,以确保所有节点数据一致。 此外,服务端与数据库之间、客户端与服务端之间,都有定期的 MD5 对账机制(例如客户端默认每 5 分钟一次), 共同保障数据的最终一致性。
务必通过控制台或 API 修改配置:一个重要原则是,切勿直接修改数据库中的配置。 因为这绕过了 Nacos 服务端的配置管理流程(比如 MD5 计算和变更通知),很可能导致配置不一致或客户端无法接收到更新通知。
⚠️ 注意事项
正确使用 @RefreshScope:需要动态刷生的 Bean 应使用 @RefreshScope 注解。 注意,刷新时这些 Bean 会被重建,考虑其状态和性能影响。
关注客户端日志:若遇配置刷新不生效,可检查客户端日志(如 nacos/config.log)。 关注 add-listener(监听器注册)、data-received(接收数据)、notify-ok(通知成功)或 notify-error(通知出错)等关键字,有助于定位问题。
理解版本差异:Nacos 2.x 相比 1.x,在通信模型(如引入 gRPC)和性能上有显著改进,但其配置动态刷新的核心原理(长轮询 + MD5 校验 + 事件驱动)保持一致。
Nacos 采用多租户隔离机制,通过其命名空间和作用域的设计,为微服务架构下的配置管理和服务发现提供了清晰的多环境隔离与逻辑管理能力。
其核心设计思想可以概括为:命名空间(Namespace)实现了顶层的环境或租户强制隔离,而分组(Group)则在命名空间内部提供了灵活的逻辑分组能力。
命名空间(Namespace):最高级别隔离,常用于环境隔离
分组(Group):服务分组,常用于业务隔离
服务名(ServiceName):具体服务标识
集群(Cluster):物理集群划分
| 概念维度 | 层级 | 核心作用 | 类比 |
|---|---|---|---|
| 命名空间 (Namespace) | 最高层 | 强隔离:隔离不同环境(如开发、生产)或不同租户的数据。 | 公司里的独立办公室,彼此完全隔离。 |
| 分组 (Group) | 命名空间内 | 逻辑区分:在同一个环境中,对服务或配置进行业务分类(如按应用、配置类型)。 | 办公室里的不同文件柜,用于分类存放资料。 |
| 服务 (Service)/配置集 (Config) | 分组内 | 实体单元:具体的服务实体或配置文件。 | 文件柜里的具体文件。 |
| 集群 (Cluster) | 服务下 | 物理/逻辑划分:对服务实例的虚拟划分,常用于容灾或流量调度。 | 无直接类比,可理解为文件的不同副本或版本。 |
应用场景
基于上述设计,Nacos的命名空间和分组能有效支撑以下场景:
多环境配置管理:这是最经典的场景。通过为开发、测试、生产环境建立不同的命名空间,实现配置的天然隔离,避免相互污染。
多租户架构:在SaaS平台中,为每个租户创建独立的命名空间,是实现数据与配置隔离的通用方案。
公共配置共享:可以将数据库连接、消息队列等公共配置放在一个特定的分组(如COMMON_GROUP)中,供同一命名空间下的多个应用共享引用。
灰度发布与流量管控:结合分组概念,可以将服务的新版本实例注册到新的分组,通过Nacos的权重设置或结合路由规则,将部分流量导向新分组,实现灰度发布。
本文作者:柳始恭
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!