MetricWriter 是 Sentinel 的指标(Metric)采集与持久化模块,核心目标是:
✅ 定时将运行时的实时监控数据(如 QPS、RT、线程数等)写入本地磁盘日志文件,供后续分析、展示或告警使用。
它通过两个关键类协同工作:
MetricTimerListener:采集器,定期从内存中拉取指标MetricWriter:写入器,负责按规则将指标安全写入磁盘
下面从 设计意图、工作机制、文件策略、线程安全、扩展性 五个维度深入解析。
🎯 一、整体设计意图
Sentinel 需要记录每个资源(如 /api/order)在每一秒的:
- 通过请求数(passQps)
- 拦截请求数(blockQps)
- 平均响应时间(rt)
- 并发线程数(threadNum)
这些数据用于:
- 控制台实时监控图表
- 熔断/流控规则动态调整依据
- 故障复盘与性能分析
但不能一直存内存 (OOM),也不能每次请求都写磁盘(性能差),所以采用:
"内存聚合 + 定时批量落盘" 策略
🧩 二、核心组件协作流程
1. MetricTimerListener ------ 定时采集任务
java
public void run() {
Map<Long, List<MetricNode>> maps = new TreeMap<>(); // 按时间戳分组
// 1. 遍历所有资源的 ClusterNode
for (Entry<ResourceWrapper, ClusterNode> e : ClusterBuilderSlot.getClusterNodeMap().entrySet()) {
ClusterNode node = e.getValue();
Map<Long, MetricNode> metrics = node.metrics(); // 获取该资源每秒的指标
aggregate(maps, metrics, node); // 合并到全局 maps
}
// 2. 加上全局 ENTRY_NODE(总入口流量)
aggregate(maps, Constants.ENTRY_NODE.metrics(), Constants.ENTRY_NODE);
// 3. 写入磁盘
for (Entry<Long, List<MetricNode>> entry : maps.entrySet()) {
metricWriter.write(entry.getKey(), entry.getValue());
}
}
💡
ClusterNode.metrics()返回的是 过去一分钟内每秒的快照(基于滑动窗口统计)
2. MetricWriter ------ 智能文件写入器
核心约束(见注释):
- 同 1 秒的数据必须写入同一个文件
- 单个文件大小有限制(防止单文件过大)
- 不同日期的数据必须分文件
- 文件命名规范 :
{appName}-metrics.log.pid{pid}.yyyy-MM-dd.[number] - 每个数据文件配一个索引文件 (
.idx),记录"时间戳 → 文件偏移量"
📁 三、文件管理策略详解
🔹 文件命名示例
假设:
- appName =
order-service - pid =
12345 - 当前时间 =
2025-12-08
则可能生成:
order-service-metrics.log.pid12345.2025-12-08
order-service-metrics.log.pid12345.2025-12-08.1
order-service-metrics.log.pid12345.2025-12-08.2
...
.1,.2表示当日第 2、3 个文件(因单文件超限而切分)
🔹 索引文件(.idx)作用
-
每写入一批新时间戳的数据,就记录:
javaoutIndex.writeLong(time); // 时间戳(秒) outIndex.writeLong(offset); // 在 .log 文件中的字节偏移位置 -
用途:快速定位某秒数据在哪个文件的哪个位置,避免全文件扫描
🔹 自动清理旧文件
java
private void removeMoreFiles() {
List<String> list = listMetricFiles(baseDir, baseFileName);
// 只保留 totalFileCount 个最新文件
for (int i = 0; i < list.size() - totalFileCount + 1; i++) {
delete old file and its .idx
}
}
- 默认保留 6 个文件(可配置)
- 防止磁盘被日志撑爆
⚙️ 四、关键机制亮点
1. 按秒对齐写入
- 所有
MetricNode的时间戳被强制设为传入的time(秒级) - 同一秒多次调用
write()会追加写入同一文件段 - 避免"同一秒数据分散在多个文件"
2. 跨天自动切文件
java
if (isNewDay(lastSecond, second)) {
closeAndNewFile(nextFileNameOfDay(time));
}
- 利用
timeSecondBase(1970-01-01 00:00:00)计算"天数" - 确保
2025-12-08 23:59:59和2025-12-09 00:00:00分属不同文件
3. 原子性 & 异常安全
write()方法加synchronized,保证多线程安全(虽然通常由单线程调度)- 捕获异常并 warn,防止任务崩溃导致指标丢失
4. 高效序列化
-
使用
node.toFatString()生成文本行(非 JSON,节省空间) -
示例格式(实际字段更多):
1701234567000|order-service|/api/create|20|5|100|3
🔐 五、线程安全与性能
| 场景 | 说明 |
|---|---|
| 读取指标 | ClusterNode.metrics() 内部使用 AtomicReferenceArray,无锁高性能 |
| 写入磁盘 | MetricWriter.write() 是 synchronized,但由单线程 ScheduledExecutorService 调用,无竞争 |
| 文件切换 | closeAndNewFile() 原子完成旧文件关闭 + 新文件创建 |
✅ 整体设计 读无锁、写串行、异步落盘,兼顾性能与一致性
🌐 六、与 Sentinel 整体架构的关系
统计 pass/block/rt 每秒快照 定时拉取 调用 写入 写入 读取 读取 StatSlot ClusterNode MetricBucket MetricTimerListener MetricWriter metrics.log metrics.log.idx Sentinel Dashboard
- 数据流:请求 → StatSlot → ClusterNode → MetricTimerListener → 磁盘
- 控制流:Dashboard 可读取这些文件展示历史监控曲线
💡 一句话总结
MetricTimerListener定时从内存统计结构中提取秒级指标,MetricWriter按日期、大小、进程 ID 等规则智能地将这些指标写入带索引的日志文件,实现高性能、可回溯、低开销的运行时监控数据持久化。
这种设计使得 Sentinel 在 不依赖外部存储 的情况下,也能提供可靠的实时+历史监控能力,是其轻量级、嵌入式特性的典型体现。