nacos 默认 namespace: public 拿不到数据

nacos 是阿里开源的配置中心/注册中心组件

在spring-boot项目中, 可以使用下面的方法启动nacos config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
nacos:
  config:
# 使用namespace=public时, 这里不能显示指定. 否则会拿不到数据
#    namespace: public
    username: admin
    password: 12345678
    server-addr: 10.0.18.92:8848
    group: ${spring.profiles.active}
# common 放通用配置, 可以被${spring.application.name}覆盖
    data-ids: ${spring.application.name},common
    type: YAML
    auto-refresh: true
    remote-first: true
    enable-remote-sync-config: true
    config-long-poll-timeout: 45000
    max-retry: 10
    config-retry-time: 3500
    bootstrap:
# 开启log
      enable: true
      log-enable: true

最坑的就是这个

1
namespace: public

public是系统默认的namespace

但是不能在这里制定, 否则会造成nacos客户端向nacos server拿不到config的情况.

这是nacos的一个bug(可以说是命名不规范吧), 具体可以参考其源码

com.alibaba.nacos.client.config.impl.ClientWorker#getServerConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout)
            throws NacosException {
        String[] ct = new String[2];
        if (StringUtils.isBlank(group)) {
            group = Constants.DEFAULT_GROUP;
        }
       
        HttpRestResult<String> result = null;
        try {
            Map<String, String> params = new HashMap<String, String>(3);
            if (StringUtils.isBlank(tenant)) {
                params.put("dataId", dataId);
                params.put("group", group);
            } else {
                params.put("dataId", dataId);
                params.put("group", group);
                params.put("tenant", tenant);
            }
            result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
        } catch (Exception ex) {
            String message = String
                    .format("[%s] [sub-server] get server config exception, dataId=%s, group=%s, tenant=%s",
                            agent.getName(), dataId, group, tenant);
            LOGGER.error(message, ex);
            throw new NacosException(NacosException.SERVER_ERROR, ex);
        }

代码中 tenant 就是namespace, 当不指定时, params 里面没有 tenant 项, 只有在指定了namespace时才会有这个请求参数

下面看server端实现

com.alibaba.nacos.config.server.service.repository.extrnal.ExternalStoragePersistServiceImpl#findConfigInfo(java.lang.String, java.lang.String, java.lang.String)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    @Override
    public ConfigInfo findConfigInfo(final String dataId, final String group, final String tenant) {
        final String tenantTmp = StringUtils.isBlank(tenant) ? StringUtils.EMPTY : tenant;
        try {
            return this.jt.queryForObject(
                    "SELECT ID,data_id,group_id,tenant_id,app_name,content,md5,type FROM config_info WHERE data_id=? AND group_id=? AND tenant_id=?",
                    new Object[] {dataId, group, tenantTmp}, CONFIG_INFO_ROW_MAPPER);
        } catch (EmptyResultDataAccessException e) { // Indicates that the data does not exist, returns null.
            return null;
        } catch (CannotGetJdbcConnectionException e) {
            LogUtil.FATAL_LOG.error("[db-error] " + e.toString(), e);
            throw e;
        }
    }

到这里就成了 tenant_id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    /**
     * Execute clone config operation.
     *
     * @param request         http servlet request .
     * @param srcUser         src user string value.
     * @param namespace       namespace string value.
     * @param configBeansList config beans list.
     * @param policy          config policy model.
     * @return RestResult for map.
     * @throws NacosException NacosException.
     */
    @PostMapping(params = "clone=true")
    @Secured(action = ActionTypes.WRITE, parser = ConfigResourceParser.class)
    public RestResult<Map<String, Object>> cloneConfig(HttpServletRequest request,
            @RequestParam(value = "src_user", required = false) String srcUser,
            @RequestParam(value = "tenant", required = true) String namespace,
            @RequestBody(required = true) List<SameNamespaceCloneConfigBean> configBeansList,
            @RequestParam(value = "policy", defaultValue = "ABORT") SameConfigPolicy policy) throws NacosException {
        Map<String, Object> failedData = new HashMap<>(4);
        if (CollectionUtils.isEmpty(configBeansList)) {
            failedData.put("succCount", 0);
            return ResultBuilder.buildResult(ResultCodeEnum.NO_SELECTED_CONFIG, failedData);
        }
        configBeansList.removeAll(Collections.singleton(null));
       
        if (NAMESPACE_PUBLIC_KEY.equalsIgnoreCase(namespace)) {
            namespace = "";
        } else if (persistService.tenantInfoCountByTenantId(namespace) <= 0) {
            failedData.put("succCount", 0);
            return ResultBuilder.buildResult(ResultCodeEnum.NAMESPACE_NOT_EXIST, failedData);
        }
       
        List<Long> idList = new ArrayList<>(configBeansList.size());
        Map<Long, SameNamespaceCloneConfigBean> configBeansMap = configBeansList.stream()
                .collect(Collectors.toMap(SameNamespaceCloneConfigBean::getCfgId, cfg -> {
                    idList.add(cfg.getCfgId());
                    return cfg;
                }, (k1, k2) -> k1));

诡异的代码出现了

1
2
3
4
5
6
        if (NAMESPACE_PUBLIC_KEY.equalsIgnoreCase(namespace)) {
            namespace = "";
        } else if (persistService.tenantInfoCountByTenantId(namespace) <= 0) {
            failedData.put("succCount", 0);
            return ResultBuilder.buildResult(ResultCodeEnum.NAMESPACE_NOT_EXIST, failedData);
        }

添加配置时, 如果namespace为public时, 最终存入数据的 tenant_id 为 StringUtils.EMPTY

获取的时候是没有这个潜规则的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    @Override
    public ConfigInfo findConfigInfo(final String dataId, final String group, final String tenant) {
        final String tenantTmp = StringUtils.isBlank(tenant) ? StringUtils.EMPTY : tenant;
        try {
            return this.jt.queryForObject(
                    "SELECT ID,data_id,group_id,tenant_id,app_name,content,md5,type FROM config_info WHERE data_id=? AND group_id=? AND tenant_id=?",
                    new Object[] {dataId, group, tenantTmp}, CONFIG_INFO_ROW_MAPPER);
        } catch (EmptyResultDataAccessException e) { // Indicates that the data does not exist, returns null.
            return null;
        } catch (CannotGetJdbcConnectionException e) {
            LogUtil.FATAL_LOG.error("[db-error] " + e.toString(), e);
            throw e;
        }
    }

所以, 当我们在客户端指定 namespace = public 时, 就死活拿不到数据了.

其中 namespace -> tenant -> tenant_id -> namespaceId 其实一直扮演着一个角色(即: 命名空间)

在这里提两点意见

1. 既然自己制定了规则, 那就要把规则落地. 不然就必定会有人为此躺坑.

2. 所谓名不正言不顺, 同一个东西在项目中尽量不要起多个名字.