Fleet 大规模 Apple MDM 负载测试指标深度解读:76k 设备批量推送场景下的 apns-mock 压测报告分析
【免费下载链接】fleetOpen device management项目地址: https://gitcode.com/GitHub_Trending/fl/fleet
本指南以 Fleet 仓库中一次真实的 Apple MDM 负载测试运行摘要(mock-apns-2026-08-21-174828Z-20m.md)为骨架,逐项拆解 76k 已注册设备、向全部主机下发 3 个配置文件的压力场景下,Fleet Server、apns-mock、RDS、Redis 与 ALB 的实测指标,并结合 collect-metrics.sh 的采集逻辑与 apple-apns-mock 的底层实现,说明每个数字的含义、采集方式、阈值依据与告警排查方法。读完本文,你将掌握如何读懂 Fleet 负载测试报告、如何定位 MDM 推送链路的瓶颈,以及如何在仓库中复现同样的指标采集与分析流程。
一次负载测试的运行背景
本次报告由 collect-metrics.sh 生成,对应runs/mdm/mock-apns/目录下的原始数据文件 mock-apns-2026-08-21-174828Z-20m.json 与可读摘要 Markdown。报告头部字段给出了本次运行的基本信息:
- Collected:2026-08-21T17:48:28Z(采集完成时间)
- Interval:20m(回看窗口)
- Window:2026-08-21T17:28:28Z 至 2026-08-21T17:48:28Z(采集覆盖的时间范围)
- Note:
76k enrolled adding 3 profiles to all hosts——即 7.6 万设备已注册,且全部主机都在接收 3 个配置文件的批量下发
metadata.note字段是运行时可自由添加的注释,用于说明该次压测正在练习什么负载。按 collect-metrics.sh 的校验规则,note 必须是单行文本、长度不超过 500 字符,且不含控制字符,以保证它能被安全地嵌入 JSON 并在 dashboard.html 中展示。
从压测规模看,这是一次典型的 Apple MDM 推送风暴(push storm)测试:7.6 万设备同时接收 3 个配置文件的 MDM 命令,意味着 Fleet Server 需要通过 mock APNs 服务向海量设备推送InstallProfile/RemoveProfile类命令,并在设备端产生密集的 SSE 长连接回流。
复现这份报告:指标采集工具链
在深入解读数据之前,先了解这些数字是如何被采集的。整个采集链路在 tools/loadtest/metrics/README.md 中有完整说明:
- collect-metrics.sh — 根据 Terraform workspace 名自动发现负载测试环境的 AWS 资源(ECS 集群、RDS 集群、ElastiCache、ALB、CloudWatch 日志组),拉取回看窗口内的 CloudWatch 指标,同时输出
.json数据文件与.md可读摘要(含阈值告警)。 - compare-metrics.sh — 对两个或多个运行做并排 diff,把增量标记为
ok/WARN/ALERT,用于跨版本捕捉回归。 - dashboard.html — 本地浏览器可视化,把
runs/文件夹拖入页面即可渲染,无需服务器。
复现一次同样格式的采集:
# 20m 回看窗口,filed under mdm 类别,附带运行注释 ./collect-metrics.sh --workspace mock-apns --interval 20m --category mdm \ --note "76k enrolled adding 3 profiles to all hosts"关键参数(--help可查看全部):
| Flag | 含义 |
|---|---|
-w, --workspace | Terraform workspace 名(必填),AWS 资源名由它推导 |
-i, --interval | 回看窗口:<N>h、<N>m或裸整数(小时),默认3h |
-c, --category | 归档类别:baseline|migration|mdm |
-n, --note | 运行注释,嵌入metadata.note |
-o, --output | 覆盖输出文件路径 |
-r, --region | AWS 区域,默认us-east-2 |
需要注意:--workspace不是自由文本,collect-metrics.sh 会从它推导资源名(fleet-<ws>-backend、fleetdm-<ws>-mysql、fleet-<ws>-redis、fleet-<ws>-apns-mock等),且要求匹配^[A-Za-z0-9][A-Za-z0-9_-]*$,防止路径穿越写出runs/目录。
MDM 场景下,报告会额外出现两个专属小节(前提是部署时启用了 Apple MDM,即var.enable_apple_mdm):apns_mock与apns_mock_redis。两个小节在本报告中均有数据,正是本次压测的核心观测对象。
顶层资源负载:Fleet Server 与压测机
Fleet Server(40 个容器)
Fleet Server: CPU=82.27% Mem=7.91% Containers=40- CPU 82.27%:这是服务级平均,由 collect_ecs_utilization 以
Sum(CpuUtilized)/Sum(CpuReserved)计算得到,与 ECS 性能面板公式一致。JSON 原始数据(fleet_server.cpu_utilization)显示 Average=82.27、Minimum=80.03、Maximum=84.29,整个 20 分钟窗口内 CPU 始终高于 80%,处于持续饱和状态。 - Mem 7.91%:内存利用率(
MemoryUtilized/MemoryReserved)很低,说明瓶颈在 CPU 而非内存。 - Containers 40:
task_counts.runningCount = desiredCount = 40,全部按期望运行。
这是本报告最值得关注的信号:在 76k 设备 + 3 配置文件的场景下,Fleet Server 的 CPU 已经顶着 80% 阈值运行。结合下面的Top SQL与错误日志可以进一步确认 CPU 花在了哪里。
Loadtest 压测容器(38 个)
Loadtest: CPU=10.77% Mem=11.15% Containers=38压测容器(模拟 osquery 设备端上报的负载)CPU 只有 10.77%,内存 11.15%,余量充足,说明压测流量本身没有成为瓶颈,观测到的服务端压力全部来自真实负载。
核心观测对象:apns-mock 推送服务
服务级平均 vs 单容器热点
apns-mock: CPU=4.86% Mem=17.6% Containers=2 (averaged across containers) PerTask CPU avg=4.86% max=9.37% Mem avg=17.6% max=20.75% RX=17.26MB TX=17.78MB AbnormalStops=0 StartSpread=21.17min ⚠ STAGGERED STARTSapns-mock 是 Fleet 对 Apple 推送服务(APNs)的本地模拟,承担“把 Fleet 的推送请求转给模拟设备”的角色。本报告特别区分了两种视角:
- 服务级平均:
Sum(Utilized)/Sum(Reserved)跨全部任务的平均。这里的 CPU=4.86% 看似健康,但 collect-metrics.sh 明确指出:服务平均会掩盖单个饱和容器——一个任务若被打满,会把它持有的所有 SSE 流全部断开,而平均值看起来仍然正常。因此必须同时看per_task。 - PerTask:
avg_pct(平均任务)与max_pct(最热任务)。CPU avg=4.86%、max=9.37%,两者都远低于告警阈值(最热任务 CPU < 95%),说明两个 mock 实例负载均衡良好,没有热点任务。spread_pct(max−avg 的差值)只有 4.51%,也印证 ALB 连接分发均匀。
网络方面 RX=17.26MB / TX=17.78MB。与普通 HTTP 服务不同,collect-metrics.sh 特别指出:SSE 流都是持有的长连接,流量体积比 ALB 的请求数更能反映推送扇出(fan-out)规模,因此这里用 RX/TX 字节而非请求数衡量吞吐。
容器健康是本报告的第一个红旗:StartSpread=21.17min,两个任务的started_at分别为17:10:49与17:31:59(见 JSON 的apns_mock.container_health.tasks),启动时间相差约 21 分钟,超过 10 分钟阈值,被标记为start_spread_alert: true,在摘要中显示为⚠ STAGGERED STARTS。这意味着有一个 apns-mock 实例在压测进行中重启过(或较晚才完成扩容),异常停止数AbnormalStops=0则说明没有容器以非零退出码崩溃。值得留意的是,重启期间它所持有的 SSE 流会断开并转移到其他实例,这可能是 Fleet Server 5xx 与错误日志的来源之一。
为什么 apns-mock 是轻量设计
从源码看,apns-mock 的 CPU 如此之低并非偶然。cmd/apple-apns-mock/README.md 说明了两个关键设计:
- SSE 处理器劫持连接而非阻塞:
eventsSSEHandler劫持net/http的连接后立即返回,每连接约 8 KB 开销,75k 实时流实测约 598 MB 总量,其中大部分是每个流约 4 KB 的 goroutine 栈。 - 实例可互换,通过 Redis 协调:Fleet 可以推给任意一个 mock 实例,由 Redis 负责把推送路由到持有对应设备流的实例。
运行 apns-mock 的关键参数
源码中给出了完整启动方式与参数表:
docker compose up -d redis go run ./cmd/apple-apns-mock --listen :8378 --redis-address 127.0.0.1:6379| Flag | 默认值 | 说明 |
|---|---|---|
--listen | :8378 | 监听地址 |
--default-ttl | 24h | 无apns-expiration头时,离线设备推送的保留时长 |
--keep-alive | 30s | SSE keep-alive 注释间隔,0关闭 |
--write-timeout | 10s | 单次 SSE 写入截止时间,停止读流的设备会被断开而非钉住 token |
--redis-address | — | 共享 Redis 地址,必填 |
--redis-key-prefix | apns: | 键与频道的命名空间,并发压测可加不同前缀隔离 |
--node-id | hostname-pid | 集群统计中的实例标识 |
--stats-interval | 5s | 实例发布计数器的间隔 |
GET /stats可查看单实例(node)与全集群(cluster)的计数;GET /memstats报告 Go runtime 真实内存占用(压测时优先看它而不是 RSS)。
apns-mock 专属 Redis:推送吞吐的真实记录仪
apns Redis: CPU=0.78% (max 1.9%) Mem=0.19% Conns=17.7 PendingItems=3 Evictions=0 StringCmds=469180 PubSubCmds=234348 NetOut=142.72MBmock APNs 使用自己专属的 ElastiCache(fleet-<ws>-apns-mock)而非 Fleet 的 Redis——collect-metrics.sh 明确说明这样设计的目的是:一次全量推送波不会扰动被测系统(Fleet Server)本身。
各指标解读(依据 collect-metrics.sh 与源码中 Redis 协调协议):
- CPU=0.78%(max 1.9%):采集的是
EngineCPUUtilization。Redis 单线程执行命令,这个指标会远早于宿主 CPU 饱和,因此它是更敏感的推送吞吐信号。本次负载极轻。 - Conns=17.7:连接数。
apns-mock每个任务只持有 1 条订阅连接加一个小的命令池,所以负载与推送速率、实例数相关,而不是与设备连接数相关。 - PendingItems=3:
CurrItems表示等待认领的待推送项(外加每个实例一个统计键)。值为 3 说明几乎没有积压,设备都能及时连上领取推送。一个持续抬升的底值意味着设备没有连上来领取推送,是需要警惕的信号。 - Evictions=0:必须为零。任何一次逐出都意味着一条待推送在设备重连前被静默丢弃,等价于真实 APNs 的投递丢失。
- StringCmds=469180 / PubSubCmds=234348:
StringBasedCmds(SET/GETDEL/INCR)与PubSubBasedCmds(PUBLISH/SUBSCRIBE)之和,是 Redis 视角看到的推送吞吐。每次推送在接收实例上发生一次 SET + 一次 PUBLISH,在持有流的实例上发生一次 GETDEL,且每条广播会扇出到所有订阅实例,因此这两类命令数直接反映推送风暴的规模。 - NetOut=142.72MB:
NetworkBytesOut随实例数增长——每条广播都会投递给每个已订阅的实例。
底层推送路由协议
cmd/apple-apns-mock/README.md 中给出了完整的 Redis 协调流程,帮助理解为什么 Redis 指标是这样分布的:
push at any instance SET <prefix>pending:<token> <ping> GET PX <ttl> PUBLISH <prefix>push {"t":"<token>"} every instance holds the token? -> GETDEL <prefix>pending:<token> value -> write it to the stream nil -> another instance got there first connect at any instance GETDEL <prefix>pending:<token>, then announce ownership涉及的键:<prefix>pending:<token>、<prefix>stats:<node-id>、<prefix>seq,以及<prefix>push频道。核心语义包括:每 token 一条待推送,新推送覆盖旧的(对齐 APNs 的 coalescing);对已连接设备立即投递且不存储;同一 token 最新连接生效,旧连接被关闭;设备停止读流时在--write-timeout后断开但保留待推送。Redis 不可达时推送返回503 ServiceUnavailable而不是静默丢弃——这对应了 ALB 的 5xx 计数。
数据层:RDS 写节点与读副本
RDS Writer(写节点)
RDS Writer: CPU=24.85% Connections=508.9 Deadlocks=0 RDS Writer: FreeMem=27.24GB CacheHit=100% Disk=N/A Threads=4.01 IOPS=13.76% SelectLat=0.83ms InsertLat=3.57ms- CPU=24.85%、无死锁、BufferCacheHit=100%、IOPS 利用率 13.76%(实例类
db.r6g.4xlarge,max_iops 20000,实际读写 IOPS 合计约 2752)——数据库层整体健康,远未饱和。 - Threads=4.01(vCPUs=16):来自 Performance Insights 的
db.load.avg(平均活跃会话 AAS)。collect-metrics.sh 的判定标准是:持续高于 vCPU 数(16)才表示查询并发导致 CPU 饱和,4.01 远低于此。 - SelectLat=0.83ms、InsertLat=3.57ms:写路径(Fleet 的负载是插入密集型的——主机 checkin、软件清单)延迟毫秒级,健康。
RDS 读副本
RDS reader-1: CPU=43.63% Connections=365.45 RDS reader-2: CPU=25.49% Connections=281.25 RDS reader-1: FreeMem=28.28GB CacheHit=99.99% ReplicaLag=7.9ms Threads=3.13 IOPS=0.29% SelectLat=0.3ms RDS reader-2: FreeMem=28.37GB CacheHit=99.99% ReplicaLag=7.1ms Threads=1.84 IOPS=0.22% SelectLat=0.35ms- reader-1 CPU 43.63% 是三个数据库节点中最高的,但仍低于 90% 的读副本阈值。两个副本的
AuroraReplicaLag分别为 7.9ms 与 7.1ms——collect-metrics.sh 指出,超过 20-50ms 才可能引起脏读,当前延迟在正常范围。 - reader-1 承担了明显更多的查询负载(CPU、连接数、AAS 均更高),这与 Top SQL 中它排在最前的两类查询吻合(见下节)。
Top SQL:CPU 花在了哪里
Top SQL (writer): #1 load=2.92 COMMIT #2 load=0.81 INSERT INTO `host_seen_times` ( `host_id` , `seen_time` ) VALUES (...) /* , ... #3 load=0.14 SELECT `c` . `command_uuid` , `c` . `request_type` , `c` . `command` , `c` . `su... #4 load=0.08 UPDATE `host_mdm_actions` ... #5 load=0.05 INSERT INTO `host_seen_times` ...这些由 flatten_top_sql 从 Performance Insights 的db.load.avg(按db.sql_tokenized分组)解析而来。写节点上COMMIT独占 load 2.92,是第二大查询(host_seen_times批量插入,load 0.81)的三倍多——事务提交本身成为写路径的头号开销,其次是主机心跳时间戳的批量 upsert。这与 Fleet 高频主机 checkin 的工作负载特征完全一致。
读副本上排名靠前的则是queries表(调度查询配置)、host_mdm(MDM 注册信息)、packs(查询包)与upcoming_activities(待执行活动)的查询,反映了设备端获取配置与待办命令的读路径。
边缘组件:Redis、ALB 与网络
Fleet 主 Redis
Redis: CPU=51.18% Mem=3.59% Redis: Connections=349.45 Evictions=0 CacheHit=84.96%三个节点中redis-1承担了主要负载(CPU 51.18%、连接 349.45),redis-2/redis-3几乎空闲(CPU 6.78%/7.28%、连接约 6),说明 Fleet 的 Redis 负载分布不均但总量远未到瓶颈(阈值 CPU < 80%、Mem < 70%)。CacheHit=84.96% 正常,Evictions=0 意味着没有因内存压力丢数据。
ALB 与网络
ALB: Latency=0s 5xx=472 Requests=42752257 Traffic=82774.7MB Network: RX=1361.43MB TX=455.84MB- Requests=42752257:20 分钟约 4275 万请求,主要由模拟设备的 osquery 上报驱动。
- 5xx=472:Fleet Server 容器返回了 472 个 HTTP 5xx(数据覆盖率为 0.25,仅 1 个数据点,需谨慎解读)。结合下面 1175 条错误日志,这部分 5xx 大概率与
mdm_macos_software_update_id查询摄取失败相关——它是本报告第二个需要排查的问题。 - Latency=0s:
TargetResponseTime平均为 0 秒(最大 1.79s),API 层面延迟极低。 - Traffic=82774.7MB:
ProcessedBytes汇总,用于发现载荷体积的意外变化。
阈值检查:三个告警的逐一拆解
本报告共触发 3 个告警(阈值定义见 collect-metrics.sh 的check_threshold逻辑):
🔴 ALERT: Fleet Server CPU avg = 82.27 (expected < 80) 🔴 ALERT: apns-mock Start Spread (min) = 21.17 (expected < 10) 🔴 ALERT: Fleet Server Errors = 1175.0 (expected 0)告警一:Fleet Server CPU = 82.27%(阈值 < 80%)
前文已述,整个窗口内 Fleet Server CPU 处于 80–84% 区间,持续超限。结合Top SQL与错误日志可形成完整因果链:大量设备的 osquery 上报(4275 万请求)驱动了高频写事务(COMMITload 2.92)与查询负载,叠加 MDM 命令下发带来的额外请求,使服务端 CPU 成为本次压测的首要瓶颈。这是规模与资源配比的问题,而非故障——apns-mock 本身(CPU 4.86%)与数据库(writer 24.85%)都余量充足,说明瓶颈集中在 Fleet Server 应用层。
告警二:apns-mock Start Spread = 21.17min(阈值 < 10min)
两个 mock 实例启动时间相差 21 分钟(一个在窗口开始前 37 分钟启动,另一个在窗口开始前 16 分钟才启动),属于“错峰启动”(STAGGERED STARTS)。start_spread_alert在 collect_service_health 与整体容器健康检查中都会计算,10 分钟阈值的依据是“正常扩容的所有容器应在几分钟内相继就绪”。对于持有 SSE 流的服务,晚到的实例意味着部分设备流在压测中经历了一次重连迁移,也可能贡献了 Fleet 端部分瞬时错误。
告警三:Fleet Server Errors = 1175(期望 0)
错误日志通过 CloudWatch Logs Insights 查询采集(collect-metrics.sh),查询会过滤掉两类已知噪音:fleet_detail_query_software错误(osquery-perf 默认对软件查询有 50% 失败率)与context canceled(容器部署期间的正常现象)。
从 JSON 中的sample_messages可见,本次 1175 条错误全部来自同一类:
POST /api/osquery/distributed/write ingestion-err: ingesting query mdm_macos_software_update_id: directIngestMDMMacOSSoftwareUpdateID empty software update device ID err: error in query ingestion即 osquery 上报的mdm_macos_software_update_id查询在摄取时遇到“空的软件更新设备 ID”,导致整条查询摄取失败并返回错误。这是压测模拟数据(设备返回空 ID)与摄取逻辑之间的数据完整性问题,而非运行时崩溃——但 1175 次的规模使它达到告警线(期望 0),值得在真实产品代码中核查directIngestMDMMacOSSoftwareUpdateID对空设备 ID 的容错处理。
未触发的阈值(健康面)
同样值得记录的是所有通过的检查:Fleet Server 内存、RDS 写/读节点 CPU、Redis 与 apns-mock Redis 的 CPU/内存、所有 Evictions、AbnormalStops、IOPS 利用率、读副本与写节点线程数对比 vCPU 等均处于阈值内。这为后续版本对比提供了健康基线。
从运行到对比:如何使用这份报告
本次运行的 JSON 与 MD 已归档在 tools/loadtest/metrics/runs/mdm/mock-apns/,同目录下还有同一天的 1h 与多次 20m 运行(mock-apns-2026-08-21-171402Z-1h、172709Z-20m、184317Z-20m、193108Z-20m),可横向对比同一场景下指标随时间的演变。跨版本回归对比则使用:
# 最近两个 MDM 运行并排对比 ./compare-metrics.sh runs/mdm/mock-apns/mock-apns-*.jsoncompare-metrics.sh会递归搜索runs/,类别子目录自动包含;--filter按 workspace 名匹配(如--filter apns),增量按ok/WARN/ALERT分级。
结论与排查清单
综合本报告,可以给出以下结论与行动建议:
- 本次压测的瓶颈在 Fleet Server 应用层 CPU(82.27%),而非数据库、Redis 或 apns-mock——扩容方向应优先考虑 Fleet Server 任务数或降低单请求开销(写事务
COMMIT开销居首)。 - apns-mock 推送链路健康:mock 与专属 Redis 的 CPU、内存、Evictions、PendingItems 全部处于极低水平,469k 字符串命令 + 234k PubSub 命令顺畅处理,无积压、无丢失。
- 两个运维级问题需跟进:apns-mock 实例错峰启动 21 分钟(检查部署/扩容策略),以及
mdm_macos_software_update_id空设备 ID 摄取错误 1175 次(检查模拟数据与摄取容错)。 - 报告本身可复现:通过 collect-metrics.sh 以
--workspace mock-apns --interval 20m --category mdm即可重新采集;源码级细节可继续阅读 cmd/apple-apns-mock/README.md(推送协议与 Redis 协调)、pkg/mdm/apnsmock(Go 客户端)与 docs/Contributing/mdm/apple/apple-apns-mock.md(设计文档)。
如果你正在规划自己的 MDM 推送压测,这份报告提供了一个可直接参照的观测模板:服务级平均与单任务热点并行看、Redis Evictions 与 PendingItems 必须为零、以及 ALB 5xx 与 Fleet 错误日志要交叉印证——这三组信号足以快速定位推送链路中的绝大多数问题。
【免费下载链接】fleetOpen device management项目地址: https://gitcode.com/GitHub_Trending/fl/fleet
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考