[图片:Recall.ai logo]

BlogCustomersAPI Docs LoginGet started BlogCustomersAPI DocsLoginGet Started

深入剖析

Pdeathsig 几乎不是你想要的

[图片:James]

James Matsuzaki 2025年2月28日

[图片:Latency] [图片:目录]

目录

Pdeathsig 几乎不是你想要的

那是一个美好的周日夜晚。我刚抵达洛杉矶参加公司的线下活动,并且第一次见到了整个团队。

不久后,我的手机收到了一条通知,“Antonio 在 Linear 上给你分配了一个 issue。” Antonio 是 Recall.ai 的工程团队负责人,他给我的任务是优化 Output Media 的启动延迟,以便我们客户的 AI 代理能够更快地启动。

我本以为这将是我这周工作中速度最快、最直接的事情,但事实并非如此,它花费的时间比预期的要长得多。

这是一个看似简单的任务如何演变成对 Linux 内核、Bubblewrap 沙箱和 Rust Tokio 线程模型中未公开的复杂性的深入研究的故事。

计划

Output Media 是 Recall.ai 的一项功能,可以从机器人输出超低延迟的音频和视频。 我们的客户使用 Output Media 构建会议中的 AI 代理、交互式应用程序等。

在底层,Output Media 的工作原理是将客户提供的网页渲染成音频和视频,然后通过机器人发出。

由于渲染网页涉及运行任意不受信任的代码,因此我们采取了很多措施来确保此功能的安全性。 我们使用的最突出的技术是沙箱。 为了性能和简单性,我们使用 Bubblewrap 对 Output Media 代码进行沙箱处理,从而限制其权限——仅足以使其正常运行。

该功能当前的状态还算不错——它可靠,并且可以使用它构建的东西是无穷无尽的。 只是_一个小问题_。 每当客户激活 Output Media 时,视频需要 12 秒才能开始流式传输,这太慢了!

造成这种情况的原因是,当 Output Media 被激活时,我们会启动一个 Chromium 实例来渲染网页。 众所周知,Chromium 复杂、庞大且资源密集,并且机器人运行在资源非常有限的环境中。

那么,可以做些什么来解决这个问题呢?

计划似乎很简单:在机器人启动时预加载 Chromium,而不是仅在 Output Media 激活时才启动它。 这样,当开发人员请求机器人加入他们的会议时,Chromium 已经准备就绪,并且他们的页面可以立即加载。

本世纪的拦路虎

我正在研究代码,重构它,以便我们可以在会议加入逻辑之前启动 Chromium。 进展顺利,所以我开始在本地测试它。

好吧……机器人正在运行,它正在加载先决条件,它正在启动 Chromium,完成了……现在它宣布了它的状态。 太好了! 让我们提交这个。

当时,我非常确信我已经完成了这项任务。 我将其推送到我们的暂存平台,并等待我们数十个集成测试成功。 但是,他们没有。

检查日志显示 Chromium 已启动,然后终止了——莫名其妙地。 奇怪的是,我们没有任何代码执行进程终止,那么这里发生了什么?

一点背景知识

Bubblewrap 是 Linux 中包含的一个轻量级程序,允许创建与程序执行内联的隔离沙箱环境。 Bubblewrap 可以利用 Linux 命名空间、seccomp 过滤器、绑定挂载和内核级 API 来生成包含的进程。

与完整的虚拟机或像 Docker 这样的容器化(资源密集型)不同,Bubblewrap 以最小的开销运行,同时仍提供强大的隔离,这在我们计算受限的机器人环境中至关重要。

我们还利用 Tokio 来确保我们机器人同时处理的所有不同类型任务的快速性能和并行处理。 这包括像将音频实时中继到转录提供商这样的操作,同时处理传入的结果并将它们保存到我们的数据库以供稍后检索,同时将这些结果转发到我们客户指定的入口端点。

Output Media 也不例外——它同时使用许多线程并同时处理它们,以确保流畅和优化的体验,而不会影响我们的客户依赖我们的机器人的其他基本任务。 这在稍后变得非常重要……

我找到了解决方案! 但这不应该解决这个问题……

在接下来的一个星期里,我每天都在调试这个进程终止问题,而且我精疲力竭。

我已经探索了我关于该进程为何终止的每一个理论,最终演变为只是将想法扔到墙上,希望其中一个会奏效。

我决定玩玩我们给 Bubblewrap 的参数,以运行我们隔离的 Output Media 代码。

糟糕,删除这一个破坏了所有东西。 好的,这一个什么也没做……那一个显然没有解决问题,我为什么要尝试更改它? 好的,这一个绝对不相关,对吗? 保存,编译。 为什么要这么久? 请工作,请工作,请工作,请工作——

“兄弟,我修复了它。”

在我们的 Bubblewrap 命令中,有一个标志 --die-with-parent,不知何故导致我们的 Chromium 随机终止。 删除此参数似乎解决了该问题。

