Chapter 01

核心概念与数据模型

起点页给了你那条脊柱:ephemeral 字段决定一条数据走 AP 还是 CP。 这一章把脊柱两端的名词逐个建立起来——先有准确的词汇表,后面三章的原理才挂得上去。

本章你将建立的 schema

  • 注册中心到底替你解决了哪个"手动会很痛"的问题,以及它自己因此背上了什么新问题。
  • Nacos 的五级数据模型:命名空间 → 分组 → 服务 → 集群 → 实例,以及"唯一定位一个服务"需要几个坐标。
  • 临时实例与持久实例的区别——以及为什么这个布尔字段是整份教程的分流开关。
  • 健康检查的两种模型(客户端心跳 vs 服务端探活),和 5s / 15s / 30s 这三个数字的含义。
  • 配置中心的三级定位:namespace + group + dataId,以及 dataId 的命名构成。

1.1注册中心解决什么问题

注册中心是一本"实时电话簿":服务实例启动时登记自己的地址,消费方按服务名查到当前可用的地址列表。

为什么需要它

没有注册中心时,order-service 要调 user-service,得把后者的 IP:Port 写进配置。一旦 user-service 扩容、重启换 IP、或某个实例宕机,调用方的配置就过期了——你要么手动改配置重启,要么自己搭一套脚本去探活和下发。注册中心把"谁还活着、地址是多少"这件事集中管理:实例自己上报,消费方实时查询。

关键的一步深入:注册中心自己也是一个分布式系统(生产环境是集群)。于是它接手了"维护电话簿"的同时,也继承了一个新问题——这本电话簿在多个节点之间如何保持一致。电话簿写得快还是读得准、网络分区时还能不能继续登记,这些取舍就是后面 AP / CP 的来源。把这句话记住:注册中心解决了调用方的地址问题,却给自己挖了个一致性问题,而 Nacos 对这个问题的回答,就是起点页那条分流线。

类比 · 带边界声明

注册中心像公司前台的"在岗名单":员工到岗打卡(注册),访客问"张三在不在"前台报当前在岗的人(发现)。类比失效之处:真实前台只有一个,而 Nacos 前台是一组互为副本的前台,它们之间还要同步各自记下的名单——这正是单机直觉解释不了、需要一致性协议的地方。

1.2数据模型:五级坐标

Nacos 用 命名空间 → 分组 → 服务 → 集群 → 实例 五层组织一切;唯一定位一个服务需要前三个坐标。

这套层级是后面所有"发现不到""配置读不到"问题的根。先看图,每一层套在上一层里:

命名空间 namespace · dev / test / prod 隔离(用 UUID 引用) 分组 group · 默认 DEFAULT_GROUP 服务 service · order-service 集群 cluster · 默认 DEFAULT,可按机房分(就近访问) 实例 instance 10.0.0.1:8848 实例 instance 10.0.0.2:8848 实例 10.0.0.3:8848
图 1.1五级嵌套:外层是隔离边界,内层是具体进程。 注意:唯一定位一个服务只需要前三层(namespace + group + service);集群和实例是服务内部的细分,不参与"找哪个服务"。
表 1.1 · 五级各自的职责
层级作用默认值 / 关键点
命名空间 namespace环境 / 租户隔离,跨命名空间完全互不可见public;代码里用 UUID 引用,不是显示名
分组 group业务维度再分组(同一环境内)DEFAULT_GROUP
服务 service逻辑服务,如 order-service唯一键 = namespace + group + service
集群 cluster服务内按机房 / 可用区分实例,支持就近访问DEFAULT;不参与服务定位
实例 instance一个具体进程 ip:port,带权重、元数据、健康标记有 ephemeral 标记(见 1.3)
比文档深一层

为什么用 namespace 而不是"搭三套 Nacos 隔离 dev/test/prod"?因为一套集群就能逻辑隔离,省机器、省运维。而 namespace 在 API 里必须用 UUID(不是 "dev" 这种名字)——隔离边界靠不可猜的 ID 守住,这也是新手最容易踩的配置错误:填了显示名,结果默默注册进了 public。

