0


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)publicstaticclassFeiShuNotifierConfiguration{@Bean@ConditionalOnMissingBean@ConfigurationProperties("spring.boot.admin.notify.feishu")publicFeiShuNotifierfeiShuNotifier(InstanceRepository repository,NotifierProxyProperties proxyProperties){returnnewFeiShuNotifier(repository,createNotifierRestTemplate(proxyProperties));}}

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

  1. 定义消息提醒实现
publicclassFeiShuNotifierextendsAbstractStatusChangeNotifierimplementsAlarmMessage{privatestaticfinalStringDEFAULT_MESSAGE=" 服务名称:#{instance.registration.name} \n 服务实例:#{instance.id} \n 服务URL:#{instance.registration.serviceUrl} \n 服务状态:【#{event.statusInfo.status}】 \n 发送时间:#{time}";privatefinalSpelExpressionParser parser =newSpelExpressionParser();privateRestTemplate restTemplate;privateString webhookUrl;privateString secret;privateExpression message;publicFeiShuNotifier(InstanceRepository repository,RestTemplate restTemplate){super(repository);this.restTemplate = restTemplate;this.message = parser.parseExpression(DEFAULT_MESSAGE,ParserContext.TEMPLATE_EXPRESSION);}@OverrideprotectedMono<Void>doNotify(InstanceEvent event,Instance instance){returnMono.fromRunnable(()->sendNotify(event, instance));}@OverrideprotectedvoidupdateLastStatus(InstanceEvent event){//原有的更新最后实例状态,在重启后会删除最后状态,导致实例重启后会过滤掉UNKNOWN:UP通知,这里在重启注册后,将最后的状态重新更新会实例中//如此实例的变化状态为OFFLINE:UP//还有一种办法是:重写shouldNotify(),去掉UNKNOWN:UP,不过滤该通知,也能够收到UP通知,但如此会在Admin重启的时候,所有服务的通知都会发一遍if(event instanceofInstanceDeregisteredEvent){String lastStatus =getLastStatus(event.getInstance());StatusInfo statusInfo =StatusInfo.valueOf(lastStatus);InstanceStatusChangedEvent instanceStatusChangedEvent =newInstanceStatusChangedEvent(event.getInstance(), event.getVersion(), statusInfo);super.updateLastStatus(instanceStatusChangedEvent);}if(event instanceofInstanceStatusChangedEvent){super.updateLastStatus(event);}}privatevoidsendNotify(InstanceEvent event,Instance instance){sendData(getText(event, instance));}@OverridepublicvoidsendData(String content){if(!isEnabled()){return;}Map<String,Object> message =createMessage(content);doSendData(JSONObject.toJSONString(message));}privatevoiddoSendData(String message){sendWebData(message);}privatevoidsendWebData(String message){HttpHeaders headers =newHttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        restTemplate.postForEntity(webhookUrl,newHttpEntity<>(message, headers),Void.class);}protectedMap<String,Object>createMessage(String content){Map<String,Object> messageJson =newHashMap<>();
        messageJson.put("msg_type","text");Map<String,Object> text =newHashMap<>();
        text.put("text", content);
        messageJson.put("content", text);Long timestamp =System.currentTimeMillis()/1000;
        messageJson.put("timestamp", timestamp);
        messageJson.put("sign",getSign(timestamp));return messageJson;}privateStringgetText(InstanceEvent event,Instance instance){Map<String,Object> root =newHashMap<>();
        root.put("event", event);
        root.put("instance", instance);
        root.put("lastStatus",getLastStatus(event.getInstance()));
        root.put("time",DateUtil.now());StandardEvaluationContext context =newStandardEvaluationContext(root);
        context.addPropertyAccessor(newMapAccessor());return message.getValue(context,String.class);}privateStringgetSign(Long timestamp){try{String stringToSign = timestamp +"\n"+ secret;Mac mac =Mac.getInstance("HmacSHA256");
            mac.init(newSecretKeySpec(stringToSign.getBytes(StandardCharsets.UTF_8),"HmacSHA256"));byte[] signData = mac.doFinal(newbyte[]{});returnnewString(Base64.encodeBase64(signData));}catch(Exception ex){
            ex.printStackTrace();}return"";}publicvoidsetRestTemplate(RestTemplate restTemplate){this.restTemplate = restTemplate;}publicStringgetWebhookUrl(){return webhookUrl;}publicvoidsetWebhookUrl(String webhookUrl){this.webhookUrl = webhookUrl;}publicStringgetSecret(){return secret;}publicvoidsetSecret(String secret){this.secret = secret;}publicStringgetMessage(){return message.getExpressionString();}publicvoidsetMessage(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启动的时候,会加载

StatusUpdater

StatusUpdateTrigger

,前者用于更新实例状态,后者用来触发状态更新,这两个类我将从头至下分部说明。

StatusUpdateTrigger

privatestaticfinalLogger log =LoggerFactory.getLogger(StatusUpdateTrigger.class);privatefinalStatusUpdater statusUpdater;privatefinalIntervalCheck intervalCheck;publicStatusUpdateTrigger(StatusUpdater statusUpdater,Publisher<InstanceEvent> publisher){super(publisher,InstanceEvent.class);this.statusUpdater = statusUpdater;this.intervalCheck =newIntervalCheck("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

publicvoidstart(){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

@OverrideprotectedPublisher<Void>handle(Flux<InstanceEvent> publisher){return publisher
                .filter((event)-> event instanceofInstanceRegisteredEvent|| event instanceofInstanceRegistrationUpdatedEvent).flatMap((event)->updateStatus(event.getInstance()));}

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

StatusUpdateTrigger.updateStatus

protectedMono<Void>updateStatus(InstanceId instanceId){returnthis.statusUpdater.updateStatus(instanceId).onErrorResume((e)->{
            log.warn("Unexpected error while updating status for {}", instanceId, e);returnMono.empty();}).doFinally((s)->this.intervalCheck.markAsChecked(instanceId));}

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

StatusUpdateTrigger.start/stop

@Overridepublicvoidstart(){super.start();this.intervalCheck.start();}@Overridepublicvoidstop(){super.stop();this.intervalCheck.stop();}

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

StatusUpdater

StatusUpdater 是真正去查询实例状态,并更新实例的类,我们在

StatusUpdateTrigger.updateStatus

中已经看到其会请求

StatusUpdater.updateStatus
publicMono<Void>updateStatus(InstanceId id){returnthis.repository.computeIfPresent(id,(key, instance)->this.doUpdateStatus(instance)).then();}

repository.computeIfPresent 会调用

EventsourcingInstanceRepository.computeIfPresent

,表示实例id存在的话,执行

doUpdateStatus

并更新状态,

doUpdateStatus

会查询实例最新状态,并通过

Instance.withStatusInfo

包装成一个新的Instance 对象。

EventsourcingInstanceRepository.computeIfPresent

@OverridepublicMono<Instance>computeIfPresent(InstanceId id,BiFunction<InstanceId,Instance,Mono<Instance>> remappingFunction){returnthis.find(id).flatMap((application)-> remappingFunction.apply(id, application)).flatMap(this::save).retryWhen(this.retryOptimisticLockException);}

其中

this::save

用来保存实例事件,此处为状态变更事件

EventsourcingInstanceRepository.save

publicMono<Instance>save(Instance instance){returnthis.eventStore.append(instance.getUnsavedEvents()).then(Mono.just(instance.clearUnsavedEvents()));}

eventStore 实际调用的是在

AdminServerAutoConfiguration

中加载的

InMemoryEventStore

InMemoryEventStore.append

publicMono<Void>append(List<InstanceEvent> events){returnsuper.append(events).then(Mono.fromRunnable(()->this.publish(events)));}

该方法将在事件保存后,发送一个Publish,这样实现了

AbstractEventHandler<InstanceEvent>

的类就能监听到该变更事件。

AdminServerNotifierAutoConfiguration

当SBA2中存在通知相关的Notifier Bean时,会开启

NotificationTrigger

,用来发送变更事件通知

@Configuration(proxyBeanMethods =false)@ConditionalOnBean(Notifier.class)@Lazy(false)publicstaticclassNotifierTriggerConfiguration{@Bean(initMethod ="start", destroyMethod ="stop")@ConditionalOnMissingBean(NotificationTrigger.class)publicNotificationTriggernotificationTrigger(Notifier notifier,Publisher<InstanceEvent> events){returnnewNotificationTrigger(notifier, events);}}

NotificationTrigger 道理同 StatusUpdateTrigger 。

NotificationTrigger.sendNotifications

protectedMono<Void>sendNotifications(InstanceEvent event){returnthis.notifier.notify(event).doOnError((e)-> log.warn("Couldn't notify for event {} ", event, e)).onErrorResume((e)->Mono.empty());}

this.notifier.notify(event) 表示会调用对应通知类的notify方法,这里已飞书为例,由于飞书继承了AbstractStatusChangeNotifier类,该处会调用

AbstractStatusChangeNotifier.notify

AbstractStatusChangeNotifier.notify

又会调用其父类

AbstractEventNotifier

的notify方法。

AbstractStatusChangeNotifier.notify

publicMono<Void>notify(InstanceEvent event){returnsuper.notify(event).then(Mono.fromRunnable(()->updateLastStatus(event)));}

AbstractEventNotifier.notify

publicMono<Void>notify(InstanceEvent event){if(!enabled){returnMono.empty();}return repository.find(event.getInstance()).filter((instance)->shouldNotify(event, instance)).flatMap((instance)->doNotify(event, instance)).doOnError((ex)->getLogger().error("Couldn't notify for event {} ", event, ex)).then();}

AbstractEventNotifier.notify中会通过

shouldNotify

判断该事件是否应该通知,该方法由子类实现,因此这里父类又调用了子类AbstractStatusChangeNotifier的实现,如果需要通知,则执行具体的

doNotify

方法。

AbstractStatusChangeNotifier.shouldNotify

protectedbooleanshouldNotify(InstanceEvent event,Instance instance){if(event instanceofInstanceStatusChangedEvent){InstanceStatusChangedEvent statusChange =(InstanceStatusChangedEvent) event;String from =getLastStatus(event.getInstance());Stringto= statusChange.getStatusInfo().getStatus();returnArrays.binarySearch(ignoreChanges, from +":"+to)<0&&Arrays.binarySearch(ignoreChanges,"*:"+to)<0&&Arrays.binarySearch(ignoreChanges, from +":*")<0;}returnfalse;}

AbstractStatusChangeNotifier.shouldNotify 采用了二分查找法来判断当前变更状态是否在忽略状态类,

<0

表示不在忽略状态内,需要通知。

使用二分查找,必须先对元素进行排序

最后这么弯弯圈圈下来,实例的状态变更事件就到了

FeiShuNotifier.doNotify

中,到此我们对SBA2的实例状态监控的分析就结束了。

标签: spring boot java spring

本文转载自: https://blog.csdn.net/weixin_40972073/article/details/127992400
版权归原作者 阿提说说 所有, 如有侵权,请联系我们删除。

“Spring Boot Admin2 实例状态监控详解”的评论:

还没有评论