ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

高并发下指标为何不失真:apm_sdk Aggregator 原子聚合与异步上报实现原理

高并发下指标为何不失真:apm_sdk Aggregator 原子聚合与异步上报实现原理 高并发下指标为何不失真apm_sdk Aggregator 原子聚合与异步上报实现原理【免费下载链接】apm_sdk按照opentelemetry标准用仓颉语言实现的APM SDK.项目地址: https://gitcode.com/Cangjie-TPC/apm_sdkapm_sdk是遵循 OpenTelemetry 标准、用仓颉语言原生实现的应用性能监测APMSDK。在高并发场景下多个线程同时累加指标却不会丢失一次计数秘诀就在于它的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 CounterLongSumHandlerAtomicInt64 CAS 自旋累加请求数累加最频繁Double Counter/UpDownDoubleSumHandlerAtomicReferenceBigdecimal CAS浮点累加Gauge瞬时值LongLastValueHandler/DoubleLastValueHandler原子load/swap内存、线程数等取最新值Max/Min/AVGDoubleMaxHandler等原子读改写 互斥锁响应时间统计HistogramDoubleExplicitBucketHistogramHandlerReentrantMutex保护多字段分位数统计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 什么时候用锁而不是 CASHistogram 一个句柄要同时更新 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组装MeterProviderMetricReader内含后台 WorkerMetricExporter打点参考 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),仅供参考
返回列表