springBoot Actuator 健康监测

spring boot 微服务做为一项在云中部署应用和服务的新技术是当下比较热门话题,而微服务的特色决定了功能模块的部署是分布式的,运行在不一样的机器上相互经过服务调用进行交互,业务流会通过多个微服务的处理和传递,在这种框架下,微服务的监控显得尤其重要。咱们知道,spring boot 在引入java

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

这个jar包以后就拥有了多个自带的mapping映射端口,其中最经常使用的是/health接口,咱们能够经过这个接口查看包括磁盘空间,redis集群链接状况,es集群链接状况,rabbit链接状况,mysql链接状况在内的众多信息,查看效果以下:mysql

能够发现,只要有一个服务DOWN以后,整个实例的状态就会呈现DOWN状态,其中不只响应体是这样,响应码也会根据DOWN的状态返回不一样的值,例如此次的请求,状态码就是503,而正常状况下是200响应码redis

因此,这些东西到底有什么用吗,咱们知道,某个实例,在链接好比es服务在在超时后,会致使相关功能不可用,介入人工解决的话,对开发人员来讲简直是一场噩梦,得时刻盯着服务的健康状态,那么有没有什么好办法呢,那就是云端每几秒检测一下实例的/health接口,看下实例的健康状态,若是呈现DOWN的状态就将该实例kill,从而重启另外一个实例代替原有实例(重启大发好(滑稽)),并且由于咱们服务大可能是集群对外提供服务的,因此一个实例挂掉,对总体并没有大碍。这样就能够实现高可用和稳定性。spring

Actuator监控分红两类:原生端点和用户自定义扩展端点,原生的主要有:sql

路径 描述
/autoconfig 提供了一份自动配置报告,记录哪些自动配置条件经过了,哪些没经过
/beans 描述应用程序上下文里所有的Bean,以及它们的关系
/env 获取所有环境属性
/configprops 描述配置属性(包含默认值)如何注入Bean
/dump 获取线程活动的快照
/health 报告应用程序的健康指标,这些值由HealthIndicator的实现类提供
/info 获取应用程序的定制信息,这些信息由info打头的属性提供
/mappings 描述所有的URI路径,以及它们和控制器(包含Actuator端点)的映射关系
/metrics 报告各类应用程序度量信息,好比内存用量和HTTP请求计数
/shutdown 关闭应用程序,要求endpoints.shutdown.enabled设置为true
/trace 提供基本的HTTP请求跟踪信息(时间戳、HTTP头等)

安全措施

若是上述请求接口不作任何安全限制,安全隐患显而易见。实际上Spring Boot也提供了安全限制功能。好比要禁用/env接口,则可设置以下:docker

endpoints.env.enabled= false

若是只想打开一两个接口,那就先禁用所有接口,而后启用须要的接口:express

endpoints.enabled = false
endpoints.health.enabled = true

另外也能够引入spring-boot-starter-security依赖apache

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

在application.properties中指定actuator的端口以及开启security功能,配置访问权限验证,这时再访问actuator功能时就会弹出登陆窗口,须要输入帐号密码验证后才容许访问。安全

management.port=8099
management.security.enabled=true
security.user.name=admin
security.user.password=admin

actuator暴露的health接口权限是由两个配置: management.security.enabledendpoints.health.sensitive组合的结果进行返回的。springboot

management.security.enabled endpoints.health.sensitive Unauthenticated Authenticated
false false Full content Full content
false true Status only Full content
true false Status only Full content
true true No content Full content

安全建议

在使用Actuator时,不正确的使用或者一些不经意的疏忽,就会形成严重的信息泄露等安全隐患。在代码审计时若是是springboot项目而且遇到actuator依赖,则有必要对安全依赖及配置进行复查。也可做为一条规则添加到黑盒扫描器中进一步把控。
安全的作法是必定要引入security依赖,打开安全限制并进行身份验证。同时设置单独的Actuator管理端口并配置不对外网开放。

实现原理

首先是接口HealthIndicator

/*
 * Copyright 2012-2014 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.actuate.health;

/**
 * Strategy interface used to provide an indication of application health.
 *
 * @author Dave Syer
 * @see ApplicationHealthIndicator
 */