想一想

A 服务注册时 namespace=dev,B 服务注册时 namespace=prod,两者服务名都叫 order-service。B 能通过服务名发现 A 吗?

展开答案(先停 10 秒)

不能。namespace 是硬隔离边界,dev 和 prod 下即使服务名、分组完全相同,也互相不可见。这正是 namespace 的设计目的:让同名服务在不同环境下并存而不串台。

推论:当"本地能发现、同事发现不到"时,第一个要核对的就是两边的 namespace(UUID)是否一致。

1.3临时实例 vs 持久实例:分流开关

实例上的 ephemeral 布尔字段,决定它的健康由谁判定、数据存在哪、以及——走 AP 还是 CP。

这是全教程最重要的一个概念。同样是"一个 ip:port",它是临时还是持久,会引出两套完全不同的机制:

表 1.2 · 临时实例 vs 持久实例
维度临时实例 ephemeral=true(默认)持久实例 ephemeral=false
健康判定客户端主动上报心跳,断了即不健康服务端主动探活(TCP/HTTP/MySQL)
存储只在服务端内存持久化到磁盘(可重启恢复)
下线行为超时自动从内存删除探活失败只标记不健康,不自动删除
一致性协议AP · Distro(异步复制)CP · Raft(多数派提交)
典型场景无状态微服务(可随时增减)有固定身份的节点(如数据库、网关)
比文档深一层

这一个字段为什么能牵动一致性协议?因为两类数据的"容错预期"根本不同。一个无状态微服务实例短暂消失,调用方重试别的实例就行,可用性比"列表绝对准确"更重要 → 选 AP。而一个被运维当成固定资产登记的持久节点,宁可写入慢一点也不能在两个 Nacos 节点上看到不一致的状态 → 选 CP。ephemeral 就是把这个容错预期编码成了一个布尔值。

默认是临时实例,这符合微服务的直觉:实例是"用完即弃"的牲畜,不是"精心照料"的宠物,消失即下线最省事,也省掉了服务端逐个探活的开销。Spring Cloud Alibaba 里默认就是临时:

application.yml YAML
spring:
  application:
    name: order-service          # 这就是服务名 service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: 3a8d2f1c-...   # 用 UUID,不是 "dev"
        group: DEFAULT_GROUP
        cluster-name: BJ          # 集群:北京机房,用于就近访问
        ephemeral: true           # 默认即 true;改 false 才是持久实例

1.4健康检查:5s / 15s / 30s

临时实例靠客户端每 5s 一次心跳维持;服务端 15s 没收到就标记不健康,30s 没收到就从内存删除。

承接 1.3:临时实例的"健康"是客户端推出来的。它的生命周期由三个时间点切分——这三个数字是面试高频,也是"实例抖动"故障的根:

时间 正常运行 · 每 5s 一次心跳 最后心跳 t0 t0 + 15s 标记 unhealthy t0 + 30s 从内存删除
图 1.2临时实例从"最后一次心跳"开始倒计时。 注意:unhealthy(15s)和删除(30s)分两段——中间这 15s 是给短暂网络抖动留的缓冲,抖动恢复就回到健康,只有持续失联才被彻底移除。

持久实例正相反:它的健康由服务端主动探活(向实例发 TCP 连接 / HTTP 请求 / MySQL 探测),失败只把它标成不健康,不会删除。这就是运维常困惑的"持久实例下线后还在列表里"——它不是 bug,是设计:持久实例代表有固定身份的节点,Nacos 不替你决定它该不该消失,要删得你显式调 API。

想一想

一个临时实例所在进程被 kill -9(没有优雅下线、来不及注销),从这一刻起,最多多久之后消费方在 Nacos 上彻底查不到它?

展开答案(先停 10 秒)

从最后一次心跳算起,约 30s 后它从服务端内存删除(15s 标 unhealthy、30s 删除)。但这只是服务端的视角——消费方本地还有一份服务列表缓存,可能要再过一个刷新周期才更新。所以"进程死了,调用方还在打它"的窗口,比 30s 更长。这条客户端缓存的尾巴,是第 2 章和第 4 章失败模式的主角。

