Spring Boot Admin2 实例状态监控详解

Posted 阿提说说

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Boot Admin2 实例状态监控详解相关的知识,希望对你有一定的参考价值。

其他相关文章:

  1. Spring Boot Admin 参考指南
  2. SpringBoot Admin服务离线、不显示健康信息的问题
  3. Spring Boot Admin2 @EnableAdminServer的加载
  4. Spring Boot Admin2 AdminServerAutoConfiguration详解

在微服务中集成Spring Boot Admin 的主要作用之一就是用来监控服务的实例状态,并且最好是当服务DOWN或者OFFLINE的时候发消息提醒,SBA2 提供了很多提醒方式,并且SBA2 已经集成了钉钉,只要进行少量配置即可将状态变更发送到钉钉,详见我的另外一篇文章《Spring Boot Admin 参考指南》。

SBA2 接入飞书

这里我要说明如何进行自定义提醒,将飞书提醒集成到SBA2中,顺便看看SBA2的状态监控具体是如何实现的。

  1. 定义配置类

FeiShuNotifierConfiguration

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnProperty(prefix = "spring.boot.admin.notify.feishu",  name = "enabled", havingValue = "true")
	@AutoConfigureBefore( AdminServerNotifierAutoConfiguration.NotifierTriggerConfiguration.class, AdminServerNotifierAutoConfiguration.CompositeNotifierConfiguration.class )
	@Lazy(false)
	public static class FeiShuNotifierConfiguration 

		@Bean
		@ConditionalOnMissingBean
		@ConfigurationProperties("spring.boot.admin.notify.feishu")
		public FeiShuNotifier feiShuNotifier(InstanceRepository repository,
											 NotifierProxyProperties proxyProperties) 
			return new FeiShuNotifier(repository, createNotifierRestTemplate(proxyProperties));
		

	

这里是模仿SBA2 定义其他通知的方式,InstanceRepository 用于实例持久化操作,NotifierProxyProperties 是通知的HTTP代理配置

  1. 定义消息提醒实现
