Spring Cloud | Spring Cloud Consul 重写服务发现逻辑

1)概述

Spring Cloud提供了完整的服务注册和服务发现逻辑,但是在devops流行的今天,简单的服务发现逻辑,并不能满足我们特殊的需求,特别是在服务众多的情况下。比如:如果一位开发同学拉取并部署了项目project-a,另一位同学也部署了project-a,也就意味着project-a此时有两套环境。而现实中一个公司可能有成百上千个微服务呢,如何保证服务调用能找到正确的服务呢?因此,每个公司都需要结合自己内部的 devops 平台对服务发现进行特殊的定制。

2)问题

如果公司的微服务数量较多,很多时候会面临以下问题:

问题1:很多公司为了保证开发效率,开发环境和测试环境共用了一套注册中心。这样做使得开发同学在开发环境无需关注注册中心。这个时候就要保证开发环境的服务和测试环境的服务相互独立。如何做到本地的服务实例,不被测试环境的注册中心发现?

问题2: 测试环境下的每个服务会有多个分支,而每个分支会有一套环境,如何保证每个服务的不同分支环境相互对立?

3)分析

问题1:

上述第一个问题解决起来并不复杂,做法也比较多,常规的做法有以下两种:

  • 禁止开发环境注册到测试环境的consul。如果用eureka的话,eureka提供了配置 register-with-eureka: false,很遗憾,consul 1.2以前的版本并没有提供该配置,如果要实现该功能,只能手动增加 @Configuration配置类。1.2版本的consul已经引入了该配置,spring.cloud.consul.discovery.register = false ,只需要将其设置为false即可。
  • 借助运维,构造单向网络。就是说可以让开发环境的服务注册到测试环境的consul,但是测试环境的consul因为网络隔离,无法拉取到开发环境的实例。从根本上解决了问题。

建议第二种做法。如果开发人员不按项目配置文件规范来的话,会带来不必要的麻烦。

问题2:

假如注册中心是consul,devops 平台要保证多个测试环境在进行服务发现的时候互不影响,要么是在ribbon负载均衡的时候着手,要么是从consul从consul agent中获取同步实例的时候着手。重写ribbon负载均衡的做法看似也能实现,但不太现实,因为负载均衡策略不唯一,相对麻烦。重写consul 同步实例的代码是个不错的做法。

4)示例

首先看一下consul相关的代码:
consul和ribbon的配置类:ConsulRibbonClientConfiguration,在该类中有一个重要的bean:ribbonServerList,该类主要作用是负责consul 同步consul agent中的实例,交给ribbon进行负载均衡。

    @Bean
    @ConditionalOnMissingBean
    public ServerList<?> ribbonServerList(IClientConfig config, ConsulDiscoveryProperties properties) {
        //consul 同步实例的关键类,也就是要重写的类
        ConsulServerList serverList = new ConsulServerList(client, properties);
        serverList.initWithNiwsConfig(config);
        return serverList;
    }

ConsulServerList也就是我们要重写的类,实现方式:

public class ConsulServerList extends AbstractServerList<ConsulServer> {

    private final ConsulClient client;
    private final ConsulDiscoveryProperties properties;

    private final Logger logger = LoggerFactory.getLogger(ConsulServerList.class);
    //项目id
    private String serviceId;
    //环境变量,区分dev test
    private String env;
    //分支id
    private String projectId;
    //是否是公共环境
    private boolean isPublic;

    public ConsulServerList(ConsulClient client, ConsulDiscoveryProperties properties) {}

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
        this.serviceId = clientConfig.getClientName();
    }

    @Override
    public List<ConsulServer> getInitialListOfServers() {
        return getServers();
    }

    @Override
    public List<ConsulServer> getUpdatedListOfServers() {
        //这里区分同步实例的方式,是走自己的方式,还是走默认方式,可以写死,也可通过配置文件指定
        if (true) {
            return getServersByTag();
        } else {
            return getServers();
        }
    }

    private List<ConsulServer> getServers() {
        if (this.client == null) {
            return Collections.emptyList();
        }
        String tag = getTag(); // null is ok

        Response<List<HealthService>> response = this.client.getHealthServices(
                this.serviceId, tag, true,
                QueryParams.DEFAULT);
        if (response.getValue() == null || response.getValue().isEmpty()) {
            return Collections.emptyList();
        }
        List<ConsulServer> servers = new ArrayList<>();
        for (HealthService service : response.getValue()) {
            servers.add(new ConsulServer(service));
        }
        return servers;
    }

    private boolean isTestProject() {
        return "test".equals(env) && (!isPublic);
    }

    private List<ConsulServer> getServersByTag() {
        if (isTestProject()) {
           //如果是测试环境并且是公共环境,则获取对应服务的公共环境实例。否则,获取对应的分支测试环境。
        } else {
           //如果是本地环境,直接获取所有实例
        }

        if (serviceIds.size() == 0) {
            return Collections.emptyList();
        }

        List<ConsulServer> servers = new ArrayList<>();
        for (String serviceName : serviceIds) {
            Response<List<HealthService>> response = this.client.getHealthServices(
                    serviceName, tag, true,
                    QueryParams.DEFAULT);
            if (response.getValue() == null || response.getValue().isEmpty()) {
                continue;
            }

            for (HealthService service : response.getValue()) {
                servers.add(new ConsulServer(service));
            }
        }
        return servers;
    }

    private String getTag() {
        return this.properties.getQueryTagForService(this.serviceId);
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("ConsulServerList{");
        sb.append("serviceId='").append(serviceId).append('\'');
        sb.append(", tag=").append(getTag());
        sb.append('}');
        return sb.toString();
    }
}

主要思路是借助docker环境变量来区分环境。在部署docker容器的时候,首先通过将env(dev或test)写入环境变量(注意是环境变量,不是系统变量),来区分本地环境或者测试环境,然后将项目分支 id 也写入环境变量,来区分是公共测试环境还是普通测试环境。再consul获取到所有的服务实例之后,通过系统变量env来进行筛选:

1)首先通过/v1/catalog/services,该接口监视一系列有效的service,采用默认参数QueryParams.DEFAULT,然后根据不同环境进行筛选。
2)如果是开发环境,则可以筛选出要访问的 serviceId 对应的测试环境所有的实例。也可以当做公共环境处理,只调用 serviceId对应的公共环境实例。
3)如果是测试环境并且是公共环境,则只筛选出要访问的 serviceId对应的测试环境的公共实例。
4)如果是测试环境非公共环境,则可通过要访问的 serviceId和项目分支id获取所需要的实例,如果获取不到,则获取指定的测试环境实例。
5)最后根据serviceId获取实例信息,接口为/v1/health/service/

注意:指定服务实例的做法比较常用,当一个服务有多个实例,只想访问其中指定的一台,这也是开发过程中常见的场景。一般是在yml文件中直接写死,手动写配置文件读取,同步实例的时候,进行筛选。类似于指定了负载均衡的范围。


附上consul 常见的 endpoits url:

url comments
/v1/catalog/register Registers a new node, service, or check
/v1/catalog/services Lists services in a given DC
/v1/catalog/datacenters 获取所有的
/v1/catalog/services 获取所有的service记录
url comments
/v1/health/checks/ 返回和服务相关联的检查
/v1/health/service/ 返回给定datacenter中给定node中service
/v1/health/state/ 返回给定datacenter中指定状态的服务,state可以是”any”, “unknown”, “passing”, “warning”, or “critical”,可用参数?dc=

猜你喜欢

转载自blog.csdn.net/woshilijiuyi/article/details/80985242