spring cloud kubernetes 学习记录(5): spring boot admin

在 kubernetes 中,虽然有 dashboard 可以查看容器的状态,但对于Spring boot 应用来说,还需要 Spring boot Admin 来监控 内存、线程等信息。

在Spring boot Admin 中,并没有 kubernetes 的支持,需要添加一些配置。

首先先创建一个正常的 Spring boot Admin 项目,然后加入以下依赖:

  
            org.springframework.cloud
            spring-cloud-kubernetes-discovery
  

在Application类上,加入 @EnableDiscoveryClient 注解。

然后定义一个服务实例转换接口:

public interface ServiceInstanceConverter {

    /**
     * 转换服务实例为要注册的应用程序实例
     * @param instance the service instance.
     * @return Instance
     */
    Registration convert(ServiceInstance instance);
}

然后定义一个默认的实现:

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();
    }

    protected URI getServiceUrl(ServiceInstance instance) {
        return UriComponentsBuilder.fromUri(instance.getUri()).path("/").build().toUri();
    }

    protected Map getMetadata(ServiceInstance instance) {
        return instance.getMetadata();
    }


    public void setManagementContextPath(String managementContextPath) {
        this.managementContextPath = managementContextPath;
    }

    public String getManagementContextPath() {
        return managementContextPath;
    }

    public void setHealthEndpointPath(String healthEndpointPath) {
        this.healthEndpointPath = healthEndpointPath;
    }

    public String getHealthEndpointPath() {
        return healthEndpointPath;
    }
}

再定义一个 Kubernetes 的服务实例转换类:

public class KubernetesServiceInstanceConverter extends DefaultServiceInstanceConverter {

    @Override
    protected URI getHealthUrl(ServiceInstance instance) {
        Assert.isInstanceOf(KubernetesServiceInstance.class,
                instance,
                "serviceInstance must be of type KubernetesServiceInstance");
        return ((KubernetesServiceInstance) instance).getUri();
    }
}

然后定义一个实例发现监听类:

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;
   }

   @EventListener
   public void onApplicationReady(ApplicationReadyEvent event) {
       discover();
   }

   @EventListener
   public void onInstanceRegistered(InstanceRegisteredEvent event) {
       discover();
   }

   @EventListener
   public void onParentHeartbeat(ParentHeartbeatEvent event) {
       discoverIfNeeded(event.getValue());
   }

   @EventListener
   public void onApplicationEvent(HeartbeatEvent event) {
       discoverIfNeeded(event.getValue());
   }

   private void discoverIfNeeded(Object value) {
       if (this.monitor.update(value)) {
           discover();
       }
   }

   protected void discover() {
       Flux.fromIterable(discoveryClient.getServices())
               .filter(this::shouldRegisterService)
               .flatMapIterable(discoveryClient::getInstances)
               .flatMap(this::registerInstance)
               .collect(Collectors.toSet())
               .flatMap(this::removeStaleInstances)
               .subscribe(v -> { }, ex -> log.error("Unexpected error.", ex));
   }

   protected Mono removeStaleInstances(Set registeredInstanceIds) {
       return repository.findAll()
               .filter(instance -> SOURCE.equals(instance.getRegistration().getSource()))
               .map(Instance::getId)
               .filter(id -> !registeredInstanceIds.contains(id))
               .doOnNext(id -> log.info("Instance ({}) missing in DiscoveryClient services ", id))
               .flatMap(registry::deregister)
               .then();
   }

   protected boolean shouldRegisterService(final String serviceId) {
       boolean shouldRegister = matchesPattern(serviceId, services) && !matchesPattern(serviceId, ignoredServices);
       if (!shouldRegister) {
           log.debug("Ignoring discovered service {}", serviceId);
       }
       return shouldRegister;
   }

   protected boolean matchesPattern(String serviceId, Set patterns) {
       return patterns.stream().anyMatch(pattern -> PatternMatchUtils.simpleMatch(pattern, serviceId));
   }

   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();
   }

   public void setConverter(ServiceInstanceConverter converter) {
       this.converter = converter;
   }

   public void setIgnoredServices(Set ignoredServices) {
       this.ignoredServices = ignoredServices;
   }

   public Set getIgnoredServices() {
       return ignoredServices;
   }

   public Set getServices() {
       return services;
   }

   public void setServices(Set services) {
       this.services = services;
   }
}