public class FeiShuNotifier extends AbstractStatusChangeNotifier implements AlarmMessage 

	private static final String DEFAULT_MESSAGE = " 服务名称:#instance.registration.name \\n 服务实例:#instance.id \\n 服务URL:#instance.registration.serviceUrl \\n 服务状态:【#event.statusInfo.status】 \\n 发送时间:#time";

	private final SpelExpressionParser parser = new SpelExpressionParser();

	private RestTemplate restTemplate;

	private String webhookUrl;

	private String secret;

	private Expression message;


	public FeiShuNotifier(InstanceRepository repository, RestTemplate restTemplate) 
		super(repository);
		this.restTemplate = restTemplate;
		this.message = parser.parseExpression(DEFAULT_MESSAGE, ParserContext.TEMPLATE_EXPRESSION);
	

	@Override
	protected Mono<Void> doNotify(InstanceEvent event, Instance instance) 
		return Mono.fromRunnable(() -> sendNotify(event, instance));
	

	@Override
	protected void updateLastStatus(InstanceEvent event) 
		//原有的更新最后实例状态,在重启后会删除最后状态,导致实例重启后会过滤掉UNKNOWN:UP通知,这里在重启注册后,将最后的状态重新更新会实例中
		//如此实例的变化状态为OFFLINE:UP
		//还有一种办法是:重写shouldNotify(),去掉UNKNOWN:UP,不过滤该通知,也能够收到UP通知,但如此会在Admin重启的时候,所有服务的通知都会发一遍
		if (event instanceof InstanceDeregisteredEvent) 
			String lastStatus = getLastStatus(event.getInstance());
			StatusInfo statusInfo = StatusInfo.valueOf(lastStatus);
			InstanceStatusChangedEvent instanceStatusChangedEvent = new InstanceStatusChangedEvent(event.getInstance(), event.getVersion(), statusInfo);
			super.updateLastStatus(instanceStatusChangedEvent);
		
		if (event instanceof InstanceStatusChangedEvent) 
			super.updateLastStatus(event);
		
	

	private void sendNotify(InstanceEvent event, Instance instance) 
		sendData(getText(event, instance));
	

	@Override
	public void sendData(String content) 
		if (!isEnabled()) 
			return;
		
		Map<String, Object> message = createMessage(content);
		doSendData(JSONObject.toJSONString(message));
	

	private void doSendData(String message) 
		sendWebData(message);
	

	private void sendWebData(String message) 
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_JSON);
		restTemplate.postForEntity(webhookUrl, new HttpEntity<>(message, headers), Void.class);
	

	protected Map<String, Object> createMessage(String content) 
		Map<String, Object> messageJson = new HashMap<>();
		messageJson.put("msg_type", "text");

		Map<String, Object> text = new HashMap<>();
		text.put("text", content);
		messageJson.put("content", text);
		Long timestamp = System.currentTimeMillis() / 1000;
		messageJson.put("timestamp", timestamp);
		messageJson.put("sign", getSign(timestamp));

		return messageJson;
	

	private String getText(InstanceEvent event, Instance instance) 
		Map<String, Object> root = new HashMap<>();
		root.put("event", event);
		root.put("instance", instance);
		root.put("lastStatus", getLastStatus(event.getInstance()));
		root.put("time", DateUtil.now());
		StandardEvaluationContext context = new StandardEvaluationContext(root);
		context.addPropertyAccessor(new MapAccessor());
		return message.getValue(context, String.class);
	

	private String getSign(Long timestamp) 
		try 
			String stringToSign = timestamp + "\\n" + secret;
			Mac mac = Mac.getInstance("HmacSHA256");
			mac.init(new SecretKeySpec(stringToSign.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
			byte[] signData = mac.doFinal(new byte[]);
			return new String(Base64.encodeBase64(signData));
		
		catch (Exception ex) 
			ex.printStackTrace();
		
		return "";
	

	public void setRestTemplate(RestTemplate restTemplate) 
		this.restTemplate = restTemplate;
	

	public String getWebhookUrl() 
		return webhookUrl;
	

	public void setWebhookUrl(String webhookUrl) 
		this.webhookUrl = webhookUrl;
	

	public String getSecret() 
		return secret;
	

	public void setSecret(String secret) 
		this.secret = secret;
	

	public String getMessage() 
		return message.getExpressionString();
	

	public void setMessage(String message) 
		this.message = parser.parseExpression(message, ParserContext.TEMPLATE_EXPRESSION);
	

这里继承了AbstractStatusChangeNotifier 用来处理实例状态变更通知,AlarmMessage 是我自己将消息发送给抽象了出来,以便其他告警也能调用。其他都比较简单,飞书的群提醒请参考飞书文档

另外,这里重写了updateLastStatus方法,在取消注册的时候将实例的最后一次状态重新更新到实例中,因为在测试中,实例如果重启,实例状态变为OFFLINE,但重启完成后,却没有收到UP的消息,查看源码后,SBA2在实例取消注册的时候,删除实例的最后一次状态,导致实例的状态变成UNKNOWN,而SBA2里面shouldNotify方法又会过滤UNKNOWN:UP的状态变更。后面会详细看下这部分的源码。

通过如上两步即可接入飞书,看效果图:

状态监控源码分析

从《Spring Boot Admin2 AdminServerAutoConfiguration详解》这篇文章我们可以知道,在SBA2启动的时候,会加载StatusUpdaterStatusUpdateTrigger,前者用于更新实例状态,后者用来触发状态更新,这两个类我将从头至下分部说明。

StatusUpdateTrigger

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

	private final StatusUpdater statusUpdater;

	private final IntervalCheck intervalCheck;

	public StatusUpdateTrigger(StatusUpdater statusUpdater, Publisher<InstanceEvent> publisher) 
		super(publisher, InstanceEvent.class);
		this.statusUpdater = statusUpdater;
		this.intervalCheck = new IntervalCheck("status", this::updateStatus);
	

StatusUpdateTrigger 继承自AbstractEventHandler类,通过构造函数,传入StatusUpdater 更新状态实例,Publisher 接收一个InstanceEvent事件。
super(publisher, InstanceEvent.class) 调用父类构造方法,将Publisher和要关注的事件InstanceEvent传入。
this.intervalCheck = new IntervalCheck(“status”, this::updateStatus) 创建了一个定时任务用来检查实例状态

接下来看下StatusUpdateTrigger 的父类AbstractEventHandler

AbstractEventHandler.start

	public void start() 
		this.scheduler = this.createScheduler();
		this.subscription = Flux.from(this.publisher).subscribeOn(this.scheduler).log(this.log.getName(), Level.FINEST)
				.doOnSubscribe((s) -> this.log.debug("Subscribed to  events", this.eventType)).ofType(this.eventType)
				.cast(this.eventType).transform(this::handle)
				.retryWhen(Retry.indefinitely().doBeforeRetry((s) -> this.log.warn("Unexpected error", s.failure())))
				.subscribe();
	

AbstractEventHandler 的start 方法会在StatusUpdateTrigger 初始化@Bean(initMethod = "start", destroyMethod = "stop") 中被调用,这里其创建了一个定时任务,并订阅了指定的事件类型eventType,如果监听到了感兴趣的事件,会调用handle方法,该方法由子类实现

StatusUpdateTrigger.handle

	@Override
	protected Publisher<Void> handle(Flux<InstanceEvent> publisher) 
		return publisher
				.filter((event) -> event instanceof InstanceRegisteredEvent
						|| event instanceof InstanceRegistrationUpdatedEvent)
				.flatMap((event) -> updateStatus(event.getInstance()));
	

在StatusUpdateTrigger 中 如果事件类型是InstanceRegisteredEvent(实例注册事件)或者InstanceRegistrationUpdatedEvent(实例更新事件),会调用更新实例状态方法
updateStatus

StatusUpdateTrigger.updateStatus

	protected Mono<Void> updateStatus(InstanceId instanceId) 
		return this.statusUpdater.updateStatus(instanceId).onErrorResume((e) -> 
			log.warn("Unexpected error while updating status for ", instanceId, e);
			return Mono.empty();
		).doFinally((s) -> this.intervalCheck.markAsChecked(instanceId));
	

StatusUpdateTrigger.updateStatus 调用了构造函数传入的StatusUpdater Bean 的updateStatus,执行具体的查询实例状态、更新实例状态操作,最后更新该实例的最后检查时间。

StatusUpdateTrigger.start/stop

	@Override
	public void start() 
		super.start();
		this.intervalCheck.start();
	

	@Override
	public void stop() 
		super.stop();
		this.intervalCheck.stop();
	

StatusUpdateTrigger 最后是Bean初始化调用方法start和销毁时调用的stop方法,分别用于启动其父类AbstractEventHandler的事件监听,和 IntervalCheck 的定时状态检查任务

StatusUpdater

StatusUpdater 是真正去查询实例状态,并更新实例的类,我们在StatusUpdateTrigger.updateStatus中已经看到其会请求StatusUpdater.updateStatus

	public Mono<Void> updateStatus(InstanceId id) 
		return this.repository.computeIfPresent(id, (key, instance) -> this.doUpdateStatus(instance)).then();

	

repository.computeIfPresent 会调用EventsourcingInstanceRepository.computeIfPresent,表示实例id存在的话,执行doUpdateStatus并更新状态,doUpdateStatus 会查询实例最新状态,并通过Instance.withStatusInfo包装成一个新的Instance 对象。

EventsourcingInstanceRepository.computeIfPresent

	@Override
	public Mono<Instance> computeIfPresent(InstanceId id,
			BiFunction<InstanceId, Instance, Mono<Instance>> remappingFunction) 
		return this.find(id).flatMap((application) -> remappingFunction.apply(id, application)).flatMap(this::save)
				.retryWhen(this.retryOptimisticLockException);
	

其中this::save 用来保存实例事件,此处为状态变更事件

EventsourcingInstanceRepository.save

	public Mono<Instance> save(Instance instance) 
		return this.eventStore.append(instance.getUnsavedEvents()).then(Mono.just(instance.clearUnsavedEvents()));
	

eventStore 实际调用的是在AdminServerAutoConfiguration中加载的InMemoryEventStore

InMemoryEventStore.append

	public Mono<Void> append(List<InstanceEvent> events) 
		return super.append(events).then(Mono.fromRunnable(() -> this.publish(events)));
	

该方法将在事件保存后,发送一个Publish,这样实现了AbstractEventHandler<InstanceEvent>的类就能监听到该变更事件。

AdminServerNotifierAutoConfiguration

当SBA2中存在通知相关的Notifier Bean时,会开启NotificationTrigger,用来发送变更事件通知

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnBean(Notifier.class)
	@Lazy(false)
	public static class NotifierTriggerConfiguration 

		@Bean(initMethod = "start", destroyMethod = "stop")
		@ConditionalOnMissingBean(NotificationTrigger.class)
		public NotificationTrigger notificationTrigger(Notifier notifier, Publisher<InstanceEvent> events) 
			return new NotificationTrigger(notifier, events);
		

	

NotificationTrigger 道理同 StatusUpdateTrigger 。

NotificationTrigger.sendNotifications

	protected Mono<Void> sendNotifications(InstanceEvent event) 
		return this.notifier.notify(event).doOnError((e) -> log.warn("Couldn't notify for event  ", event, e))
				.onErrorResume((e) -> Mono.empty()1 Spring Boot Actuator 

Spring Boot Actuator 是 Spring Boot 提供的对应用的自省和监控功能,如健康检查,审计,指标收集,HTTP 跟踪等,可以帮助开发和运维人员监控和管理 Spring Boot 应用。该模块采集应用的内部信息,并暴露给外部的模块,支持 HTTP 和 JMX,并可以与一些第三方监控系统(如 Prometheus)整合。

1.1 Actuator endpoint
端点 Endpoint 是 Actuator 的核心组成部分,用来监视应用程序的各种状态。 Spring Boot Actuator 内置很多 Endpoint,总体上看分成三类:

应用配置类:主要包括配置信息、Spring Bean 的信息、配置文件信息、环境信息等;
度量指标类:应用在运行期间的信息,包括堆栈、健康状态、线程池信息、HTTP请求统计等;
操作控制类:如 shutdown,提供了对应用的关闭等操作类功能。
1.2 添加依赖
在 pom.xml 中添加 Actuator 的 starter:

org.springframework.boot spring-boot-starter-actuator 1.3 访问端点 添加依赖后,启动服务,可通过如下请求查看暴露的端点:

http://localhost:9099/actuator

该请求返回:


“_links”:
“self”:
“href”: “http://localhost:9099/actuator”,
“templated”: false
,
“health-path”:
“href”: “http://localhost:9099/actuator/health/*path”,
“templated”: true
,
“health”:
“href”: “http://localhost:9099/actuator/health”,
“templated”: false



从返回的结果可以看出默认只开放了 /actuator/health 端点。访问该端点 http://localhost:9099/actuator/health:

“status”:“UP”
其他未开放的端点可以独立配置开启或禁用。

在 application.yml 中添加如下配置,开放所有的端点,并显示详细的 health:

management:
endpoints:
web:
exposure:
include: ‘*’
endpoint:
health:
show-details: always
重启服务,再次查看暴露的端点,可以看出有如下端点:

2 Spring Boot Admin

Spring Boot Actuator 提供了各种端点,Spring Boot Admin 能够将 Actuator 中的信息进行界面化的展示,并提供实时报警功能。

在微服务环境中,使用 Spring Boot Admin,通常包括服务端和客户端,服务端只运行 Spring Boot Admin Server,收集各个客户端的数据,并以可视化界面显示出来。客户端运行 Spring Boot Admin Client,或者通过服务发现与注册获取应用的信息。

这里的 demo 我就不在 Spring Boot Admin Server了,将当前 hero-springboot-demo 既作为 server、也作为 client 使用。在后面的实战篇章中会独立 Admin Server,同时客户端也不使用 client,而是通过服务注册与发现。

2.1 添加依赖
在 pom.xml 中添加 Spring Boot Admin Server 的依赖:

de.codecentric spring-boot-admin-starter-server 2.7.4 de.codecentric spring-boot-admin-starter-client 2.7.4 需要注意版本号,由于 Spring Boot 版本使用的是 2.7.x,Spring Boot Admin Server 的版本也要用 2.7.x,千万别乱搞!

2.2 开启 Admin Server
在启动类 DemoApplication 上添加注解 @EnableAdminServer 开启 Spring Boot Admin Server。

@EnableAdminServer
@EnableAsync
@MapperScan(“com.yygnb.demo.mapper”)
@SpringBootApplication
public class DemoApplication


2.3 配置客户端
在 application.yml 中添加如下配置:

配置 Admin Server 的 context-path;
为客户端配置 Admin Server 的地址。
spring:
application:
name: hero-springboot-demo
boot:
admin:
client:
url: ‘http://localhost:9099/monitor’
context-path: ‘/monitor’
2.4 访问 Admin Server
重启服务,在浏览器中访问 Spring Boot Admin Server:

http://localhost:9099/monitor
可以看到当前应用的作为客户端注册到 Admin Server 上:

再次强调,上述操作仅仅针对demo学习,非真实的企业级开发!

3 自定义告警

当应用状态异常时,Spring Boot Admin 会自动实时告警,而告警的方式可以由我们自定义。这里模拟日志的方式。

在 config 包下创建类 DemoNotifier,该类继承自 AbstractEventNotifier:

@Slf4j
@Component
public class DemoNotifier extends AbstractEventNotifier

protected DemoNotifier(InstanceRepository repository) 
    super(repository);


@Override
protected Mono<Void> doNotify(InstanceEvent event, Instance instance) 
    return Mono.fromRunnable(() -> log.error("Instance info: , , ",
            instance.getRegistration().getName(), event.getInstance(),
            event.getType()));


此时,注册到这个Admin Server的其他客户端启动、停止等,当前应用都会监听到事件,输出日志。实战中可以在这里面发送邮件、消息等。

4 登录访问

上面配置的 Admin Server 无需登录就可以访问,在真实开发中需要登录后才能访问。admin server 也提供了登录页面。

4.1 添加依赖

在 pom.xml 添加 Spring Security 的依赖:

org.springframework.boot spring-boot-starter-security # 4.2 配置用户名密码 在 application.yml 中配置登录的用户名和密码:

spring:
application:
name: hero-springboot-demo
boot:
admin:
client:
url: ‘http://localhost:9099/monitor’
context-path: ‘/monitor’
security:
user:
name: admin
password: 111111
上面的配置在之前的基础上增加了:spring.security.user 的配置。

4.3 添加配置类

在 config 包下添加 Spring Security 的配置类 SecurityConfig:

@Configuration
public class SecurityConfig

private final String adminContextPath;

public SecurityConfig(AdminServerProperties adminServerProperties) 
    this.adminContextPath = adminServerProperties.getContextPath();


@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception 
    SavedRequestAwareAuthenticationSuccessHandler successHandler =
            new SavedRequestAwareAuthenticationSuccessHandler();
    successHandler.setTargetUrlParameter("redirectTo");
    successHandler.setDefaultTargetUrl(adminContextPath + "/");
    return http.authorizeHttpRequests(auth -> auth.antMatchers(
                                    adminContextPath + "/assets/**",
                                    adminContextPath + "/login",
                                    adminContextPath + "/instances",
                                    adminContextPath + "/actuator/**"
                            ).permitAll()
                            .antMatchers(adminContextPath + "/**").authenticated()
                            .anyRequest().permitAll()
            ).formLogin(form -> form.loginPage(adminContextPath + "/login")
                    .successHandler(successHandler)
            ).logout(logout -> logout.logoutUrl(adminContextPath + "/logout"))
            .csrf(AbstractHttpConfigurer::disable)
            .build();


上面配置文件中的 adminContextPath 就是前面配置的 spring.boot.admin.context-path,即 /monitor。

上面配置包括几个部分:

仅对路径 /monitor/** 请求权限控制;
登录页面和登录成功后的默认地址;
表单登录配置;
禁用 CSRF。

4.4 测试运行

重启服务,访问之前开发的 computer 等接口,可以正常访问;如果访问 /monitor 等路径,就会跳转 Spring Boot Admin 提供的登录页:

使用配置的用户名密码(admin/111111)登录,登录成功后进入 Admin Server 页面。

以上是关于Spring Boot Admin2 实例状态监控详解的主要内容,如果未能解决你的问题,请参考以下文章

使用 Spring Boot Admin 监控应用状态

服务监控之spring-boot-admin

Spring Boot应用监控的实战教程

springboot(二十):使用spring-boot-admin对spring-boot服务进行监控

Spring boot admin 节点状态一直为DOWN的排查

Spring Boot 2.X(十六):应用监控之 Spring Boot Actuator 使用及配置