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大量報錯,因此經過修改定時任務的頻率和過濾非法數據來保證服務的可用性。

相關文章
相關標籤/搜索