解决 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
  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 Cloud 的服务应用配置 context-path 后 Spring Boot Admin 监控不到信息的问题_第1张图片

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

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

什么信息都没有,这让我很纳闷,当时不知道是 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 Cloud 的服务应用配置 context-path 后 Spring Boot Admin 监控不到信息的问题_第3张图片

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

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

(三)简单的源码分析

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) 方法内打断点查看(因为心跳机制,几秒后会跳入)

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

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

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

可以看到 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

 

 

 

 

 

你可能感兴趣的:(springboot,Spring,Cloud,Spring,Cloud,应用篇,Spring,Cloud,Finchley)