分享

我们如何使用 Webpack 将启动时间减少 80%

 黄爸爸好 2022-05-07 发布于上海

图片

我们在 RudderStack 使用的开发方式之一是安全快速地构建,然后根据需要进行优化,这种模式使我们能够优先考虑客户问题,跟上 RudderStack 的快速增长的脚步。

但在某些情况下,这种方式会导致开发体验的流失。发生这种情况时,我们使用帕累托原则重新集中精力,力求在消除技术债务中投入的时间能得到最大的回报。

这种不太好的开发体验的一个例子是 Control Plane 的主后端服务的部署时间过长。过去在生产环境中部署需要 5 分钟,更甚的是,在开发过程中,根据硬件的不同,重启需要 40-90 秒,这成了一个主要的痛点,拖慢了我们团队的进度,我们知道,是时候重新关注和解决它了,我们是这样做的。

Control Plane 是什么?

首先,我解释一下我所说的“Control Plane(控制台)”,Rudderstack 的架构分为两部分:数据台和控制台。控制台是 Rudderstack 平台的大脑,它是存储资源和配置的地方,你的组织、工作区、基础设施和账单中的用户管理和协作都在控制台中进行。

图片

从架构的角度来看,控制台由一个以集群模式运行的后端应用、几个附属微服务和一个前端应用组成。对于我们的后端服务,我们使用 Node.js 和 Typescript,用 ts-node 来启动和运行应用程序。但是如上所述,这是有代价的,让我们深入了解里面发生了什么。

解决我们启动时间的问题

我们知道 Node.js 不是问题的原因,原生的 HTTP 服务器几乎是立即重启,我们使用的 koa web 框架精简且轻量级。所以,我们需要做一些分析来查明原因,使用 clinic.js 来帮助分析,它简单而易用。

果然,在设置好 clinic 并进行了几次测试运行之后,我们生成了一些火焰图(火焰图是一种显示每个方法和依赖项需要多少执行(CPU)时间的方式),它们揭示了问题。

带有源代码和过程的火焰图:

图片

没有源代码的过程火焰图:

图片

不管是否包含 rudder-config-backend 源代码,图表都是一样的,所以我们知道源代码不是问题,并且可以确定开销来自 Typescript,尤其是 ts-node。

这是有道理的,因为每当进程重新启动时,整个源代码都必须从零开始转换为 Javascript,而且没有任何缓存;这与我们在集群模式下部署服务器时遇到的较大延迟一致。每个工作进程都必须独立编译 Typescript 文件,因此重新启动需要很多时间,有时还会导致资源匮乏。具体来说,我们在服务器启动期间,可以看到内存不足错误和 CPU 利用率在增加。

虽然在生产中使用 ts-node 并不是一种坏的做法 (如果设置得当),但在我们的案例中,我们意识到它会产生大量的开销,然而我们严重依赖 TypeORM 和 reflect-metadata,这使得 ts-node 很有吸引力。消除这种依赖需要大量的工作,并可能通过限制我们的工具集而导致 DX 的进一步退化。所以,我们只有一个选择:删除 Typescript。

当然,不是完全删除 Typescript,只是在生产环境。至少在理论上,让一个 node 进程加载.js 文件,而不是用 ts-node 包装器,这将大大减少启动时间,正如我们在第二个火焰图中观察到的那样。当然,我们可以采取不同的方法来实现这一点,但每一种方法都有利弊。

方法一:使用 tsc

我们最初的方法是使用 tsc 二进制文件,和安装的 Typescript 版本一起打包,并增加一个编译步骤。事实证明,这比想象的更棘手,因为几位工程师在 2 年多的时间里用不同的方法开发了配置的后端。因此,我们遇到了一些问题:

  • 多个依赖项用了不同的模块,tsc 一次只能处理一种方式。

  • Typescript 输出一个真实的、一对一的源到分发目录、使用了不同格式的 imports —— 有些是相对于 package.json,有些是别名。

  • Typescript 在设计上不会修改依赖项的导入路径,带有模块的 Node.js 对文件名应该如何表示有严格的要求。

方法二:用 ttypescript 和 ttsc 扩展 Typescript

可以使用几个补丁来修改 tsc 的行为,绕过 Typescript 的转译限制。不幸的是,这些解决方案虽然不是很复杂,但需要需要大量的混合和匹配来覆盖所有用例,并且对项目添加了额外的依赖项,例如 typescript-transformer-append-js-extension。

退一步说,我们意识到将不得不牺牲 Typescript 模块提供的一些便利,并重写应用程序的某些部分,尤其是在导入模块方面。

但是,如果有一个解决方案可以找出依赖关系,以及如何以声明的方式导入它们呢?

进入 webpack

webpack 是一个传统的 JavaScript 模块打包器,创建的目的是通过有效地将前端应用分割成块,快速地将其传送到用户的浏览器。作为最古老、最成熟的打包工具之一,至今仍在积极地维护中,webpack 拥有一个庞大的插件生态系统,适应任何类型的复杂应用,并且它对 Node.js 提供了一流的支持。

由于 webpack 就是为此目的而构建的,让它来处理模块解析和转换.ts 文件,相比其它类 hack 和猴子补丁方法,感觉更自然。我们努力了几次让 webpack 与 TypeORM 一起工作,主要是因为 TypeORM 顽固的设定。例如,数据库迁移文件必须在类名末尾包含时间戳,这意味着源文件不能缩小,导入 / 导出名称不能被篡改。但经过几次尝试,我们成功了。果然,通过 webpack 及其插件处理,每个文件都简化了构建过程。通过高效缓存,后续构建的速度会更快,从而获得更好的 DX 和更短的部署窗口。集群模式的部署现在大约需要 12 秒,缩短了近 5 分钟!——从服务请求开始。请记住,这是 8 个节点进程共享的资源,每个节点进程启动一个 koa 的 web 服务器和通过 TypeORM 连接到数据库。

