概述
orderservice 只是多配了一行 namespace,重启后再访问订单接口,控制台直接抛 no available instance for userservice------三个 userservice 实例明明还活着,却一个都调不到。这就是 Nacos 的环境隔离:不同命名空间的服务,互相看不见。
纲要
- 问题场景:dev / test / prod 挤在同一个空间里,谁在调谁
namespace与cluster的分工:环境维度 vs 机房维度- 控制台创建命名空间,以及「为什么配置里填的是 ID 不是名称」
- 服务侧配置:
spring.cloud.nacos.discovery.namespace - 别只顾着 discovery:配置中心的
spring.cloud.nacos.config.namespace同样要隔离 public空间的定位:什么该放公共,什么必须隔离- 多环境切换的工程化做法:
spring.profiles.active+ 占位符,不手改配置 - 验证隔离是否生效,以及六个高频踩坑点
同一空间下的三套环境,是怎么互相踩的
服务注册进 Nacos 时,如果不做任何额外配置,它会被放进默认的 public 命名空间。问题就出在这里:本地开发的 order-service、测试环境的一整组服务、生产环境的同一批服务,只要 spring.application.name 一样,它们就是同一个服务的不同实例,注册在同一个服务列表里。
text
public
├── userservice
│ ├── 192.168.1.10:8081 ← 开发同学本机
│ ├── 192.168.2.20:8081 ← 测试环境
│ └── 10.0.0.30:8081 ← 生产环境
└── orderservice
├── 192.168.1.11:8088 ← 开发同学本机
└── 10.0.0.31:8088 ← 生产环境
负载均衡器只认服务名,不认「这个实例属于哪个环境」。于是负载一转到另外两个实例上,事情就失控了:
- 开发同学在本地跑
orderservice联调,请求随机落到生产 的userservice上。他以为在改测试数据,实际动的是线上库。 - 测试环境的自动化脚本批量下单,被分发到生产 的
orderservice,测试流量直接写进订单表。 - 灰度验证阶段,新版本
userservice和线上老版本混在一个列表里,一半请求走了未验证的代码。
这些都是事故级别的问题,所以 Nacos 需要一层环境维度上的硬隔离。注意这里说的是「硬」:不是靠约定、靠命名前缀去区分,而是注册中心层面直接让它们互相不可见。
还有一点容易被忽略------Nacos 既是注册中心,也是配置中心。开发环境的 pattern.name: 本地环境local 这种配置如果和生产共用一份,服务隔开了、配置没隔开,照样出错。所以隔离要同时覆盖服务 和配置两块。
namespace 和 cluster:一个管「能不能看见」,一个管「优先找谁」
这两个概念挨得很近,但解决的是完全不同的问题,先看层级。
#mermaid-svg-gs9u6zuoHKAOH17D{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-gs9u6zuoHKAOH17D .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gs9u6zuoHKAOH17D .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gs9u6zuoHKAOH17D .error-icon{fill:#552222;}#mermaid-svg-gs9u6zuoHKAOH17D .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gs9u6zuoHKAOH17D .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gs9u6zuoHKAOH17D .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gs9u6zuoHKAOH17D .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gs9u6zuoHKAOH17D .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gs9u6zuoHKAOH17D .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gs9u6zuoHKAOH17D .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gs9u6zuoHKAOH17D .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gs9u6zuoHKAOH17D .marker.cross{stroke:#333333;}#mermaid-svg-gs9u6zuoHKAOH17D svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gs9u6zuoHKAOH17D p{margin:0;}#mermaid-svg-gs9u6zuoHKAOH17D .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gs9u6zuoHKAOH17D .cluster-label text{fill:#333;}#mermaid-svg-gs9u6zuoHKAOH17D .cluster-label span{color:#333;}#mermaid-svg-gs9u6zuoHKAOH17D .cluster-label span p{background-color:transparent;}#mermaid-svg-gs9u6zuoHKAOH17D .label text,#mermaid-svg-gs9u6zuoHKAOH17D span{fill:#333;color:#333;}#mermaid-svg-gs9u6zuoHKAOH17D .node rect,#mermaid-svg-gs9u6zuoHKAOH17D .node circle,#mermaid-svg-gs9u6zuoHKAOH17D .node ellipse,#mermaid-svg-gs9u6zuoHKAOH17D .node polygon,#mermaid-svg-gs9u6zuoHKAOH17D .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gs9u6zuoHKAOH17D .rough-node .label text,#mermaid-svg-gs9u6zuoHKAOH17D .node .label text,#mermaid-svg-gs9u6zuoHKAOH17D .image-shape .label,#mermaid-svg-gs9u6zuoHKAOH17D .icon-shape .label{text-anchor:middle;}#mermaid-svg-gs9u6zuoHKAOH17D .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gs9u6zuoHKAOH17D .rough-node .label,#mermaid-svg-gs9u6zuoHKAOH17D .node .label,#mermaid-svg-gs9u6zuoHKAOH17D .image-shape .label,#mermaid-svg-gs9u6zuoHKAOH17D .icon-shape .label{text-align:center;}#mermaid-svg-gs9u6zuoHKAOH17D .node.clickable{cursor:pointer;}#mermaid-svg-gs9u6zuoHKAOH17D .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gs9u6zuoHKAOH17D .arrowheadPath{fill:#333333;}#mermaid-svg-gs9u6zuoHKAOH17D .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gs9u6zuoHKAOH17D .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gs9u6zuoHKAOH17D .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gs9u6zuoHKAOH17D .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gs9u6zuoHKAOH17D .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gs9u6zuoHKAOH17D .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gs9u6zuoHKAOH17D .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gs9u6zuoHKAOH17D .cluster text{fill:#333;}#mermaid-svg-gs9u6zuoHKAOH17D .cluster span{color:#333;}#mermaid-svg-gs9u6zuoHKAOH17D 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-gs9u6zuoHKAOH17D .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gs9u6zuoHKAOH17D rect.text{fill:none;stroke-width:0;}#mermaid-svg-gs9u6zuoHKAOH17D .icon-shape,#mermaid-svg-gs9u6zuoHKAOH17D .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gs9u6zuoHKAOH17D .icon-shape p,#mermaid-svg-gs9u6zuoHKAOH17D .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gs9u6zuoHKAOH17D .icon-shape .label rect,#mermaid-svg-gs9u6zuoHKAOH17D .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gs9u6zuoHKAOH17D .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gs9u6zuoHKAOH17D .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gs9u6zuoHKAOH17D :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} namespace: dev
group: DEFAULT_GROUP
namespace: prod
group: DEFAULT_GROUP
服务 userservice
cluster: HZ
cluster: SH
实例 192.168.2.20:8081
实例 192.168.2.21:8081
实例 192.168.2.30:8081
namespace 在最外层。dev 和 prod 是两棵独立的树,服务列表、配置列表全部各管各的,中间没有任何查询路径可以跨过去。cluster 在里面,是同一个 namespace 内的机房划分。
两者对照:
| 维度 | namespace | cluster |
|---|---|---|
| 划分维度 | 环境(dev / test / prod) | 机房 / 地域(HZ / SH / BJ) |
| 作用范围 | 整个 Nacos 数据的最外层 | 同一个 namespace 内部 |
| 隔离效果 | 服务列表、配置列表完全隔离,互相不可见 | 实例之间仍然互相可见 |
| 影响调用的方式 | 看不见 = 调不到,直接报错 | 只影响负载均衡的优先顺序(同集群优先) |
| 配置方式 | spring.cloud.nacos.discovery.namespace: <ID> |
spring.cloud.nacos.discovery.cluster-name: HZ |
| 不配置时的默认值 | public 空间 |
DEFAULT 集群 |
一句话记住区别:namespace 管「能不能看见」,cluster 管「优先找谁」。
userservice 的 HZ 集群挂了,调用方还能退到 SH 集群,因为两个集群在同一个 namespace 里互相可见;但如果 orderservice 在 dev、userservice 在 prod,那就是两个世界,连"退而求其次"的机会都没有,直接 no available instance。
顺带说下 group。它在 namespace 内部做业务分组,比如把订单和支付放一个 group。这一层是可选的 ,日常不配就是 DEFAULT_GROUP,工程里也很少真的去动它。环境隔离要的是 namespace,别把这两者搞反。
创建命名空间
后台操作,三步。地址是 Nacos 控制台:
bash
# Nacos 控制台(默认端口 8848,路径 /nacos)
http://localhost:8848/nacos
- 左侧菜单点 命名空间 ,进来能看到一个名为
public的保留空间------这是 Nacos 默认生成的,所有没显式指定 namespace 的服务都躺在这里。 - 点 新建命名空间 ,表单只需两项必填:命名空间名称 (填
dev)和描述 (填开发环境)。 - 第三项 命名空间 ID 可以留空,留空时系统会生成一个 UUID。提交后就能在列表里看到它,把那个 ID 复制下来。
这一步的产物长这样:
text
命名空间列表
┌──────────────┬──────────────────────────────────────┬────────────────┐
│ 命名空间名称 │ 命名空间 ID │ 描述 │
├──────────────┼──────────────────────────────────────┼────────────────┤
│ public │ (保留空间,空白) │ 保留空间 │
│ dev │ 4d6ce343-9e1b-44df-a90f-2cf2b6b3d177 │ 开发环境 │
└──────────────┴──────────────────────────────────────┴────────────────┘
这里有个绕不过去的坑 :代码里要填的是 namespace 的 ID ,不是名称。填 dev 不会报错,服务能正常启动,但你会发现它仍然注册在 public 里------因为 Nacos 找不到叫 dev 的命名空间,就静默回退到默认空间了。这种错误没有任何日志提示,只能靠去控制台确认。
同理,public 这个名称对应的是空字符串的 ID,不要试图把 public 当 ID 写进配置。
给服务配置 namespace
命名空间只能通过改配置来绑定,控制台上不能拖拽。打开 order-service 的 application.yml,在 spring.cloud.nacos.discovery 下加 namespace。
工程(day01-SpringCloud01/代码/cloud-demo)的终态里,这几行是注释状态 的示例写法,出处就是 order-service/src/main/resources/application.yml:
yaml
spring:
cloud:
nacos:
server-addr: nacos:8848 # nacos服务地址
# discovery:
# namespace: 4d6ce343-9e1b-44df-a90f-2cf2b6b3d177 # dev环境
# ephemeral: false # 是否是临时实例
要用的时候把注释解开,并把 ID 换成你自己环境生成的 UUID:
yaml
spring:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
namespace: 4d6ce343-9e1b-44df-a90f-2cf2b6b3d177 # 命名空间,填ID,不是名称
# cluster-name: HZ # 集群维度另算,和namespace互不影响
# ephemeral: false # 非临时实例,与本章无关,配套示例
改完重启服务。重启是必须的------namespace 在服务注册时生效,改配置文件不会热更新注册信息。重启后回到控制台:public 空间下的 orderservice 会先从列表里消失(或被标记为下线),切到 dev 命名空间才看得到它。
配置中心的 namespace 别漏了
服务隔离只做了 discovery。配置中心如果是 Nacos,还有独立的一项,两个是并列的:
yaml
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
namespace: 4d6ce343-9e1b-44df-a90f-2cf2b6b3d177 # 服务注册/发现的命名空间
config:
namespace: 4d6ce343-9e1b-44df-a90f-2cf2b6b3d177 # 配置拉取的命名空间
file-extension: yaml
只配 discovery 的后果是:服务在 dev 空间互相发现,但配置仍然从 public 拉。你以为改了 dev 的配置,实际服务吃的是公共空间那份------隔离做了一半,问题反而更难排查。两个 namespace 建议成对出现,值保持一致(除非你有意让某些环境共享一份配置,那是另一种设计)。
public 空间:放所有环境共享的东西
隔离不是把所有东西都切开。public 的价值恰恰在于它是所有 namespace 都能读到的地方------任何 namespace 下的服务,只要没显式指定 config 的 namespace,就会落到 public 去拉配置。适合放的东西:
| 配置类型 | 放哪里 | 理由 |
|---|---|---|
| 公共中间件模板(Redis / MQ 连接参数模板) | public |
各环境结构一致,只是值可能不同 |
| 各环境通用的日志格式、线程池默认值 | public |
与环境无关,改一次全员生效 |
数据库地址、pattern.name 这类环境特有的值 |
各自 namespace | 环境间必须不同,放公共必然出错 |
| 服务注册信息 | 各自 namespace | 这是隔离的核心,绝不进 public |
判断标准很简单:这份配置在不同环境里要不要取不同的值 。要,就放各自 namespace;不要,放 public 省事。反过来说,生产环境的服务注册信息如果一不小心留在了 public,那它对 dev、test 全都可见,隔离形同虚设------这也是最常见的翻车方式。
多环境切换的工程化做法
手工改 yml 里的 UUID 迟早会出错:提交代码时把 dev 的 ID 带进生产构建,或者改完一处忘了另一处。工程上更稳的做法是把 namespace ID 抽成外部变量,用 Spring 的 profile 或 Maven profile 注入。
配置文件按环境拆开,主配置只留占位:
yaml
# application.yml ------ 主配置,不写死任何环境的值
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev} # 由启动参数或环境变量决定
application:
name: orderservice
cloud:
nacos:
server-addr: ${NACOS_ADDR:localhost:8848}
discovery:
namespace: ${nacos.namespace-id} # 占位符,不写死
config:
namespace: ${nacos.namespace-id}
file-extension: yaml
各环境的 ID 单独存放,互不污染:
yaml
# application-dev.yml
nacos:
namespace-id: 4d6ce343-9e1b-44df-a90f-2cf2b6b3d177 # dev
yaml
# application-prod.yml
nacos:
namespace-id: 8f2b1c04-77aa-4d1e-9c31-6b0e5a2d9f88 # prod(换成自己的)
启动时指定即可,不需要动任何配置文件:
bash
# 本地开发,走 dev 空间
java -jar order-service.jar --spring.profiles.active=dev
# 需要临时覆盖时,直接传参数,比改配置安全
java -jar order-service.jar --spring.profiles.active=test \
--nacos.namespace-id=8f2b1c04-77aa-4d1e-9c31-6b0e5a2d9f88
容器化部署时把 nacos.namespace-id 对应的值配成环境变量,由 CI/CD 注入,同一个镜像就能部署到任意环境,构建产物和运行环境彻底解耦。
验证隔离是否生效
改完配置、重启服务后,按这三处确认:
控制台的命名空间切换。 服务列表左上角切换命名空间,切到 dev 能看到 orderservice,切回 public 就看不到------说明注册位置对了。
跨空间调用必然失败。 orderservice 在 dev、userservice 还在 public 时,访问订单接口会拿到 500,日志里是这行:
text
java.lang.IllegalStateException: No instances available for userservice
at org.springframework.cloud.loadbalancer...
末尾还常见一句 Load balancer does not have available server for client: userservice。看到这个报错,第一反应不是去查 userservice 挂没挂,而是先确认两边 namespace 是否一致。
报错文本里的实例数量。 如果日志提示"该服务有 N 个实例但都不可用"这类信息,别被误导------跨 namespace 的服务是压根不会被查出来的,调用方眼里它就跟没注册一样。
实战坑
| 坑 | 表现 | 正确做法 |
|---|---|---|
| 配置里填了 namespace 名称 | 服务照样启动,但仍在 public,无任何报错 |
必须填 UUID |
只配 discovery 不配 config |
服务隔开了,配置还是公共那份 | 两个 namespace 一并配 |
| 各环境用了同一个 namespace | 隔离看起来配了,实际毫无效果 | 每个环境一个独立 ID |
| 切换 namespace 后没重启 | 旧实例仍注册在原空间,新空间查不到 | 重启并确认注册成功 |
生产服务误注册在 public |
全环境可见,等于没隔离 | public 只放共享配置,不放服务注册 |
拿 public 当环境名写进配置 |
行为等同于不配 namespace | public 保留空间用空字符串表示 |
API 速览
| 配置项 | 作用 | 取值 |
|---|---|---|
spring.cloud.nacos.discovery.namespace |
服务注册/发现的命名空间 | namespace 的 ID(UUID) |
spring.cloud.nacos.config.namespace |
配置拉取的命名空间 | 同上,通常与 discovery 一致 |
spring.cloud.nacos.discovery.cluster-name |
实例所属集群(机房) | HZ / SH,默认 DEFAULT |
spring.cloud.nacos.discovery.server-addr |
Nacos 服务端地址 | localhost:8848 / nacos:8848 |
| 控制台路径 | 命名空间管理 | http://localhost:8848/nacos → 命名空间 |
官方文档
总结
环境隔离解决的是「同一批服务在三套环境下互相串门」这个问题,手段是把它们丢进不同的 namespace。要点收一收:
- 隔离的粒度是 namespace,作用是让不同空间的服务互相不可见,不是"优先避开",是根本查不到。
- 配置里写的是 namespace 的 ID ,写名称会静默退到
public,这是最隐蔽的一个坑。 discovery和config是两个独立的开关,服务隔离了、配置没隔离,等于白做。- cluster 和 namespace 别混:namespace 决定能不能看见,cluster 决定优先找谁。
public只放跨环境共享的配置,服务注册信息一律下沉到各自的 namespace。