解决 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 的配置如下:

 
  1. eureka:

  2. client:

  3. serviceUrl:

  4. defaultZone: http://localhost:8761/eureka/

  5.  
  6. server:

  7. port: 8382

  8. <span style="color:#ff0000;"><strong>servlet:

  9. context-path: /gateway</strong></span>

  10. spring:

  11. application:

  12. name: spring-demo-service-feign

  13.  
  14. feign:

  15. hystrix:

  16. enabled: true

  17.  
  18. # Ribbon 的负载均衡策略

  19. spring-demo-service:

  20. ribbon:

  21. NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule

  22.  
  23. management:

  24. endpoints:

  25. web:

  26. exposure:

  27. include: '*'

  28. endpoint:

  29. health:

  30. show-details: ALWAYS

  31. info:

  32. version: 1.0.0

其他的不用配置,以此启动 eureka serverspring-demo-servicespring-demo-service-feignspringboot-admin 服务

访问 http://localhost:8788/,登录后

解决 Spring Cloud 的服务应用配置 context-path 后 Spring Boot Admin 监控不到信息的问题

可以看到,spring-demo-service-feign 的服务是 DOWN 的状态,点击 spring-demo-service-feign 查看

解决 Spring Cloud 的服务应用配置 context-path 后 Spring Boot Admin 监控不到信息的问题

什么信息都没有,这让我很纳闷,当时不知道是 context-path 造成的,下面先说下解决方案,在通过源码简单分析一下。

(二)对上面的问题,我们可以通过再加几个属性配置来解决

修改 spring-demo-service-feign 的配置文件:

 
  1. eureka:

  2. client:

  3. serviceUrl:

  4. defaultZone: http://localhost:8761/eureka/

  5.  
  6. <span style="color:#ff0000;"># 如果项目配置有 server.servlet.context-path 属性,想要被 spring boot admin 监控,就要配置以下属性

  7. instance:

  8. metadata-map:

  9. management:

  10. context-path: /gateway/actuator

  11. health-check-url: http://localhost:${server.port}/gateway/actuator/health

  12. status-page-url: http://localhost:${server.port}/gateway/actuator/info

  13. home-page-url: http://localhost:${server.port}/</span>

  14.  
  15. server:

  16. port: 8382

  17. servlet:

  18. context-path: /gateway

  19. spring:

  20. application:

  21. name: spring-demo-service-feign

  22.  
  23. feign:

  24. hystrix:

  25. enabled: true

  26.  
  27. # Ribbon 的负载均衡策略

  28. spring-demo-service:

  29. ribbon:

  30. NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule

  31.  
  32. management:

  33. endpoints:

  34. web:

  35. exposure:

  36. include: '*'

  37. endpoint:

  38. health:

  39. show-details: ALWAYS

  40. info:

  41. version: 1.0.0

上面的红色部分的配置,就是解决方案,修改完后,重新启动 spring-demo-service-feign 服务,在来查看 Spring Boot Admin 如下

解决 Spring Cloud 的服务应用配置 context-path 后 Spring Boot Admin 监控不到信息的问题

这个时候就会发现,spring-demo-service-feign 这个服务状态已经为 UP 了,点击 spring-demo-service-feign 进入查看,监控的信息也都有了,下面我们来分析一下为什么。

解决 Spring Cloud 的服务应用配置 context-path 后 Spring Boot Admin 监控不到信息的问题

