编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 性能优化实践

    • README
    • 公共方法论

      • 性能工程总论原理
      • 跨平台指标体系
      • 性能求证方法论
      • 数据采集与观测
        • 01.可观测性三件套
          • 1.1 Metrics / Logs / Traces
          • 1.2 Profile:第四种信号
          • 1.3 四类信号的关系
        • 02.数据源的物理层级
          • 2.1 硬件层:PMU 与硬件计数器
          • 2.2 内核层:tracepoint / kprobe / eBPF
          • 2.3 运行时层:VM / GC / 调度钩子
          • 2.4 框架层:UI 框架回调与生命周期
          • 2.5 应用层:业务埋点
        • 03.采集方法学
          • 3.1 采样 vs 全量
          • 3.2 拉模式 vs 推模式
          • 3.3 在线 vs 离线
          • 3.4 侵入 vs 无侵入
        • 04.无侵入式钩子原理
          • 4.1 字节码 / 中间码插桩
          • 4.2 PLT/GOT Hook(C/C++ 动态库)
          • 4.3 Inline Hook 与 Method Swizzling
          • Inline Hook(C/C++ 通用)
          • Method Swizzling(Objective-C)
          • 4.4 浏览器 API 代理
          • 4.5 安全与稳定性边界
        • 05.采集数据的可信度
          • 5.1 测量原理:观察者效应
          • 5.2 时钟与时间戳陷阱
          • 单调时钟 vs 墙钟
          • 跨核 / 跨线程时间戳
          • 5.3 采样偏差
          • 5.4 误差量化
        • 06.采集系统的成本控制
          • 6.1 性能开销预算
          • 6.2 采样降级策略
          • 6.3 上报与聚合
        • 07.跨平台采集对照
        • 一句话总结
      • 归因方法与火焰图
      • 性能预算防劣化
      • 性能体系全景图
      • 性能优化误区集
    • 体系建设篇

    • 资源专项篇

    • 流水线专项

    • 业务专项篇

    • 交付防御篇

  • 程序编程原理

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

  • 专栏
  • 性能优化实践
  • 公共方法论
杨充
2026-05-27
目录

数据采集与观测

# 采集与可观测性原理

📊 学习成本预估 | 难度:⭐⭐⭐⭐(4/5)| 阅读:约 25 分钟 | 实操:1 小时 🔗 前置阅读:卷零·02 | ➡️ 后续延伸:卷一·01

性能工程的"地基"。没有可观测性,就没有性能工程。本文回答:"数据从哪里来?怎么采集?采得对不对?代价多大?"

# 目录介绍

  • 01.可观测性三件套
    • 1.1 Metrics / Logs / Traces
    • 1.2 Profile:第四种信号
    • 1.3 四类信号的关系
  • 02.数据源的物理层级
    • 2.1 硬件层:PMU 与硬件计数器
    • 2.2 内核层:tracepoint / kprobe / eBPF
    • 2.3 运行时层:VM / GC / 调度钩子
    • 2.4 框架层:UI 框架回调与生命周期
    • 2.5 应用层:业务埋点
  • 03.采集方法学
    • 3.1 采样 vs 全量
    • 3.2 拉模式 vs 推模式
    • 3.3 在线 vs 离线
    • 3.4 侵入 vs 无侵入
  • 04.无侵入式钩子原理
    • 4.1 字节码 / 中间码插桩
    • 4.2 PLT/GOT Hook(C/C++ 动态库)
    • 4.3 Inline Hook 与 Method Swizzling
    • 4.4 浏览器 API 代理
    • 4.5 安全与稳定性边界
  • 05.采集数据的可信度
    • 5.1 测量原理:观察者效应
    • 5.2 时钟与时间戳陷阱
    • 5.3 采样偏差
    • 5.4 误差量化
  • 06.采集系统的成本控制
    • 6.1 性能开销预算
    • 6.2 采样降级策略
    • 6.3 上报与聚合
  • 07.跨平台采集对照

# 01.可观测性三件套

# 1.1 Metrics / Logs / Traces

工业界共识的可观测性"三大支柱":

