在上篇文章中,讲解了 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
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
其他的不用配置,以此启动 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/
# 如果项目配置有 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}/
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 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 services = new HashSet<>(Collections.singletonList("*"));
public InstanceDiscoveryListener(DiscoveryClient discoveryClient,
InstanceRegistry registry,
InstanceRepository repository) {
this.discoveryClient = discoveryClient;
this.registry = registry;
this.repository = repository;
}
......
protected Mono 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 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 的源码
源码下载:https://github.com/shmilyah/spring-cloud-componets