springcloud — 微服务注册与发现之consul组件原理解析(二)

回顾之前文章:
1. 微服务注册与发现之consul组件治理能力(一)

其中介绍了consul的概念和基于springcloud进行集成的操作步骤,本文将从consul组件原理细讲相关的原理;
对应官方文档地址:https://www.consul.io/docs/internals/architecture
consul中文文档地址:https://www.springcloud.cc/spring-cloud-consul.html

Consul 有两种角色:

  • Client 客户端 :无状态,将 HTTP 和 DNS 接口请求转发给局域网内的 Consul Server集群。
    简单来说,Client 扮演的是代理的角色,例如说 Java 应用请求 Consul Client,然后 Consul Client 转发请求给 Consul Server。

  • Server 服务端 :保存配置信息,高可用集群。在局域网内与本地客户端通讯,通过广域网与其他数据中心通讯。每个数据中心的 Server 数量推荐为 3 个或是 5 个。

一个 Consul 客户端架构如下图所示:
在这里插入图片描述

1、 核心功能

对应官方文档地址:https://www.consul.io/intro
Consul 的核心功能如下:

1.1、注册中心:
  • 服务发现(Service Discovery) :Consul 提供了通过 DNS 或者 HTT P接口的方式,来注册服务和发现服务。一些外部的服务通过 Consul,很容易的找到它所依赖的服务。
  • 健康检测(Health Checking) :Consul 的 Client 提供了健康检查的机制,可以避免流量被转发到有故障的服务上。
1.2、配置中心
  • Key/Value 存储(KV Store) :应用程序可以根据自己的需要使用 Consul 提供的 Key/Value 存储。Consul 提供了简单易用的 HTTP 接口,结合其他工具可以实现动态配置、功能标记、领袖选举等等功能。
1.3、多数据中心
  • 多数据中心(Multi Datacenter) :Consul 支持开箱即用的多数据中心。这意味着用户不需要建立额外的抽象层,让业务扩展到多个区域。
2、 consul组件功能
1、自动配置

在spring-cloud-consul-config.jar!/META-INF/spring.factories中,有如下配置:

# Auto Configuration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.consul.config.ConsulConfigAutoConfiguration

# Bootstrap Configuration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.consul.config.ConsulConfigBootstrapConfiguration
@Configuration
@ConditionalOnConsulEnabled
@ConditionalOnProperty(name = "spring.cloud.consul.config.enabled", matchIfMissing = true)
public class ConsulConfigAutoConfiguration {

	public static final String CONFIG_WATCH_TASK_SCHEDULER_NAME = "configWatchTaskScheduler";

	@Configuration
	@ConditionalOnClass(RefreshEndpoint.class)
	protected static class ConsulRefreshConfiguration {
		@Bean
		@ConditionalOnProperty(name = "spring.cloud.consul.config.watch.enabled", matchIfMissing = true)
		public ConfigWatch configWatch(ConsulConfigProperties properties,
				ConsulPropertySourceLocator locator, ConsulClient consul,
			    @Qualifier(CONFIG_WATCH_TASK_SCHEDULER_NAME) TaskScheduler taskScheduler) {
			return new ConfigWatch(properties, consul, locator.getContextIndexes(), taskScheduler);
		}

		@Bean(name = CONFIG_WATCH_TASK_SCHEDULER_NAME)
		@ConditionalOnProperty(name = "spring.cloud.consul.config.watch.enabled", matchIfMissing = true)
        public TaskScheduler configWatchTaskScheduler() {
			return new ThreadPoolTaskScheduler();
		}
	}
}

其中,ConfigWatch的主要代码如下:

