遥望星星
发布于 2020-04-16 / 2379 阅读
29

Nacos 注册中心与配置中心落地实践

改了 Nacos 配置页面,服务怎么没反应

"我把限流阈值从 500 改成 200 了,Nacos 上点了发布,等了五分钟还是没生效。"

这是组内被问过最多的一个问题,我自己也卡过。Nacos 同时做注册中心和配置中心,但这两块的机制完全不同——注册是实时的,配置热更新是有条件的。这篇把我们落地 Nacos 1.2.0 的过程和规划方式整理一下。

为什么从 Eureka 换过来

直接原因是 Eureka 2.0 从 2018 年起就停了,Netflix 全家桶那批组件(Hystrix、Zuul、Ribbon)基本都进入了维护状态。我们评估的时候列了这么几条:

对比项Eureka + Spring Cloud ConfigNacos 1.2.0
一致性模型AP,客户端缓存AP(临时实例)/ CP(持久化实例)可切
健康检测客户端心跳,30 秒周期客户端心跳 5 秒,15 秒标记不健康,30 秒剔除
配置管理Config Server 从 Git 拉,改完要刷 /actuator/bus-refresh自带控制台,改完推送
配置粒度按文件namespace / group / dataId 三级
控制台只有 Eureka 页面,配置没有界面注册和配置都在一个页面上
部署两个组件一个

最打动我的是"改完配置不用发 bus-refresh"。以前用 Spring Cloud Config 那套,改个配置要走 Git 提交、Config Server 拉取、然后手动 POST 一次刷新,还经常漏刷。Nacos 改完点发布,两三秒就推到位。

部署

3 节点集群,MySQL 5.7 做共享存储(Nacos 1.x 的集群模式用集中式存储,不是 gossip 协议):

# conf/cluster.conf,每个节点都写全
10.0.0.21:8848
10.0.0.22:8848
10.0.0.23:8848

# conf/application.properties
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://10.0.0.5:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=nacos
db.password=xxx

建表脚本在 conf/nacos-mysql.sql,执行完 11 张表。前面挂了一台 Nginx 做负载均衡,指向三个节点的 8848。

注意 cluster.conf不能写 127.0.0.1,否则节点之间互相找不到,集群起不来。这个错误在日志文件 logs/nacos.log 里的表现是反复打印 "failed to initialize member"。

服务注册:临时实例和持久化实例

这是 Nacos 特有而 Eureka 没有的概念,注册时指定:

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 10.0.0.21:8848
        namespace: 8f3a2b1c-xxxx-xxxx
        group: TRADE_GROUP
        ephemeral: true      # 默认 true
  • ephemeral: true(默认):临时实例,走 AP 模式(Distro 协议),靠客户端心跳保活。心跳停了自动剔除。我们的业务服务全用这个。
  • ephemeral: false:持久化实例,走 CP 模式(Raft),由服务端主动探测健康状态。实例不会因心跳停止而被删,只会标记不健康。适合数据库、Redis 这类"注册到 Nacos 上让别人发现"的基础设施。

我们给几个 MySQL 实例配了持久化实例(用 Nacos 的 OpenAPI 注册),这样应用可以通过 Nacos 拿到数据库地址,数据库主从切换时改一处就行。

健康保护阈值,这个默认值要留意

Nacos 有个 ProtectThreshold,默认 0.75。意思是:当某个服务的健康实例数低于总数的 75% 时,Nacos 会把不健康的实例也返回给消费者

设计意图是防止雪崩——假设 10 台挂了 8 台,如果只返回 2 台健康的,这两台马上也会被压垮。不如全返回,让流量分摊。

但这个行为经常让人困惑:"明明有个实例挂了,为什么还调它?"我们的处理是把它调到 0.5,并且对核心服务单独配。另外服务端剔除实例有 30 秒延迟,加上客户端的本地缓存(默认 1 秒拉取一次),实际感知一个实例下线要 30 秒以上。这是 AP 模型的固有代价。

配置中心:dataId 的规则要背下来

Nacos 的 dataId 完整格式是:

${spring.application.name}-${spring.profile.active}.${file-extension}

例如 order-service-prod.yaml

客户端配置:

# bootstrap.yml —— 必须写在 bootstrap 里,application.yml 太晚了
spring:
  application:
    name: order-service
  profiles:
    active: prod
  cloud:
    nacos:
      config:
        server-addr: 10.0.0.21:8848
        namespace: 8f3a2b1c-xxxx-xxxx
        group: TRADE_GROUP
        file-extension: yaml
        # 公共配置,比如所有服务共用的 Redis 地址
        shared-configs:
          - data-id: common-redis.yaml
            group: COMMON_GROUP
            refresh: true
          - data-id: common-datasource.yaml
            group: COMMON_GROUP
            refresh: true
        # 业务扩展配置
        extension-configs:
          - data-id: sentinel-rules.yaml
            group: SENTINEL_GROUP
            refresh: true