但这说不通。 父进程是机器人的主循环,它肯定仍在运行。 那么,删除 --die-with-parent 标志是如何解决问题的呢?

让我们深入研究

[图片:深入研究]

还记得我在几段前提供有关 Bubblewrap 的背景信息吗? 好吧,--die-with-parent 使用了前面提到的“内核级 API”。 从该标志的名称来看,我推断它的目的是确保如果父执行程序退出或崩溃,它的退出将向下传播到在 Bubblewrap 中执行的子程序。

从理论上讲,这对于确保你不会最终得到一堆滞留的进程,从而减少了手动清理过程的需求,非常有用。 但显然它比那更微妙一些,因为当设置 --die-with-parent 时,我们的子进程被随机终止,即使它的父进程继续存活。

所以我的下一步实际上是研究 Bubblewrap 的 源代码

/* If --die-with-parent was specified, use PDEATHSIG to ensure SIGKILL
 * is sent to the current process when our parent dies.
 */
static void
handle_die_with_parent (void)
{
    if (opt_die_with_parent && prctl (PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) != 0)
        die_with_error ("prctl");
}

当我第一次看到这段代码时,我非常失望。 它太简单了! 它所做的只是调用本机函数来设置 PR_SET_PDEATHSIG。 此功能完全由 Linux 内核处理。 当进行此调用时,内核会在进程的内部数据结构中记录此首选项。

我们遇到的关于 PR_SET_PDEATHSIG 的所有文档都有些误导。 注释块中的短语“When our parent dies(当我们的父进程死亡时)”听起来像是指父_进程_。 但实际上,它指的是父_线程_。 这个细微的细节造成了所有的差异,而我们最长的时间都错过了这个细节。

实际原因

在研究了 Bubblewrap 的源代码和 Linux 内核文档后,我们最终发现了真正的罪魁祸首。 问题不在于进程死亡,而在于 Linux 内核在使用 PR_SET_PDEATHSIG 时如何解释线程的生存期。

当设置 --die-with-parent 标志时,Bubblewrap 使用 Linux 内核的 prctl(PR_SET_PDEATHSIG, SIGKILL) 调用。 此内核功能旨在在其父线程死亡时向子进程发送 SIGKILL 信号——不一定是在整个父进程退出时。

以下是该问题在我们的代码库中的表现形式:

我们的机器人代码使用 Tokio,这是一个异步运行时,它管理着一个工作线程池。 Tokio 有效地利用线程停车和取消停车来节省资源。 当任务暂时不活动时,它们的线程可以被“停车”(与休眠同义),如果它们保持空闲一段时间,它们可能会完全停止。

当 Tokio 运行时中的一个线程使用 PR_SET_PDEATHSIG 启动 Bubblewrap 时,Linux 内核会将该特定线程关联为 Bubblewrap 进程的“父进程”。 这意味着当该线程退出或被 Tokio 的内部调度程序回收时,内核会认为“父进程”已死亡,即使整个机器人进程仍在运行。

在某些情况下,Tokio 的调度程序会停车启动 Bubblewrap 的父线程。 如果该线程空闲的时间足够长,Tokio 最终会清理它,完全终止它。 然后,Linux 内核错误地将其解释为父进程的死亡,从而触发 PR_SET_PDEATHSIG 并向 Bubblewrap 发送 SIGKILL。 由于 Bubblewrap 本身在沙箱内运行 Chromium,因此 Chromium 也被杀死了,从而导致我们观察到的看似随机的崩溃。

这解释了为什么删除 --die-with-parent 标志可以解决问题:我们禁用了错误地将线程终止解释为进程死亡的机制。

这种行为不是 Tokio 或 Bubblewrap 中的错误,而是 Linux 内核中难以找到的怪癖。 由于 PR_SET_PDEATHSIG 跟踪的是父线程,而不是整个进程,因此它与动态停车和回收线程的运行时意外地交互。 Tokio 的线程管理、Bubblewrap 的进程隔离和 Linux 内核内部机制之间的这种微妙的交互在我们在最初检查的任何来源中都完全没有记录,这使得它成为一个特别棘手的调试挑战。

隧道尽头的光芒

[图片:隧道尽头的光芒]

经过所有的调试和调查,我得以重新加快速度并将此优化推向完成。 结果是减少了 10 多秒的等待时间。 使用 Output Media 的首帧时间现在平均仅为 2-3 秒,从而为我们的客户带来了更加愉快的体验。 非常值得。

通过一次 API 调用访问会话数据

从 Zoom、Google Meet、Microsoft Teams、Webex 和其他视频会议平台获取“音频”、“视频”、“转录”和“元数据”。

集成平台:

[图片:会议平台列表]

开始构建 ->阅读 API 文档 ->

[图片:Recall.ai logo]

公司

招聘职位 - 我们正在招聘!

资源

API 文档博客客户集成合作伙伴

安全

安全门户隐私使用条款

© 2025 Hyperdoc Inc.

[图片:linkedin-icon logo]