隔离云的解剖
如果您曾尝试过我们的多租户分布式 JavaScript 云 Deno Deploy,您可能理解这些好评中的一些原因。
(如果您还没有尝试,今天就部署一个 Fresh 网站吧!)
实现如此快的部署速度并非偶然。设计 Deno Deploy 是一个重新构想我们希望应用部署开发体验如何的机会,我们希望专注于速度和安全性。我们没有部署虚拟机或容器,而是选择了 V8 隔离环境(isolate),这使我们能够以显著降低的开销安全地运行不受信任的代码。
这篇博文解释了 V8 隔离环境的含义,并概述了 Deno Deploy 隔离云的主要架构组件。
Deno Deploy 项目和部署
在 Deno Deploy 中,用户和组织将他们的应用程序作为项目进行管理。Deploy 项目可以链接到 GitHub 仓库,从而在每次 git push
时重新部署代码。项目还允许应用程序管理环境变量、配置 TLS 证书和关联域名。Deploy 面板也开箱即用地提供了日志和分析视图,以提高应用程序行为的可见性。
如前所述,Deploy 项目可以链接到 GitHub 仓库,以便每次更改推送到后端仓库时,运行中的应用程序都会更新。在 Deploy 中,这些更新中的每一个都称为部署。部署是应用程序的不可变快照。每个部署都有一个唯一的 URL,可用于通过互联网测试或以其他方式与应用程序交互。一个项目可以有多个部署,但在任何给定时间只有一个部署可以被标记为生产环境。
图 1 显示了 deno.land 的部署仪表盘。每个部署都显示其唯一的 deno.dev URL,以及用于创建部署的 git 提交信息。“Prod”标签表示当前在 deno.land 上运行的生产部署。

将请求路由到部署
但是,用户的 HTTP 请求是如何到达 Deno Deploy 云中正在运行的部署的呢?
每个部署都通过一个或多个公共 URL 暴露到互联网。在 DNS 解析期间,所有这些 URL 都映射到 Deno Deploy 的公共 IPv4 或 IPv6 地址。这确保了所有部署流量都路由到 Deploy 服务器。
根据用户在全球所处的位置,将所有流量发送到同一个目的地可能会非常低效。例如,如果用户位于澳大利亚,而所有 Deploy 服务器都位于美国东海岸,那么每个 HTTP 请求(包括所有必要的 TCP 握手)都需要跨越数千英里环绕全球。
由于 Deno Deploy 旨在在边缘运行 JavaScript,因此至关重要的是,请求应智能地路由到离用户最近的边缘位置。Deploy 利用 任播(anycast)路由,在全球 30 多个边缘位置共享相同的 IPv4 和 IPv6 地址,如图 2 所示。

一旦传入请求被路由到适当的边缘位置,它就会被移交给一个 runner 进程。顾名思义,runner 负责运行应用程序,包括接收流量并将其路由到正确的部署。
Deploy 维护着一个域名和部署之间的映射表来达到此目的。根据所使用的协议,HTTP/1.1 Host
头或 HTTP/2 :authority
伪头用于确定请求的目标域名。然后,TLS 服务器名称指示(SNI)用于确定连接使用哪个 TLS 证书。最后,runner 检查映射表以确定要将请求发送到哪个正确的部署。(另外,Deploy 的 HTTP 流量会重定向到 HTTPS,因此 SNI 可以可靠地使用。)
作为隔离管理程序的 Runner
在传统云中,虚拟机监控程序(hypervisor)负责创建、运行、监控和销毁虚拟机。在 Deploy 中,runner 充当隔离管理程序(isolate hypervisor),具有相同的目的,但处理的是运行 V8 隔离环境的进程,而不是虚拟机。
在深入探讨之前,我们需要了解什么是隔离环境。V8 是 Deno、Google Chrome 和其他几个流行运行时所使用的 JavaScript 引擎,它将隔离环境描述为:
隔离环境表示 V8 引擎的独立实例。V8 隔离环境具有完全独立的状态。来自一个隔离环境的对象不能在其他隔离环境中使用。
这意味着隔离环境是一个独立的执行上下文,包括代码、变量以及运行 JavaScript 应用程序所需的一切。
为了帮助理解这一点,想象一个带有多个标签页的浏览器窗口。每个标签页执行单个网页的 JavaScript,但每个标签页的 JavaScript 代码通常不能与来自其他标签页的代码交互。在这个例子中,每个浏览器标签页都在其自己的 V8 隔离环境中执行 JavaScript 代码。
Deno Deploy 云采取了类似的方法。每个部署都在其自己的进程中,在其自己的 V8 隔离环境中执行其自己的 JavaScript 代码。传入请求映射到部署后,runner 负责将请求传递给部署进程。如果请求的部署尚未运行,runner 会首先启动一个新的部署实例。为了节省资源,runner 还会终止运行一段时间但未收到流量的部署。
图 3 显示了 runner 进程和部署进程的高级架构视图。

安全考量
允许任意数量的用户上传和执行不受信任的代码是一个极其困难的技术挑战。分层安全通常被认为是良好的实践,但在这种情况下它是必要的,因为没有单一的安全机制可以解决 Deno Deploy 的用例。
本节讨论了 Deploy 改进其安全状况的一些方法。
使用更严格的 Deno 运行时。 Deno 非常重视安全性。开源的 Deno CLI 使用其内置的权限系统来限制 JavaScript 应用程序可以执行的操作。Deno Deploy 更进一步,使用了一个更严格的 Deno 自定义版本。例如,部署可以读取自己的静态资产,但不能写入文件系统。
依赖 V8 沙盒。 V8 被设计用于在恶意环境(浏览器)中运行不受信任的 JavaScript 代码。虽然 V8 隔离环境本身不能提供完美的沙盒,但它们对于许多用例来说已经足够好。V8 也是一个备受关注并有公司支持的公共项目。这使得其代码库不断接受审计、模糊测试和测试,以确保其安全性。偶尔会有 安全漏洞 报告,Google 对此非常重视。Deno 团队致力于与 V8 版本保持同步,并遵循 V8 团队关于 运行不受信任代码的建议。
在单独的进程中运行部署。 V8 隔离环境本身就已经相互…隔离…了。Deno Deploy 更进一步,借助操作系统的帮助来提高每个部署的隔离性。
管理程序对资源利用率的监控。 runner 持续跟踪所有运行中部署的指标。这对于计费目的来说是必要的,同时也允许 runner 强制执行资源利用配额。消耗过多资源的部署会被终止,以防止服务降级。
限制网络访问。 云提供商和操作系统允许自定义和锁定网络访问。Deploy 还对内部控制平面流量和终端用户数据平面流量采用独立的网络。
限制允许的系统调用。 作为额外的安全层,Deploy 使用 seccomp 过滤来限制用户代码允许执行的系统调用。
使用 Rust 作为实现语言。 对于 Deploy 来说,JavaScript 是云中的语言,但 Rust 是云的语言。Rust 强制执行内存安全,从而消除了整类错误。方便的是,Rust 还提供了与 C/C++ 媲美的性能。
下一步?
每天,随着我们不断迭代和改进 Deno Deploy,越来越多的开发者和企业选择与我们一起运行生产就绪的应用程序和基础设施。我们相信我们的架构决策使他们能够专注于为用户构建产品,而无需担心安全性或性能。
请查看 Deno Deploy,并告诉我们您的想法!
您觉得解决这些挑战有意思吗?我们正在招聘!