这个周末,我一直在把玩 deno 的 rusty_v8 以及 deno_core(录了几个 rusty_v8 的视频,预计四月第二周发)。rusty_v8 是 google v8 engine 的 Rust 零成本封装,而 deno_core 在 rusty_v8 的基础上进一步封装了一些额外的功能。众所周知,v8 是 chrome 内部的 javascript 执行引擎,它优异的 JIT 能力,以及高效的垃圾回收,使得 chrome 成为最快最成功的浏览器。v8 仅仅被用在浏览器中有些暴殄天物,于是十多年前(2009),Ryan Dahl 把 v8 引入了服务端,创建了 node.js —— node 以简单容易上手的编程模型(单线程,异步处理)和大量的前端拥趸一举成为广受欢迎的服务端开发工具;而 3 年前,Ryan Dahl 自我革命,重新用 v8 从零打造 deno,意欲让 deno 成为下一代服务器开发的王者。
如果你没听过 deno,或者并不了解 deno,建议你去看看 2018 年 Ryan 那个颠覆性的演讲:10 Things I regret about Node.js [1]。这个演讲对 node 的信仰者的打击是巨大的,对他们而言,这就好像普罗米修斯把火送到了人间,让世人在黑暗中得到了光明和温暖,却又对冲着自己顶礼膜拜的人们说我错了,火是不好的。这些年来,很多愤怒的 node 工程师还没真正了解 deno(甚至连 Ryan 的演讲都没看过),就急忙人云亦云地指出 deno 成百上千的不足,来掩饰自己心中的不安和对未来的迷茫。
可以用 deno 做下一代的沙箱么?
在 Ryan 的演讲中,第二个 regret 是 Security,我认为可能是 deno 相对于 node 做出的最重要的架构上的重塑。v8 倾尽全力打造了一个安全的沙箱,node 却只关心其 javascript 解释器,对沙箱所带来的安全性弃若敝履。站在 09 年那个 web 2.0 才刚刚开蒙的时代,这么做无可厚非 —— 谁能预料到十年后,服务器的世界就进入了一个沙箱(VM / container / wasm)横行的时代呢?
相对于 VM 巨大的资源占用和难以忍受的启动时间,容器技术在效率和安全性隔离性之间达到了一个不错的平衡。然而,虽然通过多种手段优化,容器的冷启动时间还是在数秒(fargate 60-90s,aws lambda 5s)[2],而容器的打包也是一个相对缓慢的过程。我们迫切需要新的沙箱技术来让冷启动和打包更上一个台阶:于是,有了尚在发展的 WASM/WASI 技术。相对于包含了语言运行时和各种库文件的容器来说,WASM 非常精简,启动很快(毫秒级),且不需要额外的打包。
不过 WASM 还需要一个编译的过程。一个小型的 Rust 项目,全量编译成 WASM,需要几十秒到几分钟时间,虽然比容器的打包快了不少,但还不是即时的。有没有一种技术,在保证安全性和隔离性的同时,冷启动时间足够短,执行效率足够高,还可以即时部署,即时更新?
有!这就是我们在浏览器上跑了将近 30 年的 javascript。毫不夸张地说,浏览器中的 JS 引擎承受的安全压力是顶级的,比如 chrome 中的 v8,每天要面对全球 26 亿用户(chrome 目前是 26.5 亿用户的主浏览器[3])的各种各样的 javascript 请求,迄今为止,v8 并未遭受过使用户蒙受巨大的损失的安全漏洞,因此它的沙箱的安全性也是顶级的。除安全外,v8 从诞生到现在一直琢磨的就是如何让 javascript 加载和运行地更快一点。Cloudflare 在其边缘计算的 cloudflare worker 中使用了 v8 来跑用户的脚本,可以让冷启动的时间低至 0-5ms [4]。这个时间低得难以置信,但考虑到相对于其它解决方案,v8 运行一个用户脚本,只是创建一个 isolate 运行这个脚本而已,所以效率很高。
正因为 Ryan 在 2018 年看到了沙箱化的服务器世界的巨大潜力,因此 deno 才义无反顾地拥抱了 v8 提供的沙箱(isolate),并把围绕着 v8 构建的一系列 op(比如文件的读写)都加入了权限的控制。
于是乎,deno 看上去像是一个服务端的 chrome。它用 isolate 隔离用户的代码,并可以在极短的时间内加载并运行几乎不可能进行任何恶意行为的用户代码(如果权限控制得当)。所以 deno deploy 用它做边缘计算(cloudflare worker 也用了 v8,但应该不是用 deno)。
然而,当人们把 deno 作为另一个 node 使用的时候,便注定会抱怨「node 已经足够好,另起炉灶的deno 有何意义」。就如同庄子讲的这个故事:
宋人有善为不龟手之药者,世世以洴澼絖为事。客闻之,请买其方百金。聚族而谋曰:‘我世世为洴澼絖,不过数金,今一朝而鬻技百金,请与之。’客得之,以说吴王。越有难,吴王使之将,冬,与越人水战,大败越人。裂地而封之。能不龟手一也,或以封,或不免于洴澼絖,则所用之异也。
所以,deno 最重要的意义在于构建一个服务端的沙箱环境。如果 deno 的缺省功能并不满足你的使用场景,那么,还可以通过在 rusty_v8,deno_core,deno_runtime 各个层级进行裁剪,构建符合你需求的沙箱环境。这个沙箱可以用来做服务端和客户端的用户代码执行(比如插件),也可以创建一个多租户的使用环境。
由 deno 想到的未来的软件开发场景...
互联网软件开发,对我而言,最大的痛点就在于部署。虽然 kubernetes 已经把部署的效率大大提升,但每次部署,还是以分钟为量级的。然而 deno deploy 给我们展示了另外一种可能:代码在提交的同时就能够被部署,并且,立等可用:
由此,对于业务代码的开发者,不需要了解一堆 devOps/kubernetes 的知识,不需要维护各种部署配置,也不需要部署代码的时候经历漫长的等待,更不会遇到莫名其妙的部署问题。git commit,打开浏览器,新代码已在线,bingo!
整个部署的过程其实就是一个状态更新和文件拷贝的过程。甚至,文件拷贝都可以避免,因为 deno 支持从 url import 或者运行,比如这样:
由于部署从原来的分钟级跃迁到秒级,且部署过程中需要的算力很少,那么,如果不涉及数据 schema 的变动,任何业务逻辑都可以无障碍地随时随地部署和撤回。因为无所谓 staging / production / test / dev 环境 —— 每个环境不过是一份份略有不同的代码(及配置),它们在运行时对应不同的 v8 isolate,所以,我们可以用 git branch 来对应不同的环境。代码在 master branch 上开发,随时 commit 随时体验最新的版本,这是 dev 环境;需要发布时,从 master merge 到 release branch,做各种集成测试,跑 regression,此时是 test / staging 环境;测试通过后,在 release branch 上打 tag,此时代码被发布到 production 环境,以 canary 方式发布。在 ingress 侧,我们可以根据用户 id,把流量发往不同版本的代码(isolate):
当然,这里所描述的场景,目前的 deno 都还不直接支持,但 deno 已经为此提供了坚实的基础。通过对 deno_runtime / deno_core 进行二次开发,这些目标并不难实现。
由此我们还可以演进出一套高效的软件开发流程:任何在研究阶段的,或者早期的,或者确定性不太高的功能,都以 javascript/typescript 的形式快速开发,零成本快速部署,快速验证。当假设得到验证(实验成功),或者功能得到确定,我们再根据需要将其核心部分用 rust 实现,部署到 runtime 中,以 op / extension 的形式暴露给 javascript/typescript,达到快速原型和极致高效的平衡 —— 其实 deno 自己,也用类似的方式演进:早期 deno 的 typescript 编译器,使用了 javascript 版本的 tsc,当效率更高的 swc(Rust 版 ts 编译器)出现后,就鸟枪换炮,大大提升了效率。
参考资料
[1] 10 Things I regret about Node.js: https://www.youtube.com/watch?v=M3BM9TB-8yA
[2] Should I run my container on AWS Fargate, AWS lambda, or both: https://awscloudfeed.com/whats-new/architecture/should-i-run-my-containers-on-aws-fargate-aws-lambda-or-both
[3] Google chrome statistics 2022: https://backlinko.com/chrome-users
[4] Eliminating cold starts with cloudflare workers: https://blog.cloudflare.com/eliminating-cold-starts-with-cloudflare-workers/