概述
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一样,它们就是同一个服务的不同实例,注册在同一个服务列表里。
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:一个管「能不能看见」,一个管「优先找谁」
这两个概念挨得很近,但解决的是完全不同的问题,先看层级。
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 控制台:
# Nacos 控制台(默认端口 8848,路径 /nacos)http://localhost:8848/nacos- 左侧菜单点命名空间,进来能看到一个名为
public的保留空间——这是 Nacos 默认生成的,所有没显式指定 namespace 的服务都躺在这里。 - 点新建命名空间,表单只需两项必填:命名空间名称(填
dev)和描述(填开发环境)。 - 第三项命名空间 ID可以留空,留空时系统会生成一个 UUID。提交后就能在列表里看到它,把那个 ID 复制下来。
这一步的产物长这样:
命名空间列表 ┌──────────────┬──────────────────────────────────────┬────────────────┐ │ 命名空间名称 │ 命名空间 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:
spring:cloud:nacos:server-addr:nacos:8848# nacos服务地址# discovery:# namespace: 4d6ce343-9e1b-44df-a90f-2cf2b6b3d177 # dev环境# ephemeral: false # 是否是临时实例要用的时候把注释解开,并把 ID 换成你自己环境生成的 UUID:
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,还有独立的一项,两个是并列的:
spring:cloud:nacos:server-addr:localhost:8848discovery: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 注入。
配置文件按环境拆开,主配置只留占位:
# application.yml —— 主配置,不写死任何环境的值spring:profiles:active:${SPRING_PROFILES_ACTIVE:dev}# 由启动参数或环境变量决定application:name:orderservicecloud:nacos:server-addr:${NACOS_ADDR:localhost:8848}discovery:namespace:${nacos.namespace-id}# 占位符,不写死config:namespace:${nacos.namespace-id}file-extension:yaml各环境的 ID 单独存放,互不污染:
# application-dev.ymlnacos:namespace-id:4d6ce343-9e1b-44df-a90f-2cf2b6b3d177# dev# application-prod.ymlnacos:namespace-id:8f2b1c04-77aa-4d1e-9c31-6b0e5a2d9f88# prod(换成自己的)启动时指定即可,不需要动任何配置文件:
# 本地开发,走 dev 空间java-jarorder-service.jar--spring.profiles.active=dev# 需要临时覆盖时,直接传参数,比改配置安全java-jarorder-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,日志里是这行:
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→ 命名空间 |
官方文档
- Nacos 命名空间(官方文档)
- Spring Cloud Alibaba Nacos Discovery
总结
环境隔离解决的是「同一批服务在三套环境下互相串门」这个问题,手段是把它们丢进不同的 namespace。要点收一收:
- 隔离的粒度是 namespace,作用是让不同空间的服务互相不可见,不是“优先避开”,是根本查不到。
- 配置里写的是 namespace 的ID,写名称会静默退到
public,这是最隐蔽的一个坑。 discovery和config是两个独立的开关,服务隔离了、配置没隔离,等于白做。- cluster 和 namespace 别混:namespace 决定能不能看见,cluster 决定优先找谁。
public只放跨环境共享的配置,服务注册信息一律下沉到各自的 namespace。