有時候紀錄檔的資訊比較多,怎麼樣才可以讓系統做到自適應取樣呢?
紀錄檔開源元件(一)java 註解結合 spring aop 實現自動輸出紀錄檔
紀錄檔開源元件(二)java 註解結合 spring aop 實現紀錄檔traceId唯一標識
紀錄檔開源元件(三)java 註解結合 spring aop 自動輸出紀錄檔新增攔截器與過濾器
紀錄檔開源元件(四)如何動態修改 spring aop 切面資訊?讓自動紀錄檔輸出框架更好用
紀錄檔開源元件(五)如何將 dubbo filter 攔截器原理運用到紀錄檔攔截器中?
系統生成的紀錄檔可以包含大量資訊,包括錯誤、警告、效能指標等,但在實際應用中,處理和分析所有的紀錄檔資料可能會對系統效能和資源產生負擔。
自適應取樣在這種情況下發揮作用,它能夠根據當前系統狀態和紀錄檔資訊的重要性,智慧地決定哪些紀錄檔需要被取樣記錄,從而有效地管理和分析紀錄檔資料。
紀錄檔取樣系統會給業務系統額外增加消耗,很多系統在接入的時候會比較排斥。
給他們一個百分比的選擇,或許是一個不錯的開始,然後根據實際需要選擇合適的比例。
自適應取樣是一個對使用者透明,同時又非常優雅的方案。
首先我們定義一個介面,返回 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點是請求高峰期,我們的輸出紀錄檔概率很低。深夜之後請求數會很少,想達到累計值就會很慢,這個時間段就會導致紀錄檔輸出很少。
如何解決這個問題呢?
我們可以通過固定時間視窗的方式,來定時調整流量概率。
我們初始化一個定時任務,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