Spring boot 2.0 Actuator 的健康检查
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring boot 2.0 Actuator 的健康检查相关的知识,希望对你有一定的参考价值。
参考技术A在当下流行的Service Mesh架构中,由于Spring boot框架的种种优点,它特别适合作为其中的应用开发框架。
说到Service Mesh的微服务架构,主要特点是将服务开发和服务治理分离开来,然后再结合容器化的Paas平台,将它们融合起来,这依赖的都是互相之间默契的配合。也就是说各自都暴露出标准的接口,可以通过这些接口互相交织在一起。
Service Mesh的架构设计中的要点之一,就是 全方位的监控 ,因此一般我们选用的服务开发框架都需要有方便又强大的监控功能支持。在Spring boot应用中开启监控特别方便,监控面也很广,还支持灵活定制。
在Spring boot应用中,要实现可监控的功能,依赖的是 spring-boot-starter-actuator 这个组件。它提供了很多监控和管理你的spring boot应用的HTTP或者JMX端点,并且你可以有选择地开启和关闭部分功能。当你的spring boot应用中引入下面的依赖之后,将自动的拥有审计、健康检查、Metrics监控功能。
具体的使用方法:
“*”号代表启用所有的监控端点,可以单独启用,例如, health , info , metrics 等。
一般的监控管理端点的配置信息,如下:
上述配置信息仅供参考,具体须参照官方文档,由于spring boot的版本更新比较快,配置方式可能有变化。
今天重点说一下Actuator监控管理中的健康检查功能,随时能掌握线上应用的健康状况是非常重要的,尤其是现在流行的容器云平台下的应用,它们的自动恢复和扩容都依赖健康检查功能。
当我们开启 health 的健康端点时,我们能够查到应用健康信息是一个汇总的信息,访问 http://127.0.0.1:10111/actuator/health 时,我们获取到的信息是 "status":"UP" ,status的值还有可能是 DOWN。
要想查看详细的应用健康信息需要配置 management.endpoint.health.show-details 的值为 always ,配置之后我们再次访问 http://127.0.0.1:10111/actuator/health ,获取的信息如下:
从上面的应用的详细健康信息发现,健康信息包含磁盘空间、redis、DB,启用监控的这个spring boot应用确实是连接了redis和oracle DB,actuator就自动给监控起来了,确实是很方便、很有用。
经过测试发现,details中所有的监控项中的任何一个健康状态是 DOWN ,整体应用的健康状态也是 DOWN 。
Spring boot的健康信息都是从 ApplicationContext 中的各种 HealthIndicator
Beans中收集到的,Spring boot框架中包含了大量的 HealthIndicators 的实现类,当然你也可以实现自己认为的健康状态。
默认情况下,最终的spring boot应用的状态是由 HealthAggregator 汇总而成的,汇总的算法是:
Spring boot框架自带的 HealthIndicators 目前包括:
有时候需要提供自定义的健康状态检查信息,你可以通过实现 HealthIndicator 的接口来实现,并将该实现类注册为spring bean。你需要实现其中的 health() 方法,并返回自定义的健康状态响应信息,该响应信息应该包括一个状态码和要展示详细信息。例如,下面就是一个接口 HealthIndicator 的实现类:
另外,除了Spring boot定义的几个状态类型,我们也可以自定义状态类型,用来表示一个新的系统状态。在这种情况下,你还需要实现接口 HealthAggregator ,或者通过配置 management.health.status.order 来继续使用 HealthAggregator 的默认实现。
例如,在你自定义的健康检查 HealthIndicator 的实现类中,使用了自定义的状态类型 FATAL ,为了配置该状态类型的严重程度,你需要在application的配置文件中添加如下配置:
在做健康检查时,响应中的HTTP状态码反应了整体的健康状态,(例如, UP 对应 200, 而 OUT_OF_SERVICE 和 DOWN 对应 503)。同样,你也需要为自定义的状态类型设置对应的HTTP状态码,例如,下面的配置可以将 FATAL 映射为 503(服务不可用):
下面是内置健康状态类型对应的HTTP状态码列表:
本文主要介绍了Spring boot中提供的应用健康检查功能的使用方法和原理,顺带介绍了一点 Actuator 的内容。主要的内容来自 spring boot 2.0.1的官方文档 和 源码,还有一些自己的想法,希望多多支持。
spring-boot-quartz, 依赖spring-boot-parent
/** * state的值代表该任务触发器的状态: STATE_BLOCKED 4 // 运行 STATE_COMPLETE 2 //完成那一刻,不过一般不用这个判断Job状态 STATE_ERROR 3 // 错误 STATE_NONE -1 //未知 STATE_NORMAL 0 //正常无任务,用这个判断Job是否在运行 STATE_PAUSED 1 //暂停状态 */
import java.util.Date; import org.quartz.CronTrigger; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.SchedulerFactory; import org.quartz.SimpleTrigger; import org.quartz.Trigger; import org.quartz.impl.StdScheduler; import org.quartz.impl.StdSchedulerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.scheduling.quartz.SchedulerFactoryBean; import cn.wa8.qweb.extract.action.Extract2DB; public class SimpleRun { private static Logger log = LoggerFactory.getLogger(SimpleRun.class); public void run() throws Exception { SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory(); Scheduler sched = schedFact.getScheduler(); JobDetail jobDetail = new JobDetail("myJob",null,SimpleJob.class); SimpleTrigger trigger = new SimpleTrigger("myTrigger", null, new Date(), null, SimpleTrigger.REPEAT_INDEFINITELY, 30L * 1000L); sched.scheduleJob(jobDetail, trigger); //sched.addJobListener(new MyTriggerListener()); SimpleJob.preDate = new Date(); sched.start(); System.out.println("starting"); /** * state的值代表该任务触发器的状态: STATE_BLOCKED 4 // 运行 STATE_COMPLETE 2 //完成那一刻,不过一般不用这个判断Job状态 STATE_ERROR 3 // 错误 STATE_NONE -1 //未知 STATE_NORMAL 0 //正常无任务,用这个判断Job是否在运行 STATE_PAUSED 1 //暂停状态 */ while (true){ if(4 == sched.getTriggerState("myTrigger", null)){ System.out.println("running"); }else if(0 == sched.getTriggerState("myTrigger", null)){ System.out.println("ending"); }else { System.out.println("error state:"+sched.getTriggerState("myTrigger", null)); } try { Thread.sleep(5*1000); } catch (Exception e) { // TODO: handle exception } } } public static void main(String[] args) { SimpleRun simpleRun = new SimpleRun(); try { simpleRun.run(); } catch (Exception e) { e.printStackTrace(); } } }
import java.util.Date; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import cn.wa8.qweb.extract.action.Extract2DB; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.StatefulJob; /*Extract2DB extract2db = new Extract2DB(); extract2db.CommonBaseExtract();*/ public class SimpleJob implements StatefulJob{ public static Date preDate ; public void execute(JobExecutionContext arg0) throws JobExecutionException { System.out.println("into Job"); Date currentDate = new Date(); Long s = (currentDate.getTime()-preDate.getTime())/1000; try { Thread.sleep(10*1000); } catch (Exception e) { e.printStackTrace(); } System.out.println(s); System.out.println("leave Job:"+Thread.currentThread().toString()); preDate =currentDate; } }
http://blog.csdn.net/u010666884/article/details/51842610
都是执行时间大于间隔时间才会出现的情况,实际做了测试和http://blog.sina.com.cn/s/blog_56d8ea900100cecq.html第2点有点不符,记录如下:
第一种情况:misfire设置时间为600秒;任务每隔2分钟执行一次;任务执行时间为3分钟; 上次执行时间 下次执行时间 状态 解释 20:23:04 20:25:04 正在执行 任务实际要执行到20:26:04,推后两分钟是20:28:04
时间到了20:26:04。日志变更为:
20:25:04 20:27:04 正在执行 任务实际要执行的20:29:04,推后两分钟是20:31:04
时间到了20:29:04,日志变更为:
20:27:04 20:29:04
连续三次发现,实际开始的时间减去应该开始的时间差是递增的;上次执行和下次执行时间反映的实际情况都是不准确的,而且会出现下次执行时间小于当前时间的情况。
注意:持续执行的到 实际启动时间 减去 应该开始时间 大于等于misfire时间;奇怪的是,不是开始参考第二种情况继续执行,而是最后一次执行后即开始长时间等待,而且上次以及下次开始时间也不更新,保持原样;直到实际启动时间+misfire时间 时刻开始继续执行,并且更新上次以及下次开始时间,再开始一个上述周期。
第二种情况:misfire设置时间为6秒,任务每隔2分钟执行一次,任务执行时间为3分钟:
上次执行时间 下次执行时间 状态 解释 18:08:12 18:10:12 正在执行 任务实际要执行到18:11:12,推后两分钟是18:13:12
时间到了18:11:12,日志变更为:
18:08:12 18:10:12 等待 超出misfire时间;任务还没有更新状态 18:08:12 18:12:12 等待 是18:12:12,而不是18:13:12。
算法描述如下:
本次任务应该开始时间为18:10:12,应该结束时间为18:12:12;实际启动时间为18:11:12;实际启动后结束时间为18:13:12;实际启动时间减去应该开始时间超出了misfire,所以状态为等待,即本次任务不执行,从而上次执行时间不变;
计算下次执行时间:当前时间为18:11:12(或者一个稍微大于该值的值),拿应该结束时间以及实际启动后结束时间和当前时间比较,取当前时间往后的最小值作为下次任务启动时间。(算法兼容下面第2点说法)
18:12:12 18:14:12 正在执行
其他引用:
org.quartz.jobStore.misfireThreshold = 60000 #60秒 默认值
那么执行第一次作业是在10:01秒,这时会设定下一次的执行时间为10:02秒,要等一个作业执行完之后才有可用线程,大概要在10:11秒才能执行前面安排的应该在10:02执行的作业,这时就会用到misfireThreshold, 因为10:11与10:02之间的差值小于6000,所以执行该作业,并以10:02为基准设置下一次执行时间为10:03,这样造成每次实际执行时间与安排时间错位
如果 org.quartz.jobStore.misfireThreshold = 6000 #秒
同样,在10:11计划执行安排在10:02的作业,发现10:11与10:02之间的差值小于6000,那么直接跳过该作业,执行本应在当前时间执行的 作业,这时候会以10:11为基准设定下次作业执行时间为10:12(状态此段区间内一直是等待,只是更改了下次作业时间)
其他情况:
quartz有个全局的参数misfireThreshold设置可以允许的超时时间,超过了就不执行,未超过就执行。
比如设置了misfireThreshold=30分钟,如果一个任务定时在10:30执行,但在10:29服务器挂了,在10:50启动,虽然任务超时了21分钟,但小于misfireThreshold,所以还是可以执行。
而如果服务器11:10才启动,那就misfire了。
对于周期性的任务,如果有misfire的情况出现,则会自动更新CronTrigger的时间周期
默认情况下会在当前时间马上执行前一个被misfire的任务
而如果设置MISFIRE_INSTRUCTION_DO_NOTHING,则不对misfire的任务做特殊处理,只从当前时间之后的下一次正常调度时间开始执行
http://blog.sina.com.cn/s/blog_56d8ea900101d2mh.html
http://www.quartz-scheduler.org/documentation/quartz-2.1.x/quick-start.html
spring-boot-quartz, 依赖spring-boot-parent
- 项目启动后输入:http://localhost/
- 数据库文件: https://github.com/leelance/spring-boot-all/blob/master/spring-boot-quartz/src/main/resources/demo-schema.sql
application.properties
# IDENTITY (ContextIdApplicationContextInitializer)
spring.application.index=WebQuartz.v1.1
spring.application.name=WebQuartz
#Server
server.port=80
server.jsp-servlet.class-name=org.apache.jasper.servlet.JspServlet
security.basic.enabled=false
management.security.enabled=false
#MVC
spring.mvc.view.prefix=/WEB-INF/views/
spring.resources.static-locations=classpath:/static/
security.basic.enabled=false
management.security.enabled=false
#LOG
logging.config=classpath:log4j2.xml
configuration
@Configuration
public class QuartzConfig {
@Bean
public Scheduler scheduler() throws IOException, SchedulerException {
SchedulerFactory schedulerFactory = new StdSchedulerFactory(quartzProperties());
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.start();
return scheduler;
}
/**
* 设置quartz属性
* @throws IOException
* 2016年10月8日下午2:39:05
*/
public Properties quartzProperties() throws IOException {
Properties prop = new Properties();
prop.put("quartz.scheduler.instanceName", "ServerScheduler");
prop.put("org.quartz.scheduler.instanceId", "AUTO");
prop.put("org.quartz.scheduler.skipUpdateCheck", "true");
prop.put("org.quartz.scheduler.instanceId", "NON_CLUSTERED");
prop.put("org.quartz.scheduler.jobFactory.class", "org.quartz.simpl.SimpleJobFactory");
prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
prop.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
prop.put("org.quartz.jobStore.dataSource", "quartzDataSource");
prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
prop.put("org.quartz.jobStore.isClustered", "true");
prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
prop.put("org.quartz.threadPool.threadCount", "5");
prop.put("org.quartz.dataSource.quartzDataSource.driver", "com.mysql.jdbc.Driver");
prop.put("org.quartz.dataSource.quartzDataSource.URL", "jdbc:mysql://localhost:3306/demo-schema");
prop.put("org.quartz.dataSource.quartzDataSource.user", "root");
prop.put("org.quartz.dataSource.quartzDataSource.password", "123456");
prop.put("org.quartz.dataSource.quartzDataSource.maxConnections", "10");
return prop;
}
}
JS
@Service
public class TaskServiceImpl {
private Logger logger = LogManager.getLogger(getClass());
@Autowired
private Scheduler scheduler;
/**
* 所有任务列表
* 2016年10月9日上午11:16:59
*/
public List<TaskInfo> list(){
List<TaskInfo> list = new ArrayList<>();
try {
for(String groupJob: scheduler.getJobGroupNames()){
for(JobKey jobKey: scheduler.getJobKeys(GroupMatcher.<JobKey>groupEquals(groupJob))){
List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
for (Trigger trigger: triggers) {
Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
String cronExpression = "", createTime = "";
if (trigger instanceof CronTrigger) {
CronTrigger cronTrigger = (CronTrigger) trigger;
cronExpression = cronTrigger.getCronExpression();
createTime = cronTrigger.getDescription();
}
TaskInfo info = new TaskInfo();
info.setJobName(jobKey.getName());
info.setJobGroup(jobKey.getGroup());
info.setJobDescription(jobDetail.getDescription());
info.setJobStatus(triggerState.name());
info.setCronExpression(cronExpression);
info.setCreateTime(createTime);
list.add(info);
}
}
}
} catch (SchedulerException e) {
e.printStackTrace();
}
return list;
}
/**
* 保存定时任务
* @param info
* 2016年10月9日上午11:30:40
*/
@SuppressWarnings("unchecked")
public void addJob(TaskInfo info) {
String jobName = info.getJobName(),
jobGroup = info.getJobGroup(),
cronExpression = info.getCronExpression(),
jobDescription = info.getJobDescription(),
createTime = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");
try {
if (checkExists(jobName, jobGroup)) {
logger.info("===> AddJob fail, job already exist, jobGroup:{}, jobName:{}", jobGroup, jobName);
throw new ServiceException(String.format("Job已经存在, jobName:{%s},jobGroup:{%s}", jobName, jobGroup));
}
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
CronScheduleBuilder schedBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withDescription(createTime).withSchedule(schedBuilder).build();
Class<? extends Job> clazz = (Class<? extends Job>)Class.forName(jobName);
JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity(jobKey).withDescription(jobDescription).build();
scheduler.scheduleJob(jobDetail, trigger);
} catch (SchedulerException | ClassNotFoundException e) {
throw new ServiceException("类名不存在或执行表达式错误");
}
}
/**
* 修改定时任务
* @param info
* 2016年10月9日下午2:20:07
*/
public void edit(TaskInfo info) {
String jobName = info.getJobName(),
jobGroup = info.getJobGroup(),
cronExpression = info.getCronExpression(),
jobDescription = info.getJobDescription(),
createTime = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");
try {
if (!checkExists(jobName, jobGroup)) {
throw new ServiceException(String.format("Job不存在, jobName:{%s},jobGroup:{%s}", jobName, jobGroup));
}
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
JobKey jobKey = new JobKey(jobName, jobGroup);
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withDescription(createTime).withSchedule(cronScheduleBuilder).build();
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
jobDetail.getJobBuilder()