这里有个大坑:Nacos config 的配置必须写在 bootstrap.yml(或 bootstrap.properties。因为配置中心要在 Spring 容器初始化之前就拉取配置。写在 application.yml 里的话,spring.cloud.nacos.config.server-addr 根本来不及读,会去连默认的 localhost:8848

Spring Boot 2.4 之后这套 bootstrap 机制会变,但当时我们都在 2.2 和 2.3 上,没问题。

回到开头那个问题:改了配置为什么不生效

答案是普通 Bean 不会因为配置变化而重新创建。Nacos 推送过来的配置会更新 Spring 的 Environment,但已经实例化好的 Bean 里的 @Value 字段不会重新注入。

要生效,得加 @RefreshScope

@RefreshScope
@Component
public class RateLimitConfig {

    @Value("${order.rateLimit:500}")
    private int rateLimit;      // 配置推送后,这个 Bean 会被销毁重建,值更新

    public int getRateLimit() {
        return rateLimit;
    }
}

原理是 @RefreshScope 把 Bean 放到了一个特殊的作用域里,收到 RefreshEvent 时把这个作用域里的 Bean 全部销毁,下次使用再重建。

三种常见的"不生效":

现象原因
加了 @Value 但类上没有 @RefreshScope最常见。Environment 变了,Bean 没重建
@ConfigurationProperties 的 Bean 不刷新同样要加 @RefreshScope,或者用 EnvironmentChangeEvent 监听
值被注入到 static 字段@Value 不支持静态字段,一直是 null

还有一类是配置本身没推过来。排查方式是看日志:

2020-04-10 14:22:31.417  INFO [order-service] c.a.n.client.config.impl.ClientWorker
  : [fixed-10.0.0.21_8848] [polling-resp] config changed. dataId=order-service-prod.yaml, group=TRADE_GROUP
2020-04-10 14:22:31.509  INFO [order-service] c.a.nacos.client.config.impl.CacheData
  : [notify-ok] dataId=order-service-prod.yaml, group=TRADE_GROUP, md5=..., listener=com.alibaba.cloud.nacos.refresh.NacosContextRefresher$1
2020-04-10 14:22:31.611  INFO [order-service] o.s.cloud.bootstrap.config.PropertySourceBootstrapConfiguration
  : Located property source: [BootstrapPropertySource {name='bootstrapProperties-order-service-prod.yaml,TRADE_GROUP'}]

三行日志分别对应"检测到变化""通知监听器""重新加载 PropertySource"。缺哪一行就知道卡在哪一步。

另外控制台上能看到监听者列表,如果 listener 是空的,说明客户端压根没订阅这个 dataId,去检查 dataId 和 namespace 对不对。

命名空间和分组怎么设计

这是我觉得最值得说清楚的一件事。三级结构:

namespace(命名空间)
  └── group(分组)
        └── dataId(配置集)

我们一开始没规划,所有东西都塞在 public 命名空间和 DEFAULT_GROUP 里,两个月后就乱了。后来重定了一套:

层级用途我们的取值
namespace环境隔离,最粗粒度dev / test / pre / prod,4 个
group业务线或用途TRADE_GROUP、USER_GROUP、COMMON_GROUP、SENTINEL_GROUP
dataId单个配置文件{服务名}-{环境}.yaml

为什么 namespace 放环境?因为 namespace 之间的配置是完全隔离的,互相看不见。测试环境的人不可能误改生产配置,这是最硬的一道保险。用 group 做环境隔离做不到这一点。

group 用来区分用途,好处是权限好控。比如 COMMON_GROUP 里放所有服务共用的 Redis、注册中心地址,只有架构组能改;SENTINEL_GROUP 放限流规则,运维能改。

命名空间的 ID 不是名字,是一串 UUID,创建的时候可以自己填一个好记的:

prod    → 8f3a2b1c-prod-env-000000000001
pre     → 8f3a2b1c-pre--env-000000000002

配置里写 ID 不写名字:

spring:
  cloud:
    nacos:
      config:
        namespace: 8f3a2b1c-prod-env-000000000001
      discovery:
        namespace: 8f3a2b1c-prod-env-000000000001

配置的优先级

这一点经常搞混,我实测的顺序(从高到低):

  1. Nacos 中的 {服务名}-{profile}.yaml
  2. Nacos 中的 {服务名}.yaml
  3. extension-configs(下标越大优先级越高)
  4. shared-configs(下标越大优先级越高)
  5. 本地 application.yml
  6. 本地 bootstrap.yml

注意 Nacos 上的配置默认覆盖本地配置。我们有个同事在本地 application.yml 里加了参数调试,怎么改都不生效,找了半小时才发现 Nacos 上有一份同名的。想让本地优先,可以配:

spring:
  cloud:
    config:
      override-none: true      # 本地配置不被远程覆盖
      allow-override: true
      override-system-properties: false

一些数字

Nacos 上了半年之后的规模:

  • 3 节点集群,注册实例 127 个(含灰度实例)
  • 配置集 384 个,改配置的平均生效时间 2.3 秒
  • 配置变更次数:日均 25 次,其中 8 次是限流规则调整
  • Nacos 单机内存占用 1.8 GB,CPU 5% 左右

出问题的时候倒是有,一次是磁盘写满导致配置写入失败,一次是有人误删了 namespace(删之前一定要确认,那个操作没有二次确认弹窗)。

留个问题

关于《Nacos 注册中心与配置中心落地实践》里这个坑,你当时是怎么处理的?欢迎在评论区聊聊你踩过的类似情况。

参考