紀錄檔開源元件(六)Adaptive Sampling 自適應取樣

2023-08-29 06:00:28

業務背景

有時候紀錄檔的資訊比較多,怎麼樣才可以讓系統做到自適應取樣呢?

拓展閱讀

紀錄檔開源元件(一)java 註解結合 spring aop 實現自動輸出紀錄檔

紀錄檔開源元件(二)java 註解結合 spring aop 實現紀錄檔traceId唯一標識

紀錄檔開源元件(三)java 註解結合 spring aop 自動輸出紀錄檔新增攔截器與過濾器

紀錄檔開源元件(四)如何動態修改 spring aop 切面資訊?讓自動紀錄檔輸出框架更好用

紀錄檔開源元件(五)如何將 dubbo filter 攔截器原理運用到紀錄檔攔截器中?

自適應取樣

是什麼?

系統生成的紀錄檔可以包含大量資訊,包括錯誤、警告、效能指標等,但在實際應用中,處理和分析所有的紀錄檔資料可能會對系統效能和資源產生負擔。

自適應取樣在這種情況下發揮作用,它能夠根據當前系統狀態和紀錄檔資訊的重要性,智慧地決定哪些紀錄檔需要被取樣記錄,從而有效地管理和分析紀錄檔資料。

取樣的必要性

紀錄檔取樣系統會給業務系統額外增加消耗,很多系統在接入的時候會比較排斥。

給他們一個百分比的選擇,或許是一個不錯的開始,然後根據實際需要選擇合適的比例。

自適應取樣是一個對使用者透明,同時又非常優雅的方案。

如何通過 java 實現自適應取樣?

介面定義

首先我們定義一個介面,返回 boolean。

根據是否為 true 來決定是否輸出紀錄檔。

/**
 * 取樣條件
 * @author binbin.hou
 * @since 0.5.0
 */
public interface IAutoLogSampleCondition {

    /**
     * 條件
     *
     * @param context 上下文
     * @return 結果
     * @since 0.5.0
     */
    boolean sampleCondition(IAutoLogContext context);

}

百分比概率取樣

我們先實現一個簡單的概率取樣。

0-100 的值,讓使用者指定,按照百分比決定是否取樣。

public class InnerRandomUtil {

    /**
     * 1. 計算一個 1-100 的亂數 randomVal
     * 2. targetRatePercent 值越大,則返回 true 的概率越高
     * @param targetRatePercent 目標百分比
     * @return 結果
     */
    public static boolean randomRateCondition(int targetRatePercent) {
        if(targetRatePercent <= 0) {
            return false;
        }
        if(targetRatePercent >= 100) {
            return true;
        }

        // 隨機
        ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
        int value = threadLocalRandom.nextInt(1, 100);

        // 隨機概率
        return targetRatePercent >= value;
    }

}

實現起來也非常簡單,直接一個亂數,然後比較大小即可。

自適應取樣

思路

我們計算一下當前紀錄檔的 QPS,讓輸出的概率和 QPS 稱反比。

/**
 * 自適應取樣
 *
 * 1. 初始化取樣率為 100%,全部取樣
 *
 * 2. QPS 如果越來越高,那麼取樣率應該越來越低。這樣避免 cpu 等資源的損耗。最低為 1%
 * 如果 QPS 越來越低,取樣率應該越來越高。增加樣本,最高為 100%
 *
 * 3. QPS 如何計算問題
 *
 * 直接設定大小為 100 的佇列,每一次在裡面放入時間戳。
 * 當大小等於 100 的時候,計算首尾的時間差,currentQps = 100 / (endTime - startTime) * 1000
 *
 * 觸發 rate 重新計算。
 *
 * 3.1 rate 計算邏輯
 *
 * 這裡我們儲存一下 preRate = 100, preQPS = ?
 *
 * newRate = (preQps / currentQps) * rate
 *
 * 範圍限制:
 * newRate = Math.min(100, newRate);
 * newRate = Math.max(1, newRate);
 *
 * 3.2 時間佇列的清空
 *
 * 更新完 rate 之後,對應的佇列可以清空?
 *
 * 如果額外使用一個 count,好像也可以。
 * 可以調整為 atomicLong 的計算器,和 preTime。
 *

程式碼實現

public class AutoLogSampleConditionAdaptive implements IAutoLogSampleCondition {

    private static final AutoLogSampleConditionAdaptive INSTANCE = new AutoLogSampleConditionAdaptive();

    /**
     * 單例的方式獲取範例
     * @return 結果
     */
    public static AutoLogSampleConditionAdaptive getInstance() {
        return INSTANCE;
    }

    /**
     * 次數大小限制,即接收到多少次請求更新一次 adaptive 計算
     *
     * TODO: 這個如何可以讓使用者可以自定義呢?後續考慮設定從預設的組態檔中讀取。
     */
    private static final int COUNT_LIMIT = 1000;

    /**
     * 自適應比率,初始化為 100.全部採集
     */
    private volatile int adaptiveRate = 100;

    /**
     * 上一次的 QPS
     *
     * TODO: 這個如何可以讓使用者可以自定義呢?後續考慮設定從預設的組態檔中讀取。
     */
    private volatile double preQps = 100.0;

    /**
     * 上一次的時間
     */
    private volatile long preTime;

    /**
     * 總數,請求計數器
     */
    private final AtomicInteger counter;

    public AutoLogSampleConditionAdaptive() {
        preTime = System.currentTimeMillis();
        counter = new AtomicInteger(0);
    }

    @Override
    public boolean sampleCondition(IAutoLogContext context) {
        int count = counter.incrementAndGet();

        // 觸發一次重新計算
        if(count >= COUNT_LIMIT) {
            updateAdaptiveRate();
        }

        // 直接計算是否滿足
        return InnerRandomUtil.randomRateCondition(adaptiveRate);
    }

}