1.5配置中心的数据模型:三级定位

一条配置由 namespace + group + dataId 三个坐标唯一定位;dataId 自身还有命名构成。

注册中心管"服务在哪",配置中心管"服务读什么参数"。两者复用同一套 namespace / group 隔离,只是最后一层从"服务"换成了 dataId:

命名空间 namespace 分组 group 配置 ID · dataId 一条配置 内容 + MD5 指纹 唯一定位 把 dataId 放大看 ↓ order-service prefix=应用名 - dev profile . yaml 扩展名 → 完整 dataId
图 1.3配置和服务共用 namespace / group 隔离,最后一层换成 dataId。 注意:dataId 不是随便起的——它由 prefix-profile.扩展名 拼成,分别对应 Spring 的应用名、激活的 profile 和文件格式,错一段就读不到。

这套命名规则在 Spring Cloud Alibaba 里是:${prefix}-${profile}.${file-extension}。prefix 默认取 spring.application.name,profile 是激活的环境,file-extension 默认是 properties(可改 yaml)。三段任意一段对不上,配置就静默读不到——这是第 3 章会展开的高频失败模式。

洞察 · 一套隔离模型复用两次

注意到没有:namespace 和 group 在服务发现和配置中心里是同一套隔离维度。学会一次,两边通用。这也意味着——配错 namespace 既会让你"发现不到服务",也会让你"读不到配置",症状不同,根因同一个。

§本章 self-check

先合上教程,把答案写在纸上或编辑器里。写完再点开对照——直接点开等于把这一节当再读一遍。

  1. 用一句话说明 namespace、group、service 三者如何共同唯一定位一个服务。集群(cluster)参与定位吗?
  2. 临时实例和持久实例,健康分别由谁判定?(一个词概括各自的判定方)
  3. 一个临时实例最后一次心跳后,多少秒被标记 unhealthy、多少秒被删除?这两段为什么要分开?
  4. dataId user-service-prod.yaml 拆成三段分别是什么?各对应 Spring 的哪个配置?
  5. (设计题)为什么 Nacos 默认把实例注册成临时的,而不是持久的?从"无状态微服务的容错预期"角度回答。
答案(先做完再展开)
  1. 唯一键 = namespace + group + service 三者拼起来;三者全相同才是"同一个服务"。集群不参与定位,它是服务内部按机房对实例的细分。
  2. 临时实例:客户端(每 5s 上报心跳);持久实例:服务端(主动探活 TCP/HTTP/MySQL)。
  3. 15s 标记 unhealthy,30s 从内存删除。分两段是为短暂网络抖动留缓冲——抖动恢复回到健康,只有持续失联才彻底移除。
  4. user-service = prefix(默认 = spring.application.name);prod = profile(spring.profiles.active);yaml = file-extension(文件格式)。
  5. 无状态微服务实例是可随时增减的"牲畜",短暂消失时调用方重试别的实例即可,可用性比列表绝对准确更重要 → 适合 AP/临时;同时省去服务端逐个探活的开销。持久实例留给有固定身份的节点。
进阶挑战 · 刚好够不着

"本机能注册、能发现,同事连同一个 Nacos 却发现不到"

你的服务在本机注册成功、控制台能看到、本机也能发现它;但同事在他机器上(配的是同一个 server-addr)就是发现不到,报 "no available server"。 按"唯一定位一个服务需要几个坐标"这条线索,列出你会按顺序排查的隔离维度(至少 3 个),并说明每个为什么会导致"发现不到"。

提示(卡住再展开)

定位一个服务 = namespace + group + service。逐个核对两边是否一致:① namespace 的 UUID 是否相同(最常见:一边填了名字默默落到 public);② group 是否相同(默认 DEFAULT_GROUP,有人改了一边);③ 服务名拼写 / 大小写;④ 是不是其实连到了两个不同的 Nacos(server-addr 看着像,实际指向不同环境)。注意:cluster 不同不影响能否发现,只影响就近策略——这是个容易误判的干扰项。