一、引言

做过分布式项目的人都知道,服务注册发现和配置管理是两个绕不开的核心需求。以前很多团队会分开做:注册发现用一套工具管服务地址,配置用另一套工具管参数,两套工具各搞各的,不仅维护麻烦,还容易出各种小问题。后来有人想到,能不能用Consul把这两个功能捏成一个整体?想法是好的,但真落地的时候,很多人都会踩坑,最常见的就是耦合太死、扩展不动。这篇就用大白话讲清楚,把Consul的两个功能整合成统一架构时,最要注意的两个核心问题:耦合和扩展性。

二、Consul的两个核心功能基础认知

在说整合之前,得先搞明白Consul本身的两个功能到底是啥,不然连基础都没搞懂,整合肯定乱。

2.1 服务注册发现

这个功能说白了就是“服务通讯录”。比如你有一个订单服务、一个支付服务,订单服务要调用支付服务,总不能把支付服务的IP写死在代码里吧?万一支付服务换IP、扩容缩容,订单服务岂不是要跟着改?服务注册发现就是解决这个问题的:所有服务启动的时候,自己把自己的名字、地址、端口这些信息报给Consul;需要调用别的服务时,就找Consul要对应名字的服务地址,Consul会给你返回最新的可用地址。

2.2 配置管理

这个功能说白了就是“集中参数仓库”。比如你有很多服务都要用到数据库的连接地址、接口超时时间、第三方密钥这些参数,以前每个服务自己存一份,改的时候要改好多个地方,还容易改漏。配置管理就是把所有公共的、服务专属的参数都存在Consul里,服务启动或者运行的时候,自己去Consul拉最新的参数,改参数只要在Consul里改一次就行。

三、整合时的核心问题1:耦合

很多人整合Consul的两个功能时,会犯一个错误:把两个功能绑得太死,导致牵一发而动全身。举个最常见的例子:很多人会把服务的配置和服务本身的注册信息存在一起,或者把配置的生效逻辑和服务的发现逻辑绑定。

3.1 耦合的具体表现

先看一个反面的整合例子,这个例子用Spring Cloud Consul(这是Java生态里用Consul的常用工具,很多团队直接拿它整合两个功能),具体场景是:一个电商项目里的商品服务,把自己的配置和服务注册绑在一起。 技术栈:Java Spring Boot 2.7 + Spring Cloud Consul 3.1.4

# 商品服务的bootstrap.yml配置,反面示例,注意注释里的问题
spring:
  application:
    name: product-service # 服务名,注册发现用
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        register: true # 开启服务注册
        service-name: ${spring.application.name} # 注册的服务名,和配置的组绑定
      config:
        enabled: true # 开启配置管理
        format: YAML
        prefix: config # 配置的根目录
        data-key: data # 配置的内容key
        # 这里的问题:配置的组和服务名绑定死了,配置必须存在config/product-service这个目录下
        group: ${spring.application.name}

这个配置看起来没问题,但有两个大问题:第一,所有商品服务的配置必须存在config/product-service这个目录下,要是哪天想把商品服务的配置拆成开发、测试、生产三个组,就没法改,因为组和服务名绑死了;第二,如果哪天不想用Consul的配置管理了,想换成Nacos的配置,那服务注册的逻辑也要跟着改,因为两个功能的配置写在同一个文件里,还互相依赖。 再看一个更严重的耦合例子:很多人会把配置的更新逻辑和服务的健康检查绑在一起。比如商品服务的健康检查逻辑是:先从Consul拉最新的配置,再检查自己的状态,要是拉配置失败,就把自己标记为不健康,从注册列表里剔除。这个逻辑乍一看没问题,但如果Consul的配置管理出问题了,所有服务都会被标记为不健康,整个集群就瘫了,本来只是配置出问题,结果连服务调用都断了。

3.2 耦合的危害