在开发过程中,结果更加突出:


之前(秒

之后(秒

改进 (%

冷启动构建时间

40 ~ 90

9  ~ 13

77 ~ 85

热重启时间


0.5 ~ 0.9

服务器就绪

与冷启动相同

1

97 ~ 98

以下是我们用来大幅减少启动时间的 webpack 配置:

  1. 安装需要的依赖:

npm install --save-dev webpack webpack-cli @types/webpack-env

webpack 和 webpack-cli 不言自明,第三个包 @types/webpack-env,会启用 webpack 的 require.Context 的自动完成功能,这需要手动指导 webpack 如何以元编程的方式处理符号,例如,在源代码目录中找到你的 ORM 实体并自动声明它们,而不是专门地一个个导入——我们有大量这样的实体!

注意:所有这些依赖项只能在开发和构建期间使用,不需要在生产构建中加载它们!

  1. 创建和导出配置文件

webpack 的配置非常简单,只需在你的项目根目录(通常是 package.json 所在的文件夹)中创建一个 webpack.config.js 文件,然后导出 webpack 配置。它看起来可能像这样:

module.exports = {// webpack config}
  1. 添加构建入口和路径

module.exports = { entry: './src/index.ts', // the file you would provide to ts-node or node binaries for execution mode: NODE_ENV, // development or production target: 'node', // webpack works differently based on target, here we use node.js output: { // directions for the built files directory path: path.resolve(__dirname, 'dist'), filename: 'index.js', },}
  1. 配置如何查找源代码文件

module.exports = {  // ...  resolve: {  // Bundle only typescript files  extensions: ['.ts'],  alias: {    // provide any import aliases you may use in your project    src: path.resolve(__dirname, 'src/'),    '@controller': path.resolve(__dirname, 'src/controllers/'),    '@service': path.resolve(__dirname, 'src/services/'),  },  },}
  1. 配置读取 ts 文件

对于这一步,你可以安装任何你喜欢的 webpack 的 typescript 加载器。我们使用 ts-loader:

npm install --save-dev ts-loader
module.exports = {  // ...  module: {  rules: [    {      test: /\.ts$/,   // this rule will only activate for files ending in .ts      use: [{ loader: 'ts-loader' }],        exclude: [  // exclude any files you don't want to include        /__tests__/,      ],    },  ],  },}
  1. 添加外部扩展,这样 webpack 就不会打包外部依赖(node 模块)

npm install --save-dev webpack-node-externals
module.exports = {  // ...  externals: [nodeExternals()],}
  1. 别忘了你的插件——webpack 一切与插件相关!

module.exports = { // ... plugins: [ ...plugins, ],}

下面是我们使用的一些插件的列表,向出色的贡献者和维护者致敬!

  • nodemon-webpack-plugin:nodemon 的标准包装器,使开发速度更快。

  • webpack-shell-plugin-next:添加构建生命周期钩子来运行 cli 命令,例如,在构建源文件之前构建 swagger 文件。

  • fork-TS-checker-webpack-plugin:在一个独立进程上运行 TS 类型检查器,以提高构建期间的性能。注意:如果你使用这个,请确保更新步骤 5 中的 module.rules.use 为:{loader: 'ts-loader', options: {transpileOnly: true}},这样 ts-loader 就不会运行类型检查。

最终的 webpack 配置

你最终的 webpack 配置应该是这样的:

const path = require('path');const nodeExternals = require('webpack-node-externals');
const { NODE_ENV = 'production',} = process.env;
module.exports = { entry: './src/index.ts', // the file you would provide to ts-node or node binaries for execution mode: NODE_ENV, // development or production target: 'node', // webpack works differently based on target, here we use node.js output: { // directions for the built files directory path: path.resolve(__dirname, 'dist'), filename: 'index.js', }, resolve: { // Bundle only typescript files extensions: ['.ts'], alias: { // provider any import aliases you may use in your project src: path.resolve(__dirname, 'src/'), '@controller': path.resolve(__dirname, 'src/controllers/'), '@service': path.resolve(__dirname, 'src/services/'), }, }, module: { rules: [ { test: /\.ts$/, // this rule will only activate for files ending in .ts use: [{ loader: 'ts-loader' }], exclude: [ // exclude any files you don't want to include, eg test files /__tests__/, ], }, ], }, externals: [nodeExternals()], plugins: [ // any plugins you may find useful ],}
优化为更多的优化铺平道路

我们从运行时的依赖项中删除了 Typescript,所以我们在最终的生产制品中不再需要它,这样我们完全摆脱了这些依赖!

它也启发我们优化了构建流水线,通过引入带缓存层、和为开发和生产不同目标的多阶段 docker 构建,使其更为高效。

更少的依赖意味着:

  • 更小的图像尺寸。

  • 减少第三方代码造成的内存泄漏的机会。

  • 更少的带宽使用。

  • 更快的传输时间。

最重要的是,它意味着面临更少的攻击,由于依赖更少、审计和解决漏洞的时间更少,让 RudderStack 对我们的客户来说更加安全。

原文链接:

https://www./blog/how-we-reduced-startup-time-by-80-with-webpack/

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多