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

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

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

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

  <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-kubernetes-discovery</artifactId>
  </dependency>

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

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

    protected Map<String, String> 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<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;
   }

   @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<Void> removeStaleInstances(Set<InstanceId> 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<String> patterns) {
       return patterns.stream().anyMatch(pattern -> PatternMatchUtils.simpleMatch(pattern, serviceId));
   }

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

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

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

   public Set<String> getIgnoredServices() {
       return ignoredServices;
   }

   public Set<String> getServices() {
       return services;
   }

   public void setServices(Set<String> 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 到此就结束了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容