耦合的危害总结起来有三个:第一,改一个功能会影响另一个,比如改配置管理的逻辑,服务注册的逻辑也可能出问题;第二,扩展的时候很麻烦,比如想把服务注册拆到别的工具,配置管理还留着Consul,因为绑死了,拆不动;第三,排查问题难,要是服务出问题,不知道是注册的问题还是配置的问题,两个逻辑混在一起,查起来特别费时间。

四、整合时的核心问题2:扩展性

扩展性就是指,以后要是需求变了,能不能轻松改。比如以后想给服务加个灰度发布的能力,或者想把配置管理的一部分功能拆出去,能不能不重构整个架构?很多人整合Consul的时候,会忽略扩展性,导致以后想改的时候特别难。

4.1 扩展性不足的具体表现

还是拿Spring Cloud Consul的例子来说,很多人整合的时候,会把Consul的两个功能的客户端代码写死在服务里。比如商品服务的代码里,同时写了调用Consul注册服务的代码和拉配置的代码,要是以后想把服务注册换成Eureka,那就要把所有服务里的注册代码都改一遍,工作量特别大。 再比如配置管理的扩展性,很多人会把所有配置都存在Consul的KV存储里,要是以后想给配置加个加密的功能,或者加个版本控制的功能,因为代码里直接写死了拉Consul KV的逻辑,改起来特别麻烦。

4.2 扩展性的设计思路

要解决扩展性的问题,核心就是“解耦”,把两个功能的逻辑分开,不要绑在一起。具体怎么做呢?举个正面的整合例子,还是用Java Spring Boot + Spring Cloud Consul的技术栈,这次把两个功能的逻辑拆成独立的模块。 第一步,先写一个服务注册的抽象类,把注册的逻辑和具体的工具(比如Consul)分开:

// 服务注册抽象类,所有注册逻辑都基于这个类,和具体工具无关
public abstract class ServiceRegister {
    // 注册服务的抽象方法,具体实现由子类完成
    public abstract void register(ServiceInfo serviceInfo);
    // 注销服务的抽象方法
    public abstract void unregister(String serviceId);
    // 服务信息类,封装服务的基本信息
    public static class ServiceInfo {
        private String serviceName;
        private String serviceId;
        private String host;
        private int port;
        // 构造方法、getter、setter省略
    }
}

第二步,写一个Consul的服务注册实现类,专门负责和Consul交互:

// Consul的服务注册实现类,只负责服务注册,不涉及配置
import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties;
import org.springframework.cloud.consul.discovery.ConsulServiceRegistry;
import org.springframework.cloud.consul.discovery.Registration;

public class ConsulServiceRegister extends ServiceRegister {
    private final ConsulServiceRegistry consulServiceRegistry;
    private final ConsulDiscoveryProperties discoveryProperties;

    // 构造方法注入Consul的注册工具
    public ConsulServiceRegister(ConsulServiceRegistry consulServiceRegistry, ConsulDiscoveryProperties discoveryProperties) {
        this.consulServiceRegistry = consulServiceRegistry;
        this.discoveryProperties = discoveryProperties;
    }

    @Override
    public void register(ServiceInfo serviceInfo) {
        // 把抽象的ServiceInfo转换成Consul要求的Registration格式
        Registration registration = new Registration();
        registration.setName(serviceInfo.getServiceName());
        registration.setId(serviceInfo.getServiceId());
        registration.setHost(serviceInfo.getHost());
        registration.setPort(serviceInfo.getPort());
        // 调用Consul的注册方法
        consulServiceRegistry.register(registration);
    }

    @Override
    public void unregister(String serviceId) {
        consulServiceRegistry.deregister(serviceId);
    }
}

第三步,写一个配置管理的抽象类,和服务注册的逻辑完全分开:

// 配置管理抽象类,和具体的配置工具无关
public abstract class ConfigManager {
    // 拉取配置的抽象方法
    public abstract String getConfig(String group, String key);
    // 更新配置的抽象方法
    public abstract void updateConfig(String group, String key, String value);
}

第四步,写一个Consul的配置管理实现类,专门负责和Consul的配置交互:

