APM 服务端本质上是一个面向移动端遥测数据的实时数据平台。它不是简单的“接收接口 + MySQL”,而要处理高吞吐写入、弱 Schema 演进、实时聚合、明细查询、符号化、Issue 聚类、告警计算和配置反控。


一、整体架构

flowchart TB
    SDK["iOS SDK"] --> Gateway["接入网关"]
    Gateway --> RawQueue["Raw Queue
Kafka / Pulsar"] RawQueue --> Clean["清洗标准化
Schema / 脱敏 / 维度补全"] Clean --> Stream["实时计算
Flink / Spark Streaming"] Clean --> RawStore["原始冷存储
OSS / HDFS / S3"] Stream --> DetailStore["明细存储
ClickHouse / Doris"] Stream --> MetricStore["指标存储
Prometheus / VictoriaMetrics / M3"] Stream --> SearchStore["搜索存储
Elasticsearch / OpenSearch"] Stream --> IssueService["Issue 聚合服务"] IssueService --> Symbol["符号化服务"] IssueService --> Alert["告警服务"] Alert --> Notify["IM / Email / Phone / Webhook"] DetailStore --> Query["查询 API / BFF"] MetricStore --> Query SearchStore --> Query IssueService --> Query Config["配置服务"] --> SDK

核心原则:

  1. 接入层只做轻逻辑,避免阻塞写入。
  2. 原始数据先入队列,后续处理异步化。
  3. 明细、指标、搜索、对象存储按查询模式分开。
  4. 符号化、Issue 聚合、告警都要异步可重放。
  5. 配置服务是反向控制链路,和数据链路同等重要。

二、接入网关

接入网关负责处理移动端上报请求。

必备能力:

能力说明
鉴权AppKey、HMAC、证书、公私钥签名
限流按 App、版本、IP、设备、事件类型限流
Schema 校验envelope 必填字段、版本、大小限制
解压解密gzip/zstd、AES、TLS
租户隔离不同 App、业务线、环境分开
时间校正记录 receive_time,处理端侧时钟漂移
快速 ACK合法数据尽快入队,不做重计算
降级Kafka 异常时本地缓冲或返回重试

上报请求要有大小限制:

单事件:建议 < 64KB
单批次:建议 < 512KB 或 1MB
Crash 附件:单独走对象存储预签名 URL
MemoryGraph/Coredump:只允许触发式上传

三、数据清洗

清洗层把 SDK 上报的事件变成平台内部标准事件。

处理步骤:

decode
  -> schema validate
  -> pii scrub
  -> normalize
  -> enrich dimensions
  -> deduplicate
  -> route by event_type

维度补全:

原始字段补全维度
IP国家、省份、城市、运营商
device_model设备名称、性能档位、内存档位
os_version大版本、小版本、是否 beta
release/buildGit commit、发布批次、灰度组
URLhost、path template、接口 owner
stack framemodule、team、owner

清洗层必须保留坏数据样本。不要简单丢弃所有解析失败事件,否则 SDK 版本问题无法追查。


四、消息队列

Kafka/Pulsar 用于削峰和解耦。

Topic 设计示例:

apm.raw.rum
apm.raw.crash
apm.raw.metric
apm.clean.rum
apm.clean.crash
apm.issue.input
apm.alert.input
apm.dead_letter

分区 key:

数据分区 key
RUM Sessionapp_id + session_id
Crashapp_id + fingerprint
指标app_id + metric_name + release
配置变更app_id

RUM Session 重建需要同一个 Session 的事件尽量落到同一分区,减少乱序处理成本。


五、实时计算

实时计算主要产出三类结果:

  1. 指标聚合:Crash 率、启动 P90、接口错误率、页面耗时。
  2. 实体聚合:Issue、Session、View、Resource。
  3. 告警输入:窗口指标、环比、基线、异常突增。

窗口建议:

粒度用途
1 分钟高优先级告警、灰度拦截
5 分钟常规告警、实时大盘
1 小时当天分析、版本对比
1 天周报、长期趋势

关键计算:

crash_free_users = 1 - crashed_users / active_users
p90_launch = quantile(0.90)(launch_duration_ms)
slow_resource_rate = slow_resource_count / total_resource_count
view_blank_rate = blank_view_count / total_view_count
foom_rate = foom_sessions / foreground_sessions

分位数建议使用 t-digest、DDSketch 或 ClickHouse quantile 函数,避免只存平均值。


六、存储设计

APM 查询模式复杂,单一存储不够。

存储适合数据原因
ClickHouse / Doris明细事件、聚合宽表、RUM 查询列式存储,适合多维筛选和聚合
TSDB指标时序适合告警和长期趋势
Elasticsearch / OpenSearch堆栈、错误消息、日志全文适合文本检索
Object Storage原始包、dSYM、MemoryGraph、Coredump成本低,适合大对象
PostgreSQL / MySQL项目、用户、权限、配置、Issue 元数据强一致业务数据
Redis热点缓存、限流、配置缓存低延迟

ClickHouse 表设计示例:

CREATE TABLE rum_events (
    app_id LowCardinality(String),
    env LowCardinality(String),
    release LowCardinality(String),
    event_date Date,
    event_time DateTime64(3),
    event_type LowCardinality(String),
    event_id String,
    session_id String,
    view_id String,
    action_id String,
    resource_id String,
    trace_id String,
    user_id String,
    device_model LowCardinality(String),
    os_version LowCardinality(String),
    network_type LowCardinality(String),
    name String,
    duration_ms UInt32,
    status_code UInt16,
    error_type LowCardinality(String),
    fingerprint String,
    payload_json String
) ENGINE = MergeTree
PARTITION BY (app_id, event_date)
ORDER BY (app_id, event_type, release, event_time, session_id);

冷热分层:

最近 7 天:明细热查
最近 30 天:聚合 + 采样明细
90 天以上:只保留聚合指标和高价值 Issue
大附件:对象存储,按需加载

七、符号化服务

iOS Crash 和卡顿堆栈必须符号化后才有诊断价值。

输入:

image_name
image_uuid
image_slide
pc_address
architecture
os_version
release
build

输出:

module
symbol
file
line
in_app
owner

服务设计:

flowchart LR
    CI["CI 上传 dSYM/BCSymbolMap"] --> SymbolStore["符号文件仓库"]
    Crash["Crash 原始地址"] --> SymbolJob["符号化任务"]
    SymbolStore --> SymbolJob
    System["系统符号库"] --> SymbolJob
    SymbolJob --> Result["符号化堆栈"]

关键要求:

  1. dSYM 必须和 release + build + UUID 绑定。
  2. CI 自动上传,不依赖人工。
  3. 支持异步重符号化。某次 dSYM 补传后,历史 Crash 要能重跑。
  4. 系统库符号按 iOS 版本和架构预建。
  5. 支持 Swift demangle。

八、Issue 聚合

APM 平台不应该把每条 Crash 都展示给研发,而应该聚合为 Issue。

聚合输入:

event_type
error_type
top_in_app_frames
exception_code
signal
thread_state
resource_host/path/status
view_name
release

Crash fingerprint 示例:

hash(
  exception_type +
  signal +
  first_3_in_app_frames_without_line +
  crashed_thread_marker
)

不同类型 Issue 的聚合口径:

类型fingerprint
Crash异常类型 + 关键栈帧
Watchdog主线程多次采样公共栈 + 阻塞类型
FOOM页面 + 内存峰值特征 + 大对象/模块
慢页面view_name + 阶段 + 版本
慢接口host + path_template + status/error
JS Errormessage 归一化 + stack

Issue 服务还要维护状态、Owner、修复版本、重复问题、噪声标记。


九、告警系统

告警不应该直接查明细库,而应该基于实时聚合指标。

告警规则类型:

类型示例
绝对阈值Crash 率 > 0.2%
环比新版本启动 P90 比老版本劣化 15%
变点过去 10 分钟网络错误率突增
TopN单 Issue 影响用户进入 Top 10
SLOCrash-free users 低于 99.9%
灰度拦截灰度组指标显著差于对照组

告警要做收敛:

同 app + 同 release + 同 issue + 同规则
  -> 合并
  -> 静默窗口
  -> 升级策略
  -> 恢复通知

不可行动的指标不要告警,只进大盘。


十、配置服务

配置服务用于反向控制 SDK。

配置内容:

{
  "config_version": "cfg_20260506_01",
  "plugins": {
    "crash": { "enabled": true, "sample_rate": 1.0 },
    "network": { "enabled": true, "sample_rate": 0.1 },
    "fps": { "enabled": true, "sample_rate": 0.01 },
    "memory_graph": { "enabled": false }
  },
  "thresholds": {
    "watchdog_ms": 2000,
    "slow_resource_ms": 3000,
    "slow_view_ms": 1000
  },
  "privacy": {
    "capture_body": false,
    "url_query_policy": "drop"
  }
}

要求:

  1. SDK 本地缓存上次可用配置。
  2. 配置有版本号和签名。
  3. 支持按 App、版本、设备、地区、用户 hash 灰度。
  4. 配置错误可自动回滚。
  5. 高危模块默认关闭,只能灰度开启。

十一、容量与成本

粗略估算:

DAU = 1000 万
每 DAU 原始事件 = 50 条
平均事件压缩后 = 300B
每日数据量 = 10,000,000 * 50 * 300B ≈ 150GB

如果网络资源、FPS、日志全量采集,数据量会迅速扩大 10 倍以上。所以服务端容量控制的第一原则是:采样策略前置到 SDK,聚合前置到流计算,长期存储保留高价值数据

成本优化:

  • 高频事件端侧采样。
  • 服务端按事件类型分表。
  • 大盘查询走预聚合表。
  • 长周期查询走降采样指标。
  • 原始明细设置 TTL。
  • 大附件对象存储化。

十二、总结

APM 服务端架构的核心是:

接得住
算得准
查得快
告得稳
能重放
可反控

它连接 C 端 SDK 和 B 端平台。SDK 采上来的只是原材料,服务端要把它加工成 Session、Issue、指标、告警、配置这些可治理对象。