解决 Spring Cloud 的服务应用配置 context-path 后 Spring Boot Admin 监控不到信息的问题
在上篇文章中,讲解了 Spring Cloud 服务使用 Spring Boot Admin 监控的搭建,但是我在做公司的传统项目改造成微服务架构的过程中,在搭建 Spring Boot Admin 的时候,遇到了一个坑,有个服务配置了 context-path 这个属性,导致 Spring Boot Admin 一直获取不到这个服务的端点信息(当时我对 Spring Boot Admin 的使用、原理还不熟悉),现在通过 Spring Boot Admin 的部分源码分析来看看怎么解决这个问题,记录一下我踩到的坑。
(一)首先,我们看下服务配置了 context-path 属性后,不做其他配置,Spring Boot Admin 是什么样子。
拿之前文章里写的服务 spring-demo-service-feign 做例子
修改 spring-demo-service-feign 的配置文件,添加 context-path 的配置如下:
-
eureka:
-
client:
-
serviceUrl:
-
defaultZone: http://localhost:8761/eureka/
-
server:
-
port: 8382
-
<span style="color:#ff0000;"><strong>servlet:
-
context-path: /gateway</strong></span>
-
spring:
-
application:
-
name: spring-demo-service-feign
-
feign:
-
hystrix:
-
enabled: true
-
# Ribbon 的负载均衡策略
-
spring-demo-service:
-
ribbon:
-
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule
-
management:
-
endpoints:
-
web:
-
exposure:
-
include: '*'
-
endpoint:
-
health:
-
show-details: ALWAYS
-
info:
-
version: 1.0.0
其他的不用配置,以此启动 eureka server、spring-demo-service、spring-demo-service-feign、springboot-admin 服务
访问 http://localhost:8788/,登录后
可以看到,spring-demo-service-feign 的服务是 DOWN 的状态,点击 spring-demo-service-feign 查看
什么信息都没有,这让我很纳闷,当时不知道是 context-path 造成的,下面先说下解决方案,在通过源码简单分析一下。
(二)对上面的问题,我们可以通过再加几个属性配置来解决
修改 spring-demo-service-feign 的配置文件:
-
eureka:
-
client:
-
serviceUrl:
-
defaultZone: http://localhost:8761/eureka/
-
<span style="color:#ff0000;"># 如果项目配置有 server.servlet.context-path 属性,想要被 spring boot admin 监控,就要配置以下属性
-
instance:
-
metadata-map:
-
management:
-
context-path: /gateway/actuator
-
health-check-url: http://localhost:${server.port}/gateway/actuator/health
-
status-page-url: http://localhost:${server.port}/gateway/actuator/info
-
home-page-url: http://localhost:${server.port}/</span>
-
server:
-
port: 8382
-
servlet:
-
context-path: /gateway
-
spring:
-
application:
-
name: spring-demo-service-feign
-
feign:
-
hystrix:
-
enabled: true
-
# Ribbon 的负载均衡策略
-
spring-demo-service:
-
ribbon:
-
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule
-
management:
-
endpoints:
-
web:
-
exposure:
-
include: '*'
-
endpoint:
-
health:
-
show-details: ALWAYS
-
info:
-
version: 1.0.0
上面的红色部分的配置,就是解决方案,修改完后,重新启动 spring-demo-service-feign 服务,在来查看 Spring Boot Admin 如下
这个时候就会发现,spring-demo-service-feign 这个服务状态已经为 UP 了,点击 spring-demo-service-feign 进入查看,监控的信息也都有了,下面我们来分析一下为什么。
(三)简单的源码分析
Spring Boot Admin 的源码地址:https://github.com/codecentric/spring-boot-admin
我们是基于 Spring Cloud Eureka 的实现,源码相关的包为 de.codecentric.boot.admin.server.cloud,Spring Boot Admin 也是以心跳机制去监听 Eureka 上注册的实例,我们看到 de.codecentric.boot.admin.server.cloud.discovery 包下有个 InstanceDiscoveryListener 类,部分代码如下:
-
/**
-
* Listener for Heartbeats events to publish all services to the instance registry.
-
*
-
* @author Johannes Edmeier
-
*/
-
public class InstanceDiscoveryListener {
-
private static final Logger log = LoggerFactory.getLogger(InstanceDiscoveryListener.class);
-
private static final String SOURCE = "discovery";
-
private final DiscoveryClient discoveryClient;
-
private final InstanceRegistry registry;
-
private final InstanceRepository repository;
-
private final HeartbeatMonitor monitor = new HeartbeatMonitor();
-
private ServiceInstanceConverter converter = new DefaultServiceInstanceConverter();
-
/**
-
* Set of serviceIds to be ignored and not to be registered as application. Supports simple
-
* patterns (e.g. "foo*", "*foo", "foo*bar").
-
*/
-
private Set<String> ignoredServices = new HashSet<>();
-
/**
-
* Set of serviceIds that has to match to be registered as application. Supports simple
-
* patterns (e.g. "foo*", "*foo", "foo*bar"). Default value is everything
-
*/
-
private Set<String> services = new HashSet<>(Collections.singletonList("*"));
-
public InstanceDiscoveryListener(DiscoveryClient discoveryClient,
-
InstanceRegistry registry,
-
InstanceRepository repository) {
-
this.discoveryClient = discoveryClient;
-
this.registry = registry;
-
this.repository = repository;
-
}
-
......
-
protected Mono<InstanceId> registerInstance(ServiceInstance instance) {
-
try {
-
Registration registration = converter.convert(instance).toBuilder().source(SOURCE).build();
-
log.debug("Registering discovered instance {}", registration);
-
return registry.register(registration);
-
} catch (Exception ex) {
-
log.error("Couldn't register instance for service {}", instance, ex);
-
}
-
return Mono.empty();
-
}
-
......
-
}
在 registerInstance(ServiceInstance instance) 方法内打断点查看(因为心跳机制,几秒后会跳入)
我们可以看到注册进来的实例 instance 的所有属性,其中有 homePageUrl、statusPageUrl、healthCheckUrl 等,我们一步一步释放断点,当进入到 spring-demo-service-feign 的实例后查看如下:
可以看到 statusPageUrl、healthCheckUrl 正是我们之前在配置文件中配置的,这样 Spring Boot Admin 就可以获取服务实例的 health 和 info 的信息了,那除了这两个端点的信息,还有其他的信息怎么获取呢?下面我们接着看
registerInstance 方法里调用了 convert 这个方法,这个方法是在 ServiceInstanceConverter 接口定义的,源码如下:
-
public interface ServiceInstanceConverter {
-
/**
-
* Converts a service instance to a application instance to be registered.
-
*
-
* @param instance the service instance.
-
* @return Instance
-
*/
-
Registration convert(ServiceInstance instance);
-
}
这个没什么好说的,接口上也有注释。那么它的实现在哪呢?它的实现类是 DefaultServiceInstanceConverter,部分源码如下
-
public class DefaultServiceInstanceConverter implements ServiceInstanceConverter {
-
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultServiceInstanceConverter.class);
-
private static final String KEY_MANAGEMENT_PORT = "management.port";
-
private static final String KEY_MANAGEMENT_PATH = "management.context-path";
-
private static final String KEY_HEALTH_PATH = "health.path";
-
/**
-
* Default context-path to be appended to the url of the discovered service for the
-
* managment-url.
-
*/
-
private String managementContextPath = "/actuator";
-
/**
-
* Default path of the health-endpoint to be used for the health-url of the discovered service.
-
*/
-
private String healthEndpointPath = "health";
-
@Override
-
public Registration convert(ServiceInstance instance) {
-
LOGGER.debug("Converting service '{}' running at '{}' with metadata {}", instance.getServiceId(),
-
instance.getUri(), instance.getMetadata());
-
Registration.Builder builder = Registration.create(instance.getServiceId(), getHealthUrl(instance).toString());
-
URI managementUrl = getManagementUrl(instance);
-
if (managementUrl != null) {
-
builder.managementUrl(managementUrl.toString());
-
}
-
URI serviceUrl = getServiceUrl(instance);
-
if (serviceUrl != null) {
-
builder.serviceUrl(serviceUrl.toString());
-
}
-
Map<String, String> metadata = getMetadata(instance);
-
if (metadata != null) {
-
builder.metadata(metadata);
-
}
-
return builder.build();
-
}
-
protected URI getHealthUrl(ServiceInstance instance) {
-
String healthPath = instance.getMetadata().get(KEY_HEALTH_PATH);
-
if (isEmpty(healthPath)) {
-
healthPath = healthEndpointPath;
-
}
-
return UriComponentsBuilder.fromUri(getManagementUrl(instance)).path("/").path(healthPath).build().toUri();
-
}
-
protected URI getManagementUrl(ServiceInstance instance) {
-
String managamentPath = instance.getMetadata().get(KEY_MANAGEMENT_PATH);
-
if (isEmpty(managamentPath)) {
-
managamentPath = managementContextPath;
-
}
-
URI serviceUrl = getServiceUrl(instance);
-
String managamentPort = instance.getMetadata().get(KEY_MANAGEMENT_PORT);
-
if (isEmpty(managamentPort)) {
-
managamentPort = String.valueOf(serviceUrl.getPort());
-
}
-
return UriComponentsBuilder.fromUri(serviceUrl)
-
.port(managamentPort)
-
.path("/")
-
.path(managamentPath)
-
.build()
-
.toUri();
-
}
-
......
-
}
从 converter 方法中可以看到 先是判断有没有设置 managementUrl,通过 getManagementUrl 方法去获取我们的项目设置的 management.context-path,getManagementUrl 方法又是通过 instance.getMetadata().get(KEY_MANAGEMENT_PATH) 来获取的,所以我们在 spring-demo-service-feign 服务配置文件中配置了 eureka.instance.metadata-map.management.context-path(这个 metadata-map 是一个 map 集合,这里 key 是 management.context-path,value 就是我们配的 /gateway/actuator),Spring Boot Admin 拿到这个配置后,就可以获取到了其他端点的 url,进而就可以取到端点信息进行监控。
至此,源码的分析,差不多就能解决我们最初的问题了。
(四)对于上面的分析,可能会有一个疑问,既然配置了 eureka.instance.metadata-map.management.context-path 就可以拿到其他所有端点的信息了,那么为什么还要配置 healthUrl呢,这里就要说到心跳机制了,从源码类 InstanceDiscoveryListener 中看到有这个注释:Listener for Heartbeats events to publish all services to the instance registry. 可以看出,Spring Boot Admin 也是有心跳机制的,在 DefaultServiceInstanceConverter :: convert 方法中,第一件事就是要获取healthUrl(通过 getHealthUrl 方法 ),这里发现 DefaultServiceInstanceConverter 的convert 方法被它的子类 EurekaServiceInstanceConverter 重写了,源码如下:
-
public class EurekaServiceInstanceConverter extends DefaultServiceInstanceConverter {
-
@Override
-
protected URI getHealthUrl(ServiceInstance instance) {
-
Assert.isInstanceOf(EurekaServiceInstance.class, instance,
-
"serviceInstance must be of type EurekaServiceInstance");
-
InstanceInfo instanceInfo = ((EurekaServiceInstance) instance).getInstanceInfo();
-
String healthUrl = instanceInfo.getSecureHealthCheckUrl();
-
if (StringUtils.isEmpty(healthUrl)) {
-
healthUrl = instanceInfo.getHealthCheckUrl();
-
}
-
return URI.create(healthUrl);
-
}
-
}
这个方法也没什么,判断有没有 healthUrl,所以为什么要设置 healthUrl,我们也有了解了,Spring Boot Admin 就是通过health 实现心跳的。
至此,我们的分析也就结束,分析的非常笼统,简单,但是能满足我们的问题解决方案,有兴趣可以详细阅读 Spring Boot Admin 的源码