// Consul的配置管理实现类,只负责配置,不涉及服务注册
import org.springframework.cloud.consul.config.ConsulConfigProperties;
import org.springframework.cloud.consul.config.ConsulPropertySourceLocator;

public class ConsulConfigManager extends ConfigManager {
    private final ConsulPropertySourceLocator propertySourceLocator;
    private final ConsulConfigProperties configProperties;

    // 构造方法注入Consul的配置工具
    public ConsulConfigManager(ConsulPropertySourceLocator propertySourceLocator, ConsulConfigProperties configProperties) {
        this.propertySourceLocator = propertySourceLocator;
        this.configProperties = configProperties;
    }

    @Override
    public String getConfig(String group, String key) {
        // 从Consul拉取对应组和key的配置
        String prefix = configProperties.getPrefix();
        String fullKey = prefix + "/" + group + "/" + key;
        // 这里简化实现,实际项目中可以调用Consul的API拉取
        return propertySourceLocator.locate(null).getProperty(fullKey).toString();
    }

    @Override
    public void updateConfig(String group, String key, String value) {
        // 简化实现,实际项目中调用Consul的API更新配置
        System.out.println("更新Consul配置:组=" + group + ",key=" + key + ",value=" + value);
    }
}

这样拆分之后,服务注册和配置管理的逻辑就完全分开了。以后要是想把服务注册换成Eureka,只要写一个EurekaServiceRegister类继承ServiceRegister就行,不用改配置管理的代码;要是想把配置换成Nacos,只要写一个NacosConfigManager类继承ConfigManager就行,不用改服务注册的代码,扩展性就好很多。

五、整合时的应用场景、优缺点和注意事项

5.1 应用场景

Consul的两个功能整合成统一架构,适合什么场景呢?第一,中小型分布式项目,团队规模不大,不想维护太多的工具,用一个Consul搞定两个功能,减少维护成本;第二,对工具的依赖度不高,以后可能会替换工具,但又想先快速落地的项目;第三,对配置和服务的联动要求不高,不需要太复杂的逻辑的项目。

5.2 技术优缺点

5.2.1 优点

第一,减少工具数量,以前要维护注册发现和配置管理两个工具,现在只要维护一个Consul,运维成本低;第二,统一的管理界面,Consul的Web界面可以同时看服务和配置,不用切换多个界面,操作方便;第三,统一的权限控制,只要给Consul配置一套权限,就能同时管服务和配置,不用给两个工具分别配权限。

5.2.2 缺点

第一,要是整合的时候没处理好耦合,反而会比分开用更麻烦;第二,Consul的配置管理功能没有专门的配置工具(比如Nacos、Apollo)强大,比如没有灰度发布、版本控制、加密存储这些高级功能,要是项目需要这些功能,整合反而会限制项目的能力;第三,单点风险,要是Consul出问题,服务注册和配置管理都会出问题,整个集群可能会瘫。

5.3 注意事项

第一,整合的时候一定要把两个功能的逻辑解耦,不要绑死,就像前面的正面例子那样,用抽象类把逻辑和具体工具分开;第二,配置的组和服务名不要绑死,要支持灵活的分组,比如可以按环境、按业务线分组;第三,健康检查不要和配置更新绑在一起,配置出问题的时候,不要影响服务的注册状态;第四,Consul本身要做高可用部署,不要用单节点的Consul,避免单点风险;第五,要是项目需要配置的高级功能,不要强行整合,比如需要灰度发布配置,就专门用一个配置工具,注册发现用Consul,分开用反而更好。

六、总结

把Consul的服务注册发现和配置管理整合成统一架构,是一个能减少维护成本的好办法,但前提是要处理好耦合和扩展性的问题。核心思路就是“解耦”:把两个功能的逻辑分开,不要绑死;把具体的实现和抽象的逻辑分开,方便以后替换工具。整合的时候,一定要结合自己项目的实际情况,要是项目的需求比较简单,整合能带来很多好处;要是项目的需求比较复杂,比如需要配置的高级功能,强行整合反而会适得其反。