高并发下指标为何不失真:apm_sdk Aggregator 原子聚合与异步上报实现原理
【免费下载链接】apm_sdk按照opentelemetry标准,用仓颉语言实现的APM SDK.项目地址: https://gitcode.com/Cangjie-TPC/apm_sdk
apm_sdk是遵循 OpenTelemetry 标准、用仓颉语言原生实现的应用性能监测(APM)SDK。在高并发场景下,多个线程同时累加指标却不会丢失一次计数,秘诀就在于它的Aggregator 原子聚合设计 +MetricReader 异步上报线程模型。本文带你快速看懂这套机制的原理。
一、问题背景:高并发为什么会让指标"失真"
想象一个电商服务,每秒上万次请求,每个请求都要给"活动请求数"这类 Counter 指标加 1。如果多个线程同时对同一个变量执行"读取 → 加一 → 写回",就可能出现:
- 线程 A 读到 100,线程 B 也读到 100;
- 两个线程都写回 101 —— 一次请求被"吞掉",指标偏小。
这就是典型的lost update(丢失更新)问题。指标一旦失真,基于它做的容量规划、报警阈值全部不可信。
📌 apm_sdk 的解法分两步:
- 写入侧:把聚合状态放进原子对象,用 CAS 保证每次累加不丢;
- 读取侧:用一个独立后台线程按固定周期采集并导出,业务线程完全不阻塞。
二、核心设计:Aggregator 分层架构
指标采集的核心逻辑由三层组成,对应源码目录结构如下:
src/sdk/metric/aggregator/ 核心指标数据计算(Aggregator) src/exporter/metric/ 指标数据处理(MetricReader)+ 数据上报(Exporter)整体数据流是:
业务线程调用
counter.add()→ 按 Attributes 维度路由到AggregatorHandle→ 原子累加 →MetricReader后台线程定时collect()→MetricExporter写入文件
每个仪器(Counter / Histogram / Gauge)在创建时都会绑定一个聚合器,聚合器再为每一组标签(Attributes)维护独立的聚合句柄,实现见 abstract_instrument_builder.cj。它还做了两个高并发下很实用的保护:
- 句柄池复用:已释放的 Handle 先进入
NonBlockingQueue池,优先复用,避免高频创建对象; - 基数上限(Cardinality Limit):单个仪器的标签分组数达到上限后,多余的序列统一归入
otel.metric.overflow,防止维度爆炸撑爆内存。
三、原子聚合:不同指标类型用不同的"原子武器"
所有聚合句柄都继承自 aggregator.cj 中的抽象基类AggregatorHandle,对外暴露两个方法:recordXxx()(写入)和aggregateThenMaybeReset()(读取+可选重置)。不同指标类型按语义选择不同的并发策略:
| 指标类型 | 实现类 | 并发策略 | 适用场景 |
|---|---|---|---|
| Long Counter | LongSumHandler | AtomicInt64+ CAS 自旋累加 | 请求数累加,最频繁 |
| Double Counter/UpDown | DoubleSumHandler | AtomicReference<Bigdecimal>+ CAS | 浮点累加 |
| Gauge(瞬时值) | LongLastValueHandler/DoubleLastValueHandler | 原子load/swap | 内存、线程数等"取最新值" |
| Max/Min/AVG | DoubleMaxHandler等 | 原子读改写 + 互斥锁 | 响应时间统计 |
| Histogram | DoubleExplicitBucketHistogramHandler | ReentrantMutex保护多字段 | 分位数统计 |
3.1 CAS 自旋:计数累加的"零丢失"写法
Long 类型的累加器 AtomicLongLongAdder 是最典型的一个:
public func add(x: Int64): Unit { while (true) { let current = atomicLong.load(); let next = current + x; if (atomicLong.compareAndSwap(current, next)) { return; } } }这就是经典的Compare-And-Swap 自旋:先读当前值,算出期望值,尝试原子替换;被别人抢先了就重读重试。循环直到成功为止,保证每一次 add 最终都会被计入——高并发下指标不偏小的根本原因。
浮点类型AtomicDoubleLongAdder因为仓颉没有原生原子浮点,把Float64包装进Bigdecimal对象、用AtomicReference承载后走同样的 CAS 思路。
3.2 sumThenReset:读取和清零一步完成
周期性采集需要"取出当前值并归零"。如果拆成"读 + 写零"两步,两步之间新进来的数据会被清零丢掉。sumThenReset()把整个过程压成一个 CAS 循环:
public func sumThenReset(): Int64 { var prev: Int64 do { prev = atomicLong.load() } while (!atomicLong.compareAndSwap(prev, 0)) return prev }读到的就是这段时间内的精确增量,天然适合 Delta 型采集。
3.3 什么时候用锁而不是 CAS?
Histogram 一个句柄要同时更新 sum、min、max、count、15 个桶计数——多字段必须一致地更新,CAS 就不够了,于是用ReentrantMutex保护整个临界区(aggregator.cj)。Gauge 这类"只关心最新值"的指标则更简单:写入端store(),读取端load(),重置时swap()原子换回初值并返回旧值,全程无锁。
💡 小结:能无锁就无锁,必须一致性才加锁,且锁的范围压到最小,这是高并发下既不失真又低延迟的关键。
四、异步上报:MetricReader 的独立 Worker 线程
业务线程只负责"写","读 + 上报"全部被甩给后台。metric_reader.cj 中的Worker通过仓颉的spawn关键字启动独立协程线程(命名为metricReader_worker_thread),核心循环逻辑是:
- 每
interval(默认 2000ms)醒来一次; - 用
AtomicBool的compareAndSwap(true, false)抢"执行权"——如果上一轮还没跑完,本轮直接跳过,绝不重叠执行,避免慢上报拖垮系统; - 遍历所有仪器执行
collect(),把聚合结果交给 Exporter; - 无论成功还是异常,都
finally里恢复标志并休眠到下一周期。
导出端 metric_exporter.cj 则把指标序列化为 JSON 按天滚动写入文件(如apm/2024-04-16/metric-0.json),支持按大小轮转、保留 N 天,写入 IO 完全不影响业务线程。
🔍 采集时还有一个细节:Delta 时序的仪器会调用sumThenReset()取增量,Cumulative 时序只取sum()不动状态——时间语义由 abstract_instrument_builder.cj 中的collect()统一处理。
五、对应用的影响:为什么可以放心接入
- 线程隔离:metric 上报线程独立运行,异常只记录日志不向外抛;
- 资源有边界:仪器 Map 上限 10000、每组标签 handler 上限 2000,超限自动归入 overflow 序列;
- 无阻塞队列池化:Handle 对象走非阻塞队列复用,减少 GC 压力。
这些设计说明见 README.md 的"SDK资源使用情况说明"章节。
六、快速上手:3 步接入 apm_sdk
- 引入静态库:在工程
module.json的path_option中加入../build/release/apm_sdk; - 创建配置:参考 basic_example 的 telemetry_config.cj,组装
MeterProvider+MetricReader(内含后台 Worker)+MetricExporter; - 打点:参考 metric_example.cj,像示例里那样在业务和
spawn异步线程中混合调用counter.add(...),多协程并发写入依然精确。
更完整的用法(silo 拦截器自动采集请求指标、trace 宏等)可直接参考 samples/ 目录下的两个示例工程。
总结
apm_sdk 让高并发指标不失真的核心思路可以浓缩为一句话:
写入侧用 CAS 原子累加消灭 lost update,读取侧用独立 Worker 线程 + CAS 防重入完成异步导出。
它不依赖任何第三方库,纯仓颉语言实现,且完全对齐 OpenTelemetry 语义。如果你的仓颉服务正面临"指标对不上账"的困扰,这套 Aggregator 原子聚合 + 异步上报的实现值得直接借鉴。
【免费下载链接】apm_sdk按照opentelemetry标准,用仓颉语言实现的APM SDK.项目地址: https://gitcode.com/Cangjie-TPC/apm_sdk
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考