原作者:洗影
受 Node.js 基金会的邀请到 Node.js Interactive 2016 North America 做了一次 talk,以下是个人的一些记录。
Node.js 相关的技术会议主要包括
今年 5 月到 Node Live Beijing 做了一次分享,当时基金会的 Tracy 跟我说 You should speak in the US,然后 8 月就收到了邮件问我有没有空参加 11 月底 12 月初的 Node.js Interactive。于是急急忙忙办了护照和签证,最后顺利在 11 月底到达美国。
到美国参加会议要申请的是 B1/B2 签证,基本上有邀请信和在职证明就可以了(签证官只看了一眼我的邀请信,然后问了一波问题,就 Approve 了)。Node.js 基金会名义上属于是 Linux 基金会旗下,所以邀请信是 Linux 基金会开的。从参会的过程可以看出来 Linux 基金会主要是帮助 Node.js 基金会处理技术无关的事务的,比如财务、开签证邀请信、协助组织会议、宣传等。从 CTC 成员那里还得知他们过来参加会议的花费也是由 Node.js 基金会提供的,不过似乎雇主是基金会成员的话还是从雇主那里出,今年最大的赞助商 Google、Microsoft、IBM 和 Intel 都派了不少人过来。现在 Node 社区出现了大量来自这些大公司的贡献者,并且这些公司之间多多少少存在一定的竞争,Node 作为一个社区驱动的项目其实在这方面和 Linux 有点类似,Node 基金会依赖 Linux 基金会这个过来人指导非技术方面的东西也是很容易理解的。
(对去年 Node Interactive 的印象主要来自录像)。
去年的 Node Interactive 还有比较多关注企业应用的分享(比如 Node at Uber/Paypal/Netflix/Godaddy),最后的 panel 也重点在讨论 Node.js for the Enterprise 这样的话题,此外有比较多 Web 开发和物联网(IoT)相关的议题。今年大家似乎已经默认企业应用已经比较成熟了,所以出现了更多 Node.js Core 的开发、JavaScript 引擎和配套的调试与追踪工具相关的话题。由于 Node.js 开始以 working group 的方式组织各方面的开发工作,各个分享里也可以看到来自不同 working group 的最新进展。
另外今年的 Node Interactive 比去年更挤了,短短两天的时间挤了 50 多个技术分享和十几个 keynote,几乎每个时段都有三个不同的场地在做分享+一个场地在做 workshop,而且没有按领域分组,有时候相似领域的分享会同时在不同的场地进行,感觉分身乏术啊……而且每个分享只有 20 分钟,对于很多话题来说非常难在这么短的时间内讲得比较深入,感觉还是有点遗憾的。
今年 Node 社区里大厂方面最大的动静可能是 Google 的参与。虽然 Node.js 一直在使用 Google 的 V8,但是去年的 Node Interactive 居然看不到 Google 的身影,而且 V8 团队长期以来把重点放在浏览器方面的优化上,对 Node 的需求有些跟不上。当年在 Lars Bak 等人转去做 Dart 之后,V8 团队的主力从丹麦 Aarhus 转移到了德国慕尼黑,主要应对 Chrome 的需求。今年 Google 加入了 Node.js 基金会,不仅慕尼黑的团队开始重视 V8 在服务端的性能优化,山景城的 Google Cloud Platform 团队也开始参与 V8 的开发,协助完成 V8 在服务端的需求。Google Cloud Platform 也加大了对 Node.js 的支持力度,提供了从 SDK、部署、追踪、监控到调试的一条龙服务,因此 Node.js 的多个 working group 都可以看到 Google 员工的身影。另外 Node.js 的一些与 Web 标准相关的开发,如 ES6 Module 整合、HTTP/2 实现等方面的工作,也得到了 Google 的协助。
另一方面,在 Node.js 基金会创立之初就很积极的微软,也在争取扩大自己在 Node.js 社区的影响力。延续去年开的头,微软依然在推动 Node.js 走向 VM Neutrality,让 Node.js 能够切换 JavaScript 引擎,使用微软开源的 ChakraCore(IE 与 Edge 底层的 JavaScript 引擎)。此外微软的 VS Code 不仅本身使用 Node.js 开发,也在努力推广自己在 Node.js 开发者中的使用,比如 keynote 里展示用 VS Code 结合 docker 插件开发 Node.js 应用部署到 Azure,同样提供了开发工具、SDK、部署、追踪、监控到调试的一条龙,果然跟 Google 相爱相杀……
值得一提的是汇集了 Node.js 核心贡献者的 Node Source 成功与 Azure、Google Cloud Platform、AWS 等主流云计算厂商达成了合作关系,帮助这些厂商完善生产环境中 Node.js 的监控、追踪和调试工具,可谓是鹬蚌相争,渔翁得利。相比起来我们阿里云的 alinode 还有很长一段路要走,不过国外 Node.js 在后端的应用比国内广泛得多,国外云计算厂商对 Node.js 的关注也是可以理解的。按照 Google 在 closing keynote 的话说,他们只是跟着开发者走,Node.js 开发者越来越多,他们自然要跟进。
相比起 Google 和微软,同样是冲着云计算来的 IBM 关注的方向反而不太一样,他们更注重 Node Core 开发计划、私有云、API Gateway、性能优化与事后调试(post-morterm diagnostics)方面的话题,这跟 IBM 收购了 StrongLoop 和他们一向对咨询服务的侧重有关。毕竟 IBM 一手促成了 Node.js 基金会的创立,目前 TSC 的 chair 也是 IBM 的员工,他们跟 Node.js Core 的关系更深一些。
另一个大厂 Intel 也是 Node.js 基金会最早的成员之一,去年 Intel 对 Node.js 在物联网的应用投入了大量精力,今年的分享主要集中在 Node.js 的性能优化,特别是小型设备的性能优化上。Intel 本来就是 V8 的贡献者,在 Node 方面也做了不少工作,比如支持 vtune profiling 的编译版本和基于 DPDK 的 libuv 优化,不过很多东西还是比较私有的。
(前面说过因为分身乏术所以不是所有分享我都听了……参考了 agenda 和听了分享的人的感想)
今年的 Node Interactive 部分话题依然类似 QCon/Goto 这种综合性技术会议的潮流,比如 Docker、Microservices,以及近期炒热的的 Cloud Native,Kubernetes 和 serverless。
另一方面,在企业应用和物联网应用话题相比去年减少的同时,出现了更多调试、追踪、优化方面的分享(这部分跟我们 alinode 的工作联系比较密切)。
和去年一样也有不少 JavaScript 引擎相关的分享:
还多了一些关于 Node.js Core 开发的分享
另外依然有一些看名字不明觉厉的分享……
其他一些偏 JS 和全栈的 talk,比如
安全方面的 talk:
其他还有一些 talk 介绍了 Node.js 在机器学习之类的领域的应用,和 Node.js 在一些受关注较少的市场的情况(比如 autodesk 的罗诗亚的 How China Does Node,呃围绕着 GFW 展开……有提到 cnpm 之类的解决方案,还有一个 Node.js & Africa Market 被临时取消了)
从前两天的分享和后两天的 collaboration summit 总结一下 Node 社区的近况和未来的开发集中的方向(其实还有更多,这里只总结个人比较关注的一些)。
由于 V8 的老 debugger protocol 已经 deprecate 两年了(在 Node 里提过 issue 但是没有被 CTC 注意到),计划在 V8 5.7 删除代码,而 node debug
目前还是用的这个协议。经过协商之后 Google 同意推迟到 5.8 再删除代码,留给 Node 团队一点时间做迁移。Google 团队已经在年初贡献了新的 inspector protocol 协议实现(基于 websocket)到 Node.js Core(PR),并且已经在 Node V6 发布,所以主要的问题是如何向 Node.js 社区通知这个消息,让大家从 node debug
转为使用 node --inspect
。
由于现在 Node.js Core 里的实现只有 server,没有 client(自动丢一个 url 让用户连 chrome 里的 client),所以考虑将 node-inspect 纳入 Node.js 基金会下,作为官方的新命令行 debugger 客户端。至于这个命令行客户端是集成到 core 里替代原来的还是让用户自己通过 npm 安装还需要讨论。
另外由于 V8 inspector 的 PR 自带 websocket 实现,所以 Node.js 自带 websocket 协议模块又有希望了……
在 Node Core 里添加和 Chrome 目前使用类似的 Trace Event,由用户自己定义追踪点,输出 chrome://tracing 兼容的日志,直接重用 Chrome 自带的可视化 GUI 来查看。另外这个 Trace Event 也可以当成一种简单的开发时日志/profiling 方案来用,与 console.log 相比更底层,针对追踪设计所以可控性更高。Google 正在和其他 JavaScript 引擎厂商合作抽象这个追踪 infrastructure,让其他厂商也来实现,这样将来切换到其他 JS 引擎也可以用。初步打算是先做 C++ 层面的 instrumentation,JavaScript 的 API 随后。
这是一个打磨了很久都没有正式开放的 API(PR)……目的是将 Node.js Core 里的所有异步操作都经过 AsyncWrap 的封装然后在这里加上 hooks,让用户能够自行在各种异步操作产生和结束等时点执行自己的代码,在不开启的时候对性能基本没有影响,对编写监控和追踪工具十分有用。由于 Node Core 里的异步操作有的用 JS 实现有的用 C++ 实现,目前 Async Hooks 只对外暴露了 JavaScript 层面的 API(虽然是通过 process.binding
这个和 C++ 交互的接口加载的)。这个实现一直有同步回 Node.js Core,只是因为还不是很完善,没有文档也没有对外宣传,现在 demo 已经比较成熟了,也有了简单的文档,决定对外公开接受反馈。
Post-morterm Diagnostics 也就是线上的事后调试(进程挂了之后的调试)。参与这两方面的公司主要有 IBM,Intel 与 Joyent。Joyent 本来就有自己的 mdb_v8 用于加载 coredump 进行事后调试,并且将 dtrace 支持集成进了 Node。IBM 则是在社区项目 llnode(给 lldb 加的分析 V8 堆栈的插件) 的基础上加以改进,并且做了 node-report (设置触发 dump 出一切有用的信息,包括 JS 堆栈、C++ 堆栈等)。Intel 就是之前说的带 vtune 的 Node 编译版本和基于 DPDK 的 libuv 了。相比来说,IBM 做的工作都归到了 Node 基金会下面并都开源了出来,更有希望成为社区的主流工具。
这也是一个弄了很久的项目,由微软主导,将 Node.js 底层的引擎替换成 ChakraCore(IE 和 Edge 底层的 JS 引擎),并且和微软研究院合作添加了 Time-travel debugging (可以倒着调试)这样的黑科技(今年还给 FSE 投稿了一篇论文(Time-Travel Debugging for JavaScript/Node.js)。原理上来说就是在 ChakraCore 的 runtime hosting API (JSRT) 上做一个 V8 API 的 shim,这样只要支持全了 Node.js 使用的 V8 API 的子集就可以切换引擎了。目前来说在 Node.js 的仓库里塞两个引擎似乎希望不大,所以把这个项目挪到了基金会下面,开发者使用的时候改用这个编译出的 node 可执行文件,理论上来说是可以无缝运行现有的 JavaScript 代码的,至于 C/C++ addon 的平滑迁移则留给了新的 NAPI 项目来做。
这是之前的 NAN API 的改进版,因为之前的 NAN API 最开始是为了让 C/C++ addon 不需要为 V8 升级而重新编译做的,目的是抹平不同版本的 V8 API 的差距,所以整体上还是非常偏重 V8 的抽象,作为 JS 引擎中立的 API 不是很适合。这个新的 API 旨在让开发者可以在切换 JavaScript 引擎不需要重新编译 C/C++ addon,所以可以算是 JavaScript 引擎的 ABI 了。目前的目标是让这个 API 到达可用的程度,推广给社区替代 NAN,并且最终让 Node.js Core 里的代码也改用这套 API(目前 Node.js Core 里的 C++ 代码都是直接 v8::xxx
这样使用 V8 API 的),这样最后能够达到让 Node.js Core 无缝在 JavaScript 引擎上迁移的效果。
这套 API 目前还是 C 的 (node_jsvmapi.h),而且打算是保证 C API 和 ABI 的稳定,再加上一个不保证稳定的 C++ Wrapper。目前实现已经有一定的可用性了,但是错误处理和性能影响评估等方面还需要加强。Google 的团队给这个 API 做了 review,似乎没什么大意见,不过也没法保证将来 V8 不会出现幅度太大的改进导致 NAPI 无法跟进。现在的计划是 Node v8.x 的时候发布,看情况可能会放在 flag 后面,v10.x 的时候再决定要不要完全走这条路。
这个 API 最大受益者是手上在推 Node-ChakraCore 的微软,另外在 Node Interactive 前 Mozilla 也宣布了加入 NAPI 的工作。IBM, Intel 等老本行就是这个的厂商也有参与。Mozilla 对 SpiderNode (使用 SpiderMonkey 作为 JS 引擎的 Node.js)投入了一定的精力,并且希望 SpiderNode 能在 WebAssembly (呃,不用介绍了吧)和 Shared Memory API (为 JavaScript 提供多线程编程的工具)方面做一些工作。现在看来主流浏览器的 JS 引擎貌似就剩一个 JavaScriptCore 没有向 Node.js 抛出橄榄枝了,虽然按照苹果现在在社区的作风似乎也是可以理解的……(TC39 有吐槽曰,Apple is the new Microsoft, Microsoft is the new Google……)
V8 正在基于 TurboFan JIT 做一个新的 dynamic FFI,用于在 JS 里直接调用 C 函数,现在还在 proof of concept 阶段的样子,目测可能类似 LuaJIT 的 FFI。如果能够做到 LuaJIT 的水平,JS 在 JIT 编译后生成的机器码里访问 C 代码的内存,开销跟 C 编译器编译出来的 C 代码访问 C 代码内存差不多,函数调用也可以被 inline 而不像现在走 binding 要多出一堆多余的动作。考虑到 Node Core 里有很多部分还是放在 JavaScript 里写的,有很多地方为了性能会通过 binding 回到 C++,有一个 fast FFI 直接写 C 会更快。不过外部的 Native Addon 被性能诱惑也开始用这个 FFI 的话,估计引擎中立 API 又有得头疼了,毕竟 JITted FFI 这种东西怎么想也是各家乐于自己搞的节奏……
由于 TC39 基本还是 pay to play,Node.js 基金会暂时无法作为非营利组织加入(吐槽:明明有 jQuery 基金会在前……),现在 Node.js 主要依靠 Node core 开发者以 invited expert 的身份,以及自己的成员公司在 TC39 的代表出席 TC39 的会议(包括 IBM,Paypal, Godaddy,Netflix 等)。目前来说 Node Core 和 ECMAScript 标准关系最密切的是 EcmasScript Module(ESM) 的实现,由于 ESM 在设计时基本没怎么考虑过 Node.js,关于同步/异步和符号静态/动态的语义跟 Node 原来的 CommonJS Module(NCJS) 差得太远,要做到两种模块加载的交互并且保证语义(特别是在有可能出现循环依赖的情况下)相当困难。
ESM 的标准原本包括一个 parser 和一个 loader,parser 部分被 ECMAScript 标准化,而 loader 部分被 TC39 决定从标准抽出,转由宿主决定实现。在 Node.js 真正开始探索 ESM 之前,还出现了 Babel 这样把 ESM 的语法转译到 NCJS 的语义的实现(但是 parser 其实完全不符合 ESM 标准)。Node.js 能够依赖 V8 实现正确的 ESM parser,但是 loader 部分需要自己实现。当时 WHATWG 做出了 loader 的标准和参考实现,于是 Node.js 试图让自己的实现迎合 WHATWG 的 loader 标准,却发现与 NCJS 模块的语义差距太远,新旧两种模块要交互会遇到非常多奇怪的 edge case 问题。而此后浏览器厂商真正实现的 loader(type=module
)又与 WHATWG 的 loader 完全不同(吐槽:WHATWG 这也能算 living standard……),相当于 Node.js 之前的功夫白费了。
于是 Node.js 提出了新的 proposal,希望能保持和已经有广泛应用的 Babel 保持交互,中间经过了无比漫长的讨论,包括这个六百多个留言的 PR 和 Node 社区成员和 TC39 成员在 twitter 上漫长的大战。最后 Node 社区决定向 TC39 提交 proposal 修改现有的标准,鉴于 Babel 的实现和 ESM 标准基本八竿子打不着边,如果要和标准保持一致难度太高,只能放弃自己的实现和 Babel 的交互。在 Node 社区成员出席 TC39 会议后,发现 Node 一直在纠结的其实是较为严格的 Source Code Module Record,但事实上它是 Abstract Module Record 的一个具体实现, Node 其实可以按照自己的需求从这个父类继承下来定一个宽松一些的 Module Record。总之现在 Node ESM 的实现还有很长的路要走,最起码也要 v10 才能做出来。
(吐槽:Node 社区负责这个实现的 Bradley Farias 在讲这段历史的时候听上去心都好累……TC39 真是牛角尖的龙卷风……)
由于 V8 的发布策略是大约每六周从 master 上挑取出稳定的 patch 切成一个分支发布(和 Node 有点类似),除了 master 以外,同一时间内只有两个分支 beta(下一个发布)和 stable(当前发布的版本)有在维护,这意味着大概每 6 周 V8 就会淘汰掉一个分支。而 Node 的 LTS 策略决定了每个 LTS 版本附带的 V8 基本是同一个,一个 LTS 有 30 个月的生命期,就会导致 Node 需要在两年多的时间里,自己维护 LTS 上那个已经淘汰掉的 V8 分支。这期间不可避免地会发现这个 V8 官方不再维护的分支有 bug 或者需要挑回来的 patch。由于 Node 对第三方依赖的维护策略是不自行改自己代码仓库里的那份代码,所有变动都必须在上游修复掉之后把 patch 挑回来合并(backport),就需要花费不少精力在关注、定位、合并上游 patch 上。现在 V8 与 Google Cloud Platform 的团队有几个人为 Node 社区做接应,帮忙盯着上游需要合回来的 bugfix,为 Node 社区定位与 backport 需要的 patch,还会对 Node 里的一些写法进行优化或修复性能 regression,让 Node 的 V8 维护工作完善了很多。
V8 从 5.6 开始终于要启用打磨了三年的 TurboFan 优化编译器,并配上 Ignition 解释器,这套新的流水线理论上能够大幅度提升原生 ES 2015+ 的性能。想想当年 Lars Bak 坚持 JavaScript 引擎不需要解释器更不需要字节码,好像有点讽刺……
Ignition 是一个基于 Register-based 字节码的解释器,一开始是为了配合小型设备的需要设计的。原来的 baseline 编译器(Full Codegen)编译器以 AST 为 source of truth,但是因为 AST 太大了所以实际保存的是源代码,recompile 动辄需要 reparse,资源消耗更大,而且基于 AST 做 OSR(比如在循环中的去优化)不如在字节码上做容易。Full Codegen 类似一个 stack machine,基本只是把 AST 翻译到机器码而不做任何优化,而 Ignition 在把 AST 编译到字节码时,会对部分缓存起来的字节码做一些简单的优化,因此编译时间有时反而更长。目前来看似乎 Ignition 的好处更多在于在小型内存上消耗少和使架构更干净了一些,编译速度和解释速度相比起 Full Codegen 暂时没有很显著的优势,虽然随着打磨应该会越来越好。
TurboFan 则是一个开发了很久的优化编译器,之前主要负责处理部分 ES6 代码和 asm.js/WebAssembly 的编译,现在已经在 Node v6 及以上包含的 v8 中开始接手 Crankshaft 无法优化的一些写法。V8 的大致计划是明年去掉原来的 Full Codegen 和 Crankshaft 两个编译器,改为 Ignition + TurboFan的组合,目前还保留原来的两个是过渡阶段的权宜之计。Crankshaft 在 ES5 上的优化确实不是 TurboFan 目前能完全赶上的,就连 TurboFan 的原生 ES 2015+ 性能相比转译到 ES5 再交给 Crankshaft 的性能依然有差距,但是 TurboFan 从架构和技术债务上来说都比 Crankshaft 有进步,逃逸分析也开始在做了,超越 Crankshaft 的性能还是很有希望的。
目前与 Crankshaft 相比,TurboFan 的优势在于可以优化很多原来根本无法优化的写法,以及对原生 ES2015+ 的优化支持。而对于编写时已经小心避开去优化的 ES5 代码,还是 Crankshaft 的编译结果更好,这也是为什么 Crankshaft 还没有完全退役的原因。Full Codegen 没有退役倒主要是因为 Crankshaft 去优化的时候回不到解释器,只能回 Full Codegen,所以捆绑了……嗯所以现在 V8 里同时有四个东西,有点尴尬,虽然是暂时的……
Node 的下一个 LTS 版本大概会包含 v8 5.6 或 5.7,不过 5.8 也有非常小的可能(时间上可能赶不上)。另外下一个 LTS 也叫 v8,名字冲突了,目前计划大概是直接跳过(类似 windows 9)或者明确写 version 8 之类的,还没有定……
由于目前 Node.js 里的 HTTP 完全针对 HTTP/1.1 实现(无状态,基于文本,一个链接一个 socket),几乎不可能在此基础上去实现 HTTP/2(有状态,以二进制传递,数据可以被分块后让多个链接共用一个 socket,可以服务器推送,还有大量的优化设计和新功能),因此目前的方向是另外写一套代码,挂在 HTTP 模块的一个引用下(require('http').HTTP2
),高层 API 方面尽量与 HTTP/1.1 靠齐,但底层设计不可能保持一致。Node.js 里 HTTP/1.1 的细节实现原本由 http_parser 处理,HTTP/2 则使用了 nghttp2,主要工作落在为 nghttp2 编写 binding 并构建 JavaScript 层面的 API 上(比如提供能让用户用于自制协议的 Http2Stream
/Http2Session
)。
目前服务端实现已经成型了(但是流的抽象等底层 API 还有待打磨),性能相比 HTTP/1.1 实现略有损失,但因为还在初级阶段,还有很多的优化空间。客户端实现方面得到了 Google 的协助,正在进行中。虽然 HTTP/2 协议本身不要求服务端必须有 TLS 保护,但目前各大浏览器都只接受与有 TLS 的 HTTP/2 服务器链接,所以新的 API 也将是偏向于默认要求用户传入 TLS 配置的。目前这部分的代码放到了另一个仓库下,等到实现完善后再决定是否真的将这个方案纳入 Node core。
Node.js 的 URL 模块在实现时并没有非常刻意地追求和浏览器端标准一致,目前跑 WHATWG 的测试基本是挂一大片的,现在决定尝试重新按照 WHATWG 的 URL 标准做一个实现,这个目前还在非常原始的阶段,和 HTTP/2 类似将另一套代码挂在 require('url').URL
下面,不过没有另开一个仓库,只是放到了 lib/internal/url.js
去实现。