@Timed(value ="consul.watch-config-keys")
public void watchConfigKeyValues() {
	if (this.running.get()) {
		for (String context : this.consulIndexes.keySet()) {

			// turn the context into a Consul folder path (unless our config format are FILES)
			if (properties.getFormat() != FILES && !context.endsWith("/")) {
				context = context + "/";
			}

			try {
				Long currentIndex = this.consulIndexes.get(context);
				if (currentIndex == null) {
					currentIndex = -1L;
				}

				log.trace("watching consul for context '"+context+"' with index "+ currentIndex);

				// use the consul ACL token if found
				String aclToken = properties.getAclToken();
				if (StringUtils.isEmpty(aclToken)) {
				    aclToken = null;
				}

				Response<List<GetValue>> response = this.consul.getKVValues(context, aclToken,
						new QueryParams(this.properties.getWatch().getWaitTime(),
								currentIndex));

				// if response.value == null, response was a 404, otherwise it was a 200
				// reducing churn if there wasn't anything
				if (response.getValue() != null && !response.getValue().isEmpty()) {
					Long newIndex = response.getConsulIndex();

					if (newIndex != null && !newIndex.equals(currentIndex)) {
						// don't publish the same index again, don't publish the first time (-1) so index can be primed
						if (!this.consulIndexes.containsValue(newIndex) && !currentIndex.equals(-1L)) {
							log.trace("Context "+context + " has new index " + newIndex);
							RefreshEventData data = new RefreshEventData(context, currentIndex, newIndex);
							this.publisher.publishEvent(new RefreshEvent(this, data, data.toString()));
						} else if (log.isTraceEnabled()) {
							log.trace("Event for index already published for context "+context);
						}
						this.consulIndexes.put(context, newIndex);
					} else if (log.isTraceEnabled()) {
						log.trace("Same index for context "+context);
					}
				} else if (log.isTraceEnabled()) {
					log.trace("No value for context "+context);
				}

			} catch (Exception e) {
				// only fail fast on the initial query, otherwise just log the error
				if (firstTime && this.properties.isFailFast()) {
					log.error("Fail fast is set and there was an error reading configuration from consul.");
					ReflectionUtils.rethrowRuntimeException(e);
				} else if (log.isTraceEnabled()) {
					log.trace("Error querying consul Key/Values for context '" + context + "'", e);
				} else if (log.isWarnEnabled()) {
					// simplified one line log message in the event of an agent failure
					log.warn("Error querying consul Key/Values for context '" + context + "'. Message: " + e.getMessage());
				}
			}
		}
	}
	firstTime = false;
}

启动配置ConsulConfigBootstrapConfiguration:

在这里插入代码片
2、服务注册​

在分布式服务中,一个服务通常有多个实例,这时候需要分别申明服务和实例,以便确认主体,合并不同实例。服务声明就是告诉consul server“我是谁”,ConsulAutoRegistration定制了包括服务名称serviceName和服务唯一标识instanceID的规则。

​ 服务注册的目的是让别的服务能够使用,通常我们访问一个http服务是通过host:port这样的uri定位。host又分为ip和域名两种类型。一个springboot服务是如何知道“我在哪”呢?spring的InetUtils使用了jdk的java.net.NetworkInterface网络接口获得当前服务IP和hostname,具体原理可以参考附录中的“java中的getHostname”。而port则是通过WebServerInitializedEvent.getWebServer(). getPort()获得。

​ consul server的地址可以直接通过配置获得,spring cloud通过AgentConsulClient代理了对 consul server的请求。

服务初始注册

  1. ConsulAutoServiceRegistrationListener.onApplicationEvent(ApplicationEvent)
  2. ConsulAutoServiceRegistration.start()
  3. ConsulAutoServiceRegistration.register()
  4. AbstractAutoServiceRegistration.register()
  5. ConsulServiceRegistry.register(Registration)
  6. ConsulClient.agentServiceRegister(NewService,String)
  7. AgentConsulClient.agentServiceRegister(NewService,String)
  8. ConsulRawClient.makePutRequest()
  • ConsulAutoServiceRegistrationListener监听服务启动事件WebServerInitializedEvent,执行服务注册。在第一步中调用了这样一个代码:this.autoServiceRegistration.setPortIfNeeded(event.getWebServer().getPort()),此处通过jdk的CAS机制实现了防止重复设置端口和并发操作。
//ConsulAutoServiceRegistration
private AtomicInteger port = new AtomicInteger(0);
void setPortIfNeeded(int port) {
	getPort().compareAndSet(0, port);
}
  • ConsulServiceRegistry是服务注册,销毁等业务功能的流程控制类。