(三)简单的源码分析

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 类,部分代码如下:

 
  1. /**

  2. * Listener for Heartbeats events to publish all services to the instance registry.

  3. *

  4. * @author Johannes Edmeier

  5. */

  6. public class InstanceDiscoveryListener {

  7. private static final Logger log = LoggerFactory.getLogger(InstanceDiscoveryListener.class);

  8. private static final String SOURCE = "discovery";

  9. private final DiscoveryClient discoveryClient;

  10. private final InstanceRegistry registry;

  11. private final InstanceRepository repository;

  12. private final HeartbeatMonitor monitor = new HeartbeatMonitor();

  13. private ServiceInstanceConverter converter = new DefaultServiceInstanceConverter();

  14.  
  15. /**

  16. * Set of serviceIds to be ignored and not to be registered as application. Supports simple

  17. * patterns (e.g. "foo*", "*foo", "foo*bar").

  18. */

  19. private Set<String> ignoredServices = new HashSet<>();

  20.  
  21. /**

  22. * Set of serviceIds that has to match to be registered as application. Supports simple

  23. * patterns (e.g. "foo*", "*foo", "foo*bar"). Default value is everything

  24. */

  25. private Set<String> services = new HashSet<>(Collections.singletonList("*"));

  26.  
  27. public InstanceDiscoveryListener(DiscoveryClient discoveryClient,

  28. InstanceRegistry registry,

  29. InstanceRepository repository) {

  30. this.discoveryClient = discoveryClient;

  31. this.registry = registry;

  32. this.repository = repository;

  33. }

  34.  
  35. ......

  36.  
  37. protected Mono<InstanceId> registerInstance(ServiceInstance instance) {

  38. try {

  39. Registration registration = converter.convert(instance).toBuilder().source(SOURCE).build();

  40. log.debug("Registering discovered instance {}", registration);

  41. return registry.register(registration);

  42. } catch (Exception ex) {

  43. log.error("Couldn't register instance for service {}", instance, ex);

  44. }

  45. return Mono.empty();

  46. }

  47.  
  48. ......

在 registerInstance(ServiceInstance instance) 方法内打断点查看(因为心跳机制,几秒后会跳入)

解决 Spring Cloud 的服务应用配置 context-path 后 Spring Boot Admin 监控不到信息的问题

我们可以看到注册进来的实例 instance 的所有属性,其中有 homePageUrl、statusPageUrl、healthCheckUrl 等,我们一步一步释放断点,当进入到 spring-demo-service-feign  的实例后查看如下:

解决 Spring Cloud 的服务应用配置 context-path 后 Spring Boot Admin 监控不到信息的问题

可以看到 statusPageUrl、healthCheckUrl 正是我们之前在配置文件中配置的,这样 Spring Boot Admin 就可以获取服务实例的 health 和 info 的信息了,那除了这两个端点的信息,还有其他的信息怎么获取呢?下面我们接着看

registerInstance 方法里调用了 convert 这个方法,这个方法是在 ServiceInstanceConverter 接口定义的,源码如下:

 
  1. public interface ServiceInstanceConverter {

  2.  
  3. /**

  4. * Converts a service instance to a application instance to be registered.

  5. *

  6. * @param instance the service instance.

  7. * @return Instance

  8. */

  9. Registration convert(ServiceInstance instance);

  10. }

这个没什么好说的,接口上也有注释。那么它的实现在哪呢?它的实现类是 DefaultServiceInstanceConverter,部分源码如下

 
  1. public class DefaultServiceInstanceConverter implements ServiceInstanceConverter {

  2. private static final Logger LOGGER = LoggerFactory.getLogger(DefaultServiceInstanceConverter.class);

  3. private static final String KEY_MANAGEMENT_PORT = "management.port";

  4. private static final String KEY_MANAGEMENT_PATH = "management.context-path";

  5. private static final String KEY_HEALTH_PATH = "health.path";

  6.  
  7. /**

  8. * Default context-path to be appended to the url of the discovered service for the

  9. * managment-url.

  10. */

  11. private String managementContextPath = "/actuator";

  12. /**

  13. * Default path of the health-endpoint to be used for the health-url of the discovered service.

  14. */

  15. private String healthEndpointPath = "health";

  16.  
  17. @Override

  18. public Registration convert(ServiceInstance instance) {

  19. LOGGER.debug("Converting service '{}' running at '{}' with metadata {}", instance.getServiceId(),

  20. instance.getUri(), instance.getMetadata());

  21.  
  22. Registration.Builder builder = Registration.create(instance.getServiceId(), getHealthUrl(instance).toString());

  23.  
  24. URI managementUrl = getManagementUrl(instance);

  25. if (managementUrl != null) {

  26. builder.managementUrl(managementUrl.toString());

  27. }

  28.  
  29. URI serviceUrl = getServiceUrl(instance);

  30. if (serviceUrl != null) {

  31. builder.serviceUrl(serviceUrl.toString());

  32. }

  33.  
  34. Map<String, String> metadata = getMetadata(instance);

  35. if (metadata != null) {

  36. builder.metadata(metadata);

  37. }

  38.  
  39. return builder.build();

  40. }

  41.  
  42. protected URI getHealthUrl(ServiceInstance instance) {

  43. String healthPath = instance.getMetadata().get(KEY_HEALTH_PATH);

  44. if (isEmpty(healthPath)) {

  45. healthPath = healthEndpointPath;

  46. }

  47.  
  48. return UriComponentsBuilder.fromUri(getManagementUrl(instance)).path("/").path(healthPath).build().toUri();

  49. }

  50.  
  51. protected URI getManagementUrl(ServiceInstance instance) {

  52. String managamentPath = instance.getMetadata().get(KEY_MANAGEMENT_PATH);

  53. if (isEmpty(managamentPath)) {

  54. managamentPath = managementContextPath;

  55. }

  56.  
  57. URI serviceUrl = getServiceUrl(instance);

  58. String managamentPort = instance.getMetadata().get(KEY_MANAGEMENT_PORT);

  59. if (isEmpty(managamentPort)) {

  60. managamentPort = String.valueOf(serviceUrl.getPort());

  61. }

  62.  
  63. return UriComponentsBuilder.fromUri(serviceUrl)

  64. .port(managamentPort)

  65. .path("/")

  66. .path(managamentPath)

  67. .build()

  68. .toUri();

  69. }

  70.  
  71. ......

  72. }

从 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 重写了,源码如下:

 
  1. public class EurekaServiceInstanceConverter extends DefaultServiceInstanceConverter {

  2.  
  3. @Override

  4. protected URI getHealthUrl(ServiceInstance instance) {

  5. Assert.isInstanceOf(EurekaServiceInstance.class, instance,

  6. "serviceInstance must be of type EurekaServiceInstance");

  7.  
  8. InstanceInfo instanceInfo = ((EurekaServiceInstance) instance).getInstanceInfo();

  9. String healthUrl = instanceInfo.getSecureHealthCheckUrl();

  10. if (StringUtils.isEmpty(healthUrl)) {

  11. healthUrl = instanceInfo.getHealthCheckUrl();

  12. }

  13. return URI.create(healthUrl);

  14. }

  15. }

这个方法也没什么,判断有没有 healthUrl,所以为什么要设置 healthUrl,我们也有了解了,Spring Boot Admin 就是通过health 实现心跳的。

至此,我们的分析也就结束,分析的非常笼统,简单,但是能满足我们的问题解决方案,有兴趣可以详细阅读 Spring Boot Admin 的源码