SpringCloudConsul源码分析(四)健康检查一

一、Consul健康检查的几种方式

参考:www.consul.io/docs/discov…

ConsulAgent负责健康检查工作,主要有下面几种方式

  • Script + Interval:定时执行脚本,根据脚本返回码确定应用是否存活,0表示存活,1表示警告,其他表示死亡。
  • HTTP + Interval:定时向应用某个端点发送http请求,根据http状态码确定应用是否存活,2xx表示存活。
  • TCP + Interval:定时发起tcp连接,超时时间内连接上表示存活。
  • Time to Live (TTL):应用在过期时间内,通过http的方式主动发送心跳给ConsulAgent,超过过期时间没有收到心跳表示死亡。
  • Docker + Interval:定时执行Docker exec命令,通过shell脚本的方式根据返回码确定应用是否存活。
  • gRPC + Interval:定时通过gRPC协议请求配置的端点,检测是否存活。

对于SpringCloudConsul来说,支持两种方式

  • HTTP + Interval
  • Time to Live (TTL)

无论是哪种方式,都是在首次调用Consul执行服务注册的报文中确定的。

二、服务注册回顾

先回顾一下,服务注册的入口是ConsulAutoServiceRegistrationListener,当Tomcat容器启动后,这里收到WebServerInitializedEvent,执行服务注册。

/**
 * 这里触发了服务注册,这个时间点是
 * 容器实例化所有单例bean后,finishRefresh阶段
 * org.springframework.context.support.AbstractApplicationContext#finishRefresh
 * 调用所有SmartLifecycle的bean的start方法
 * org.springframework.context.support.DefaultLifecycleProcessor#onRefresh
 * 调用WebServerStartStopLifecycle的start方法,servlet容器启动之后
 * org.springframework.boot.web.servlet.context.WebServerStartStopLifecycle#start
 * @param applicationEvent
 */
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
    if (applicationEvent instanceof WebServerInitializedEvent) {
        WebServerInitializedEvent event = (WebServerInitializedEvent) applicationEvent;
        ApplicationContext context = event.getApplicationContext();
        this.autoServiceRegistration.setPortIfNeeded(event.getWebServer().getPort());
        this.autoServiceRegistration.start();
    }
}
复制代码
  • autoServiceRegistration对应的是ConsulAutoServiceRegistration实例,它的start方法的流程是由父类规定的
ConsulAutoServiceRegistration
public void start() {
    super.start();
}
复制代码
  • 父类AbstractAutoServiceRegistration由spring-cloud-common提供抽象注册流程
public void start() {
    // 1. 判断ConsulAutoServiceRegistration.isEnabled
    if (!isEnabled()) {
        return;
    }
    if (!this.running.get()) {
        // 2. 发布InstancePreRegisteredEvent事件
        this.context.publishEvent(
                new InstancePreRegisteredEvent(this, getRegistration()));
        // 3. 调用ConsulAutoServiceRegistration.register
        // 实际是调用AbstractAutoServiceRegistration#register
        register();
        // 4. 发布InstanceRegisteredEvent事件
        this.context.publishEvent(
                new InstanceRegisteredEvent<>(this, getConfiguration()));
        // 5. 设置状态running=true
        this.running.compareAndSet(false, true);
    }

}
复制代码
  • AbstractAutoServiceRegistration#register

  调用ConsulServiceRegistry的register方法(http://localhost:8500/v1/agent/service/register)直接注册一个ConsulAutoRegistration实例

protected void register() {
    this.serviceRegistry.register(getRegistration());
}
复制代码

  这里getRegistration()是获取自动注入的ConsulAutoRegistration实例,关键点就是ConsulAutoRegistration里的数据组成的报文

@Bean
@ConditionalOnMissingBean
public ConsulAutoRegistration consulRegistration(
        AutoServiceRegistrationProperties autoServiceRegistrationProperties,
        ConsulDiscoveryProperties properties, ApplicationContext applicationContext,
        ObjectProvider<List<ConsulRegistrationCustomizer>> registrationCustomizers,
        ObjectProvider<List<ConsulManagementRegistrationCustomizer>> managementRegistrationCustomizers,
        HeartbeatProperties heartbeatProperties) {
    return ConsulAutoRegistration.registration(autoServiceRegistrationProperties,
            properties, applicationContext, registrationCustomizers.getIfAvailable(),
            managementRegistrationCustomizers.getIfAvailable(), heartbeatProperties);
}
复制代码

三、ConsulAutoRegistration

  ConsulAutoRegistration封装了NewService,而NewService是consul中对于服务实例的抽象,是向Consul注册的数据报文来源。

ConsulAutoRegistration.registration:构建ConsulAutoRegistration

public static ConsulAutoRegistration registration(
			AutoServiceRegistrationProperties autoServiceRegistrationProperties,
			ConsulDiscoveryProperties properties, ApplicationContext context,
			List<ConsulRegistrationCustomizer> registrationCustomizers,
			List<ConsulManagementRegistrationCustomizer> managementRegistrationCustomizers,
			HeartbeatProperties heartbeatProperties) {

	// NewService是consul中对于服务实例的抽象
	NewService service = new NewService();
	// 优先spring.cloud.consul.discovery.serviceName
	// 否则spring.application.name
	// 默认application
	String appName = getAppName(properties, context.getEnvironment());
	// instanceId
	service.setId(getInstanceId(properties, context));
	if (!properties.isPreferAgentAddress()) {
		// return this.preferIpAddress ? this.ipAddress : this.hostname
		service.setAddress(properties.getHostname());
	}
	service.setName(normalizeForDns(appName));
	service.setTags(createTags(properties));
	service.setEnableTagOverride(properties.getEnableTagOverride());
	service.setMeta(getMetadata(properties));

	if (properties.getPort() != null) {
		service.setPort(properties.getPort());
		// 设置Check属性,表示consul健康检查的方式
		setCheck(service, autoServiceRegistrationProperties, properties, context,
				heartbeatProperties);
	}

	ConsulAutoRegistration registration = new ConsulAutoRegistration(service,
			autoServiceRegistrationProperties, properties, context,
			heartbeatProperties, managementRegistrationCustomizers);
	// 触发所有ConsulRegistrationCustomizer的customize方法,一般是给consul注册服务实例打tag
	customize(registrationCustomizers, registration);
	return registration;
}
复制代码

setCheck方法:

public static void setCheck(NewService service,
			AutoServiceRegistrationProperties autoServiceRegistrationProperties,
			ConsulDiscoveryProperties properties, ApplicationContext context,
			HeartbeatProperties heartbeatProperties) {
	// spring.cloud.consul.discovery.registerHealthCheck = true
	if (properties.isRegisterHealthCheck() && service.getCheck() == null) {
		Integer checkPort = service.getPort();
		service.setCheck(createCheck(checkPort, heartbeatProperties, properties));
	}
}
复制代码

createCheck方法:设置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());
	}
	// 1.如果spring.cloud.consul.discovery.heartbeat.enabled=true(默认是false),采用ttl方式进行健康检查
	if (ttlConfig.isEnabled()) {
		// 默认ttl是30s
		check.setTtl(ttlConfig.getTtl());
		return check;
	}
	// 2. 否则采用http+interval的健康检查方式,consulAgent定时请求应用http端点,返回200表示健康
	// 如果自定义了healthCheckUrl,使用自定义的健康检查接口
	if (properties.getHealthCheckUrl() != null) {
		check.setHttp(properties.getHealthCheckUrl());
	}
	// 否则使用springboot-actuator自带的/actuator/health
	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;
}
复制代码

