我们如何让云浏览器成本降低3倍、速度提升3倍——Firecracker VM架构实践
我们将浏览器会话成本从每小时0.06美元降至0.02美元,同时让浏览器启动和扩展速度更快。
我们的云浏览器需要同时做到三件事:快速启动、保持隔离、成本低廉。这就是我们重建Browser Use Cloud的原因——新会话在不到一秒内启动,每个浏览器小时成本从0.06美元降至0.02美元。
这比听起来要困难得多。一个浏览器包含Chromium、文件系统、Cookie、缓存、代理设置、下载文件,有时还包括已登录的客户会话。如果一个浏览器能读取另一个浏览器的状态,就会产生安全问题。
通常的解决方案是虚拟机(VM)。VM是计算机中的计算机:它拥有自己的CPU、内存、磁盘和网络设备。它与宿主机上的其他一切隔离,如果浏览器崩溃、泄露信息或受到攻击,损害仅限于VM内部。
然而,对于云浏览器来说,普通VM过于笨重。我们需要不断创建它们,有时一次创建数千个,并在会话结束后立即销毁。如果每个浏览器都需要一个缓慢且昂贵的VM,产品也会变得缓慢且昂贵。
我们的问题是:能否在不让用户等待或付费的情况下,为每个浏览器提供自己的VM?我们现在通过Firecracker(一种轻量级VM系统)实现了这一点。
每个Browser Use Cloud会话都在自己独立的微型VM中运行。这些VM运行在EC2(亚马逊的云服务器租赁服务)上。
这就是不寻常之处。Firecracker通常运行在裸金属服务器上,你需要租用整台物理机。为了降低客户成本,我们将其运行在常规EC2上,而AWS已经将你的服务器置于VM内部。
这理论上应该很慢。嵌套VM会使内存和CPU操作更加昂贵,而且Chromium启动需要时间。本文介绍我们如何让这种架构变得快速高效。
但首先,我们为什么要重建基础设施?
为什么我们放弃Unikernel
我们曾经使用Unikraft运行云浏览器,它构建了名为Unikernel的小型类VM容器。Unikernel不启动完整的Linux系统,而是加载为你的用途构建的小型镜像。Unikernel启动迅速,空闲时成本低廉,因为无人使用时可以关闭它们。
Unikraft在浏览器闲置时关闭它们方面表现良好,但在流量激增时快速增加浏览器方面表现不佳。如果突然有更多用户同时请求浏览器,你需要快速扩展浏览器容量。Unikraft没有良好的内置自动扩展功能,因此工程师必须手动更改变量来增加实例。
在流量爆发期间,系统不会自动响应,而是需要人工调整。这导致了问题:一次负载测试导致生产环境宕机45分钟。因此,我们在Firecracker上重建了我们的架构。
Firecracker提供了一个层,通过它可以创建、监控和运行VM。它为每个VM提供CPU、内存、磁盘和网络设备,并使其与宿主机及其他VM保持隔离。
教会浏览器自我扩展
Firecracker为每个浏览器提供了自己的VM。但它并没有从根本上解决旧系统的问题:决定运行多少个VM、将它们放在哪里以及何时增加更多VM。
因此,我们构建了自己的控制平面。控制平面监控我们的浏览器集群,并决定是否应扩展或缩减。
当用户请求浏览器时,控制平面会选择一台有空闲空间的机器。当流量上升时,它会启动更多机器。当流量下降时,它会停止向要移除的机器发送新浏览器。
它实时检查集群。这比等待CloudWatch(AWS的监控服务,通常以一分钟为窗口进行反应)要快得多。它还知道通用指标无法获知的信息:仍在启动的浏览器、我们试图移除的机器以及不应接收新会话的机器。
为什么我们在VM内部运行VM
一旦我们有了控制平面,下一个问题就是它应该添加哪种类型的机器。
在AWS上运行Firecracker的常规方式是使用.metal实例。这意味着你租用整台物理服务器,Firecracker直接在其上运行。
我们选择了常规EC2。常规EC2机器获取更快,维护成本更低。我们的宿主机从预构建镜像启动,并在启动后约30秒开始提供浏览器服务。我们添加宿主机越快,需要支付的闲置容量就越少,传递给客户的成本也就越低。
问题在于常规EC2本身就是一个VM。AWS在其自己的隔离层内运行我们的宿主机,然后我们在该宿主机内运行浏览器VM。换句话说,每个浏览器都是一个VM中的VM。
这不是使用Firecracker的正常方式。当浏览器VM需要宿主机帮助时,请求会经过两个VM层而不是一个,从而增加延迟。
我们认为这种权衡是值得的,因为常规EC2提供了更快的扩展速度和更低的成本。为了减轻嵌套虚拟化的影响,我们专注于让Firecracker尽可能快速。
从请求到可用的浏览器
当用户请求浏览器时,控制平面会选择一台有空闲空间的机器。该机器恢复一个已保存的浏览器VM,在其中启动Chromium,等待Chromium准备好被控制,然后返回一个连接URL。
该URL是用户代理连接的目标。Browser Use通过WebSocket使用Chrome DevTools Protocol(CDP)控制Chromium。CDP是Chrome的远程控制API:点击此按钮、输入此文本、读取此页面、截取此屏幕截图。
有三件事使这个过程变慢:恢复VM的内存、启动Chromium以及保持浏览器的隐蔽性以避免被反机器人安全系统检测。
第一个瓶颈:内存
第一个瓶颈是内存。
生产环境的浏览器不是从头启动的。我们从快照恢复它:一个已启动并在Chromium启动前暂停的已保存VM。恢复VM比启动VM快得多。
我们最初的恢复仍然太慢。当恢复的VM首次访问内存时,宿主机必须将该内存映射回来。这个事件称为缺页中断。在嵌套VM中,每次缺页中断都很昂贵,因为它可能跨越两个VM层。
在早期冷启动期间,缺页中断占所有VM退出的72%。从恢复到CDP就绪的浏览器需要9.8秒。
解决方案是以更大的块映射内存。之前,VM以4KB页面恢复内存。现在,它使用2MB页面。每个页面覆盖512倍的内存,因此浏览器在唤醒时触发的缺页中断大大减少。更少的缺页中断意味着更少的嵌套VM层遍历。
我们现在还使用自定义的userfaultfd处理程序自行处理缺页中断。userfaultfd是Linux用于处理缺失内存页面的API。在VM开始运行之前,我们的处理程序加载Chromium最可能首先访问的内存。
我们的处理程序防止Chromium在启动时收到大量缺页中断。宿主机已加载热页面,其余页面在浏览器需要它们之前到达。
这些更改将从恢复VM到浏览器准备好接受命令的时间从9.8秒缩短到3.1秒。它们还将浏览器VM必须停止并请求宿主机处理缺失内存的次数从每次恢复约100,000次减少到约1,100次,减少了约91倍。
我们还进行了一些较小的优化。VM花费了500毫秒寻找一个不存在的旧PS/2键盘。我们禁用了此检查。
此外,我们更改了宿主机等待浏览器就绪的方式。之前,宿主机通过HTTP请求不断轮询VM。这产生了额外的VM退出,即浏览器VM必须暂停以便宿主机为其处理工作的时刻。
现在,浏览器驱动程序将就绪消息写入其日志,宿主机通过vsock(宿主机与VM之间的快速通信通道)读取该日志。宿主机在不到一毫秒内看到就绪消息。
第二个瓶颈:Chromium启动
下一个瓶颈是CPU。
当Chromium启动时,它非常消耗资源。它同时创建渲染器、合成器和V8隔离区。之后,浏览器自动化要安静得多。代理点击、等待、读取、再次点击。
由于Chromium启动后更加安静,我们可以将许多浏览器打包到同一实例中。单个宿主机可以容纳许多浏览器,因为浏览器大部分时间都在等待:等待页面、网络响应或下一个代理操作。
我们分两个阶段处理启动爆发。在浏览器恢复和Chromium启动期间,我们让虚拟CPU保持未固定状态。这意味着Linux可以将浏览器的CPU工作迁移到宿主机上,而不是将其锁定到固定核心。这分散了爆发负载。
一旦浏览器报告就绪,我们将这些虚拟CPU固定到稳定核心。这意味着浏览器VM现在在特定核心上运行。稳定的放置使我们能够在不猜测的情况下将更多浏览器打包到同一宿主机上。我们知道哪些核心已被占用,哪些仍有空间,以及哪些浏览器可能相互干扰。
启动阶段就像让人群通过每扇敞开的门进入。一旦所有人都在里面,分配座位效果更好。
从一开始就固定核心会使情况更糟。当许多浏览器同时启动时,它们会堆积到相同的热核心上,导致某些启动失败。
我们还变得对超线程更加谨慎。一个物理CPU核心通常显示为两个逻辑CPU,称为兄弟线程。这些兄弟线程仍然共享相同的物理核心。如果两个浏览器VM各获得一个兄弟线程,它们会争夺同一核心。在嵌套环境下,这种争用表现为启动失败。为防止这种情况,每个浏览器现在获得其使用的物理核心的两个兄弟线程。
最后,我们为每个固定的vCPU线程赋予实时优先级。这告诉Linux在浏览器VM需要CPU时立即运行它,而不是将其暂停在不太重要的工作之后。在此更改之前,一个1000浏览器的测试在创建后不久就丢失了17%的会话。更改后,相同测试丢失了零个会话。
无需屏幕保持隐蔽
最后一个瓶颈是隐蔽性。
无头浏览器在无可见窗口的情况下运行。有头浏览器像你笔记本电脑上的浏览器一样运行,带有窗口、图形和渲染帧。
普通的无头Chromium很容易被具有反机器人措施的网站检测到。根据我们的隐蔽性基准测试,普通的无头Chromium只有2%的时间能避免被网站屏蔽。相同的Chromium,以有头模式运行并带有可见窗口,仅通过渲染内容就有50%的时间能避免被屏蔽。
这就是为什么大多数提供商运行有头浏览器。他们为显示服务器、GPU和合成器付费,这些组件为没人看的屏幕绘制帧。
我们完全以无头模式运行浏览器。这之所以可能,是因为我们修改了浏览器本身。
第一个组件是我们的Chromium分支。许多隐蔽工具通过在浏览器启动后向每个页面注入JavaScript来隐藏自动化。例如,它们覆盖浏览器属性如navigator.webdriver(一个告诉网站浏览器是否被自动化控制的标志),使页面看到false而不是true。网站通常可以检测到这些值何时被覆盖。为了避免这种情况,我们在Chromium的最底层进行修补,使我们的补丁从一开始就不会暴露。