//ConsulServiceRegistry.register(ConsulRegistration reg)
this.client.agentServiceRegister(reg.getService(),
	                this.properties.getAclToken());
    NewService service = reg.getService();
if (this.heartbeatProperties.isEnabled() && this.ttlScheduler != null
					&& service.getCheck() != null
					&& service.getCheck().getTtl() != null) {
	this.ttlScheduler.add(reg.getInstanceId());
}

具体的服务注册请求由ConsulClient完成。reg.getService()获得了服务注册需要的所有信息NewService。NewService.check数据中,http指定了consul server健康检查请求地址。而ttl则指定agent心跳检查的间隔时间。spring cloud consul这两个字段是互斥的,当客户端主动做心跳检测时就不做健康检查。NewService内容如下:

{
    "id": "resource-server-1-8081",
    "name": "resource-server-1",
    "tags": [
        {
            "secure": false
        }
    ],
    "address": "172.17.0.1",
    "meta": null,
    "port": 8081,
    "enableTagOverride": null,
    "check": {
        "script": "null",
        "interval": "1s",
        "ttl": "null",
        "http": "http://172.17.0.1:8081/actuator/health",
        "method": "null",
        "header": {},
        "tcp": "null",
        "timeout": "null",
        "deregisterCriticalServiceAfter": "null",
        "tlsSkipVerify": null,
        "status": "null"
    }
}

ConsulAutoRegistration创建NewService.Check的代码如下:

public static NewService.Check createCheck(Integer port,
		HeartbeatProperties ttlConfig, ConsulDiscoveryProperties properties) {
	NewService.Check check = new NewService.Check();
	if (StringUtils.hasText(properties.getHealthCheckCriticalTimeout())) {
		check.setDeregisterCriticalServiceAfter(
				properties.getHealthCheckCriticalTimeout());
	}
	// 如果启用心跳检测,则不做consul server健康检查
	if (ttlConfig.isEnabled()) {
		check.setTtl(ttlConfig.getTtl());
		return check;
	}

	Assert.notNull(port, "createCheck port must not be null");
	Assert.isTrue(port > 0, "createCheck port must be greater than 0");
	//健康检查地址默认使用hostname+端口,也可以通过配置
	if (properties.getHealthCheckUrl() != null) {
		check.setHttp(properties.getHealthCheckUrl());
	}
	else {
		check.setHttp(String.format("%s://%s:%s%s", properties.getScheme(),
				properties.getHostname(), port, properties.getHealthCheckPath()));
	}
	check.setHeader(properties.getHealthCheckHeaders());
	check.setInterval(properties.getHealthCheckInterval());
	check.setTimeout(properties.getHealthCheckTimeout());
	check.setTlsSkipVerify(properties.getHealthCheckTlsSkipVerify());
	return check;
}

3、心跳检测

心跳检测是agent主动向server汇报自身健康状况的机制。当超过健康检查ttl时间没有汇报自身状态时,consul server认为应用进入了critical状态。

4、健康检查

spring cloud consul client响应健康检查是一个非常独特的请求链路,当consul server请求client的“/actuator/health”时,client又请求了consul server获得所有服务列表。只有在获得了所有服务列表时才认为服务是正常启动的。

  1. HealthEndpointWebExtension.health(SecurityContext)
  2. HealthEndpoint.health()
  3. CompositeHealthIndicator.health()
  4. DiscoveryCompositeHealthIndicator$Holder.health()
  5. DiscoveryClientHealthIndicator.health()
  6. CompositeDiscoveryClient.getServices()
  7. ConsulClient.getCatalogServices(QueryParams)
  8. CatalogConsulClient.getCatalogServices(QueryParams,String)
5、服务发现

consul agent通过http请求获得所有的可用服务。具体如果使用交由服务调用方。

在应用启动时创建了ConsulCatalogWatch,并创建了一个固定周期的线程。ConsulCatalogWatch.catalogServicesWatch()调用ConsulClient获得所有service,并发出HeartbeatEvent通知相关监听者更新服务内容。

//ConsulCatalogWatch
@Override
public void start() {
	if (this.running.compareAndSet(false, true)) {
		this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(
				this::catalogServicesWatch,
				this.properties.getCatalogServicesWatchDelay());
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RachelHwang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值