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= |