每次累加次數超過限定次數之後,我們就更新一下對應的紀錄檔概率。

最後的概率計算和上面的百分比類似,不再贅述。

/**
 * 更新自適應的概率
 *
 * 100 計算一次,其實還好。實際應該可以適當調大這個閾值,本身不會經常變化的東西。
 */
private synchronized void updateAdaptiveRate() {
    //消耗的毫秒數
    long costTimeMs = System.currentTimeMillis() - preTime;
    //qps 的計算,時間差是毫秒。所以次數需要乘以 1000
    double currentQps = COUNT_LIMIT*1000.0 / costTimeMs;
    // preRate * preQps = currentRate * currentQps; 保障取樣均衡,伺服器壓力均衡
    // currentRate = (preRate * preQps) / currentQps;
    // 更新比率
    int newRate = 100;
    if(currentQps > 0) {
        newRate = (int) ((adaptiveRate * preQps) / currentQps);
        newRate = Math.min(100, newRate);
        newRate = Math.max(1, newRate);
    }
    // 更新 rate
    adaptiveRate = newRate;
    // 更新 QPS
    preQps = currentQps;
    // 更新上一次的時間內戳
    preTime = System.currentTimeMillis();
    // 歸零
    counter.set(0);
}

自適應程式碼-改良

問題

上面的自適應演演算法一般情況下都可以執行的很好。

但是有一種情況會不太好,那就是流量從高峰期到低峰期。

比如凌晨11點是請求高峰期,我們的輸出紀錄檔概率很低。深夜之後請求數會很少,想達到累計值就會很慢,這個時間段就會導致紀錄檔輸出很少。

如何解決這個問題呢?

思路

我們可以通過固定時間視窗的方式,來定時調整流量概率。

java 實現

我們初始化一個定時任務,1min 定時更新一次。

public class AutoLogSampleConditionAdaptiveSchedule implements IAutoLogSampleCondition {

    private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor();

    /**
     * 時間分鐘間隔
     */
    private static final int TIME_INTERVAL_MINUTES = 5;

    /**
     * 自適應比率,初始化為 100.全部採集
     */
    private volatile int adaptiveRate = 100;

    /**
     * 上一次的總數
     *
     * TODO: 這個如何可以讓使用者可以自定義呢?後續考慮設定從預設的組態檔中讀取。
     */
    private volatile long preCount;

    /**
     * 總數,請求計數器
     */
    private final AtomicLong counter;

    public AutoLogSampleConditionAdaptiveSchedule() {
        counter = new AtomicLong(0);
        preCount = TIME_INTERVAL_MINUTES * 60 * 100;

        //1. 1min 後開始執行
        //2. 中間預設 5 分鐘更新一次
        EXECUTOR_SERVICE.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                updateAdaptiveRate();
            }
        }, 60, TIME_INTERVAL_MINUTES * 60, TimeUnit.SECONDS);
    }

    @Override
    public boolean sampleCondition(IAutoLogContext context) {
        counter.incrementAndGet();

        // 直接計算是否滿足
        return InnerRandomUtil.randomRateCondition(adaptiveRate);
    }

}

其中更新概率的邏輯和上面類似:

/**
 * 更新自適應的概率
 *
 * QPS = count / time_interval
 *
 * 其中時間維度是固定的,所以可以不用考慮時間。
 */
private synchronized void updateAdaptiveRate() {
    // preRate * preCount = currentRate * currentCount; 保障取樣均衡,伺服器壓力均衡
    // currentRate = (preRate * preCount) / currentCount;
    // 更新比率
    long currentCount = counter.get();
    int newRate = 100;
    if(currentCount != 0) {
        newRate = (int) ((adaptiveRate * preCount) / currentCount);
        newRate = Math.min(100, newRate);
        newRate = Math.max(1, newRate);
    }
    // 更新自適應頻率
    adaptiveRate = newRate;
    // 更新 QPS
    preCount = currentCount;
    // 歸零
    counter.set(0);
}

小結

讓系統自動化分配資源,是一種非常好的思路,可以讓資源利用最大化。

實現起來也不是很困難,實際要根據我們的業務量進行觀察和調整。

開源地址

auto-log https://github.com/houbb/auto-log