在 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:
可以看到有三个,用 lable 过滤一下:
只有两个,打开 spring boot admin 看一下:
spring boot admin 到此就结束了。