Node.js 最佳实践列表(2021 年 7 月)
Node.js 最佳实践
1. Project Structure Practices
2. Error Handling Practices
3. Code Style Practices
4. Testing And Overall Quality Practices
5. Going To Production Practices
6. Security Best Practices
7. Draft: Performance Best Practices
8. Docker Best Practices
内容和所有权利均归 github.com/goldbergyoni/nodebestpractices 所有
Node.js 最佳实践
目录
- 项目结构实践 (5)
- 错误处理实践 (12)
- 代码风格实践 (12)
- 测试和整体质量实践 (13)
- 进入生产实践 (19)
- 安全实践 (25)
- 绩效实践(2)(正在进行中️✍️)
- Docker实践(十五)
广告DigitalOcean 是一家云托管公司,提供基础设施即服务 (IaaS),以及可扩展且可靠的云托管解决方案。使用此链接,您将获得 200 美元的积分,可在 DigitalOcean 上随意使用,新用户有效期为 60 天。兑换免费积分。
1. Project Structure Practices
1.1 按组件构建解决方案
TL;DR:大型应用程序最严重的陷阱是维护包含数百个依赖项的庞大代码库——这样的单体式架构会减慢开发人员在尝试添加新功能时的速度。相反,应该将代码划分为组件,每个组件都有自己的文件夹或专用的代码库,并确保每个单元都保持小巧简洁。访问下方的“阅读更多”查看正确的项目结构示例
否则:当编写新功能的开发人员难以意识到其变更的影响,并担心破坏其他依赖组件时,部署会变得更慢、风险也更大。当所有业务部门未分离时,扩展也会变得更加困难。
1.2 对组件进行分层,将 Web 层保持在边界内
TL;DR:每个组件都应该包含“层”——一个用于 Web、逻辑和数据访问代码的专用对象。这不仅可以清晰地分离关注点,还能显著简化系统的模拟和测试。虽然这是一种非常常见的模式,但 API 开发人员倾向于将 Web 层对象(例如 Express req、res)传递给业务逻辑层和数据层,从而混合使用多个层——这使得您的应用程序依赖于特定的 Web 框架,并且只能由特定的 Web 框架访问。
否则:将 Web 对象与其他层混合的应用程序无法通过测试代码、CRON 作业、消息队列触发器等进行访问
1.3 将常用实用程序包装为 npm 包
TL;DR:在构成大型代码库的大型应用中,诸如记录器、加密等横切关注点实用程序应该由你的代码包装,并作为私有 npm 包公开。这样可以在多个代码库和项目之间共享它们。
否则:你必须发明你的部署和依赖关系
1.4 分离 Express 的“应用程序”和“服务器”
TL;DR:避免将整个Express应用定义在一个大文件中的坏习惯——将您的“Express”定义至少分成两个文件:API 声明 (app.js) 和网络相关部分 (WWW)。为了获得更好的结构,请将您的 API 声明放在组件中。
否则:你的 API 将只能通过 HTTP 调用进行测试(速度更慢,生成覆盖率报告也更困难)。在一个文件中维护数百行代码可能不太方便。
🔗阅读更多:分离 Express 的“应用程序”和“服务器”
1.5 使用环境感知、安全和分层配置
简而言之:一个完美无缺的配置设置应该确保:(a) 密钥可以从文件和环境变量中读取;(b) 机密信息不包含在已提交的代码中;(c) 配置具有层级结构,以便于查找。有一些软件包可以帮助满足大部分需求,例如rc、nconf、config和convict。
否则:任何配置要求不满足都会让开发或 DevOps 团队陷入困境。
2. Error Handling Practices
2.1 使用 Async-Await 或 Promise 进行异步错误处理
TL;DR:用回调函数处理异步错误可能是最快的地狱之路(也就是毁灭金字塔)。你能给你的代码最好的礼物是使用一个可靠的 Promise 库或 async-await,它们可以让你使用更紧凑、更熟悉的代码语法,比如 try-catch。
否则: Node.js 回调风格,函数(err,response),是一种很有前途的方式,因为错误处理与随意代码、过度嵌套和尴尬的编码模式混合在一起,导致代码难以维护
2.2 仅使用内置 Error 对象
TL;DR:许多错误会以字符串或某些自定义类型抛出——这会使错误处理逻辑和模块间的互操作性变得复杂。无论您是拒绝 Promise、抛出异常还是发出错误——仅使用内置 Error 对象(或扩展内置 Error 对象的对象)将提高一致性并防止信息丢失。ESLintno-throw-literal
规则会严格检查这一点(尽管它有一些限制,但可以通过使用 TypeScript 并设置@typescript-eslint/no-throw-literal
规则来解决)。
否则:调用某个组件时,如果不确定返回的错误类型,那么正确的错误处理就会变得更加困难。更糟糕的是,使用自定义类型描述错误可能会导致关键错误信息(例如堆栈跟踪)丢失!
2.3 区分操作错误和程序员错误
简而言之:操作错误(例如,API 收到了无效输入)是指已知情况,其错误影响已被充分理解,并可以妥善处理。另一方面,程序员错误(例如,尝试读取未定义的变量)是指未知的代码故障,需要优雅地重启应用程序。
否则:当出现错误时,你当然可以重启应用程序,但为什么要因为一个可以预见的小操作错误而让大约 5000 名在线用户宕机呢?相反的做法也不理想——当出现未知问题(程序员错误)时,保持应用程序正常运行可能会导致不可预测的行为。区分两者,可以让我们根据具体情况采取更巧妙的行动,并采取更平衡的策略。
2.4 集中处理错误,而不是在中间件中
TL;DR:错误处理逻辑(例如发送给管理员的邮件和日志记录)应该封装在一个专用的集中式对象中,当出现错误时,所有端点(例如 Express 中间件、cron 作业、单元测试)都会调用该对象
否则:不在单个地方处理错误将导致代码重复,并可能导致错误处理不当
2.5 使用 Swagger 或 GraphQL 记录 API 错误
简而言之:让你的 API 调用者知道可能出现哪些错误,以便他们能够妥善处理这些错误,避免崩溃。对于 RESTful API,这通常使用像 Swagger 这样的文档框架来实现。如果你使用 GraphQL,你也可以利用你的 schema 和注释。
否则: API 客户端可能仅仅因为收到了无法理解的错误而崩溃并重启。注意:API 的调用者可能是你自己(这在微服务环境中很常见)。
🔗阅读更多:在 Swagger 或 GraphQL 中记录 API 错误
2.6 当陌生人来访时,优雅地退出流程
TL;DR:当发生未知错误(开发者错误,参见最佳实践 2.3)时,应用程序的健康状况存在不确定性。常见的做法是使用Forever或PM2等进程管理工具谨慎地重启进程。
否则:当发生不熟悉的异常时,某些对象可能处于错误状态(例如,全局使用的事件发射器由于某些内部故障而不再触发事件),并且所有未来的请求都可能失败或行为异常
2.7 使用成熟的记录器来提高错误可见性
TL;DR:一套成熟的日志工具,例如Pino或Log4js,将加速错误发现和理解。所以,忘掉 console.log 吧。
否则:浏览 console.logs 或手动浏览混乱的文本文件,如果没有查询工具或合适的日志查看器,可能会让你忙到很晚
2.8 使用你喜欢的测试框架测试错误流
TL;DR:无论是专业的自动化 QA 测试还是简单的手动开发者测试,都要确保您的代码不仅能够满足积极的场景,还能处理并返回正确的错误。像 Mocha 和 Chai 这样的测试框架可以轻松应对这种情况(请参阅“Gist 弹出窗口”中的代码示例)。
否则:如果没有测试,无论是自动测试还是手动测试,你就无法指望你的代码返回正确的错误。如果没有有意义的错误,就没有错误处理。
2.9 使用 APM 产品发现错误和停机时间
TL;DR:监控和性能产品(又名 APM)会主动评估你的代码库或 API,以便它们可以自动突出显示你遗漏的错误、崩溃和缓慢的部分
否则:您可能会花费大量精力来测量 API 性能和停机时间,但您可能永远不会知道在实际场景下哪些代码部分最慢,以及这些部分如何影响用户体验
2.10 捕获未处理的承诺拒绝
TL;DR:除非开发人员没有忘记显式处理,否则 Promise 中抛出的任何异常都会被吞噬并丢弃。即使你的代码已订阅process.uncaughtException
!可以通过注册事件来解决这个问题process.unhandledRejection
否则:你的错误将被吞噬,不留任何痕迹。无需担心
2.11 快速失败,使用专用库验证参数
TL;DR:断言 API 输入可以避免以后更难追踪的严重错误。除非你使用像ajv和Joi这样非常酷的辅助库,否则验证代码通常很冗长。
否则:想想看——你的函数需要一个数字参数“Discount”,但调用者忘记传递这个参数了。之后,你的代码会检查 Discount!=0(允许的折扣金额大于零),如果满足条件,它就会允许用户享受折扣。我的天哪,这真是个讨厌的 bug。你发现了吗?
2.12 在返回之前始终等待承诺以避免部分堆栈跟踪
TL;DR:return await
返回 Promise 时务必这样做,以便获取完整的错误堆栈跟踪。如果
函数返回的是 Promise,则必须将该函数声明为async
函数,并await
在返回之前明确声明该 Promise。
否则:返回 Promise 且未执行 await 操作的函数将不会出现在堆栈跟踪中。
此类缺失的帧可能会使理解导致错误的流程变得复杂,
尤其是在异常行为的原因位于缺失函数内部的情况下。
3. Code Style Practices
3.1 使用 ESLint
简而言之: ESLint是检查潜在代码错误和修复代码样式的事实上的标准,它不仅可以识别细微的间距问题,还可以检测严重的代码反模式,例如开发人员抛出未归类的错误。虽然 ESLint 可以自动修复代码样式,但其他工具(例如Prettier和Beautify)在格式化修复方面更强大,并且可以与 ESLint 配合使用。
否则:开发人员将专注于繁琐的间距和线宽问题,并且可能会浪费时间过度思考项目的代码风格
3.2 Node.js 特定插件
TL;DR:在涵盖原始 JavaScript 的 ESLint 标准规则之上,添加 Node.js 特定的插件,如eslint-plugin-node、eslint-plugin-mocha和eslint-plugin-node-security
否则:许多错误的 Node.js 代码模式可能会被忽略。例如,开发人员可能会 require(variableAsPath) 文件,并将变量作为路径传入,这允许攻击者执行任何 JS 脚本。Node.js 的 linters 可以检测到此类模式并及早发出警告。
3.3 代码块的花括号在同一行开始
TL;DR:代码块的开始花括号应该与开始语句位于同一行
代码示例
// Do
function someFunction() {
// code block
}
// Avoid
function someFunction()
{
// code block
}
否则:不遵循此最佳实践可能会导致意外结果,如下面的 StackOverflow 线程所示:
🔗阅读更多: “为什么结果会根据花括号的位置而变化?”(StackOverflow)
3.4 正确划分语句
无论您是否使用分号来分隔语句,了解不正确的换行符或自动插入分号的常见陷阱,将有助于您消除常见的语法错误。
TL;DR:使用 ESLint 来了解分离问题。Prettier或Standardjs可以自动解决这些问题。
否则:如上一节所述,JavaScript 解释器会自动在语句末尾添加分号(如果语句末尾没有分号),或者认为语句未在应有的位置结束,这可能会导致一些不良结果。您可以使用赋值语句并避免使用立即调用的函数表达式来避免大多数意外错误。
代码示例
// Do
function doThing() {
// ...
}
doThing()
// Do
const items = [1, 2, 3]
items.forEach(console.log)
// Avoid — throws exception
const m = new Map()
const a = [1,2,3]
[...m.values()].forEach(console.log)
> [...m.values()].forEach(console.log)
> ^^^
> SyntaxError: Unexpected token ...
// Avoid — throws exception
const count = 2 // it tries to run 2(), but 2 is not a function
(function doSomething() {
// do something amazing
}())
// put a semicolon before the immediate invoked function, after the const definition, save the return value of the anonymous function to a variable or avoid IIFEs altogether
🔗阅读更多: “半 ESLint 规则”
🔗阅读更多: “无意外的多行 ESLint 规则”
3.5 命名你的函数
TL;DR:请为所有函数命名,包括闭包和回调函数。避免使用匿名函数。这在分析 Node 应用时尤其有用。为所有函数命名,可以让您在检查内存快照时轻松理解所查看的内容。
否则:使用核心转储(内存快照)调试生产问题可能会变得很困难,因为您会注意到匿名函数会消耗大量内存
3.6 对变量、常量、函数和类使用命名约定
TL;DR:命名常量、变量和函数时使用小驼峰命名法 (lowerCamelCase) ,命名类时使用大驼峰命名法 ( UpperCamelCase )(首字母也大写)。这有助于您轻松区分普通变量/函数和需要实例化的类。使用描述性名称,但尽量保持简短。
否则: JavaScript 是世界上唯一允许直接调用构造函数(“类”)而无需先实例化的语言。因此,类和函数构造函数以 UpperCamelCase 开头来区分。
3.6 代码示例
// for class name we use UpperCamelCase
class SomeClassExample {}
// for const names we use the const keyword and lowerCamelCase
const config = {
key: "value",
};
// for variables and functions names we use lowerCamelCase
let someVariableExample = "value";
function doSomething() {}
3.7 优先使用 const,而不是 let,抛弃 var
TL;DR:使用const
意味着变量一旦赋值,就无法重新赋值。使用 preferringconst
可以避免将同一个变量用于不同的用途,并使代码更清晰。如果需要重新赋值,例如在 for 循环中,请使用let
来声明它。另一个重要方面let
是,使用它声明的变量仅在定义它的块作用域内有效。它var
是函数作用域,而不是块作用域,既然你已经拥有并可以使用它,那么在 ES6 中不应该使用它。const
let
否则:当跟踪频繁变化的变量时,调试会变得更加麻烦
🔗阅读更多:JavaScript ES6+:var、let 还是 const?
3.8 首先引用模块,而不是函数内部
TL;DR:在每个文件的开头,在任何函数之前和外部,引入依赖模块。这个简单的最佳实践不仅能帮助你轻松快速地在文件顶部找到依赖关系,还能避免一些潜在的问题。
否则: Node.js 会同步运行 Requires 函数。如果在函数内部调用它们,可能会在更关键的时刻阻塞其他请求的处理。此外,如果某个必需模块或其任何依赖项抛出错误并导致服务器崩溃,最好尽快发现问题,但如果该模块是在函数内部调用的,则可能并非如此。
3.9 通过文件夹来引用模块,而不是直接引用文件
TL;DR:在文件夹中开发模块/库时,请放置一个 index.js 文件,该文件暴露模块的内部结构,以便每个使用者都能访问它。这可以作为模块的“接口”,方便将来的更改,而不会违反约定。
否则:更改文件的内部结构或签名可能会破坏与客户端的接口
3.9 代码示例
// Do
module.exports.SMSProvider = require("./SMSProvider");
module.exports.SMSNumberResolver = require("./SMSNumberResolver");
// Avoid
module.exports.SMSProvider = require("./SMSProvider/SMSProvider.js");
module.exports.SMSNumberResolver = require("./SMSNumberResolver/SMSNumberResolver.js");
3.10 使用===
运算符
TL;DR:优先使用严格相等运算符===
,而不是较弱的抽象相等运算符==
。==
会在将两个变量转换为相同类型后进行比较。 中没有类型转换===
,并且两个变量必须属于同一类型才能相等。
否则:与==
运算符进行比较时,不相等的变量可能返回 true
3.10 代码示例
"" == "0"; // false
0 == ""; // true
0 == "0"; // true
false == "false"; // false
false == "0"; // true
false == undefined; // false
false == null; // false
null == undefined; // true
" \t\r\n " == 0; // true
如果与以下语句一起使用,则上述所有语句都将返回 false===
3.11 使用 Async Await,避免回调
简而言之: Node 8 LTS 现在全面支持 Async-await。这是一种处理异步代码的新方法,它取代了回调和 Promise。Async-await 是非阻塞的,它使异步代码看起来像同步的。你能给你的代码最好的礼物就是使用 Async-await,它提供了更紧凑、更熟悉的代码语法,就像 try-catch 一样。
否则:以回调风格处理异步错误可能是最快的方法——这种风格强制检查所有错误,处理尴尬的代码嵌套,并且很难推断代码流
3.12 使用箭头函数表达式(=>)
TL;DR:虽然建议在处理接受承诺或回调的旧 API 时使用 async-await 并避免使用函数参数 - 但箭头函数使代码结构更紧凑并保持根函数的词法上下文(即this
)
否则:较长的代码(在 ES5 函数中)更容易出现错误并且难以阅读
4. Testing And Overall Quality Practices
4.1 至少编写 API(组件)测试
简而言之:大多数项目由于时间紧迫,或者“测试项目”经常失控而被放弃,所以没有进行任何自动化测试。因此,优先考虑 API 测试,因为它是最简单的编写方式,并且比单元测试覆盖范围更广(您甚至可以使用Postman等工具编写无需代码的 API 测试)。之后,如果您有更多资源和时间,可以继续进行高级测试类型,例如单元测试、数据库测试、性能测试等。
否则:你可能会花费大量时间编写单元测试,却发现系统覆盖率只有 20%
4.2 每个测试名称包含 3 个部分
TL;DR:让测试在需求层面上表达清楚,这样即使对不熟悉代码内部的 QA 工程师和开发人员来说,也能一目了然。测试名称中要说明测试的内容(被测单元)、测试环境以及预期结果。
否则:部署失败,名为“添加产品”的测试失败。这能告诉你到底是什么出了问题吗?
4.3 AAA模式的结构测试
TL;DR:将测试结构划分为三个相互独立、功能明确的部分:安排、执行和断言(AAA)。第一部分包括测试设置,然后是被测单元的执行,最后是断言阶段。遵循此结构可确保读者无需耗费脑力和 CPU 即可理解测试计划。
否则:你不仅每天要花很长时间去理解主要代码,而且现在本来应该是一天中简单的部分(测试)也会让你的大脑紧张。
4.4 使用 linter 检测代码问题
TL;DR:使用代码 linter 检查基本质量并尽早发现反模式。在任何测试之前运行它,并将其添加为预提交的 git-hook,以最大限度地减少审查和纠正任何问题所需的时间。另请参阅第 3 节“代码风格实践”
否则:您可能会将一些反模式和可能存在漏洞的代码传递到您的生产环境中。
4.5 避免使用全局测试装置和种子,每次测试都添加数据
TL;DR:为了避免测试耦合,并方便推理测试流程,每个测试都应该添加并处理其自身的一组数据库行。每当测试需要提取或假设某些数据库数据存在时,它必须显式地添加该数据,并避免修改任何其他记录。
否则:考虑这样一种情况:由于测试失败而中止部署,团队现在将花费宝贵的调查时间,最终得出一个令人悲伤的结论:系统运行良好,但测试相互干扰并破坏了构建
4.6 持续检查易受攻击的依赖项
简而言之:即使是像 Express 这样最知名的依赖项也存在已知漏洞。您可以使用社区和商业工具(例如 🔗 npm audit和 🔗 snyk.io)轻松修复这些漏洞,这些工具可以在每次构建时从您的 CI 中调用。
否则:如果没有专用工具,想要保持代码不受漏洞影响,就需要不断关注有关新威胁的在线出版物。相当繁琐
4.7 标记你的测试
TL;DR:不同的测试必须在不同的场景下运行:快速冒烟测试、无IO测试、开发人员保存或提交文件时运行的测试、完整的端到端测试通常在提交新的拉取请求时运行等等。这可以通过使用关键字(例如 #cold #api #sanity)标记测试来实现,这样您就可以使用测试工具进行 grep 并调用所需的子集。例如,您可以这样使用Mocha仅调用健全性测试组:mocha --grep 'sanity'
否则:运行所有测试,包括执行数十个数据库查询的测试,每当开发人员进行一个小的更改时,速度都会非常慢,并且使开发人员无法运行测试
4.8 检查测试覆盖率,它有助于识别错误的测试模式
TL;DR:像Istanbul / NYC这样的代码覆盖率工具之所以出色,有三个原因:免费(无需任何操作即可生成报告);有助于识别测试覆盖率的下降;最后,同样重要的是,它能够突出显示测试不匹配的情况:通过查看彩色代码覆盖率报告,您可能会注意到一些从未测试过的代码区域,例如 catch 子句(这意味着测试只会调用正常路径,而不会关注应用程序在遇到错误时的行为)。如果覆盖率低于某个阈值,则将其设置为构建失败。
否则:不会有任何自动指标告诉你,你的大部分代码是否未被测试覆盖
4.9 检查过期的软件包
TL;DR:使用您常用的工具(例如npm outdated
npm -check-updates)来检测已安装的过期软件包,将此检查注入到您的 CI 管道中,甚至在严重的情况下使构建失败。例如,严重情况可能是:已安装的软件包落后 5 个补丁提交(例如,本地版本为 1.3.1,而仓库版本为 1.3.8),或者被其作者标记为已弃用 - 终止构建并阻止部署此版本
否则:您的产品将运行已被其作者明确标记为有风险的软件包
4.10 使用类似生产的环境进行端到端测试
TL;DR:包含实时数据的端到端 (e2e) 测试曾经是持续集成 (CI) 流程中最薄弱的环节,因为它依赖于多个像数据库这样的重型服务。请使用尽可能接近实际生产环境的环境,例如 a-continue(此处漏掉了 -continue,需要补充内容。根据otherwise子句判断,这应该提到 docker-compose)。
否则:如果没有 docker-compose,团队必须为每个测试环境(包括开发人员的机器)维护一个测试数据库,并保持所有这些数据库同步,这样测试结果就不会因环境而异
4.11 使用静态分析工具定期重构
TL;DR:使用静态分析工具有助于提供客观的方法来提升代码质量,并保持代码的可维护性。您可以将静态分析工具添加到您的持续集成 (CI) 构建中,以便在发现代码异味时自动终止构建。与普通的 linting 相比,它的主要优势在于能够在多个文件的上下文中检查质量(例如检测重复项)、执行高级分析(例如代码复杂性),并跟踪代码问题的历史记录和进度。您可以使用的两个工具示例是Sonarqube(超过 2,600颗星)和Code Climate(超过 1,500颗星)。
否则:由于代码质量差,错误和性能始终是一个问题,没有新的库或最先进的功能可以解决
4.12 谨慎选择 CI 平台(Jenkins、CircleCI、Travis 以及世界其他地方)
简而言之:您的持续集成平台 (CICD) 将托管所有质量工具(例如测试、lint),因此它应该拥有一个充满活力的插件生态系统。Jenkins曾经是许多项目的默认选择,因为它拥有最大的社区和非常强大的平台,但代价是设置复杂,需要较长的学习时间。如今,使用CircleCI等 SaaS 工具来构建 CI 解决方案变得更加容易。这些工具可以构建灵活的 CI 流水线,而无需管理整个基础设施。最终,这是一个在稳健性和速度之间权衡的问题——请谨慎选择。
缺点:选择一些小众供应商可能会在需要高级定制时阻碍你。另一方面,选择 Jenkins 可能会浪费宝贵的基础设施设置时间。
4.13 隔离测试中间件
TL;DR:当中间件包含大量跨越多个请求的逻辑时,最好单独测试它,而无需唤醒整个 Web 框架。这可以通过对 {req, res, next} 对象进行存根和监视来轻松实现。
否则: Express 中间件中的错误 === 所有或大多数请求中的错误
5. Going To Production Practices
5.1. 监控
简而言之:监控就是在客户发现问题之前就发现问题——显然,这一点应该被赋予前所未有的重要性。市场上充斥着各种各样的方案,因此,请先定义必须遵循的基本指标(我的建议已在内),然后再考虑其他一些吸引人的功能,最终选择符合所有条件的解决方案。点击下方“要点”查看解决方案概览
否则:失败===让客户失望。简单
5.2. 使用智能日志提高透明度
TL;DR:日志可以是一个愚蠢的调试语句仓库,也可以是一个漂亮的仪表盘,用来讲述你的应用的故事。从第一天开始就规划你的日志平台:如何收集、存储和分析日志,以确保能够真正提取所需的信息(例如错误率、通过服务和服务器跟踪整个事务等)。
否则:你最终会得到一个难以推理的黑匣子,然后你开始重写所有日志语句以添加其他信息
5.3. 将任何可能的操作(例如 gzip、SSL)委托给反向代理
TL;DR: Node 在执行 CPU 密集型任务(例如 gzip 压缩、SSL 终止等)方面非常糟糕。您应该使用“真正的”中间件服务,例如 nginx、HAproxy 或云供应商服务。
否则:你可怜的单线程将继续忙于执行基础设施任务,而不是处理你的应用程序核心,性能也会相应下降
🔗阅读更多:将任何可能的操作(例如 gzip、SSL)委托给反向代理
5.4. 锁依赖关系
TL;DR:您的代码必须在所有环境中保持一致,但令人惊讶的是,npm 默认允许依赖项在不同环境中漂移——当您在不同环境中安装软件包时,它会尝试获取软件包的最新补丁版本。您可以使用 npm 配置文件 .npmrc 来解决这个问题,该文件会指示每个环境保存每个软件包的精确版本(而非最新版本)。或者,为了进行更细粒度的控制,请使用npm shrinkwrap
。*更新:从 NPM 5 开始,依赖项默认处于锁定状态。新的软件包管理器 Yarn 也默认为我们提供了依赖项。
否则: QA 会彻底测试代码,并批准一个在生产环境中表现不同的版本。更糟糕的是,同一生产集群中的不同服务器可能会运行不同的代码。
5.5. 使用正确的工具保障流程正常运行
TL;DR:进程必须持续运行,并在发生故障时重新启动。对于简单的场景,像 PM2 这样的进程管理工具可能就足够了,但在如今“dockerized”的世界中,还应该考虑使用集群管理工具。
否则:在没有明确策略的情况下运行数十个实例,并且使用太多工具(集群管理、docker、PM2)可能会导致 DevOps 混乱
5.6. 利用所有 CPU 核心
简而言之: Node 应用的基本形式是运行在单个 CPU 核心上,其他所有 CPU 都处于空闲状态。您需要复制 Node 进程并充分利用所有 CPU——对于中小型应用,您可以使用 Node Cluster 或 PM2。对于大型应用,可以考虑使用 Docker 集群(例如 K8S、ECS)或基于 Linux 初始化系统(例如 systemd)的部署脚本来复制进程。
否则:你的应用很可能只会利用其可用资源的 25% (!) 甚至更少。需要注意的是,典型的服务器通常有 4 个或更多的 CPU 核心,而 Node.js 的简单部署只会利用 1 个(即使使用像 AWS beanstalk 这样的 PaaS 服务!)
5.7. 创建“维护端点”
TL;DR:通过安全的 API 公开一组系统相关信息,例如内存使用情况和 REPL 等。虽然强烈建议使用标准且久经考验的工具,但有些有价值的信息和操作使用代码更容易实现。
否则:你会发现你正在执行许多“诊断部署”——将代码运送到生产环境只是为了提取一些用于诊断目的的信息
5.8. 使用 APM 产品发现错误和停机时间
简而言之:应用程序监控和性能产品(又称 APM)会主动评估代码库和 API,因此它们能够自动超越传统监控,衡量跨服务和层级的整体用户体验。例如,一些 APM 产品可以突出显示最终用户端加载速度过慢的事务,并提示根本原因。
否则:您可能会花费大量精力来测量 API 性能和停机时间,但您可能永远不会知道在实际场景下哪些代码部分最慢,以及这些代码部分如何影响用户体验
5.9. 让你的代码可以投入生产
TL;DR:以最终目标为导向编写代码,从第一天开始就为生产环境做好规划。这听起来有点模糊,所以我整理了一些与生产维护密切相关的开发技巧(点击下方的 Gist)。
否则:即使是世界冠军 IT/DevOps 人员也不会拯救一个编写糟糕的系统
5.10. 测量并保护内存使用情况
长话短说: Node.js 与内存的关系颇具争议:V8 引擎对内存使用量有软限制(1.4GB),而且 Node 代码中存在已知的内存泄漏路径——因此监控 Node 进程的内存至关重要。在小型应用中,你可以使用 Shell 命令定期测量内存,但在中大型应用中,可以考虑将内存监控功能集成到健壮的监控系统中。
否则:你的进程内存可能会像沃尔玛那样每天泄漏一百兆字节
5.11. 将前端资产从 Node 中取出
TL;DR:使用专用中间件(nginx、S3、CDN)提供前端内容,因为 Node 的单线程模型在处理大量静态文件时性能会受到很大影响
否则:你的单个 Node 线程将忙于传输数百个 html/images/angular/react 文件,而不是将所有资源分配给它天生的任务——提供动态内容
5.12. 无状态,几乎每天都会关闭你的服务器
TL;DR:将任何类型的数据(例如用户会话、缓存、上传的文件)存储在外部数据存储中。考虑定期“关闭”你的服务器,或者使用明确强制执行无状态行为的“无服务器”平台(例如 AWS Lambda)。
否则:特定服务器故障将导致应用程序停机,而不仅仅是关闭故障机器。此外,由于依赖于特定服务器,横向扩展弹性将变得更加困难。
5.13. 使用自动检测漏洞的工具
简而言之:即使是像 Express 这样最知名的依赖项,也时常存在已知漏洞,这些漏洞可能会危及系统安全。不过,使用社区和商业工具可以轻松解决这个问题,这些工具会持续检查漏洞并发出警告(本地或 GitHub 上),有些甚至可以立即修复漏洞。
否则:如果没有专用工具,想要保持代码不受漏洞影响,就需要你不断关注有关新威胁的在线出版物。相当繁琐
5.14. 为每个日志语句分配一个事务id
也称为关联 ID / 传输 ID / 跟踪 ID / 请求 ID / 请求上下文 / 等。
TL;DR:在单个请求中为每个日志条目分配相同的标识符,即 transaction-id: {某个值}。这样,在检查日志中的错误时,就可以轻松推断出前后发生了什么。在 Node 14 版本之前,由于 Node 的异步特性,这一点并不容易实现,但自从 AsyncLocalStorage 出现后,这一点变得比以往任何时候都更容易实现。请参阅内文中的代码示例
否则:在没有上下文(之前发生了什么)的情况下查看生产错误日志会使推断问题变得更加困难和缓慢。
🔗阅读更多:为每个日志语句分配 'TransactionId'
5.15. 设置NODE_ENV=production
TL;DR:将环境变量设置NODE_ENV
为“生产”或“开发”,以标记是否应激活生产优化 - 许多 npm 包会确定当前环境并针对生产优化其代码
否则:省略这个简单的属性可能会大大降低性能。例如,当使用 Express 进行服务器端渲染时,省略NODE_ENV
这个属性会使速度降低三倍!
5.16. 设计自动化、原子化和零停机部署
简而言之:研究表明,执行多次部署的团队可以降低出现严重生产问题的概率。快速自动化的部署无需高风险的手动步骤和服务停机,可以显著改善部署流程。您应该结合使用 Docker 和 CI 工具来实现这一点,因为它们已成为精简部署的行业标准。
否则:长时间部署 -> 生产停机和人为错误 -> 团队对部署缺乏信心 -> 部署和功能减少
5.17. 使用 Node.js 的 LTS 版本
TL;DR:确保您使用的是 Node.js 的 LTS 版本,以便接收关键错误修复、安全更新和性能改进
否则:新发现的错误或漏洞可能会被用来攻击生产环境中运行的应用程序,并且您的应用程序可能会不受各种模块的支持,并且更难维护
5.18. 不要在应用程序内路由日志
TL;DR:日志目的地不应该由开发人员在应用程序代码中硬编码,而应该由应用程序运行的执行环境定义。开发人员应该stdout
使用记录器实用程序写入日志,然后让执行环境(容器、服务器等)将stdout
流传输到适当的目的地(即 Splunk、Graylog、ElasticSearch 等)。
否则:应用程序处理日志路由===难以扩展,日志丢失,关注点分离不佳
5.19. 使用以下方式安装软件包npm ci
TL;DR:您必须确保生产代码使用的软件包版本与您测试过的软件包版本完全一致。运行npm ci
以下命令,严格执行与 package.json 和 package-lock.json 匹配的依赖项的全新安装。建议在自动化环境(例如持续集成流水线)中使用此命令。
否则: QA 会彻底测试代码,并批准一个在生产环境中表现不同的版本。更糟糕的是,同一生产集群中的不同服务器可能会运行不同的代码。
6. Security Best Practices
6.1. 遵循 linter 安全规则
TL;DR:使用安全相关的 Linter 插件(例如eslint-plugin-security)尽早捕获安全漏洞和问题,最好是在编写代码时就发现。这有助于捕获安全漏洞,例如使用 eval、调用子进程或导入带有字符串字面量的模块(例如用户输入)。点击下方的“阅读更多”查看安全 Linter 可以捕获的代码示例
否则:开发过程中可能只是一个简单的安全漏洞,却在生产环境中变成了一个重大问题。此外,项目可能没有遵循一致的代码安全实践,从而导致漏洞被引入,或敏感信息被提交到远程代码库。
6.2. 使用中间件限制并发请求
简而言之: DOS 攻击非常普遍,而且相对容易实施。可以使用外部服务来实现速率限制,例如云负载均衡器、云防火墙、nginx、rate-limiter-flexible包,或者(对于较小且不太重要的应用程序)使用速率限制中间件(例如express-rate-limit) 。
否则:应用程序可能会受到攻击,导致拒绝服务,真实用户将收到降级或不可用的服务。
6.3 从配置文件中提取机密信息或使用包对其进行加密
TL;DR:切勿将纯文本机密存储在配置文件或源代码中。相反,请使用机密管理系统,例如 Vault 产品、Kubernetes/Docker Secrets,或使用环境变量。作为最后的手段,存储在源代码控制中的机密必须进行加密和管理(滚动密钥、过期、审计等)。使用预提交/推送钩子来防止意外提交机密。
否则:即使是私有代码库的源代码控制,也可能被误公开,从而暴露所有机密。外部人员访问源代码控制可能会无意中泄露相关系统(数据库、API、服务等)的访问权限。
6.4. 使用 ORM/ODM 库防止查询注入漏洞
TL;DR:为了防止 SQL/NoSQL 注入和其他恶意攻击,请始终使用 ORM/ODM 或数据库库,这些库可以转义数据或支持命名或索引参数化查询,并负责验证用户输入是否符合预期类型。切勿仅使用 JavaScript 模板字符串或字符串连接将值注入查询,因为这会使您的应用程序面临各种漏洞。所有信誉良好的 Node.js 数据访问库(例如Sequelize、Knex、mongoose)都内置了针对注入攻击的保护措施。
否则:在使用 MongoDB for NoSQL 时,未经验证或未清理的用户输入可能会导致操作员注入,而不使用适当的清理系统或 ORM 将很容易允许 SQL 注入攻击,从而造成巨大的漏洞。
6.5. 通用安全最佳实践的收集
长话短说:这是一些与 Node.js 无直接关系的安全建议——Node 的实现与其他语言并无太大区别。点击“阅读更多”即可浏览。
6.6. 调整 HTTP 响应标头以增强安全性
简而言之:您的应用程序应该使用安全标头来阻止攻击者使用常见攻击,例如跨站脚本 (XSS)、点击劫持和其他恶意攻击。您可以使用诸如Helmet之类的模块轻松配置这些安全标头。
否则:攻击者可能会直接攻击您的应用程序用户,从而导致巨大的安全漏洞
6.7. 持续自动检查易受攻击的依赖项
TL;DR:在 npm 生态系统中,一个项目有很多依赖项是很常见的。随着新漏洞的出现,应始终检查依赖项。使用npm audit或snyk等工具来跟踪、监控和修补易受攻击的依赖项。将这些工具与您的 CI 设置集成,以便在易受攻击的依赖项投入生产之前将其捕获。
否则:攻击者可能会检测您的 Web 框架并攻击其所有已知漏洞。
6.8. 使用 bcrypt 或 scrypt 保护用户的密码/秘密
TL;DR:密码或机密(例如 API 密钥)应使用安全的哈希 + 盐函数(如bcrypt
、scrypt
或最坏情况)进行存储pbkdf2
。
否则:未使用安全功能存储的密码和机密容易受到暴力破解和字典攻击,最终导致其泄露。
6.9. 转义 HTML、JS 和 CSS 输出
简而言之:发送到浏览器的不受信任的数据可能会被执行,而不仅仅是显示出来,这通常被称为跨站脚本 (XSS) 攻击。可以使用专用库来缓解这种情况,这些库将数据明确标记为纯内容,并且永远不会被执行(例如编码、转义)。
否则:攻击者可能会将恶意 JavaScript 代码存储在您的数据库中,然后将其原样发送给不良客户端
6.10. 验证传入的 JSON 模式
TL;DR:验证传入请求的主体负载并确保其符合预期,如果不符合则快速失败。为了避免在每个路由中编写繁琐的验证代码,您可以使用基于 JSON 的轻量级验证模式,例如jsonschema或joi。
否则:您的慷慨和宽容态度会大大增加攻击面,并鼓励攻击者尝试多种输入,直到找到某种组合来使应用程序崩溃
6.11. 支持黑名单 JWT
TL;DR:使用 JSON Web Tokens(例如Passport.js)时,默认情况下没有撤销已颁发令牌访问权限的机制。一旦发现恶意用户活动,只要他们持有有效令牌,就无法阻止他们访问系统。可以通过实施不受信任令牌的黑名单来缓解这种情况,该黑名单会在每次请求时进行验证。
否则:第三方可能会恶意使用过期或放错位置的令牌来访问应用程序并冒充令牌的所有者。
6.12. 防止针对授权的暴力攻击
TL;DR:一种简单而强大的技术是使用两个指标来限制授权尝试:
- 第一个是同一用户唯一 ID/名称和 IP 地址连续尝试失败的次数。
- 第二个指标是某个 IP 地址在一段较长时间内尝试访问失败的次数。例如,如果某个 IP 地址在一天内尝试访问失败 100 次,则将其封锁。
否则:攻击者可以发出无限次自动密码尝试来访问应用程序上的特权帐户
6.13. 以非 root 用户身份运行 Node.js
TL;DR: Node.js 经常以 root 用户身份运行,且拥有无限权限。例如,这是 Docker 容器中的默认行为。建议创建一个非 root 用户,并将其添加到 Docker 镜像中(示例如下),或者通过使用“-u username”参数调用容器来以该用户身份运行进程。
否则:设法在服务器上运行脚本的攻击者将获得对本地计算机的无限控制权(例如,更改 iptable 并将流量重新路由到他的服务器)
6.14. 使用反向代理或中间件限制有效载荷大小
简而言之: body 负载越大,单线程处理起来就越吃力。这为攻击者提供了一个机会,让他们无需大量请求(例如 DOS/DDOS 攻击)就能瘫痪服务器。为了缓解这种情况,可以限制边缘服务器(例如防火墙、ELB)传入请求的 body 大小,或者配置express body 解析器使其仅接受小负载。
否则:您的应用程序将不得不处理大量请求,无法处理它必须完成的其他重要工作,从而导致性能影响和易受 DOS 攻击。
6.15 避免使用 JavaScript eval 语句
TL;DR: eval
这是邪恶的,因为它允许在运行时执行自定义 JavaScript 代码。这不仅是一个性能问题,也是一个重要的安全隐患,因为恶意 JavaScript 代码可能源自用户输入。另一个应该避免的语言特性是new Function
构造函数,setTimeout
并且setInterval
永远不应该传递动态 JavaScript 代码。
否则:恶意 JavaScript 代码会设法渗透到传入的文本eval
或其他实时执行的 JavaScript 语言函数中,并获得页面 JavaScript 权限的完全访问权限。此漏洞通常表现为 XSS 攻击。
6.16. 防止恶意的 RegEx 超载你的单线程执行
简而言之:正则表达式虽然方便易用,但对 JavaScript 应用程序,尤其是 Node.js 平台,构成了真正的威胁。用户输入的文本匹配可能需要大量的 CPU 周期来处理。正则表达式的处理效率可能非常低,以至于一个验证 10 个单词的请求就足以阻塞整个事件循环 6 秒,并导致 CPU 负载过高。因此,建议使用像validator.js这样的第三方验证包,而不是自己编写正则表达式模式,或者使用safe-regex来检测易受攻击的正则表达式模式。
否则:编写不良的正则表达式可能会受到正则表达式 DoS 攻击,从而完全阻塞事件循环。例如,流行的moment
软件包在 2017 年 11 月被发现存在恶意正则表达式漏洞。
6.17. 避免使用变量加载模块
TL;DR:避免引用/导入另一个带有路径参数的文件,因为担心它可能源自用户输入。此规则可以扩展用于访问一般文件(例如fs.readFile()
)或其他带有源自用户输入的动态变量的敏感资源访问。Eslint -plugin-security linter 可以捕获此类模式并及早发出警告。
否则:恶意用户输入可能会找到用于要求篡改文件的参数,例如,文件系统上先前上传的文件,或访问已经存在的系统文件。
6.18. 在沙盒中运行不安全的代码
TL;DR:当需要运行运行时提供的外部代码(例如插件)时,请使用任何类型的“沙盒”执行环境,将主代码与插件隔离并保护起来。这可以通过使用专用进程(例如cluster.fork()
)、无服务器环境或充当沙盒的专用 npm 包来实现。
否则:插件可以通过无限循环、内存过载和访问敏感进程环境变量等多种选项进行攻击
6.19. 处理子进程时要格外小心
TL;DR:尽可能避免使用子进程,如果仍然需要,请验证并过滤输入以减轻 Shell 注入攻击。建议使用child_process.execFile
which,因为它的定义是只执行具有一组属性的单个命令,并且不允许 Shell 参数扩展。
否则:由于恶意用户输入传递给未清理的系统命令,子进程的简单使用可能会导致远程命令执行或 shell 注入攻击。
6.20. 向客户端隐藏错误详细信息
TL;DR:集成的 Express 错误处理程序默认隐藏错误详细信息。但是,您很有可能使用自定义 Error 对象实现自己的错误处理逻辑(许多人认为这是最佳实践)。如果这样做,请确保不要将整个 Error 对象返回给客户端,因为其中可能包含一些敏感的应用程序详细信息。
否则:敏感的应用程序详细信息(例如服务器文件路径、正在使用的第三方模块以及可能被攻击者利用的应用程序的其他内部工作流程)可能会从堆栈跟踪中发现的信息中泄露
6.21. 为 npm 或 Yarn 配置 2FA
简而言之:开发链中的任何步骤都应该使用 MFA(多因素身份验证)进行保护,而 npm/Yarn 为攻击者提供了绝佳的机会,他们可以获取某些开发者的密码。攻击者可以利用开发者凭证,将恶意代码注入到广泛安装在项目和服务中的库中。如果公开发布,甚至可能通过网络传播。在 npm 中启用双因素身份验证,攻击者几乎无法篡改你的软件包代码。
另外: 您听说过 eslint 开发人员密码被劫持的故事吗?
6.22. 修改会话中间件设置
简而言之:每个 Web 框架和技术都有其已知的弱点——告知攻击者我们使用了哪个 Web 框架对他们来说非常有帮助。使用会话中间件的默认设置可能会将您的应用暴露给特定于模块和框架的劫持攻击,其方式与X-Powered-By
标头类似。请尝试隐藏任何可识别并暴露您技术栈的信息(例如 Node.js、Express)。
否则: Cookie 可能会通过不安全的连接发送,攻击者可能会使用会话标识来识别 Web 应用程序的底层框架以及特定于模块的漏洞
6.23. 通过明确设置进程崩溃时间避免 DOS 攻击
TL;DR:如果错误未得到处理,Node 进程就会崩溃。许多最佳实践甚至建议即使捕获并处理了错误也退出。例如,Express 会因任何异步错误而崩溃 - 除非你用 catch 子句包裹路由。这为攻击者提供了一个绝佳的攻击点,他们可以识别导致进程崩溃的输入并反复发送相同的请求。目前没有即时补救措施,但一些技巧可以减轻痛苦:当进程因未处理的错误而崩溃时,发出严重级别警报;验证输入并避免因无效的用户输入而导致进程崩溃;用 catch 包裹所有路由,并考虑在请求内部发生错误时(而不是全局发生错误时)不崩溃。
否则:这只是一个合理的猜测:考虑到许多 Node.js 应用程序,如果我们尝试向所有 POST 请求传递一个空的 JSON 主体,那么少数应用程序将会崩溃。这时,我们只需重复发送相同的请求即可轻松关闭这些应用程序。
6.24. 防止不安全的重定向
TL;DR:不验证用户输入的重定向可能使攻击者能够发起网络钓鱼诈骗、窃取用户凭据并执行其他恶意操作。
否则:如果攻击者发现您没有验证外部用户提供的输入,他们可能会通过在论坛、社交媒体和其他公共场所发布特制链接来利用此漏洞,让用户点击它。
6.25 避免将机密信息发布到 npm 注册表
TL;DR:应采取预防措施,避免意外将机密信息发布到公共 npm 注册表的风险。.npmignore
可以使用文件来忽略特定的文件或文件夹,或者使用files
数组package.json
作为允许列表。
否则:您的项目的 API 密钥、密码或其他秘密可能会被任何接触到它们的人滥用,这可能会导致财务损失、冒充和其他风险。
7. Draft: Performance Best Practices
我们的贡献者正在开发此部分。你想加入吗?
7.1. 不要阻塞事件循环
TL;DR:避免 CPU 密集型任务,因为它们会阻塞大多数单线程事件循环,并根据上下文将其卸载到专用线程、进程甚至不同的技术。
否则:由于事件循环被阻塞,Node.js 将无法处理其他请求,从而导致并发用户的延迟。3000个用户正在等待响应,内容已准备好提供,但一个请求阻止服务器将结果发回
7.2. 优先使用原生 JS 方法,而不是 Lodash 等用户空间工具
简而言之:使用像lodash
和这样的实用程序库,而underscore
不是原生方法,通常会带来更大的损失,因为它会导致不必要的依赖,并降低性能。
请记住,随着新 V8 引擎和新 ES 标准的推出,原生方法得到了改进,其性能现在比实用程序库高出约 50%。
否则:您将不得不维护性能较差的项目,而您本来可以简单地使用现有的功能或处理更多的行以交换更多的文件。
8. Docker Best Practices
🏅 非常感谢Bret Fisher,我们从他那里学到了很多以下做法
8.1 使用多阶段构建以获得更精简、更安全的 Docker 镜像
TL;DR:使用多阶段构建仅复制必要的生产环境构件。许多构建时依赖项和文件并非应用程序运行所必需的。使用多阶段构建,这些资源可以在构建期间使用,而运行时环境仅包含必需的内容。多阶段构建是摆脱超重和安全威胁的简单方法。
否则:更大的图像将需要更长的时间来构建和发布,仅构建工具可能包含漏洞,并且仅适用于构建阶段的秘密可能会被泄露。
多阶段构建的示例 Dockerfile
FROM node:14.4.0 AS build
COPY . .
RUN npm ci && npm run build
FROM node:slim-14.4.0
USER node
EXPOSE 8080
COPY --from=build /home/node/app/dist /home/node/app/package.json /home/node/app/package-lock.json ./
RUN npm ci --production
CMD [ "node", "dist/app.js" ]
8.2. 使用node
命令引导,避免npm start
TL;DR:用于CMD ['node','server.js']
启动应用时,避免使用不向代码传递操作系统信号的 npm 脚本。这可以避免子进程、信号处理、正常关闭以及僵尸进程等问题。
否则:当没有信号传递时,您的代码将永远不会收到有关关闭的通知。否则,它将失去正常关闭的机会,可能会丢失当前请求和/或数据。
阅读更多:使用 node 命令引导容器,避免使用 npm start
8.3. 让 Docker 运行时处理复制和正常运行时间
TL;DR:使用 Docker 运行时编排器(例如 Kubernetes)时,可以直接调用 Node.js 进程,无需中间进程管理器或复制进程的自定义代码(例如 PM2、Cluster 模块)。运行时平台拥有最高的数据量和可见性,可用于做出布局决策——它最了解需要多少个进程、如何分布它们以及在崩溃时该如何处理。
否则:容器因资源不足而持续崩溃,进程管理器会无限期地重启它。如果 Kubernetes 意识到这一点,它可以将其迁移到其他可用的实例。
8.4. 使用 .dockerignore 防止机密泄露
TL;DR:包含一个.dockerignore
文件,用于过滤掉常见的机密文件和开发工件。这样做可以防止机密文件泄露到镜像中。此外,构建时间也会显著缩短。此外,请确保不要递归复制所有文件,而是明确选择要复制到 Docker 的文件。
否则:常见的个人秘密文件(如.env
、.aws
和).npmrc
将与任何有权访问该图像的人共享(例如 Docker 存储库)
8.5. 生产前清理依赖关系
TL;DR:虽然在构建和测试生命周期中有时需要开发依赖项,但最终交付到生产环境的镜像应该是最小化的,并且没有开发依赖项。这样做可以保证只交付必要的代码,并最大限度地减少潜在攻击(即攻击面)。使用多阶段构建(参见专用项目符号)时,可以通过先安装所有依赖项,最后运行以下命令来实现:npm ci --production
否则:许多臭名昭著的 npm 安全漏洞都是在开发包中发现的(例如eslint-scope)
🔗 阅读更多:删除开发依赖项
8.6. 优雅地关闭
TL;DR:处理进程的 SIGTERM 事件并清理所有现有连接和资源。这应该在响应正在进行的请求时完成。在 Dockerized 运行时中,关闭容器并非罕见事件,而是日常工作中经常发生的情况。实现这一点需要一些周到的代码来协调几个移动部件:负载均衡器、保持连接、HTTP 服务器和其他资源
否则:立即关闭意味着无法回应成千上万的失望用户
8.7. 使用 Docker 和 v8 设置内存限制
TL;DR:始终使用 Docker 和 JavaScript 运行时标志配置内存限制。Docker 限制有助于做出周全的容器放置决策,而 --v8 的 max-old-space 标志有助于及时启动 GC 并防止内存利用率过低。实际上,将 v8 的旧空间内存设置为略小于容器限制的值。
否则:需要docker定义来执行周全的扩展决策,并防止其他进程资源匮乏。如果不同时定义v8的限制,它将无法充分利用容器资源——如果没有明确的指示,当使用其主机资源的50-60%左右时,它就会崩溃。
8.8. 规划高效缓存
TL;DR:如果操作正确,从缓存中重建整个 Docker 镜像几乎可以瞬间完成。更新较少的指令应该放在 Dockerfile 的顶部,而经常更改的指令(例如应用程序代码)应该放在底部。
否则: Docker 构建将会非常漫长,即使进行微小的更改也会消耗大量资源
8.9. 使用明确的图像引用,避免使用latest
标签
TL;DR:指定明确的镜像摘要或版本标签,切勿引用latest
。开发人员通常认为指定latest
标签就能获得存储库中最新的镜像,但事实并非如此。使用摘要可以确保服务的每个实例都运行完全相同的代码。
此外,引用镜像标签意味着基础镜像可能会发生变化,因为镜像标签无法确保确定性安装。如果希望进行确定性安装,可以使用 SHA256 摘要来引用精确镜像。
否则:基础映像的新版本可能会在部署到生产环境中时带来重大变化,从而导致应用程序出现意外行为。
8.10. 优先选择较小的 Docker 基础镜像
TL;DR:大型镜像更容易受到漏洞影响,并增加资源消耗。使用更精简的 Docker 镜像(例如 Slim 和 Alpine Linux 变体)可以缓解此问题。
否则:构建、推送和拉取图像将花费更长时间,恶意行为者可能会使用未知的攻击媒介,并且会消耗更多资源。
8.11. 清除构建时机密,避免在参数中存在机密
避免 Docker 构建环境中的机密信息泄露。Docker 镜像通常在多个环境(例如 CI 和镜像仓库)中共享,这些环境不像生产环境那样经过严格审查。一个典型的例子是 npm 令牌,它通常作为参数传递给 dockerfile。此令牌在需要使用后很长时间仍保留在镜像中,并允许攻击者无限期地访问私有 npm 镜像仓库。可以通过复制机密文件(例如.npmrc
,然后使用多阶段构建将其删除,请注意,构建历史记录也应删除)或使用 Docker build-kit secret 功能(该功能不会留下任何痕迹)来避免这种情况。
否则:每个有权访问 CI 和 docker 注册表的人都将获得访问一些宝贵的组织机密的额外奖励
8.12. 扫描图像以查找多层漏洞
TL;DR:除了检查代码依赖关系外,漏洞还会扫描最终交付到生产环境的镜像。Docker 镜像扫描器不仅会检查代码依赖关系,还会检查操作系统二进制文件。这种端到端安全扫描覆盖范围更广,能够验证构建过程中是否有恶意人员注入恶意代码。因此,建议将此扫描作为部署前的最后一步。市面上有一些免费和商业扫描器,它们也提供 CI/CD 插件。
否则:您的代码可能完全没有漏洞。但是,由于应用程序普遍使用的操作系统级二进制文件(例如 OpenSSL、TarBall)存在漏洞,您的代码仍然可能被黑客攻击。
8.13 清理 NODE_MODULE 缓存
TL;DR:在容器中安装依赖项后,请删除本地缓存。为了加快将来的安装速度而复制依赖项毫无意义,因为以后不会再进行任何安装 - Docker 镜像是不可变的。只需一行代码就可以减少数十 MB 的空间(通常为镜像大小的 10% 到 50%)。
否则:由于一些文件永远不会被使用,最终交付生产的图像重量将增加 30%
8.14. 通用 Docker 实践
TL;DR:这是一些 Docker 建议的集合,与 Node.js 无直接关系——Node 的实现与其他语言的实现并无太大区别。点击“阅读更多”即可浏览。
8.15. 检查 Dockerfile
TL;DR:检查 Dockerfile 的 Lint 代码是识别 Dockerfile 中与最佳实践不同的问题的重要步骤。通过使用专门的 Docker Linter 检查潜在缺陷,可以轻松识别性能和安全性改进,从而节省大量时间或避免生产代码中的安全问题。
错误原因: Dockerfile 创建者错误地将 Root 保留为生产用户,并且使用了来自未知源仓库的镜像。只需使用简单的 linter 即可避免这种情况。
贡献者✨
感谢为这个存储库做出贡献的这些出色的人!