类型 含义 数据形态 典型用途 存储成本
Metrics 数值化指标,时间序列 时间戳 + 标签 + 数值 监控、告警、趋势 低
Logs 离散事件文本 时间戳 + 上下文 + 文本 故障复盘、调试 高
Traces 请求跨多个组件的执行路径 Trace ID + Span 树 分布式归因 中

三者关系:

Metrics(What 出问题了)
   │
   ▼
Traces(出问题在 哪个 链路环节)
   │
   ▼
Logs(出问题时 发生了 什么)
1
2
3
4
5
6
7

# 1.2 Profile:第四种信号

近年提出的第四类信号,专属于性能领域:

类型 含义 数据形态
Profiles 程序运行时函数级耗时 / 资源分布 调用栈 + 计数(如火焰图)

为什么需要 Profile:

  • Metrics 告诉你"CPU 100%",但不告诉你"哪个函数在烧 CPU"。
  • Logs 告诉你"渲染卡了",但不告诉你"哪行代码导致"。
  • Profile 把执行时间归因到代码行 / 函数 / 调用栈。

# 1.3 四类信号的关系

信号           粒度           回答的问题             代价
──────────────────────────────────────────────────────
Metrics       指标级         "怎样 / 多少?"         低
Logs          事件级         "发生过什么?"          中
Traces        请求级         "在哪一步发生?"        中
Profiles      函数级         "什么代码导致?"        高
1
2
3
4
5
6

性能工程的关键认知:四类信号必须互相关联才能形成完整归因链。一个完整的卡顿排查:

  1. Metric 告警:FPS P99 > 50ms
  2. Trace 定位:在 ListView 滚动期间发生
  3. Profile 归因:65% 时间花在 Bitmap.decodeStream
  4. Log 验证:当时图片 URL 是 4K 大图

单一信号无法解决问题。可观测性体系的设计目标,是让四类信号通过统一上下文(Trace ID / 用户 ID / 会话 ID)互相串联。

# 02.数据源的物理层级

性能数据存在于系统的不同层级,了解层级是设计采集方案的前提。

# 2.1 硬件层:PMU 与硬件计数器

CPU 内置 PMU(Performance Monitoring Unit),可统计:

  • Cycles(CPU 周期数)
  • Instructions(指令数)
  • Cache Miss(缓存未命中)
  • Branch Misprediction(分支预测失败)
  • TLB Miss

由 PMU 计算的关键指标:

IPC = Instructions / Cycles            (越高越好,Intel 一般 > 2 算优秀)
Cache Miss Rate = Misses / Accesses    (越低越好)
1
2

采集方式:

平台 工具
Linux(Android 嵌入式) perf_event_open / perf / simpleperf
macOS / iOS Instruments CPU Counters / sys_perf_event
Web 不可访问(沙箱限制)

意义:当 CPU 利用率高但任务推进慢时,PMU 数据能区分是 CPU bound 还是 memory bound(cache miss 严重)。

# 2.2 内核层:tracepoint / kprobe / eBPF

Linux 内核提供三种 tracing 机制:

机制 含义 用途
tracepoint 内核预埋的静态事件点 调度、IO、文件系统
kprobe / uprobe 动态插入的探针 任意函数入口 / 出口
eBPF 内核态可编程沙箱 灵活、低开销自定义采集

eBPF 的革命性:

  • 在内核态执行用户提供的程序,无须改动内核代码。
  • 开销极低(事件级),可生产环境长期运行。
  • Android 12+ 已内置 eBPF 支持,是新一代 APM 基石。

典型用途:

  • off-CPU 分析(线程在等什么)
  • IO 延迟分布
  • 系统调用归因
  • 内存分配追踪

# 2.3 运行时层:VM / GC / 调度钩子

各运行时提供性能事件回调:

运行时 关键钩子
Android ART GC 回调、ClassLoad、JIT 编译事件、Looper.setMessageLogging
iOS Objective-C/Swift Method Swizzling、os_signpost、Mach 消息
V8 (Web/Node) Performance Observer、v8.startProfiling、Heap Snapshot
.NET EventListener、ETW
嵌入式 RTOS OS hook(FreeRTOS trace)

