可观测性与堵点定位
实时语音链路出现“几秒后才出字幕”时,先不要猜是网络还是模型。Muxiva Studio 的 Observe 页面把 Runtime 的 Node 执行、Edge 背压以及 Node 内部缓冲放在同一张运行视图中。
第一次定位
- 用
muxiva studio examples/voice-agent/graph.json打开本地诊断环境,在 Studio 点击 Run。 - 说一句完整的话,同时打开顶部的 ◎ Observe。
- 先看 Hotspots,再点击红色或黄色的 Node / Edge 行。
- 右侧会展示导致判定的原始数值和建议动作。
| 看到的现象 | 它回答的问题 |
|---|---|
| Node rate、Processed | Node 有没有收到数据、当前每秒处理多少次 |
| Avg / Max process | on_process 本身是否耗时 |
| Edge rate、Frames | 数据是否真的经过这条连接 |
| Queue、Oldest | 下游是否来不及消费、最老数据已等待多久 |
| Drops / Full | 是否已经丢数据或阻塞生产者 |
| Media speed | Audio Edge 每秒传输了多少秒音频;1.00× 约等于实时速度 |
| Node metrics | Node 自己的隐藏缓冲,例如 RTC Source 内部缓存时长 |
状态阈值是面向实时交互的诊断默认值,而不是业务 SLA:Edge 队列达到 40% 或最老 Frame 等待 200 ms 会变黄;达到 80%、等待 1 s 或发生丢帧会变红。Node 平均处理超过 10 ms 会提示,超过 50 ms 会标红。Node 内部输入缓存达到 200 ms / 1 s 时也分别变黄 / 变红。
Important
增大 Edge capacity 通常只能推迟失败,并会增加端到端延迟。先比较生产和消费速率,再处理慢 Node、阻塞 I/O 或 Node 内部没有及时排空的队列。
终端日志
Voice Demo 会把完整输出保存到项目本地的 .muxiva/runtime.log。运行期间可在另一个终端查看:
每五秒会输出一次可机器检索的摘要;有堵点时同时输出具体实体:
[MUXIVA][OBSERVE][SUMMARY] session=1 nodes=7 edges=8 queued=46 drops=0 bottlenecks=2 dashboard=Studio/Observe
[MUXIVA][OBSERVE][EDGE][CRITICAL] edge=audio-to-qwen queue=29/32 oldest_ms=1640 drops=0
[MUXIVA][OBSERVE][NODE][CRITICAL] node=agora-audio-source avg_process_ms=0.12 ingress_queue_ms=5120
快速过滤:
顶部 Session 选择器与跨会话趋势
Observe 顶部首先展示 Session 下拉框。选项以 Session #<session_id> 为主键,并附带
开始时间和 RTC Channel;选中后,Summary、Hotspots、Semantic Trace、Media Dump、
Nodes、Edges 和右侧详情全部只读取该 Session,当前 Runtime 不会混入历史视图。
Studio 每 5 秒保存一个有界快照,并在会话结束时保存最终快照。通过顶部选择器可以:
- 比较不同 Runtime Session 的总 Frame、最大积压、丢帧和最慢 Node 平均耗时;
- 选择一个历史会话,查看 Queued frames、Slowest Node avg、Drops 和 Frames processed 趋势;
- Studio 重启后继续查看历史。
采样由 Studio Server 后台执行,不依赖 Observe 页面保持打开。
数据保存在项目本地:
同一份数据也通过 Bearer 鉴权 API 提供:GET /api/v1/observability/history 返回会话摘要,GET /api/v1/observability/history/{run_id} 返回该会话的采样点。
该目录默认被 Git 忽略。历史最多在内存中保留 5,000 个采样点;文件超过 16 MiB 时自动压缩到最近 2,500 个采样点。这份指标历史不会保存 Frame Payload、用户语音、对话文本或凭据。只有显式打开下面介绍的媒体 Dump 开关,才会保存原始音视频。
按轮次追踪每一条 Text、Event 和 Signal
打开 ◎ Observe → Semantic trace,查看 Graph 中实际传播的语义,而不仅是性能计数器。最新 Turn 默认展开,每一行会显示:
- 相对 Runtime 启动时间;
- Text、Event 或 Signal 类型;
- 生产或消费它的 Node 与 Port;
OUT →或→ IN边界方向;- 文本、Topic/Name 与 Payload 摘要。
点击一行,可以查看有界的完整 Payload,以及 Frame ID、Trace ID、Stream ID 和 Sequence。类型选择器与搜索框可按 Node、Port、Topic、文本片段、Frame 或 Trace 过滤。同一个 Frame ID 先出现在上游输出、再出现在下游输入,表示传播成功;缺少对应输入行,就能直接确定消息消失在哪个边界。
Studio 优先根据 builtin.voice_turn_controller 输出的 muxiva.turn.started 与 Payload 中的 turn_id、generation 展示 Turn。原始 muxiva.voice.speech.started 只表示活动观察,不创建业务 Turn。没有 Turn 标记的普通 Graph 会显示为一个 Session flow。Runtime 调度器不拥有业务 Turn ID;显式 Voice Turn Controller Node 拥有并输出该语义。
语义追踪覆盖 Graph 的 Text/Event Frame Port,以及图内 Signal 控制平面,包括 Signal 的发出与送达。它不会把进程内 NotificationBus 消息冒充成 Graph Event;NotificationBus 仍通过其显式消费者或日志集成观察。
Trace 有明确上限:内存中保留最近 4 个会话,每个会话最多 10,000 条记录;单条展示 Payload 最多 4 KiB,单会话 Payload 最多 4 MiB。溢出与截断会在页面提示。对话内容在 Studio 重启时清空,不写入可观测性历史文件。鉴权 API 为 GET /api/v1/observability/traces 和 GET /api/v1/observability/traces/<run-id>。
Warning
Semantic Trace 包含对话文本以及 Event/Signal 的结构化 Payload;截图和复制出的 JSON 都应按敏感数据处理。
检查每个 Node Port 的音频或视频
指标能回答“链路堵在哪里”,媒体 Dump 则能让你直接听到或看到“这个边界实际经过了什么”。打开 ◎ Observe → Node media dumps,在运行前或运行中开启 Dump Audio + Video。Studio 每次启动时,该开关都默认关闭。
开启后,Studio 会按以下组合分别生成文件:
例如,qwen-input-resampler.audio_out · OUTPUT 是重采样 Node 实际发出的音频;qwen-audio-realtime.audio_in · INPUT 是 Qwen Node 实际收到的音频。对比相邻的两条轨道,就能直接发现数据损坏、静音、采样率错误以及 Node 之间的断点。
- Audio 会被封装成标准 WAV,可在 Observe 中直接播放或下载。
- RGBA8、YUV420p Video 会保存为带平面与 stride 元数据的原始帧序列,可在 Observe 的 Canvas 中回放或下载。
- 文件和 manifest 保存在
.muxiva/observability/media/<run-id>/,Studio 重启后仍可查看;最多保留最近 4 个会话。 - 采集使用容量为 256 Frame 的异步有界队列;单文件上限 64 MiB,单会话上限 256 MiB。诊断队列满时只丢弃 Dump 副本,不会丢 Runtime Frame、不会阻塞实时链路;页面会显示丢弃和截断数量。
Warning
Dump 可能包含隐私敏感的麦克风音频或摄像头画面。只在排障期间开启,用完立即关闭,也不要发布 .muxiva/observability/media。项目模板默认 Git Ignore 该目录,但复制或下载后的文件仍需由你负责保护。
对应的鉴权 API 为:GET /api/v1/observability/media、GET /api/v1/observability/media/<run-id>、PUT /api/v1/observability/media(请求体 {"enabled":true|false}),以及 GET /api/v1/observability/media-artifacts/<run-id>/<artifact-id>。
Prometheus 抓取
Studio 提供标准 Prometheus 文本端点:
它与 Studio API 使用同一个 Bearer Token。手工验证:
# Token 是 Studio 启动 URL 中 # 后面的值
curl -H "Authorization: Bearer $MUXIVA_STUDIO_ACCESS_TOKEN" \
http://127.0.0.1:5678/metrics
生产式本地抓取应固定端口和私有 Token:
export MUXIVA_STUDIO_ACCESS_TOKEN="$(openssl rand -hex 32)"
./examples/voice-agent/run.sh --studio --port 5678
Prometheus 配置示例:
scrape_configs:
- job_name: muxiva-local
static_configs:
- targets: ["127.0.0.1:5678"]
authorization:
type: Bearer
credentials: "替换为 MUXIVA_STUDIO_ACCESS_TOKEN"
主要指标使用 muxiva_node_* 和 muxiva_edge_* 前缀,包括 Node 回调、耗时、错误、自定义 Counter/Gauge,以及 Edge 队列、吞吐、丢帧和阻塞。
OpenTelemetry OTLP/HTTP
OTLP 导出器位于 Studio 应用层,不会把 OpenTelemetry SDK 或网络客户端耦合进 Runtime Core。当前支持标准的 OTLP/HTTP JSON,向指标端点发送 ExportMetricsServiceRequest JSON;默认基础端点会追加 /v1/metrics。协议规则见 OpenTelemetry OTLP 规范。
连接本地 OpenTelemetry Collector:
export OTEL_EXPORTER_OTLP_ENDPOINT="http://127.0.0.1:4318"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/json"
export OTEL_METRIC_EXPORT_INTERVAL="10000"
./examples/voice-agent/run.sh --studio
厂商提供完整 Metrics URL 时使用 signal-specific 配置;它不会自动追加路径:
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="https://collector.example.com/v1/metrics"
export OTEL_EXPORTER_OTLP_METRICS_PROTOCOL="http/json"
export OTEL_EXPORTER_OTLP_METRICS_HEADERS="authorization=Bearer%20YOUR_TOKEN"
Observe 顶部 Session 选择器下方会显示 OTLP configured、exporting 或最近一次错误。导出在独立线程执行,不阻塞 Node 回调和 Runtime 调度;上一次请求未结束时不会无限堆积新请求。
Note
当前实现只导出 Metrics,不导出 Trace 和 Log;采样历史是项目本地开发能力,不代替 Prometheus、Tempo、Jaeger 或厂商后台的长期保留。
自定义 Node 暴露内部指标
Runtime 自动测量回调和 Edge,但它无法猜到 SDK 或模型客户端内部还有一层队列。Node 应通过 ctx 上报非敏感的整数 Counter / Gauge;这些值会出现在 Observe 的 Node metrics 中。
名称只能包含 ASCII 字母、数字、点、下划线和连字符,最长 64 字符;值必须是非负整数。不要把 API Key、原始语音或用户文本写入指标。
如何区分三类延迟
- Node 平均处理时间高、输入 Edge 堆积:算子回调慢或在同步等待网络/磁盘。
- Node 处理很快,但它报告的
ingress.queue_duration_ms高:算子内部 SDK 队列或轮询节奏有 bug。 - Agora/Qwen 输入 Frame 持续增长,但
input.audio_peak_pcm16和input.audio_mean_abs_pcm16始终接近 0:浏览器发布的是静音或错误输入设备;先修复麦克风, 不要排查模型网络。 - Runtime 所有队列健康,而云端首包仍慢:重点检查厂商服务时延、地域、会话配置与网络;结合厂商侧 request/session ID 排查。