在生产环境的高并发写入场景下,RocksDB 的写放大(Write Amplification Factor, WAF)是导致 I/O 抖动和吞吐量下降的罪魁祸首。很多时候,我们发现磁盘 I/O 跑满,怀疑是 Compaction 引起的 WAF 飙升,但又不能重启服务去修改配置或开启繁重的 Trace。
通常大家会尝试去读取 LOG 文件,或者直接解析 rocksdb.stats 属性。但直接看累积 WAF(Cumulative WAF)是无法反映“当前趋势”的,因为历史数据会稀释当前的真实情况。
要在不重启 RocksDB 的前提下,动态、精准地获取**当前(区间/滑动窗口内)**由 Compaction 引起的 WAF 趋势,目前业内最专业、最平滑的方案主要有以下三种,按精度和侵入性从高到低排列。
方案一:基于 EventListener 监听 Compaction 事件(最精准、最实时)
这是目前最优雅的非侵入式(对核心读写流程无损)方案。RocksDB 提供了丰富的事件监听器,其中 OnCompactionCompleted 会在每次 Compaction 任务结束时同步回调,并传入 CompactionJobInfo 结构体。
1. 核心原理
CompactionJobInfo 中包含了单次 Compaction 的所有核心度量指标:
input_data_size:本次 Compaction 输入的数据总字节数。output_data_size:本次 Compaction 输出的数据总字节数。elapsed_micros:本次 Compaction 耗时。cf_name:Column Family 名称(WAF 在不同 CF 间差异极大,必须分 CF 统计)。
通过这些指标,我们可以通过公式计算出单次 Compaction 的写放大(Job-level WAF):
$$\text{WAF}_{\text{job}} = \frac{\text{output_data_size}}{\text{input_data_size}}$$
2. C++ 代码实现
你可以在初始化 RocksDB 时注册自定义的监听器(此步骤需要代码支持,但后续运行期间无需重启,属于一次性基础设施建设):
#include "rocksdb/listener.h"
#include <iostream>
#include <mutex>
class DynamicWafTracker : public rocksdb::EventListener {
public:
void OnCompactionCompleted(rocksdb::DB* db, const rocksdb::CompactionJobInfo& ci) override {
std::lock_guard<std::mutex> lock(mutex_);
uint64_t bytes_read = ci.stats.input_to_next_level_bytes + ci.stats.input_update_bytes;
uint64_t bytes_written = ci.stats.output_to_next_level_bytes;
// 计算单次 Job 的写放大
double job_waf = 0.0;
if (bytes_read > 0) {
job_waf = static_cast<double>(bytes_written) / bytes_read;
}
// 打印或推送到监控系统(如 Prometheus StatsD)
std::cout << "[WAF Monitor] CF: " << ci.cf_name
<< " | Job WAF: " << job_waf
<< " | Written: " << bytes_written / 1024.0 / 1024.0 << " MB"
<< " | Reason: " << (int)ci.compaction_reason << std::endl;
// 累加到滑动窗口计数器中,供外部 Prometheus Pull 接口查询区间 WAF
interval_bytes_read_[ci.cf_name] += bytes_read;
interval_bytes_written_[ci.cf_name] += bytes_written;
}
// 提供给监控线程定期拉取区间 WAF 的接口(拉取后清空,实现 Sliding Window)
double GetAndResetIntervalWaf(const std::string& cf_name) {
std::lock_guard<std::mutex> lock(mutex_);
uint64_t read = interval_bytes_read_[cf_name];
uint64_t written = interval_bytes_written_[cf_name];
interval_bytes_read_[cf_name] = 0;
interval_bytes_written_[cf_name] = 0;
return read == 0 ? 0.0 : static_cast<double>(written) / read;
}
private:
std::mutex mutex_;
std::unordered_map<std::string, uint64_t> interval_bytes_read_;
std::unordered_map<std::string, uint64_t> interval_bytes_written_;
};
优点:
- 零轮询开销:完全由 Compaction 线程结束时触发,不占用查询链路。
- 绝对精准:拿到的是字节级别的真实物理 I/O 数据。
- 多维度:能获取到 Compaction 触发原因(如
kLevelMaxLevelSize、kLevelL0FilesNum),便于根因分析。
方案二:动态轮询 rocksdb.cf-stats 属性并计算差值(免代码改动)
如果你的 RocksDB 已经在生产环境中运行,且没有提前注册 EventListener,你依然可以通过暴露出的 GetProperty 接口,在不重启的情况下动态计算区间 WAF(Interval WAF)。
1. 核心原理
RocksDB 内部维护了累积的读写统计。我们不能直接看输出的 Cumulative WAF(那是从数据库启动至今的平均值),而是应该在外部监控系统(或旁路 Agent)中,以固定步长 $t$(例如每 10 秒)轮询获取累积指标,计算两帧之间的差值(Delta)。
调用方法:
std::string stats_str;
db->GetProperty(cf_handle, "rocksdb.cf-stats", &stats_str);
解析返回的文本(或者使用更结构化的 GetMapProperty):
在返回的表格中,重点关注 Sum 行的以下两个指标:
Ingest(GB):用户写入的总数据量(包含 MemTable 的 Ingest)。Write(GB):Compaction 写入的总数据量。
2. 趋势计算公式
设 $T_1$ 时刻拿到的累积写入量为 $I_1$(Ingest)和 $W_1$(Write),$T_2$ 时刻(如 10 秒后)拿到的累积量为 $I_2$ 和 $W_2$。
则该 10 秒区间内的动态 Compaction WAF 趋势为:
$$\text{WAF}_{\text{interval}} = \frac{W_2 - W_1}{I_2 - I_1}$$
注意:如果该区间内用户无写入($I_2 - I_1 = 0$),但 Compaction 仍在后台异步进行,此时公式的分母为 0。在监控系统展示时,需要将此类情况单独挂起,或直接定义此时的 WAF 为无穷大(即纯后台整理状态)。
优点:
- 即插即用:只要能调用
GetProperty接口即可,不需要改动 RocksDB 的初始化逻辑,不需要重启。 - 开销极低:几十毫秒级的文本解析,对生产环境无影响。
方案三:读取动态 Statistics 计数器(高频度量首选)
如果你在实例启动时开启了 RocksDB 的 Statistics(通常生产环境都会开启以对接 Prometheus/Grafana),你可以直接通过 API 动态读取特定的 Ticker 值,避免解析字符串。
1. 核心 Tickers
通过 db->GetOptions().statistics->getTickerCount(...) 实时获取以下 Tickers 的 Delta 值:
| Ticker 名称 | 含义 |
|---|---|
COMPACT_READ_BYTES |
Compaction 过程中从磁盘读取的字节数 |
COMPACT_WRITE_BYTES |
Compaction 过程中写入到磁盘的字节数 |
BYTES_WRITTEN |
用户实际写入(Write/Put)的字节数 |
2. 动态趋势计算方式
在外部监控(如 Prometheus + Grafana)中,不要直接画这两个指标的绝对值,而是使用 rate() 或 irate() 函数计算它们的速率,然后进行实时相除。
Grafana PromQL 表达式:
# 动态 Compaction 写放大趋势
rate(rocksdb_compact_write_bytes_total[1m])
/
rate(rocksdb_bytes_written_total[1m])
优点:
- 高度整合:完全契合 Prometheus/Grafana 生态。
- 高吞吐友好:对高频轮询友好,比解析
cf-stats字符串性能更好。
总结与生产建议
- 如果你处于开发/架构设计阶段:强烈建议接入 方案一(EventListener)。它是最干净、指标最丰富、能精确到“哪一次 Compaction 导致了突发写放大”的方案。
- 如果你在排查生产突发故障(无法重启):立刻使用 方案二(轮询 cf-stats 算 Delta)。写一个简单的 Python 或 Shell 脚本,每 5 秒
GetProperty一次,计算差值,即可瞬间定位当前真实的 WAF 趋势。 - 如果已有标准的监控大盘:直接基于 方案三(Statistics Tickers),在 Grafana 上配置
rate()算子,直观展示实时 WAF 折线图。