采集示例:

  • Android:Choreographer.postFrameCallback 采集帧时钟
  • iOS:os_signpost_interval_begin/end 标记区间
  • Web:PerformanceObserver 监听 longtask / paint / largest-contentful-paint

# 2.4 框架层:UI 框架回调与生命周期

UI 框架本身暴露大量回调:

框架 关键回调
Android Activity / Fragment 生命周期、ViewTreeObserver、FrameMetrics
iOS UIViewController 生命周期、viewDidAppear、displayLink
React / Vue componentDidMount / mounted / Profiler API
Flutter WidgetsBinding.addObserver / Timeline

# 2.5 应用层:业务埋点

业务自身定义关键时刻(如"购买按钮点击 → 订单页可见")。

埋点设计四原则:

  1. 语义清晰:事件名表达业务含义,不依赖技术细节。
  2. 上下文完整:随事件携带页面、用户、版本、网络等元数据。
  3. 时钟一致:所有埋点使用同一时钟源(避免时间戳跨线程错乱)。
  4. 可演进:保留事件版本字段,便于口径变更追溯。

# 03.采集方法学

# 3.1 采样 vs 全量

模式 优点 缺点 典型场景
全量 数据完整、可精确归因 开销大、存储成本高 崩溃、关键事件
采样 开销小、可长期运行 偶发问题易遗漏 CPU profile、调用栈

采样的两种方式:

  • 时间采样(Time-based):固定周期触发(如每 10ms 抓一次栈),适合 CPU 分析。
  • 事件采样(Event-based):按特定事件(如每分配 100 次)抓栈,适合内存分析。

采样率与误差:

采样率越高 → 数据越准 → 性能开销越大

经验法则:
  生产环境 CPU profile 采样率:99Hz(每 ~10ms 一次)
  开销约:1-3%
1
2
3
4
5

# 3.2 拉模式 vs 推模式

模式 谁主动 适用
Pull 采集器主动读 /proc/stat、PerformanceObserver.getEntries
Push 被采集对象主动报 框架回调、埋点

端侧实践:

  • 短期高频指标(CPU%、内存)→ Pull(轮询)
  • 离散事件(崩溃、生命周期)→ Push(回调)

# 3.3 在线 vs 离线

模式 含义 用途
在线(Online) 设备运行时实时采集 + 上报 线上监控、告警
离线(Offline) 录制完整 trace 文件,事后分析 性能回归、深度分析

典型工具:

  • Android Perfetto / Systrace:录制完整系统级 trace
  • iOS Instruments:录制 .trace 文件
  • Chrome DevTools Performance:录制时间轴
  • 嵌入式 ftrace:录制内核 trace

实践组合:

  • 线上:轻量 Metrics + 异常时触发 Profile 录制 + 上报关键 Trace
  • 线下:在自动化测试场景下,强制录制完整 Trace 用于回归

# 3.4 侵入 vs 无侵入

模式 特点
侵入式 业务代码显式调用 SDK API,逻辑可见但维护成本高
无侵入式 通过钩子 / 插桩自动采集,业务无感

APM 的趋势是"无侵入式",但需要谨慎处理稳定性 / 兼容性问题(详见下一节)。

# 04.无侵入式钩子原理

无侵入采集是 APM 的核心技术。原理上分四类:

# 4.1 字节码 / 中间码插桩

在编译 / 加载阶段重写代码字节,插入采集逻辑。

平台 技术
Android Java ASM / Javassist / Transform API(Gradle Plugin)
Android Kotlin KSP / 编译器插件
iOS Swift SwiftSyntax / Sourcery(编译期)
Web Babel / SWC plugin、AST 改写

示例:监控所有 Activity.onCreate:

原方法:
  public void onCreate(Bundle b) { /* 业务 */ }

ASM 后:
  public void onCreate(Bundle b) {
    long __t = System.nanoTime();
    try { /* 业务 */ }
    finally { APMTracer.trace("onCreate", System.nanoTime() - __t); }
  }
1
2
3
4
5
6
7
8
9

优点:性能影响极小(编译期完成),覆盖完整。 缺点:增加构建时间,需维护版本兼容性。

# 4.2 PLT/GOT Hook(C/C++ 动态库)