四、TTL健康检查的实现方式

ttl的实现方式是在注册结束后,把服务id封装为ConsulHeartbeatTask放入线程池,持续请求consul发送心跳

  • ConsulServiceRegistry#register注册并启动心跳
// 1. http://localhost:8500/v1/agent/service/register 向consul注册服务实例
this.client.agentServiceRegister(reg.getService(),
		this.properties.getAclToken());
NewService service = reg.getService();
// 2. 如果健康检查的方式是ttl方式,则提交一个定时任务,持续向consul agent发送心跳
// spring.cloud.consul.discovery.heartbeat.enable=true
if (this.heartbeatProperties.isEnabled() && this.ttlScheduler != null
		&& service.getCheck() != null
		&& service.getCheck().getTtl() != null) {
	this.ttlScheduler.add(reg.getInstanceId());
}

复制代码
  • ConsulHeartbeatAutoConfiguration注入了TtlScheduler
@Bean
@ConditionalOnMissingBean
public TtlScheduler ttlScheduler(HeartbeatProperties heartbeatProperties,
        ConsulClient consulClient) {
    return new TtlScheduler(heartbeatProperties, consulClient);
}
复制代码
  • ttlScheduler.add(String instanceId)把instanceId封装为ScheduledFuture放入线程池定时执行
public void add(String instanceId) {
	ScheduledFuture task = this.scheduler.scheduleAtFixedRate(
			new ConsulHeartbeatTask(instanceId), this.configuration
					.computeHeartbeatInterval().toStandardDuration().getMillis());
	ScheduledFuture previousTask = this.serviceHeartbeats.put(instanceId, task);
	if (previousTask != null) {
		previousTask.cancel(true);
	}
}
复制代码
private class ConsulHeartbeatTask implements Runnable {
	private String checkId;
	ConsulHeartbeatTask(String serviceId) {
		this.checkId = serviceId;
		if (!this.checkId.startsWith("service:")) {
			this.checkId = "service:" + this.checkId;
		}
	}
	/**
	 * 调用http://localhost:8500/v1/agent/check/pass/service:testConsulApp-8080向consul发送心跳
	 */
	@Override
	public void run() {
		TtlScheduler.this.client.agentCheckPass(this.checkId);
	}
}
复制代码

五、总结

  • SpringCloudConsul支持两种健康检查方式:1)http+interval 2)TTL。
  • SpringCloudConsul默认使用http+interval,默认使用springboot-actuator自带的/actuator/health端点,可以通过设置spring.cloud.consul.discovery.healthCheckUrl设置自定义的端点。
  • 通过设置spring.cloud.consul.discovery.heartbeat.enable=true可以将健康检查方式设置为TTL,由应用主动向consul发送心跳,过期时间默认30s,可以通过设置spring.cloud.consul.discovery.heartbeat.ttlValue来改变过期时间。
  • 服务注册: PUT http://localhost:8500/v1/agent/service/register
{
    "ID": "testConsulApp-8080",
    "Name": "testConsulApp",
    "Tags": [
        "secure=false"
    ],
    "Address": "127.0.0.1",
    "Meta": {},
    "Port": 8080,
    "Check": {
        "TTL": "30s"
    }
}
复制代码
  • 服务注销: PUT http://localhost:8500/v1/agent/service/deregister/testConsulApp-8080