概述
服务注册进 Nacos 之后,一个服务往往有多个实例,这些实例如果分散在全国不同的机房,调用方该怎么选?Nacos 在「服务 → 实例」之间又插了一层「集群」,就是为了让调用尽量落在同机房。
纲要
- 三级结构:服务(service)/ 集群(cluster)/ 实例(instance)
- 为什么需要集群:跨机房调用延迟高,同机房优先才能压住响应时间
- 给实例配置集群:
spring.cloud.nacos.discovery.cluster-name - 验证:Nacos 控制台里的集群分组与实例分布
- 集群划分的实践含义:集群名怎么取、什么时候分、什么时候不用配
- 与 namespace 的区别(环境维度 vs 机房维度)
- 本篇只解决「集群是前提」,同机房优先要靠 NacosRule ------ 留给下一篇
- 常见坑:消费者没配集群、大小写不一致、指望跨机房零延迟、改完不重启
三级结构:服务 → 集群 → 实例
先回忆一下已经用过的两层结构。userservice 提供用户查询,orderservice 提供订单查询,它们都是服务 ;userservice 部署了 8081、8082、8083 三个进程,三个都是 userservice 的实例。所以一开始是两层:一个服务包含多个实例。
单机房部署的问题在于风险集中。所有实例都放在一个机房里,机房一旦出问题(断电、断网、甚至更严重的灾害),整个服务就全挂了。解决办法是把同一个服务的多个实例分散到不同机房 ------ 阿里、京东这类业务就是这么干的,上海、杭州、北京各放一批。这就是容灾。
但分散之后带来一个新问题:杭州的 orderservice 要调 userservice,它可以调杭州本地的实例,也可以调北京的实例,该怎么选?
Nacos 的答案是在「服务」和「实例」之间加一层「机房/地域」的划分,把同一机房内的多个实例称为一个集群。于是分级模型变成三级:
#mermaid-svg-6H7th7dxui05Qu2k{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6H7th7dxui05Qu2k .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6H7th7dxui05Qu2k .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6H7th7dxui05Qu2k .error-icon{fill:#552222;}#mermaid-svg-6H7th7dxui05Qu2k .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6H7th7dxui05Qu2k .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6H7th7dxui05Qu2k .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6H7th7dxui05Qu2k .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6H7th7dxui05Qu2k .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6H7th7dxui05Qu2k .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6H7th7dxui05Qu2k .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6H7th7dxui05Qu2k .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6H7th7dxui05Qu2k .marker.cross{stroke:#333333;}#mermaid-svg-6H7th7dxui05Qu2k svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6H7th7dxui05Qu2k p{margin:0;}#mermaid-svg-6H7th7dxui05Qu2k .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6H7th7dxui05Qu2k .cluster-label text{fill:#333;}#mermaid-svg-6H7th7dxui05Qu2k .cluster-label span{color:#333;}#mermaid-svg-6H7th7dxui05Qu2k .cluster-label span p{background-color:transparent;}#mermaid-svg-6H7th7dxui05Qu2k .label text,#mermaid-svg-6H7th7dxui05Qu2k span{fill:#333;color:#333;}#mermaid-svg-6H7th7dxui05Qu2k .node rect,#mermaid-svg-6H7th7dxui05Qu2k .node circle,#mermaid-svg-6H7th7dxui05Qu2k .node ellipse,#mermaid-svg-6H7th7dxui05Qu2k .node polygon,#mermaid-svg-6H7th7dxui05Qu2k .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6H7th7dxui05Qu2k .rough-node .label text,#mermaid-svg-6H7th7dxui05Qu2k .node .label text,#mermaid-svg-6H7th7dxui05Qu2k .image-shape .label,#mermaid-svg-6H7th7dxui05Qu2k .icon-shape .label{text-anchor:middle;}#mermaid-svg-6H7th7dxui05Qu2k .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6H7th7dxui05Qu2k .rough-node .label,#mermaid-svg-6H7th7dxui05Qu2k .node .label,#mermaid-svg-6H7th7dxui05Qu2k .image-shape .label,#mermaid-svg-6H7th7dxui05Qu2k .icon-shape .label{text-align:center;}#mermaid-svg-6H7th7dxui05Qu2k .node.clickable{cursor:pointer;}#mermaid-svg-6H7th7dxui05Qu2k .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6H7th7dxui05Qu2k .arrowheadPath{fill:#333333;}#mermaid-svg-6H7th7dxui05Qu2k .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6H7th7dxui05Qu2k .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6H7th7dxui05Qu2k .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6H7th7dxui05Qu2k .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6H7th7dxui05Qu2k .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6H7th7dxui05Qu2k .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6H7th7dxui05Qu2k .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6H7th7dxui05Qu2k .cluster text{fill:#333;}#mermaid-svg-6H7th7dxui05Qu2k .cluster span{color:#333;}#mermaid-svg-6H7th7dxui05Qu2k div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6H7th7dxui05Qu2k .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6H7th7dxui05Qu2k rect.text{fill:none;stroke-width:0;}#mermaid-svg-6H7th7dxui05Qu2k .icon-shape,#mermaid-svg-6H7th7dxui05Qu2k .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6H7th7dxui05Qu2k .icon-shape p,#mermaid-svg-6H7th7dxui05Qu2k .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6H7th7dxui05Qu2k .icon-shape .label rect,#mermaid-svg-6H7th7dxui05Qu2k .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6H7th7dxui05Qu2k .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6H7th7dxui05Qu2k .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6H7th7dxui05Qu2k :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 服务 userservice
集群 HZ(杭州机房)
集群 SH(上海机房)
实例 127.0.0.1:8081
实例 127.0.0.1:8082
实例 127.0.0.1:8083
三级各自代表什么,别混:
| 层级 | 对应实体 | 说明 |
|---|---|---|
| 服务 service | 同一个应用 | userservice,由 spring.application.name 决定 |
| 集群 cluster | 地理 / 机房维度的划分 | HZ、SH,同一机房内的实例归为一组 |
| 实例 instance | 一个具体进程 | IP:port,真正的网络终点 |
为什么要引入集群
这不是为了概念好看。回到那个问题:杭州机房的 orderservice 要访问 userservice,本地机房有实例,外地机房也有实例,选哪个?
答案是本地优先。原因很直接:同机房的访问走的是局域网,距离短、链路少,延迟通常在毫秒级以内;一旦跨集群,比如从杭州调到广州或北京,物理距离几百上千公里,往返延迟会高出一两个数量级。所以服务调用应该尽可能落在本地集群,只有本地集群不可用时才退到其他集群。
Nacos 引入集群概念,本质就是为了减少跨集群调用的比例,而不是消灭跨集群调用。
#mermaid-svg-GtVxSHGTMUUb2R59{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GtVxSHGTMUUb2R59 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GtVxSHGTMUUb2R59 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GtVxSHGTMUUb2R59 .error-icon{fill:#552222;}#mermaid-svg-GtVxSHGTMUUb2R59 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GtVxSHGTMUUb2R59 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GtVxSHGTMUUb2R59 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GtVxSHGTMUUb2R59 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GtVxSHGTMUUb2R59 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GtVxSHGTMUUb2R59 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GtVxSHGTMUUb2R59 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GtVxSHGTMUUb2R59 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GtVxSHGTMUUb2R59 .marker.cross{stroke:#333333;}#mermaid-svg-GtVxSHGTMUUb2R59 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GtVxSHGTMUUb2R59 p{margin:0;}#mermaid-svg-GtVxSHGTMUUb2R59 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GtVxSHGTMUUb2R59 .cluster-label text{fill:#333;}#mermaid-svg-GtVxSHGTMUUb2R59 .cluster-label span{color:#333;}#mermaid-svg-GtVxSHGTMUUb2R59 .cluster-label span p{background-color:transparent;}#mermaid-svg-GtVxSHGTMUUb2R59 .label text,#mermaid-svg-GtVxSHGTMUUb2R59 span{fill:#333;color:#333;}#mermaid-svg-GtVxSHGTMUUb2R59 .node rect,#mermaid-svg-GtVxSHGTMUUb2R59 .node circle,#mermaid-svg-GtVxSHGTMUUb2R59 .node ellipse,#mermaid-svg-GtVxSHGTMUUb2R59 .node polygon,#mermaid-svg-GtVxSHGTMUUb2R59 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GtVxSHGTMUUb2R59 .rough-node .label text,#mermaid-svg-GtVxSHGTMUUb2R59 .node .label text,#mermaid-svg-GtVxSHGTMUUb2R59 .image-shape .label,#mermaid-svg-GtVxSHGTMUUb2R59 .icon-shape .label{text-anchor:middle;}#mermaid-svg-GtVxSHGTMUUb2R59 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GtVxSHGTMUUb2R59 .rough-node .label,#mermaid-svg-GtVxSHGTMUUb2R59 .node .label,#mermaid-svg-GtVxSHGTMUUb2R59 .image-shape .label,#mermaid-svg-GtVxSHGTMUUb2R59 .icon-shape .label{text-align:center;}#mermaid-svg-GtVxSHGTMUUb2R59 .node.clickable{cursor:pointer;}#mermaid-svg-GtVxSHGTMUUb2R59 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GtVxSHGTMUUb2R59 .arrowheadPath{fill:#333333;}#mermaid-svg-GtVxSHGTMUUb2R59 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GtVxSHGTMUUb2R59 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GtVxSHGTMUUb2R59 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GtVxSHGTMUUb2R59 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GtVxSHGTMUUb2R59 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GtVxSHGTMUUb2R59 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GtVxSHGTMUUb2R59 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GtVxSHGTMUUb2R59 .cluster text{fill:#333;}#mermaid-svg-GtVxSHGTMUUb2R59 .cluster span{color:#333;}#mermaid-svg-GtVxSHGTMUUb2R59 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GtVxSHGTMUUb2R59 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GtVxSHGTMUUb2R59 rect.text{fill:none;stroke-width:0;}#mermaid-svg-GtVxSHGTMUUb2R59 .icon-shape,#mermaid-svg-GtVxSHGTMUUb2R59 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GtVxSHGTMUUb2R59 .icon-shape p,#mermaid-svg-GtVxSHGTMUUb2R59 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GtVxSHGTMUUb2R59 .icon-shape .label rect,#mermaid-svg-GtVxSHGTMUUb2R59 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GtVxSHGTMUUb2R59 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GtVxSHGTMUUb2R59 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GtVxSHGTMUUb2R59 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 优先
本地不可用时才走
orderservice(杭州集群)
userservice HZ 实例
userservice SH 实例
还要注意一点:现在工程里的实例,集群名默认是 default。在 Nacos 控制台点进服务详情就能看到 ------ 集群名称 DEFAULT,也就是没有划分集群。下面来给它配上。
给实例配置集群
修改 user-service 的 application.yml,在 spring.cloud.nacos 下加 discovery.cluster-name:
yaml
server:
port: 8081
spring:
application:
name: userservice
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称,HZ 表示杭州
cluster-name 的值是自定义 的,HZ 只是「杭州」的缩写,你写 hangzhou、hz-1、room-a 都一样,Nacos 不解释它的语义,只按字符串分组。
启动两个 user-service 实例,它们就都归到了 HZ 集群。如果要再起一个属于上海集群的实例,有两种写法:
- 改一份
application.yml把cluster-name写成SH再启动; - 不动配置文件,用启动参数覆盖:
bash
java -jar user-service.jar -Dserver.port=8083 -Dspring.cloud.nacos.discovery.cluster-name=SH
用 IDEA 调试时,是在 Run Configuration 的 VM options 里填:
text
-Dserver.port=8083 -Dspring.cloud.nacos.discovery.cluster-name=SH
这里有个坑:用 IDEA 启动多个实例时,别在同一个实例上反复重启改集群名 。比如你已经在 8081 上跑了一个实例,把它改回 SH 再重启,那这个端口上的实例就从 HZ 跑到 SH 去了,原来的 HZ 实例没了。要多机房模拟,就多复制几份启动配置,各自固定集群名。
对应到 order-service,写法一样,只是值不同:
yaml
server:
port: 8088
spring:
application:
name: orderservice
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: SH # 订单服务部署在上海机房
上文的配置位置以讲义为准。本工程终态代码里,
user-service的 Nacos 连接信息放在bootstrap.yml(spring.application.name、spring.profiles.active、spring.cloud.nacos.config),集群属性加在application.yml的spring.cloud.nacos.discovery下即可,两者最终都会合并进环境。终态仓库里这段cluster-name目前是注释状态:
yaml
# discovery:
# cluster-name: HZ
它被留在 user-service/src/main/resources/application.yml 里,只是因为仓库快照没有开放这段演示配置,实际演示时按上面取消注释即可。
验证:在 Nacos 控制台看集群分组
实例启动后,打开 Nacos 控制台(默认 http://localhost:8848/nacos),走一遍:
- 左侧菜单进入 服务管理 → 服务列表;
- 找到
userservice,列表里会显示「集群数目」这一列; - 点服务名进详情,看到按集群分组的实例分布。
预期看到的内容大致是这样:
text
服务名: userservice
集群数目: 2
集群: HZ
- 127.0.0.1:8081 (临时实例)
- 127.0.0.1:8082 (临时实例)
集群: SH
- 127.0.0.1:8083 (临时实例)
到这一步,实例的集群属性就配好了。服务列表里的「集群数目」从 1 变成 2,就是配置生效的直接证据。
集群划分的实践含义
cluster-name 一旦配下去,工程里就多了一个维度,几个实际问题得想清楚。
集群名怎么取? 没有硬性规范,机房、可用区(AZ)、区域名都行,业务自定义。常见做法是用城市/机房缩写(HZ、SH)或物理机房代号(room-a)。关键是同一个机房的实例必须写完全相同的字符串。
什么时候需要划分? 只有实例跨机房、跨可用区、跨区域 部署时才需要。单体机房部署时完全可以不配,Nacos 会把实例都算到 DEFAULT 集群里,功能不受影响。
划分带来什么? 它是后续「同机房优先」的前提条件 。不配集群,所有实例都在一个默认集群里,也就无所谓同机房优先了。至于「怎么让消费者优先调同机房」,那不是本节的配置能完成的 ------ 默认的 ZoneAvoidanceRule 并不会按同集群优先挑实例,需要把负载均衡规则换成 NacosRule。所以这里只要记住:集群划分是先决条件,策略配置在下一篇。
和 namespace(环境隔离)是一回事吗? 不是。这两个概念层级完全不同,初学者最容易把它们混在一起。
| 维度 | cluster(集群) | namespace(命名空间) |
|---|---|---|
| 解决的问题 | 同一环境内,机房/可用区维度的就近调用 | 多环境之间的资源隔离(dev / test / prod) |
| 划分依据 | 物理位置(机房、区域) | 环境(开发、测试、生产) |
| 配置项 | spring.cloud.nacos.discovery.cluster-name |
spring.cloud.nacos.discovery.namespace |
| 是否隔离资源 | 否,同一命名空间内互相可见 | 是,不同命名空间的服务默认互相看不见 |
| 典型值 | HZ、SH、room-a |
namespace 的 ID(UUID) |
一句话:cluster 是同一环境内的机房维度,namespace 是环境维度 。生产环境里的 userservice 和测试环境里的 userservice 应该用 namespace 隔开;而生产环境内部的杭州、上海机房之间,才用 cluster 区分。层级不同,别混用。
常见坑
- 只给提供者配了
cluster-name。userservice配了 HZ/SH,order-service没配(还在DEFAULT),那么「同机房优先」根本无从谈起 ------ 消费端自己都不在任何集群里,拿什么跟别人比。要玩同机房优先,消费者和提供者两边都得配。 - 集群名大小写不一致 。
HZ和hz在 Nacos 看来是两个不同的集群,会被拆成两组,永远命中不了对方。约定好命名后全工程统一。 - 跨机房却期望零延迟。集群分组只降低「跨机房调用的比例」,不改变跨机房调用本身的网络开销。真出现本地集群不可用、流量全打到外地机房的情况,延迟该高还是高。
- 实例分错集群,改完不重启 。
cluster-name是启动时注册到 Nacos 的,改完必须重启服务才会以新集群重新注册。热改配置文件不生效。
API / 配置速览
| 项 | 内容 |
|---|---|
| 配置项 | spring.cloud.nacos.discovery.cluster-name |
| 默认值 | DEFAULT(不配时所有实例同属一个默认集群) |
| 取值 | 自定义字符串,同机房实例必须一致 |
| 覆盖方式 | 配置文件写死,或 VM 参数 -Dspring.cloud.nacos.discovery.cluster-name=SH |
| 生效时机 | 服务启动注册时,改后需重启 |
| 验证入口 | Nacos 控制台 → 服务管理 → 服务列表 → 详情(按集群分组) |
| 下游依赖 | 同机房优先需切换到 com.alibaba.cloud.nacos.ribbon.NacosRule |
官方文档
总结
多级存储模型把原来的「服务 → 实例」两层扩成「服务 → 集群 → 实例」三层,多的这一层代表机房/地域。它要解决的是微服务跨机房部署后,调用方在本地机房和外地机房之间怎么选的问题 ------ 本地优先,因为局域网延迟远低于跨机房。
配置本身只有一行:spring.cloud.nacos.discovery.cluster-name。真正需要记住的是三件事:集群名是纯字符串分组,大小写必须统一;单体机房可以不配,跨机房才需要;集群划分只是前提,把「同一集群优先」真正落到调用链上,还得靠 NacosRule。另外别把 cluster 和 namespace 搞混,前者是环境内的机房维度,后者是环境之间的隔离维度。