Linux/Android 动态链接器使用 PLT/GOT 表寻址外部符号。修改 GOT 表即可拦截系统调用。

典型用途:

  • 监控 malloc/free 实现内存追踪
  • 监控 pthread_create 监控线程创建
  • 监控 open/read/write 实现 IO 追踪

代表实现:bytehook(字节)、xhook、bhook。

限制:

  • 仅对外部调用生效,对静态链接函数无效。
  • Android 7+ 后,部分动态库(libart 等)加固使 hook 难度增加。

# 4.3 Inline Hook 与 Method Swizzling

# Inline Hook(C/C++ 通用)

直接修改函数前几个字节为跳转指令,跳到代理函数。

特点:

  • 强大,可 hook 任意函数。
  • 危险,对 ABI / 架构高度敏感(ARM vs ARM64 vs x86 不同)。
  • 可能与系统 ASan、PAC(Pointer Authentication)冲突。

# Method Swizzling(Objective-C)

利用 OC 的动态消息机制,交换两个方法的实现指针:

原 Method A 的 IMP <─→ 新 Method A' 的 IMP
1

典型用途:

  • 全局监控 viewDidAppear 计算页面加载时间
  • 拦截 NSURLSession 计算网络耗时

限制:仅对 OC runtime 有效,纯 Swift 类无 dynamic 标记则无效。

# 4.4 浏览器 API 代理

Web 环境无法 hook 内核 / 二进制,但可代理 JS API:

// 拦截 fetch 计算网络耗时
const _fetch = window.fetch;
window.fetch = function (...args) {
  const start = performance.now();
  return _fetch.apply(this, args).finally(() => {
    APM.trace('fetch', performance.now() - start);
  });
};
1
2
3
4
5
6
7
8

典型代理目标:fetch、XMLHttpRequest、addEventListener、setTimeout、history.pushState。

# 4.5 安全与稳定性边界

无侵入 hook 是双刃剑,常见风险:

风险 案例 防御
兼容性崩溃 OS 版本变化导致符号偏移 多版本灰度 + 黑名单兜底
启动卡死 hook 自身耗时导致主线程长时间阻塞 hook 内严禁同步 IO / 锁
安全风险 hook 被恶意利用 / 触发系统加固检测 仅对自身进程内符号 hook
性能退化 全量 hook 高频函数(如 malloc) 按需开启 + 降采样

铁律:

Hook 越底层,威力越大,副作用也越大。生产环境的无侵入 SDK 必须满足"hook 失败不影响业务"的兜底原则。

# 05.采集数据的可信度

# 5.1 测量原理:观察者效应

任何测量都会扰动被测对象。性能领域尤其严重:

  • 打点本身耗时(几百 ns 到几 us)
  • 触发额外的内存分配
  • 引发额外上下文切换
  • 改变 JIT 优化决策

经验数据:

采集方式 单次开销 适合场景
计数器累加 < 100ns 高频路径
nanoTime 时间戳 ~50-200ns 区间测量
调用栈采样(不展开) ~1-10us 偶发采样
调用栈展开 + 符号化 ~100us-1ms 离线分析
内存分配 trace ~500ns / 次 限频开启

# 5.2 时钟与时间戳陷阱

# 单调时钟 vs 墙钟

时钟 特点 用途
墙钟(Wall Clock) 可被用户 / NTP 调整 显示日期
单调时钟(Monotonic) 只增不减 性能区间测量 ✅

铁律:性能区间测量必须用单调时钟:

  • Android:SystemClock.elapsedRealtimeNanos() / System.nanoTime()
  • iOS:mach_absolute_time() / CACurrentMediaTime()
  • Web:performance.now()
  • C/C++:clock_gettime(CLOCK_MONOTONIC)

❌ 用 System.currentTimeMillis() 测耗时是常见错误。NTP 调时会让你测到负数。

# 跨核 / 跨线程时间戳

不同 CPU 核心的 TSC 可能不同步。多线程时间戳直接对比可能错乱。

对策:

  • 在同一线程上打点,跨线程用消息传递时间戳。
  • 使用系统提供的同步时钟 API(多数现代 OS 已抽象)。