最后定义一个自动配置类:

@Configuration
@ConditionalOnSingleCandidate(DiscoveryClient.class)
@ConditionalOnBean(AdminServerMarkerConfiguration.Marker.class)
@ConditionalOnProperty(prefix = "spring.boot.admin.discovery", name = "enabled", matchIfMissing = false)
@AutoConfigureAfter(value = AdminServerAutoConfiguration.class, name = {
       "org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClientAutoConfiguration",
       "org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration"})
public class AdminServerDiscoveryAutoConfiguration {

   @Bean
   @ConditionalOnMissingBean
   @ConfigurationProperties(prefix = "spring.boot.admin.discovery")
   public InstanceDiscoveryListener instanceDiscoveryListener(ServiceInstanceConverter serviceInstanceConverter,
                                                              DiscoveryClient discoveryClient,
                                                              InstanceRegistry registry,
                                                              InstanceRepository repository) {
       InstanceDiscoveryListener listener = new InstanceDiscoveryListener(discoveryClient, registry, repository);
       listener.setConverter(serviceInstanceConverter);
       return listener;
   }

   @Configuration
   @ConditionalOnMissingBean({ServiceInstanceConverter.class})
   @ConditionalOnBean(KubernetesClient.class)
   public static class KubernetesConverterConfiguration {
       @Bean
       @ConfigurationProperties(prefix = "spring.boot.admin.discovery.converter")
       public KubernetesServiceInstanceConverter serviceInstanceConverter() {
           return new KubernetesServiceInstanceConverter();
       }
   }

   @Configuration
   public static class SecuritySecureConfig extends WebSecurityConfigurerAdapter {
       private final String adminContextPath;

       public SecuritySecureConfig(AdminServerProperties adminServerProperties) {
           this.adminContextPath = adminServerProperties.getContextPath();
       }

       @Override
       protected void configure(HttpSecurity http) throws Exception {
           SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
           successHandler.setTargetUrlParameter("redirectTo");
           successHandler.setDefaultTargetUrl(adminContextPath + "/");

           http.authorizeRequests()
                   .antMatchers(adminContextPath + "/assets/**").permitAll() 
                   .antMatchers(adminContextPath + "/login").permitAll()
                   .antMatchers(adminContextPath + "/actuator/**").permitAll()
                   .anyRequest().authenticated() 
                   .and()
                   .formLogin().loginPage(adminContextPath + "/login").successHandler(successHandler).and() 
                   .logout().logoutUrl(adminContextPath + "/logout").and()
                   .httpBasic().and() 
                   .csrf()
                   .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())  
                   .ignoringAntMatchers(
                           adminContextPath + "/instances",   
                           adminContextPath + "/actuator/**"  
                   );
       }
   }

}

在 application.yaml 中,加入配置:

spring:
  boot:
    admin:
      discovery:
        enabled: true

部署到 Kubernetes 中,即可访问。

但启动之后,会发现 Spring boot Admin 会显示 Kubernetes 中所有的 service,这没有任何意义,并会引发报错,所以需要过滤掉不是 Spring Boot 的项目。

这里,要用到 spring boot Kubernetes discovery 的 lable 过滤功能,可以根据 service 的 lable 进行过滤,只展示对应 lable 的 service。

在 Kubernetes 的 Service yaml 中,加入以下属性:

metadata:
  labels:
    admin: admin-enabled

给对应的 service 加入 admin: admin-enabled lable,并在application.yaml 中加入以下配置即可:

spring:
  cloud:
    kubernetes:
      discovery:
        serviceLabels:
          admin: admin-enabled

查看以下所有的 service:

services

可以看到有三个,用 lable 过滤一下:


services

只有两个,打开 spring boot admin 看一下:


spring boot admin

spring boot admin 到此就结束了。

你可能感兴趣的:(spring cloud kubernetes 学习记录(5): spring boot admin)