Discovery Client OUT_OF_SERVICE 临时修复

问题描述

eureka client开启健康检查,actuator开启probes并且显示所有详情,确保不是其它indicator导致的。

spring cloud版本Hoxton.SR10,spring boot版本2.3.9.RELEASE,k8s的版本不重要。

服务启动后,eureka server显示服务状态为OUT_OF_SERVICE,调用服务actuator health端点,除了eureka server indicator显示的是OUT_OF_SERVICE,其它全部是up。

问题查找

搜遍整个google,没有解决办法,但是github上有个issue,看了下comments,基本上也帮不上忙,开始debug。

确定方向

spring cloud netflix eureka会有EurekaHealthCheckHandler将服务的健康状态同步到eureka的InstanceStatus,所以确定是由健康检查的indicators引起的。

源码查看

通过EurekaHealthCheckHandler的getStatus的方法,找到调用方为DiscoveryClient的refreshInstanceInfo方法,

然后再找到refreshInstanceInfo的调用方是InstanceInfoReplicator的run方法,然后再找到InstanceInfoReplicator线程初始化的代码在DiscoveryClient的initScheduledTasks。

那就清楚了,discovery client初始化的时候会启动一个定时任务,定时去获取InstanceInfo。我们继续debug。

EurekaHealthCheckHandler的getStatus在第一次调用时会返回OUT_OF_SERVICE,然后接下来只要服务没有问题,会一直返回UP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected Status getStatus(StatusAggregator statusAggregator) {
Status status;

Set<Status> statusSet = new HashSet<>();
if (healthIndicators != null) {
// 第一次调用时会返回OUT_OF_SERVICE
statusSet.addAll(
healthIndicators.values().stream().map(HealthIndicator::health)
.map(Health::getStatus).collect(Collectors.toSet()));
}

if (reactiveHealthIndicators != null) {
statusSet.addAll(reactiveHealthIndicators.values().stream()
.map(ReactiveHealthIndicator::health).map(Mono::block)
.filter(Objects::nonNull).map(Health::getStatus)
.collect(Collectors.toSet()));
}

status = statusAggregator.getAggregateStatus(statusSet);
return status;
}

debug发现是ReadinessStateHealthIndicator返回OUT_OF_SERVICE。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ReadinessStateHealthIndicator extends AvailabilityStateHealthIndicator {

public ReadinessStateHealthIndicator(ApplicationAvailability availability) {
// 注意,这里会有个mapping,如果REFUSING_TRAFFIC,就会是OUT_OF_SERVICE
super(availability, ReadinessState.class, (statusMappings) -> {
statusMappings.add(ReadinessState.ACCEPTING_TRAFFIC, Status.UP);
statusMappings.add(ReadinessState.REFUSING_TRAFFIC, Status.OUT_OF_SERVICE);
});
}

@Override
protected AvailabilityState getState(ApplicationAvailability applicationAvailability) {
return applicationAvailability.getReadinessState();
}

}

继续往里看,找到applicationAvailability的实现类ApplicationAvailabilityBean,找到调用方法getState。

1
2
3
4
5
@Override
public <S extends AvailabilityState> S getState(Class<S> stateType) {
AvailabilityChangeEvent<S> event = getLastChangeEvent(stateType);
return (event != null) ? event.getState() : null;
}

代码逻辑很简单,获取事件后,再获取内部的state。ApplicationAvailabilityBean实现了ApplicationListener,那么现在就明白了,在调用getState之前还未接收到事件。

1
2
3
4
5
6
@Override
@SuppressWarnings("unchecked")
public <S extends AvailabilityState> AvailabilityChangeEvent<S> getLastChangeEvent(Class<S> stateType) {
// this.events 是空的
return (AvailabilityChangeEvent<S>) this.events.get(stateType);
}

问题原因

在服务未完全启动的时候就调用了indicator的getState的地方有三处,第一处,没有问题。

1
2
3
4
5
6
7
8
public void start(int initialDelayMs) {
if (started.compareAndSet(false, true)) {
instanceInfo.setIsDirty(); // for initial register
// 这里有延迟,不会有问题,服务延迟设置是5s
Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}

第二处,没有问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
@Override
public String getId() {
return "statusChangeListener";
}

@Override
public void notify(StatusChangeEvent statusChangeEvent) {
// 这里大概率只会在服务启动完后才会调用
logger.info("Saw local status change event {}", statusChangeEvent);
instanceInfoReplicator.onDemandUpdate();
}
};

第三处,那肯定是这了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void registerHealthCheck(HealthCheckHandler healthCheckHandler) {
if (instanceInfo == null) {
logger.error("Cannot register a healthcheck handler when instance info is null!");
}
if (healthCheckHandler != null) {
this.healthCheckHandlerRef.set(healthCheckHandler);
// schedule an onDemand update of the instanceInfo when a new healthcheck handler is registered
if (instanceInfoReplicator != null) {
// 没有延迟,在configuration的时候就调用了,这个时候服务不一定启动好
instanceInfoReplicator.onDemandUpdate();
}
}
}

修复方法

  1. actuator关闭probes;
  2. eureka client关闭健康检查;
  3. 修改ReadinessStateHealthIndicator ReadinessState.REFUSING_TRAFFIC map为 Status.DOWN(DOWN和OUT_OF_SERVICE区别还是很大的,和ReadinessStateHealthIndicator的设计思想有出入。);
  4. 如果events为空,ApplicationAvailabilityBean的getState返回ReadinessState.ACCEPTING_TRAFFIC。

修复方法分析

  1. 我们的服务是部署在k8s上的并且开启了探针,升级到这个版本有一部分是为了这个探针,no;
  2. 关闭后可能因为一些原因,会导致服务不可用,但是服务状态还是UP,no;
  3. 无法修改源码,no;
  4. events为空的时候,ReadinessState怎么能是ACCEPTING_TRAFFIC?那怎么办?先让代码跑起来吧!等待官方修复。

临时修复

主要思路就是,继承ApplicationAvailabilityBean,重写getState,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class IvcApplicationAvailabilityBean extends ApplicationAvailabilityBean {
@Override
public <S extends AvailabilityState> S getState(Class<S> stateType) {
AvailabilityChangeEvent<S> event = getLastChangeEvent(stateType);
// 源码如果event为空会直接返回null,返回null会触发调用方法的default,所以这里我直接返回我的default
return (event != null) ? event.getState() : getDefaultState(stateType);
}

@SuppressWarnings("unchecked")
private <S extends AvailabilityState> S getDefaultState(Class<S> stateType) {
if (stateType == LivenessState.class) {
// return null也是可以的
return (S)LivenessState.BROKEN;
} else if (stateType == ReadinessState.class) {
// 丑陋的代码
return (S)ReadinessState.ACCEPTING_TRAFFIC;
}
return null;
}
}

替换官方的ApplicationAvailabilityBean,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class IvcBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
String beanName = "applicationAvailability";

BeanDefinitionBuilder beanDefinitionBuilder =
BeanDefinitionBuilder.rootBeanDefinition(IvcApplicationAvailabilityBean.class);

beanDefinitionRegistry.registerBeanDefinition(beanName, beanDefinitionBuilder.getBeanDefinition());
}

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory)
throws BeansException {

}
}

好了,这下子如果events为空,ReadinessState会返回ACCEPTING_TRAFFIC,mapping后就不会出现OUT_OF_SERVICE了.

可能的官方修法

Eureka Server可以手动设置服务状态为OUT_OF_SERVICE,然后要UP的话需要手动delete。

那么健康检查的OUT_OF_SERVICE,是不是也可以手动delete(至少现在不行)。