public interface HealthIndicator {

    /**
     * Return an indication of health.
     * @return the health for
     */
    Health health();

}

能够看到这个接口有不少实现类,默认的实现类就是/health接口返回的那些信息,包括

RabbitHealthIndicator

MongoHealthIndicator

ElasticsearchHealthIndicator

等等…..

其中接口的返回值是Health类

包括了总体的状态Status和健康明细details,Status有4个状态描述:

其中

UNKNOWN

UP

都会返回200,而剩下都是返回503服务不可用

通常状况不会直接实现这个接口,而是现实它的抽象类AbstractHealthIndicator

/*
 * Copyright 2012-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.actuate.health;

import org.springframework.boot.actuate.health.Health.Builder;

/**
 * Base {@link HealthIndicator} implementations that encapsulates creation of
 * {@link Health} instance and error handling.
 * <p>
 * This implementation is only suitable if an {@link Exception} raised from
 * {@link #doHealthCheck(org.springframework.boot.actuate.health.Health.Builder)} should
 * create a {@link Status#DOWN} health status.
 *
 * @author Christian Dupuis
 * @since 1.1.0
 */
public abstract class AbstractHealthIndicator implements HealthIndicator {

    @Override
    public final Health health() {
        Health.Builder builder = new Health.Builder();
        try {
            doHealthCheck(builder);
        }
        catch (Exception ex) {
            builder.down(ex);
        }
        return builder.build();
    }

    /**
     * Actual health check logic.
     * @param builder the {@link Builder} to report health status and details
     * @throws Exception any {@link Exception} that should create a {@link Status#DOWN}
     * system status.
     */
    protected abstract void doHealthCheck(Health.Builder builder) throws Exception;

}

能够看到抽象类有一个final标志的health()方法,表明着这个方法是不能够用来重写的,咱们包括自定义的健康检查项目均可以用doHealthCheck来重写咱们具体的实现,下面是一个es的原生实现类:

/*
 * Copyright 2012-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.actuate.health;

import java.util.List;

import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.Requests;

/**
 * {@link HealthIndicator} for an Elasticsearch cluster.
 *
 * @author Binwei Yang
 * @author Andy Wilkinson
 * @since 1.3.0
 */
public class ElasticsearchHealthIndicator extends AbstractHealthIndicator {

    private static final String[] allIndices = { "_all" };

    private final Client client;

    private final ElasticsearchHealthIndicatorProperties properties;

    public ElasticsearchHealthIndicator(Client client,
            ElasticsearchHealthIndicatorProperties properties) {
        this.client = client;
        this.properties = properties;
    }

    @Override
    protected void doHealthCheck(Health.Builder builder) throws Exception {
        List<String> indices = this.properties.getIndices();
        ClusterHealthResponse response = this.client.admin().cluster()
                .health(Requests.clusterHealthRequest(indices.isEmpty() ? allIndices
                        : indices.toArray(new String[indices.size()])))
                .actionGet(this.properties.getResponseTimeout());

        switch (response.getStatus()) {
        case GREEN:
        case YELLOW:
            builder.up();
            break;
        case RED:
        default:
            builder.down();
            break;
        }
        builder.withDetail("clusterName", response.getClusterName());
        builder.withDetail("numberOfNodes", response.getNumberOfNodes());
        builder.withDetail("numberOfDataNodes", response.getNumberOfDataNodes());
        builder.withDetail("activePrimaryShards", response.getActivePrimaryShards());
        builder.withDetail("activeShards", response.getActiveShards());
        builder.withDetail("relocatingShards", response.getRelocatingShards());
        builder.withDetail("initializingShards", response.getInitializingShards());
        builder.withDetail("unassignedShards", response.getUnassignedShards());
    }

}

能够看到它继承了AbstractHealthIndicator而且实现了doHealthCheck方法,经过检测es集群的健康状态来映射实例的es健康状态,咱们知道es的绿色和黄色表明正常和预警,红色表明有问题,以后在拼接详细明细到builder这个构造器中。

