我们如何将云浏览器的成本降低至三分之一并提升速度
我们将浏览器会话成本从每小时 $0.06 降至 $0.02,同时让浏览器启动和扩展速度更快。
译文尚不完整,未译段落保留原文;可打开原文对照。
我们的云浏览器需要同时做到三件事:快速启动、保持隔离、成本低廉。这就是我们重建 Browser Use Cloud 的原因,使得新会话能在一秒内启动,每个浏览器小时的成本降至 $0.02,此前为 $0.06。
这比听起来要难。浏览器包含 Chromium、文件系统、cookie、缓存、代理设置、下载内容,有时还有已登录的客户会话。如果一个浏览器能读取另一个浏览器的状态,就会产生安全问题。
常规答案是虚拟机,即 VM。VM 是计算机中的计算机:它拥有自己的 CPU、内存、磁盘和网络设备。它与宿主机上的其他一切分离,如果浏览器崩溃、泄露信息或遭到攻击,损害会停留在 VM 内部。
然而,普通 VM 对云浏览器来说太重了。我们需要不断创建它们,有时一次创建数千个,并在会话结束时立即销毁。如果每个浏览器都需要一个缓慢、昂贵的 VM,产品也会变得缓慢且昂贵。
对我们来说,问题在于我们能否让每个浏览器都拥有自己的 VM,同时不让用户等待或为此付费。我们现在通过 Firecracker 实现了这一点,这是一个轻量级 VM 系统。
每个 Browser Use Cloud 会话都在自己独立的微型 VM 中运行。这些 VM 运行在 EC2 上,即亚马逊的租用云服务器。
这就是不寻常的地方。Firecracker 通常运行在裸金属服务器上,即你租用整台物理机器。为了降低客户成本,我们将其运行在常规 EC2 上,AWS 已经将你的服务器置于一个 VM 之中。
这本应很慢。嵌套 VM 会使内存和 CPU 操作开销增大,而 Chromium 启动也需要时间。本文将介绍我们如何让这套配置变得快速且高效。
但首先,我们为什么要重建基础设施?
我们为何接管基础设施层
当我们最初基于 Unikraft 构建浏览器基础设施时,它开箱即用地提供了我们需要的许多功能:快速启动时间、浏览器快照、缩容至零以及强大的隔离性。
随着工作负载的增长,我们遇到了另一个挑战:扩展底层 EC2 集群本身。
虽然 Unikraft 能够很好地处理浏览器生命周期管理,但当时我们的配置无法使用原生的 EC2 水平自动扩缩容。我们知道这一功能在 Unikraft 的路线图上,但我们急需它,因此我们决定接管该技术栈的这一部分,直接基于 Firecracker 进行构建。
需要指出的是,这并非关于浏览器的扩缩容。Unikraft 已经有效地处理了浏览器的待机、唤醒和缩容至零。我们遇到的限制是在基础设施层,而非浏览器层。配置新节点是一个手动过程,当请求容量超过机器容量时,可能会导致停机。
换句话说,我们离开并不是因为他们无法运行高效的浏览器。我们接管基础设施层是因为我们需要平台中尚未提供的功能。我们仍然期待未来能继续与 Unikraft 合作 :)
Firecracker 提供了一个层,你可以通过它创建、监控和运行 VM。它为每个 VM 提供 CPU、内存、磁盘和网络设备,并使其与宿主机及其他 VM 保持隔离。
教会浏览器自行扩展
Firecracker 为每个浏览器提供了自己的虚拟机。但它并没有从根本上解决导致旧系统崩溃的问题:运行多少个虚拟机、将它们放在哪里,以及何时增加更多虚拟机。
因此我们构建了自己的控制平面。控制平面监控我们的浏览器集群,并决定我们是否应该扩容或缩容。
当用户请求浏览器时,控制平面会选择一台有空闲容量的机器。当流量上升时,它会启动更多机器。当流量下降时,它会停止向我们想要移除的机器发送新的浏览器。
它实时检查集群。这比等待 AWS 的监控服务 CloudWatch 要快得多,后者通常以一分钟为窗口做出反应。它还知道通用指标所不知道的事情:仍在启动的浏览器、我们正试图移除的机器,以及不应接收新会话的机器。
为什么我们在虚拟机内运行虚拟机
一旦我们有了控制平面,下一个问题是它应该添加什么样的机器。
在 AWS 上运行 Firecracker 的通常方式是使用 .metal 实例。这意味着你租用了整台物理服务器,Firecracker 直接在其上运行。
我们选择了普通 EC2。普通 EC2 机器获取更快,保留成本也更低。我们的主机从预构建的镜像启动,启动后约 30 秒即可开始提供浏览器服务。我们能越快添加主机,需要为闲置容量支付的费用就越少,传递给客户的成本也越低。
问题在于,普通 EC2 本身就是一个虚拟机。AWS 将我们的主机运行在其自身的隔离层内,然后我们在该主机内运行浏览器虚拟机。换句话说,每个浏览器都是虚拟机中的虚拟机。
这不是使用 Firecracker 的常规方式。当浏览器虚拟机需要主机的帮助时,请求要经过两层虚拟机而不是一层,从而增加了延迟。
我们决定这种权衡是值得的,因为普通 EC2 能给我们更快的扩容速度和更低的成本。为了减轻嵌套虚拟化的影响,我们专注于让 Firecracker 尽可能快速。
从请求到可用的浏览器
当用户请求浏览器时,控制平面会选择一台有空闲容量的机器。该机器恢复一个已保存的浏览器虚拟机,在其中启动 Chromium,等待 Chromium 准备好被控制,然后返回一个连接 URL。
用户的智能体连接的就是这个 URL。Browser Use 通过 WebSocket 使用 Chrome DevTools Protocol(即 CDP)来控制 Chromium。CDP 是 Chrome 的远程控制 API:点击这个按钮、输入这段文本、读取这个页面、截取这张截图。
有三件事导致这个过程耗时更长:恢复虚拟机的内存、启动 Chromium,以及让浏览器保持隐蔽、不被反机器人安全机制检测到。
第一个减速点:内存
第一个瓶颈是内存。
生产环境的浏览器不是从零启动的。我们从快照恢复它:一个已保存的虚拟机,它已经启动并暂停在 Chromium 启动之前。恢复虚拟机比启动虚拟机要快得多。
我们的第一批恢复方案仍然太慢。当恢复的虚拟机首次访问内存时,主机必须将该内存重新映射回来。这一事件称为缺页异常。在嵌套虚拟机中,每次缺页异常的开销都很大,因为它可能跨越两层虚拟机。
在一次早期冷启动中,缺页异常占所有虚拟机退出的72%。从恢复到浏览器准备好CDP,耗时9.8秒。
修复方法是按更大的块映射内存。之前,虚拟机按4KB页恢复内存。现在,它使用2MB页。每页覆盖的内存多512倍,因此浏览器在唤醒过程中触发的缺页异常要少得多。缺页异常减少意味着穿越嵌套虚拟机层的次数减少。
We also now handle page faults ourselves with a custom handler for userfaultfd, a Linux API for handling missing memory pages. Before the VM starts running, our handler loads the memory Chromium is most likely to access first.
我们的处理器防止Chromium在启动时收到大量缺页异常。主机已经加载了热点页面,其余页面会在浏览器需要它们之前到达。
这些更改将从恢复虚拟机到浏览器准备好接受命令的时间从9.8秒缩短到3.1秒。它们还将浏览器虚拟机必须暂停并请求主机处理缺失内存的次数从每次恢复约100,000次减少到约1,100次,下降约91倍。
我们还做了较小的优化。虚拟机花费500ms寻找一个不存在的旧PS/2键盘。我们禁用了此检查。
此外,我们改变了主机等待浏览器就绪的方式。之前,主机不断用HTTP请求轮询虚拟机。这产生了额外的虚拟机退出,即浏览器虚拟机必须暂停以便主机为其处理工作的时刻。
Now, the browser driver writes a ready message to its log, and the host reads that log over vsock, a fast communication channel between the host and the VM. The host sees the ready message in under a millisecond.
第二个减速点:Chromium启动
下一个瓶颈是CPU。
当Chromium启动时,它既贪婪又苛刻。它同时创建渲染器、合成器和V8隔离区。之后,浏览器自动化要安静得多。智能体点击、等待、读取、再次点击。
由于Chromium启动后更安静,我们可以将许多浏览器打包到同一实例中。单个主机可以容纳许多浏览器,因为浏览器大部分时间都在等待:等待页面、网络响应或下一个智能体操作。
我们分两个阶段处理启动突发。当浏览器恢复且Chromium启动时,我们让其虚拟CPU不固定。这意味着Linux可以将浏览器的CPU工作在主机上移动,而不是将其锁定到固定核心。这分散了突发负载。
一旦浏览器报告就绪,我们就将这些虚拟CPU固定到稳定的核心。这意味着浏览器虚拟机现在在特定核心上运行。稳定的放置使我们能够在同一主机上打包更多浏览器,而无需猜测。我们知道哪些核心被占用,哪些仍有空间,以及哪些浏览器可能相互干扰。
启动阶段就像让人群通过每一扇开着的门进入。一旦所有人都进入,分配的座位就更有效。
从一开始就固定会使情况更糟。当许多浏览器同时启动时,它们堆积在相同的热点核心上,导致一些启动失败。
我们还对超线程变得格外谨慎。一个物理 CPU 核心通常会表现为两个逻辑 CPU,称为兄弟线程。这些兄弟线程仍然共享同一个物理核心。如果两个浏览器虚拟机各得到一个兄弟线程,它们就会争夺同一个核心。在嵌套环境下,这种争用表现为启动失败。为防止这种情况,现在每个浏览器都会获得它所使用的物理核心的两个兄弟线程。
接下来,我们为每个固定的 vCPU 线程赋予实时优先级。这会告诉 Linux,当浏览器虚拟机需要 CPU 时立即运行它,而不是让它排在不太重要的工作之后。在这一改动之前,一项 1,000 个浏览器的测试在会话创建后不久就损失了 17%。改动之后,同样的测试损失为零。
无屏幕下的隐身
最后一个瓶颈是隐身。
无头浏览器在没有可见窗口的情况下运行。有头浏览器则像你笔记本电脑上的浏览器一样运行,带有窗口、图形和渲染的帧。
普通的无头 Chromium 很容易被具有反机器人措施的网站检测到。根据我们的隐身基准测试,普通的无头 Chromium 只有 2% 的时间能避免被网站封禁。同一个 Chromium,以有头模式运行并带有可见窗口,仅通过渲染内容就能在 50% 的时间里避免封禁。
这就是为什么大多数供应商运行有头浏览器。他们为一个显示服务器、一个 GPU 和一个为没人观看的屏幕绘制帧的合成器付费。
我们完全以无头方式运行浏览器。这之所以可能,是因为我们修改了浏览器本身。
第一个组件是我们的Chromium 分支。许多隐身工具通过在浏览器启动后向每个页面注入 JavaScript 来隐藏自动化痕迹。例如,它们会覆盖浏览器属性,比如 navigator.webdriver,这是一个告诉网站浏览器是否正被自动化控制的标志,这样页面看到的就是 false 而不是 true。网站通常能检测到这些值是否被覆盖。为了避免这种情况,我们在 Chromium 的最底层进行修补,这样我们的补丁从一开始就不会暴露。
第二个组件是我们的指纹。浏览器指纹由网站读取的关于你的浏览器和机器的详细信息组成,包括你的操作系统、屏幕尺寸、字体、图形、输出、音频、时区、语言以及数百个其他细节。检测机器人的系统会检查这些细节看起来像真实用户的浏览器还是伪造的自动化环境。我们在 macOS、Windows 和 Linux 上使用了数万个真实指纹。
我们的浏览器在我们的隐身基准测试中81% 的时间避免了封禁,在 Halluminate BrowserBench 上为 84.8%,是所有供应商中最高的。由于没有显示,浏览器运行成本更低,也更容易扩展。
连接到正确的浏览器
一旦浏览器就绪,用户通过 CDP 连接到它。公开 URL 是一个 WebSocket URL。
在浏览器集群前面是简单的边缘路由器。路由器接收 WebSocket 连接,询问控制平面该浏览器位于何处,然后将原始 CDP 字节转发到正确的虚拟机。
路由器不决定浏览器在哪里运行。如果一个路由器宕机,另一个路由器可以接管新的连接。控制平面负责放置。路由器只负责搬运字节。
成果
你的每个浏览器会话都由一个从快照恢复的微型虚拟机组成,该虚拟机运行在常规 EC2 内部,其内部运行着无头 Chromium。
虚拟机冷启动时间低于 400 毫秒。端到端来看,通过公共 API,浏览器创建延迟在 p50 时为 825 毫秒,在 p99 时为 1.35 秒。我们在一次 10,000 个会话的压力测试中测量了这一数据,其中每个浏览器都成功启动。
该基础设施上的浏览器现已上线。开始使用 Browser Use Cloud。
目前最大的剩余成本是 Chromium 本身。恢复后启动 Chromium 在 p50 时仍需约 545 毫秒。
因此,任何进一步的改进都必须来自浏览器本身。
下一步:跳过 Chromium 启动
目前,我们在 Chromium 启动之前对虚拟机进行快照。这使快照保持简单:每个浏览器都从同一个干净的点唤醒,然后自行启动 Chromium。
但 Chromium 启动现在是最大的剩余成本。下一步是在 Chromium 已经运行之后进行快照。这样,新会话就完全不必启动浏览器了。它唤醒时浏览器已经处于活动状态。
这很复杂,因为一个正在运行的浏览器拥有打开的设备、计时器、图形状态、网络状态和指纹状态。在我们冻结它之前,需要将所有这些东西置于安全状态。在我们恢复它之后,每个浏览器仍然需要看起来像它自己的浏览器,而不是上一个浏览器的克隆。
这就是我们接下来正在研究的内容。
最快的浏览器是你几乎不需要启动的浏览器。我们通过在不该运行的地方运行 Firecracker,将虚拟机启动时间控制在 400 毫秒以下。接下来,我们将让新会话在 Chromium 已经运行的情况下唤醒。
上述基础设施上的浏览器现已在 cloud.browser-use.com 上线。