• SpringBoot生产监控


    一、健康监控简介

    1、介绍

    开发完成后,生产就绪需要做哪些工作呢?我认为,以下三方面的工作最重要。

    • 提供健康检测接口。传统采用 ping 的方式对应用进行探活检测并不准确。有的时候,应用的关键内部或外部依赖已经离线,导致其根本无法正常工作,但其对外的 Web 端口或管理端口是可以 ping 通的。我们应该提供一个专有的监控检测接口,并尽可能触达一些内部组件。

    • 暴露应用内部信息。应用内部诸如线程池、内存队列等组件,往往在应用内部扮演了重要的角色,如果应用或应用框架可以对外暴露这些重要信息,并加以监控,那么就有可能在诸如 OOM 等重大问题暴露之前发现蛛丝马迹,避免出现更大的问题。

    • 建立应用指标 Metrics 监控。Metrics 可以翻译为度量或者指标,指的是对于一些关键信息以可聚合的、数值的形式做定期统计,并绘制出各种趋势图表。这里的指标监控,包括两个方面:一是,应用内部重要组件的指标监控,比如 JVM 的一些指标、接口的 QPS 等;二是,应用的业务数据的监控,比如电商订单量、游戏在线人数等。

    2、SpringBoot准备工作

    Spring Boot 有一个 Actuator 模块,封装了诸如健康检测、应用内部信息、Metrics 指标等生产就绪的功能。今天这一讲后面的内容都是基于 Actuator 的,因此我们需要先完成 Actuator 的引入和配置

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-actuatorartifactId>
    dependency>
    

    Actuator 自带了很多开箱即用提供信息的端点(Endpoint),可以通过** JMX 或 Web **两种方式进行暴露。考虑到有些信息比较敏感,这些内置的端点默认不是完全开启的,你可以通过官网查看这些默认值。在这里,为了方便后续 Demo,我们设置所有端点通过 Web 方式开启。

    默认情况下,Actuator 的 Web 访问方式的根地址为 /actuator,可以通过management.endpoints.web.base-path 参数进行修改

    management:
      server:
        port: 45678
      endpoints:
        web:
          exposure:
            include: "*"
          base-path: /admin
    

    现在访问http://localhost:45678/admin可以查看 Actuator 的所有功能 URL

    3、其他

    大部分端点提供的是只读信息,比如查询 Spring 的 Bean、ConfigurableEnvironment、定时任务、SpringBoot 自动配置、Spring MVC 映射等;少部分端点还提供了修改功能,比如优雅关闭程序、下载线程 Dump、下载堆 Dump、修改日志级别等。

    我们可以访问这里,查看所有这些端点的功能,详细了解它们提供的信息以及实现的操作。此外推荐一个很好的工具, Spring Boot 管理工具Spring Boot Admin,它把大部分 Actuator 端点提供的功能封装为了 Web UI。可以参考:Spring Boot Admin服务监控

    二、健康检测触达关键组件

    1、内置组件健康详情

    健康检测接口可以让监控系统或发布工具知晓应用的真实健康状态,比 ping 应用端口更可靠。不过,要达到这种效果最关键的是,我们能确保健康检测接口可以探查到关键组件的状态。好在 Spring Boot Actuator帮我们预先实现了诸如数据库、InfluxDB、Elasticsearch、Redis、RabbitMQ 等三方系统的健康检测指示器 HealthIndicator。

    通过 Spring Boot 的自动配置,这些指示器会自动生效。当这些组件有问题的时候,HealthIndicator 会返回 DOWN OUT_OF_SERVICE 状态,health 端点 HTTP 响应状态码也会变为 503,我们可以以此来配置程序健康状态监控报警。

    management:
      server:
        port: 45678
      endpoints:
        web:
          exposure:
            include: "*"
          base-path: /admin
      endpoint:
        health:
          show-details: always
    

    我们可以修改配置文件,把 management.endpoint.health.show-details 参数设置为 always,让所有用户都可以直接查看各个组件的健康情况(如果配置为 when-authorized,那么可以结合 management.endpoint.health.roles 配置授权的角色)。访问 health 端点可以看到,数据库、磁盘、RabbitMQ、Redis 等组件健康状态是 UP,整个应用的状态也是 UP

    2、自定义组件健康详情

    如果程序依赖一个很重要的三方服务,我们希望这个服务无法访问的时候,应用本身的健康状态也是 DOWN,首先创建User类以及配置bean

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private long userId;
        private String userName;
    }
    
    
    @Configuration
    public class Config {
        @Bean
        public RestTemplate restTemplate(){
            return new RestTemplate();
        }
    }
    
    

    比如三方服务有一个 user 接口,出现异常的概率是 50%:

    @Slf4j
    @RestController
    @RequestMapping("user")
    public class UserServiceController {
    
        @GetMapping
        public User getUser(@RequestParam("userId") long id) {
            //一半概率返回正确响应,一半概率抛异常
            if (ThreadLocalRandom.current().nextInt() % 2 == 0) {
                return new User(id, "name" + id);
            } else {
                throw new RuntimeException("error");
            }
        }
    }
    

    要实现这个 user 接口是否正确响应和程序整体的健康状态挂钩的话,很简单,只需定义一个 UserServiceHealthIndicator实现 HealthIndicator接口即可。

    在 health 方法中,我们通过 RestTemplate 来访问这个 user 接口,如果结果正确则返回 Health.up(),并把调用执行耗时和结果作为补充信息加入 Health 对象中。如果调用接口出现异常,则返回 Health.down(),并把异常信息作为补充信息加入 Health 对象中:

    @Component
    @Slf4j
    public class UserServiceHealthIndicator implements HealthIndicator {
    
        @Autowired
        private RestTemplate restTemplate;
    
        @Override
        public Health health() {
            long begin = System.currentTimeMillis();
            long userId = 2L;
            User user = null;
            try {
                user = restTemplate.getForObject("http://localhost:8080/user?userId=" + userId, User.class);
                if (user != null && user.getUserId() == userId) {
                    return Health.up()
                            .withDetail("user", user)
                            .withDetail("took", System.currentTimeMillis() - begin)
                            .build();
                } else {
                    return Health.down().withDetail("took", System.currentTimeMillis() - begin).build();
                }
            } catch (Exception ex) {
                log.warn("health check failed!", ex);
                return Health.down(ex).withDetail("took", System.currentTimeMillis() - begin).build();
            }
        }
    }
    

    此时访问http://localhost:45678/admin/health即可发现UserService已经成功被检测
    在这里插入图片描述

    3、自定义多 HealthIndicator 聚合

    我们再来看一个聚合多个 HealthIndicator 的案例,也就是定义一个 CompositeHealthContributor 来聚合多个 HealthContributor,实现一组线程池的监控

    首先,在 ThreadPoolProvider 中定义两个线程池,其中 demoThreadPool 是包含一个工作线程的线程池,类型是 ArrayBlockingQueue,阻塞队列的长度为 10;还有一个 ioThreadPool 模拟 IO 操作线程池,核心线程数 10,最大线程数 50

    public class ThreadPoolProvider {
    
        //一个工作线程的线程池,队列长度10
        private static ThreadPoolExecutor demoThreadPool = new ThreadPoolExecutor(
                1, 1,
                2, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10),
                new CustomizableThreadFactory("springThread-pool-"));
    
        //核心线程数10,最大线程数50的线程池,队列长度50
        private static ThreadPoolExecutor ioThreadPool = new ThreadPoolExecutor(
                10, 50,
                2, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                new CustomizableThreadFactory("io-Thread-pool-"));
    
        public static ThreadPoolExecutor getDemoThreadPool() {
            return demoThreadPool;
        }
    
        public static ThreadPoolExecutor getIOThreadPool() {
            return ioThreadPool;
        }
    
    }
    

    然后,我们定义一个接口,来把耗时很长的任务提交到这个 demoThreadPool 线程池,以模拟线程池队列满的情况:

    @GetMapping("slowTask")
        public void slowTask() {
            ThreadPoolProvider.getDemoThreadPool().execute(() -> {
                try {
                    TimeUnit.HOURS.sleep(1);
                } catch (InterruptedException e) {
                }
            });
        }
    

    做了这些准备工作后,让我们来真正实现自定义的 HealthIndicator 类,用于单一线程池的健康状态。我们可以传入一个 ThreadPoolExecutor,通过判断队列剩余容量来确定这个组件的健康状态,有剩余量则返回 UP,否则返回 DOWN,并把线程池队列的两个重要数据,也就是当前队列元素个数和剩余量,作为补充信息加入 Health

    public class ThreadPoolHealthIndicator implements HealthIndicator {
        private ThreadPoolExecutor threadPool;
    
        public ThreadPoolHealthIndicator(ThreadPoolExecutor threadPool) {
            this.threadPool = threadPool;
        }
    
        @Override
        public Health health() {
            Map<String, Integer> detail = new HashMap<>();
            detail.put("queue_size", threadPool.getQueue().size());
            detail.put("queue_remaining", threadPool.getQueue().remainingCapacity());
            if (threadPool.getQueue().remainingCapacity() > 0) {
                return Health.up().withDetails(detail).build();
            } else {
                return Health.down().withDetails(detail).build();
            }
        }
    }
    

    再定义一个 CompositeHealthContributor,来聚合两个 ThreadPoolHealthIndicator 的实例,分别对应 ThreadPoolProvider 中定义的两个线程池:

    @Component
    public class ThreadPoolsHealthContributor implements CompositeHealthContributor {
    
        private Map<String, HealthContributor> contributors = new HashMap<>();
    
        ThreadPoolsHealthContributor() {
            this.contributors.put("demoThreadPool", new ThreadPoolHealthIndicator(ThreadPoolProvider.getDemoThreadPool()));
            this.contributors.put("ioThreadPool", new ThreadPoolHealthIndicator(ThreadPoolProvider.getIOThreadPool()));
        }
    
        @Override
        public HealthContributor getContributor(String name) {
            return contributors.get(name);
        }
    
        @Override
        public Iterator<NamedContributor<HealthContributor>> iterator() {
            return contributors.entrySet().stream()
                    .map((entry) -> NamedContributor.of(entry.getKey(), entry.getValue())).iterator();
        }
    }
    
    

    启动后当看到一个 demoThreadPool 为 DOWN 导致父 threadPools 为 DOWN,进一步导致整个程序的 status 为 DOWN:

    三、对外暴露应用内部重要组件的状态

    1、内部状态数据暴露

    除了可以把线程池的状态作为整个应用程序是否健康的依据外,我们还可以通过 Actuator 的 InfoContributor 功能,对外暴露程序内部重要组件的状态数据

    @Component
    public class ThreadPoolInfoContributor implements InfoContributor {
        private static Map threadPoolInfo(ThreadPoolExecutor threadPool) {
            Map<String, Object> info = new HashMap<>();
            info.put("poolSize", threadPool.getPoolSize());
            info.put("corePoolSize", threadPool.getCorePoolSize());
            info.put("largestPoolSize", threadPool.getLargestPoolSize());
            info.put("maximumPoolSize", threadPool.getMaximumPoolSize());
            info.put("completedTaskCount", threadPool.getCompletedTaskCount());
            return info;
        }
    
        @Override
        public void contribute(Info.Builder builder) {
            builder.withDetail("demoThreadPool", threadPoolInfo(ThreadPoolProvider.getDemoThreadPool()));
            builder.withDetail("ioThreadPool", threadPoolInfo(ThreadPoolProvider.getIOThreadPool()));
        }
    }
    

    访问 /admin/info 接口,可以看到这些数据

    2、JMX MBean

    如果开启了JMX,即spring.jmx.enabled=true,可以通过 jconsole 工具,在 org.springframework.boot.Endpoint 中找到 Info 这个 MBean,然后执行 info 操作可以看到,我们刚才自定义的 InfoContributor 输出的有关两个线程池的信息:

    在这里插入图片描述

    四、指标 Metrics 快速定位

    指标是指一组和时间关联的、衡量某个维度能力的量化数值。通过收集指标并展现为曲线图、饼图等图表,可以帮助我们快速定位、分析问题

    五、总结

    健康检测可以帮我们实现负载均衡的联动;应用信息以及 Actuaor 提供的各种端点,可以帮我们查看应用内部情况,甚至对应用的一些参数进行调整;而指标监控,则有助于我们整体观察应用运行情况,帮助我们快速发现和定位问题

  • 相关阅读:
    在win10里顺利安装了apache2.4.41和php7.4.29以及mysql8.0.33
    GPU是什么?GPU有多重要?
    远程访问云服务器CentOS下 jupyter服务 【保姆级教程】
    开源项目-SeaTunnel-UI数据集成系统
    数据库实验三——数据更新操作中经典题、难题以及易错题合集(含数据导入导出操作详细教程)
    springboot整合全文搜索引擎Elasticsearch Spring Boot 28
    文心一言 VS 讯飞星火 VS chatgpt (82)-- 算法导论8.1 1题
    Spring AOP全面详解(超级详细)
    重生奇迹MU套装大全中的极品属性
    普通交换机可以改成POE供电吗?
  • 原文地址:https://blog.csdn.net/lemon_TT/article/details/127089969