同理,咱们也能够自定义咱们本身的实现,例如:

@Component
public class CusDiskSpaceHealthIndicator extends AbstractHealthIndicator {

    private final FileStore fileStore;
    private final long thresholdBytes;

    @Autowired
    public CusDiskSpaceHealthIndicator(
        @Value("${health.filestore.path:/}") String path,
        @Value("${health.filestore.threshold.bytes:10485760}") long thresholdBytes)
        throws IOException {
        fileStore = Files.getFileStore(Paths.get(path));
        this.thresholdBytes = thresholdBytes;
    }
    // 检查逻辑
    @Override
    protected void doHealthCheck(Health.Builder builder) throws Exception {
        long diskFreeInBytes = fileStore.getUnallocatedSpace();
        if (diskFreeInBytes >= thresholdBytes) {
            builder.up();
        } else {
            builder.down();
        }

        long totalSpaceInBytes = fileStore.getTotalSpace();
        builder.withDetail("disk.free", diskFreeInBytes);
        builder.withDetail("disk.total", totalSpaceInBytes);
    }
}

实际问题

在公司的一个实际项目中,因为监控平台会定时检查实例的健康状态,若是不健康的话会将实例的docker进行kill,以后进行重启,最近天天晚上会发送一个报警信息,显示实例被删除重启,时间点是固定的,大概在晚上的凌晨0点到2点之间,因而介入调查问题缘由。

首先重启报警是由于实例健康状态的缘由,也就是/health接口返回503,这点是确定的,并且每次发生的时间点是晚上0-2点,不是白天,确定是定时任务搞得鬼,因而开始查找全部的定时job,排除每隔一段时间就进行的定时任务,那确定就是只有在某个点才触发的操做,因而点位到了一个job,以后就是看看具体的实现了。

查看实现后,发现该实现依赖了两个外部的服务,一个是mogodb一个是es服务器,问题被缩小了,以后该怎么办呢?

因为天天发生故障的时间点是半夜,没法直接查看/health接口的返回((⊙﹏⊙)),并且springBoot1.X版本也没有相关的日志打印(坑),因此看到了health接口的具体源码实现后,决定本身写个aop来代理打印出具体失败时候的日志。。。

要实现aop代理的话咱们注意到Health类,它包含了因此的接口返回信息,因而决定找到返回是Health的类的方法,而且该方法是能够重写的方法才行,由于咱们的项目是基于cgLib的代理,cgLib是基于继承的,若是方法被final标注的话,意味着这个方法代理对象是没有的,说的是谁呢,对,就是你。。。

因而开始像上找调用这个方法的类,找到了它:

能够看到这个是个avaBean,被spring托管,且返回值有health,能够很方便的实现代理,因而对他进行@AfterReturn的代理

@Pointcut("execution(* org.springframework.boot.actuate.endpoint.HealthEndpoint.invoke(..))")
    public void healthCheckFacade() {
 }

很容易就拿到了Health类并在Status是Down和OUT_OF_SERVICE时进行了打印,以后就是等待复现,找出这个臭虫(bug)

次日,如期而至,查找找历史日志,能够看到

[{"details":{"error":"org.elasticsearch.ElasticsearchTimeoutException: java.util.concurrent.TimeoutException: Timeout waiting for task."},"status":{"code":"DOWN","description":""}}]

这么一段话,没错,就是那个定时任务搞得鬼,致使es服务器Red状态,健康检查DOWN状态才重启的

解决方法有两个,首先看这个错误java.util.concurrent.TimeoutException: Timeout waiting for task,在从es服务器上对应的时间段看日志,能够发现这个时间点,有不少的看到blukload的的错误日志,而咱们使用的es服务器是5.X版本的,log4j是有一个bug的,致使内存泄露,贸然升级的话,对生产数据时有风险的,因而经过调整参数重启来解决,此外为啥会有这多日志,原来是由于这个点进行了大量的update操做,并且有些update是没有mapppingId的,致使es大量报错,因此经过修改定时任务的频率和过滤非法数据来保证服务的可用性。

相关文章
相关标签/搜索