做 Spring Cloud 开发的同学,对 Nacos 肯定不陌生。配置文件里配好 Nacos 地址,服务启动后就会自动注册上去。但你是否注意到一个细节:当服务进程被停掉或 kill 掉之后,Nacos 控制台上的服务实例会很快消失,而不是变成红色的不健康状态。这与早期使用 Zookeeper 或 Dubbo 时,节点信息会保留的情况不太一样。
为什么 Nacos 默认选择直接删除掉下线的实例呢?
临时 vs 持久
在 Nacos 的设计中,服务实例分为两种类型:临时实例(Ephemeral) 和 持久实例(Persistent)。Spring Cloud Alibaba 默认采用的配置就是临时实例。
两者的核心区别在于心跳维持机制和实例下线后的处理方式。
临时实例(默认)
- 心跳机制:由客户端(即你的微服务)主动向 Nacos Server 发送心跳,默认间隔为5秒。
- 下线处理:如果 Nacos Server 在预设的时间窗口内(例如30秒)未收到心跳,则判定该实例已下线,并直接从服务列表中剔除。
持久实例
- 心跳机制:客户端仅在注册时通知 Nacos Server,之后不再主动发送心跳。由 Nacos Server 端主动探测该实例的IP端口是否可达。
- 下线处理:如果探测失败,Nacos Server 只会将该实例标记为“不健康”,但不会删除其注册信息。这条记录会一直保留在服务列表中。
为什么要默认选临时?
可能有些从传统架构转型过来的开发者会疑惑:保留历史记录不是更方便排查问题吗?在物理机或固定IP的时代,这确实是个好思路。机器重启后,IP不变,记录仍有价值。
然而,随着 Docker 和 K8s 的普及,环境发生了根本性变化。想象一下你的服务部署在 Kubernetes 中的场景:
- 每次应用发布或滚动更新,旧的 Pod 会被销毁,新的 Pod 被创建。
- 关键点在于:新 Pod 的 IP 地址通常是全新的。
如果你为这些动态的微服务应用配置了持久实例,那么每次发布后,Nacos 中就会残留一堆标记为“不健康”的旧 IP。频繁发布几次,服务列表里就可能堆积成百上千个无效地址。这不仅浪费存储空间,更严重的是,当其他服务客户端拉取服务列表时,还需要额外花费资源去过滤这些早已不存在的死节点。
因此,Nacos 默认选择临时实例,正是为了适应云原生环境下“IP 是临时资源”的现实。服务实例下线即清理,为新实例腾出位置,始终保持服务列表的简洁与有效。
深层原因:CAP 的取舍
除了应对IP动态变化,还有一个更底层的架构原因涉及分布式系统的 CAP 理论。
持久实例(CP 模式)
为了实现数据的强一致性和持久化,持久实例在 Nacos 集群内部通常采用 Raft 协议 进行数据同步。Raft 是强一致性协议,这意味着如果集群中的 Leader 节点发生故障,在选举出新 Leader 期间,整个注册中心将处于不可写状态。对于需要频繁注册、下线的微服务应用而言,因网络抖动或短暂故障就导致服务无法注册,这是难以接受的。
临时实例(AP 模式)
临时实例则采用 Nacos 自研的 Distro 协议。它的设计哲学是:高可用性(Availability)优先于强一致性(Consistency)。即使集群内节点间的数据未能实时完全同步,也要确保服务能够成功注册和被发现,保证整个调用链路的基本可用,不因注册中心的内部状态而报错。
什么时候该用持久实例?
那么,持久实例就一无是处了吗?并非如此。它主要适用于那些 IP 地址相对固定、且属于关键基础设施 的服务,例如数据库(MySQL、Redis)。
有时,我们希望将数据库地址也注册到 Nacos 中,供微服务动态发现。对于数据库这类“重资产”,即使它暂时不可用,我们也不希望其注册信息从列表中消失。我们更期待它仅被标记为异常,待故障修复后,同样的 IP 和端口能自动恢复服务,业务无需修改配置。
总结
回到最初的问题:Nacos 服务注册为什么默认是临时实例?
核心原因在于,在现代微服务与云原生架构下,服务的可用性比记住所有历史状态更重要。
- 对于微服务应用:它们是动态的、可随时调度和销毁的资源,应使用临时实例(AP 模式)。实例下线即刻清理,确保服务列表的实时有效性,避免客户端调用到无效节点。
- 对于数据库/基础设施:它们是相对固定的、重要的资源,可以考虑使用持久实例(CP 模式)。保留其注册记录,便于运维监控与故障恢复。
因此,在 Kubernetes 等动态环境中使用 Nacos 时,建议保持默认的临时实例配置。若误开启持久化模式,控制台可能会逐渐堆积大量无法自动清理的无效数据,反而影响服务发现效率。希望这个设计背后的思考,能帮助你在使用类似注册中心时做出更合适的选择。关于微服务架构的更多实践与原理,欢迎在云栈社区交流讨论。