在上篇文章中,讲解了 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 的源码