# 5.3 采样偏差

偏差类型 描述 例子
选择偏差 仅采集到部分场景 只采集前台启动,漏掉后台拉起
生存偏差 仅采集"未崩溃用户" 排除 OOM 用户 → 内存数据偏低
时间偏差 采集时机不均匀 凌晨用户少、网络好 → 数据偏好
设备偏差 高端机用户多 → 整体数据偏好 必须按机型切片

对策:

  • 采集前定义"采集口径",并文档化。
  • 多维度切片(机型 / 网络 / 时段 / 版本)后再聚合。
  • A/A 实验验证采集本身无偏。

# 5.4 误差量化

任何采集都应附带误差范围。例如:

冷启动 P95 = 1.82s ± 0.04s(95% CI, n=12000, 采样率 100%)
帧时长 P99 = 23.5ms ± 0.8ms(n=8000, 采样率 100Hz)
1
2

误差未声明的指标 = 不可信指标。

# 06.采集系统的成本控制

# 6.1 性能开销预算

APM SDK 的健康开销:

维度 经验阈值
CPU 开销 < 1%(高频)/ < 3%(profile 期间)
内存常驻 < 5MB
内存峰值(dump 时) < 30MB
启动耗时 < 30ms
包体积 < 500KB
上报流量 < 50KB / DAU / 天

超过预算的 APM 是问题,不是解药。

# 6.2 采样降级策略

设备压力大时,APM 应主动降级:

正常态:全量采集帧时长 + 99Hz CPU 采样
压力态:仅采集异常帧 + 关闭 CPU 采样
极端态:只保留崩溃 / ANR 采集
1
2
3

触发条件:

  • 内存压力(onTrimMemory / didReceiveMemoryWarning)
  • CPU 持续高占用
  • 电量低于阈值
  • 用户主动开启省电模式

# 6.3 上报与聚合

阶段 优化点
本地聚合 客户端先做 直方图 / 计数,避免上报原始事件
批量上报 累积到一定量或定时上报,避免高频网络
压缩 gzip / protobuf,文本日志可减小 70%
网络感知 WiFi 优先、弱网降频
时序保序 上报失败重试时保持事件顺序,便于复盘
去重 同一事件多次上报需具备幂等键

# 07.跨平台采集对照

信号 / 场景 Android iOS Web 嵌入式 (Linux)
CPU 利用率 /proc/[pid]/stat host_processor_info Long Tasks API(粗) /proc/stat
线程级 CPU /proc/[pid]/task/[tid]/stat thread_info 不可见 /proc/[pid]/task
内存 RSS/PSS /proc/[pid]/status, smaps task_vm_info 不可见(隔离) /proc/[pid]/status
帧时钟 Choreographer / FrameMetrics CADisplayLink / MetricKit requestAnimationFrame / PerformanceObserver(frame) 显示控制器 IRQ
系统 trace Perfetto / Systrace / atrace os_signpost + Instruments DevTools Performance ftrace / lttng
函数 profile simpleperf / perf Time Profiler / dtrace DevTools Profiler perf / pprof
调用栈采样 Thread.getStackTrace / unwind backtrace Profiler API libunwind
内存分配追踪 Allocation Tracker / bytehook malloc Allocations Instrument Allocation Profiler tcmalloc / jemalloc
崩溃捕获 setUncaughtExceptionHandler + 信号 Mach exception + 信号 window.onerror signal / sigaction
ANR / 卡顿 Watchdog 线程 + signalQUIT RunLoop observer + Watchdog Long Tasks > 50ms 心跳超时
网络拦截 OkHttp Interceptor / 代理 URLProtocol swizzle fetch / XHR 代理 自定义
GC 事件 ART trace ARC(无 GC) PerformanceObserver(gc) —
eBPF Android 12+ 部分支持 不开放 不可访问 完整支持 ⭐

# 一句话总结

可观测性的本质,是用最小的扰动获取最大的可解释性。
设计采集系统时,永远问三个问题:采的对吗?采的全吗?采的代价值吗?

上次更新: 2026/06/07, 10:26:12
性能求证方法论
归因方法与火焰图

← 性能求证方法论 归因方法与火焰图→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式