欢迎访问Spring Cloud中国社区

《重新定义Spring Cloud实战》由Spring Cloud中国社区倾力打造,基于Spring Cloud的Finchley.RELEASE版本,本书内容宽度足够广、深度足够深,而且立足于生产实践,直接从生产实践出发,包含大量生产实践的配置。欢迎加微信Software_King进群答疑,国内谁在使用Spring Cloud?欢迎登记

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

张书康 · 4月前 · 841 ·

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进行负载均衡。

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

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

  1. public class ConsulServerList extends AbstractServerList<ConsulServer> {
  2. private final ConsulClient client;
  3. private final ConsulDiscoveryProperties properties;
  4. private final Logger logger = LoggerFactory.getLogger(ConsulServerList.class);
  5. //项目id
  6. private String serviceId;
  7. //环境变量,区分dev test
  8. private String env;
  9. //分支id
  10. private String projectId;
  11. //是否是公共环境
  12. private boolean isPublic;
  13. public ConsulServerList(ConsulClient client, ConsulDiscoveryProperties properties) {}
  14. @Override
  15. public void initWithNiwsConfig(IClientConfig clientConfig) {
  16. this.serviceId = clientConfig.getClientName();
  17. }
  18. @Override
  19. public List<ConsulServer> getInitialListOfServers() {
  20. return getServers();
  21. }
  22. @Override
  23. public List<ConsulServer> getUpdatedListOfServers() {
  24. //这里区分同步实例的方式,是走自己的方式,还是走默认方式,可以写死,也可通过配置文件指定
  25. if (true) {
  26. return getServersByTag();
  27. } else {
  28. return getServers();
  29. }
  30. }
  31. private List<ConsulServer> getServers() {
  32. if (this.client == null) {
  33. return Collections.emptyList();
  34. }
  35. String tag = getTag(); // null is ok
  36. Response<List<HealthService>> response = this.client.getHealthServices(
  37. this.serviceId, tag, true,
  38. QueryParams.DEFAULT);
  39. if (response.getValue() == null || response.getValue().isEmpty()) {
  40. return Collections.emptyList();
  41. }
  42. List<ConsulServer> servers = new ArrayList<>();
  43. for (HealthService service : response.getValue()) {
  44. servers.add(new ConsulServer(service));
  45. }
  46. return servers;
  47. }
  48. private boolean isTestProject() {
  49. return "test".equals(env) && (!isPublic);
  50. }
  51. private List<ConsulServer> getServersByTag() {
  52. if (isTestProject()) {
  53. //如果是测试环境并且是公共环境,则获取对应服务的公共环境实例。否则,获取对应的分支测试环境。
  54. } else {
  55. //如果是本地环境,直接获取所有实例
  56. }
  57. if (serviceIds.size() == 0) {
  58. return Collections.emptyList();
  59. }
  60. List<ConsulServer> servers = new ArrayList<>();
  61. for (String serviceName : serviceIds) {
  62. Response<List<HealthService>> response = this.client.getHealthServices(
  63. serviceName, tag, true,
  64. QueryParams.DEFAULT);
  65. if (response.getValue() == null || response.getValue().isEmpty()) {
  66. continue;
  67. }
  68. for (HealthService service : response.getValue()) {
  69. servers.add(new ConsulServer(service));
  70. }
  71. }
  72. return servers;
  73. }
  74. private String getTag() {
  75. return this.properties.getQueryTagForService(this.serviceId);
  76. }
  77. @Override
  78. public String toString() {
  79. final StringBuffer sb = new StringBuffer("ConsulServerList{");
  80. sb.append("serviceId='").append(serviceId).append('\'');
  81. sb.append(", tag=").append(getTag());
  82. sb.append('}');
  83. return sb.toString();
  84. }
  85. }

主要思路是借助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/<service> 返回和服务相关联的检查
/v1/health/service/<service> 返回给定datacenter中给定node中service
/v1/health/state/<state> 返回给定datacenter中指定状态的服务,state可以是”any”, “unknown”, “passing”, “warning”, or “critical”,可用参数?dc=