We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
原文链接:https://jsblog.insiderattack.net/event-loop-best-practices-nodejs-event-loop-part-5-e29b2b50bfe2 欢迎回到 NodeJS 事件循环系列文章。在事件循环系列的前面几篇文章中,我们讨论了关于事件循环不同阶段的一些细节,比如 setImmediate,nextTick,定时器, I/O 等等。我相信你现在对 NodeJS 事件循环有了一个比较深的理解,因此,我会在本篇文章中总结一下前面几篇文章,讨论一下最佳实践,为了提高 Node 应用的性能我们应该做什么和不做什么。同时你也可以阅览前面几篇文章。
很多人一开始会因为不理解事件循环,错误处理,异步控制导致对 NodeJS 应用产生偏见。现在你已经理解了事件循环,我相信你也能够理解接下来所讲的这些最佳实践。
在一个被频繁调用的函数里面,要避免涉及同步的 I/O 操作比如 fs.readFileSync, fs.renameSync 等等。这些同步操作降低你的 NodeJS 应用的性能,在每次调用执行的时候,会阻塞事件循环直到这些 I/O 操作完成。一个比较合适的做法是如果你需要读取这些配置文件,那么请在应用启动的时候完成。
在 NodeJS 中,有两种类型的函数:
所以这里给出的建议是,你编写函数的时候:
如果你的函数内即可以同步返回也可以异步返回,那么会导致一些不可预料的结果,比如:
const cache = {}; function readFile(fileName, callback) { if (cache[filename]) { return callback(null, cache[filename]) } fs.readFile(fileName, (err, fileContent) => { if (err) return callback(err); cache[fileName] = fileContent; callback(null, fileContent); }); }
我们执行一下上面的代码,为了方便描述,我们这里省略了错误处理:
function letsRead(){ readFile('myfile.txt', (err, result) => { // 此处省略错误处理 console.log('file read complete'); }); console.log('file read initiated') }
如果我们执行两次上面的代码,我们会发现有两种输出结果:
file read initiated file read complete file read complete file read initiated
为什么呢?
因为在第一次执行 letsRead 函数的时候,myfile.txt 内容不在 cache 里面,因此会调用异步的 fs.readFile 函数来访问文件系统。在这种情况下,file read initiated 就会在 readFile 函数执行完成之前打印出来。
当第二次调用 letsRead 函数的时候,myfile.txt 内容已经在 cache 里面了,那么不再需要调用 fs.readFile 函数来访问文件系统,所以此时 readFile 的回调会被同步执行,所以此时 file read complete 会比 file read initiated 先打印。
当你的应用变得复杂的时候,这个混杂异步同步返回机制的函数会引起很多难以追踪的问题,因此建议不要编写这种混杂同步异步返回的函数。
我们对上面的代码有两种修改方式:
正如我们知道第一种改法可能会引起性能问题,所以我们采取第二种方式,采用 process.nextTick 方式进行改写:
const cache = {}; function readFile(fileName, callback) { if (cache[filename]) { return process.nextTick(() => callback(null, cache[filename])); } fs.readFile(fileName, (err, fileContent) => { if (err) return callback(err); cache[fileName] = fileContent; callback(null, fileContent); }); }
process.nextTick 将会把 callback 的执行时机延后,现在如果你多次执行 letsRead 函数,你也只会得到一种输出结果:
file read initiated file read complete file read initiated file read complete
你也可以使用 setImmediate 来达到目的,但是我比较喜欢使用 process.nextTick 因为它的执行时机更早。
尽管 process.nextTick 非常有用,但是递归调用 process.nextTick 可以引起 I/O 饿死问题。
在 0.12 版本之后的 Node 已经去掉了 process.maxTickDepth 这个参数了,所以在新版本中的 Node 没有办法可以限制 next tick 队列的长度。
在 NodeJs 中 dns 模块有两种方式来将一个域名解析成 IP 地址,一个是 dns.lookup, 一个是 dns.resolve4,dns.resolve6 等等。虽然这两种方式看起来很相似,但是它们实际工作原理非常不同。
dns.lookup 的行为就像是使用 ping 命令来解析域名,它调用了操作系统中的 getaddrinfo 函数,这个函数并不是异步调用的,为了使它能够异步调用,它使用了 libuv 的线程池来运行,这也导致了如果有多个 dns.lookup 函数同时调用,那么线程池中的线程都会被占用,其他的 I/O 操作例如文件 I/O,crypto 函数甚至是更多的 dns.lookup 函数会被饿死。
dns.resolve 函数和其他 dns.resolve*() 函数会不一样,根据官方文档:
这些功能实现与dns.lookup()截然不同。它们不仅没有使用getaddrinfo(3)并且通过网络执行DNS查询。使用异步网络通信,并且没有使用libuv线程池。 因此,这些函数不会像使用libuv线程池的dns.lookup()函数一样会对其它进程有负面影响。 它们不像dns.lookup()一样使用相同的配置文件。例如,它们不会使用来自/etc/hosts配置。
NodeJs 使用 c-ares 这个依赖库来提供 DNS 解析能力,这个库不使用 libuv 的线程池,并且直接运行在网络上。
所以我们使用 dns.resolve 系列的方法来取代 dns.lookup 是可取的,它不会增加线程池的负载,除非你需要使用 /etc/nsswitch.conf, /etc/hosts 这样的系统配置文件,否则不要轻易使用 dns.lookup。
然而我们遇到了一个更大的问题。
在通过 NodeJS 来向 www.example.com 发起一个 HTTP 请求的时候,首先它会将 www.example.com 这个域名解析成 ip 地址,然后它是用这个解析得到的 IP 地址异步地建立一个 TCP 链接。所以发送一个 HTTP 请求是有两个步骤的。
但是,Node 在 http 和 https 两个模块内部都使用了 dns.lookup 这个函数来解析 IP 地址。如果 DNS 服务器或者网络服务有问题,那么多个 HTTP 请求会消耗完线程池的资源,导致对后续的请求停止响应。更糟糕的是很多常用的模块比如 request 模块都使用了像 http 或者 https 来解析域名,这样也会导致类似的问题。
如果你发现文件 I/O,crypto 或者其他依赖线程池的任务性能有下降,那么你可以通过以下来提高性能:
请注意下面这些代码是没有经过优化的,如果需要使用到生产环境,还需要做很多的考虑。下面这些代码需要使用 Node v8.0.8 以上的版本运行,因为在早期版本的 tls.connect 函数中 lookup 这个配置选项不被支持的。
const dns = require('dns'); const http = require('http'); const https = require('https'); const tls = require('tls'); const net = require('net'); const request = require('request'); const httpAgent = new http.Agent(); const httpsAgent = new https.Agent(); const createConnection = ({ isHttps = false } = {}) => { const connect = isHttps ? tls.connect : net.connect; return function(args, cb) { return connect({ port : args.port, host : args.host, lookup : function(hostname, args, cb) { dns.resolve(hostname, function(err, ips) { if (err) { return cb(err); } return cb(null, ips[0], 4); }); } }, cb); } }; httpAgent.createConnection = createConnection(); httpsAgent.createConnection = createConnection({isHttps: true}); function getRequest(reqUrl) { request({ method: 'get', url: reqUrl, agent: httpsAgent }, (err, res) => { if (err) throw err; console.log(res.body); }) } getRequest('https://example.com');
正如我们所知,线程池是用于处理很多除了文件 I/O 以外的任务的,它有可能会变成应用的性能瓶颈。如果你觉得应用在处理文件 I/O 或者像 crypto 这样的操作耗时比较多的时候,你可以通过设置 UV_THREADPOOL_SIZE 环境变量来提高性能。
监控事件循环的延迟时间对于防止事件循环中断是非常重要的,它也可以提供预警,强制重启或者执行一些拓展服务。
监控事件循环的延迟时间最简单的方式就是检查一个定时器执行它的回调函数的时候额外花费了多少时间。假设我们设定了一个 500 ms 的定时器,如果它执行回调的时候花费了 550 ms,那么我们可以推断这个事件循环大概延迟了 50 ms。这个 50 ms 可能是执行事件循环其他阶段所花费的时间,下面我们会通过 loopbench 这个工具来进行监控:
const LoopBench = require('loopbench'); const loopBench = LoopBench(); console.log(`loop delay: ${loopBench.delay}`); console.log(`loop delay limit: ${loopBench.limit}`); console.log(`is loop overloaded: ${loopBench.overlimit}`);
你可以在健康检查的接口中返回以上代码的结果,这可以用于监控应用的性能。
健康检查的接口的返回例子如下:
{ "message": "application is running", "data": { "loop_delay": "1.2913 ms", "loop_delay_limit": "42 ms", "is_loop_overloaded": false } }
如果检测到接口返回中显示事件循环已经超过负载了,那么你可以返回一个 503 的错误码。这也可以帮助你实现高可用的负载均衡。
References:
The text was updated successfully, but these errors were encountered:
No branches or pull requests
原文链接:https://jsblog.insiderattack.net/event-loop-best-practices-nodejs-event-loop-part-5-e29b2b50bfe2
欢迎回到 NodeJS 事件循环系列文章。在事件循环系列的前面几篇文章中,我们讨论了关于事件循环不同阶段的一些细节,比如 setImmediate,nextTick,定时器, I/O 等等。我相信你现在对 NodeJS 事件循环有了一个比较深的理解,因此,我会在本篇文章中总结一下前面几篇文章,讨论一下最佳实践,为了提高 Node 应用的性能我们应该做什么和不做什么。同时你也可以阅览前面几篇文章。
文章系列目录
很多人一开始会因为不理解事件循环,错误处理,异步控制导致对 NodeJS 应用产生偏见。现在你已经理解了事件循环,我相信你也能够理解接下来所讲的这些最佳实践。
Avoid sync I/O inside repeatedly invoked code blocks
在一个被频繁调用的函数里面,要避免涉及同步的 I/O 操作比如 fs.readFileSync, fs.renameSync 等等。这些同步操作降低你的 NodeJS 应用的性能,在每次调用执行的时候,会阻塞事件循环直到这些 I/O 操作完成。一个比较合适的做法是如果你需要读取这些配置文件,那么请在应用启动的时候完成。
Functions should be completely async or completely sync
在 NodeJS 中,有两种类型的函数:
所以这里给出的建议是,你编写函数的时候:
如果你的函数内即可以同步返回也可以异步返回,那么会导致一些不可预料的结果,比如:
我们执行一下上面的代码,为了方便描述,我们这里省略了错误处理:
如果我们执行两次上面的代码,我们会发现有两种输出结果:
为什么呢?
因为在第一次执行 letsRead 函数的时候,myfile.txt 内容不在 cache 里面,因此会调用异步的 fs.readFile 函数来访问文件系统。在这种情况下,file read initiated 就会在 readFile 函数执行完成之前打印出来。
当第二次调用 letsRead 函数的时候,myfile.txt 内容已经在 cache 里面了,那么不再需要调用 fs.readFile 函数来访问文件系统,所以此时 readFile 的回调会被同步执行,所以此时 file read complete 会比 file read initiated 先打印。
当你的应用变得复杂的时候,这个混杂异步同步返回机制的函数会引起很多难以追踪的问题,因此建议不要编写这种混杂同步异步返回的函数。
我们对上面的代码有两种修改方式:
正如我们知道第一种改法可能会引起性能问题,所以我们采取第二种方式,采用 process.nextTick 方式进行改写:
process.nextTick 将会把 callback 的执行时机延后,现在如果你多次执行 letsRead 函数,你也只会得到一种输出结果:
你也可以使用 setImmediate 来达到目的,但是我比较喜欢使用 process.nextTick 因为它的执行时机更早。
Too many nextTicks
尽管 process.nextTick 非常有用,但是递归调用 process.nextTick 可以引起 I/O 饿死问题。
在 0.12 版本之后的 Node 已经去掉了 process.maxTickDepth 这个参数了,所以在新版本中的 Node 没有办法可以限制 next tick 队列的长度。
dns.lookup() vs dns.resolve*()
在 NodeJs 中 dns 模块有两种方式来将一个域名解析成 IP 地址,一个是 dns.lookup, 一个是 dns.resolve4,dns.resolve6 等等。虽然这两种方式看起来很相似,但是它们实际工作原理非常不同。
dns.lookup 的行为就像是使用 ping 命令来解析域名,它调用了操作系统中的 getaddrinfo 函数,这个函数并不是异步调用的,为了使它能够异步调用,它使用了 libuv 的线程池来运行,这也导致了如果有多个 dns.lookup 函数同时调用,那么线程池中的线程都会被占用,其他的 I/O 操作例如文件 I/O,crypto 函数甚至是更多的 dns.lookup 函数会被饿死。
dns.resolve 函数和其他 dns.resolve*() 函数会不一样,根据官方文档:
NodeJs 使用 c-ares 这个依赖库来提供 DNS 解析能力,这个库不使用 libuv 的线程池,并且直接运行在网络上。
所以我们使用 dns.resolve 系列的方法来取代 dns.lookup 是可取的,它不会增加线程池的负载,除非你需要使用 /etc/nsswitch.conf, /etc/hosts 这样的系统配置文件,否则不要轻易使用 dns.lookup。
然而我们遇到了一个更大的问题。
在通过 NodeJS 来向 www.example.com 发起一个 HTTP 请求的时候,首先它会将 www.example.com 这个域名解析成 ip 地址,然后它是用这个解析得到的 IP 地址异步地建立一个 TCP 链接。所以发送一个 HTTP 请求是有两个步骤的。
但是,Node 在 http 和 https 两个模块内部都使用了 dns.lookup 这个函数来解析 IP 地址。如果 DNS 服务器或者网络服务有问题,那么多个 HTTP 请求会消耗完线程池的资源,导致对后续的请求停止响应。更糟糕的是很多常用的模块比如 request 模块都使用了像 http 或者 https 来解析域名,这样也会导致类似的问题。
如果你发现文件 I/O,crypto 或者其他依赖线程池的任务性能有下降,那么你可以通过以下来提高性能:
请注意下面这些代码是没有经过优化的,如果需要使用到生产环境,还需要做很多的考虑。下面这些代码需要使用 Node v8.0.8 以上的版本运行,因为在早期版本的 tls.connect 函数中 lookup 这个配置选项不被支持的。
Concerns about the Threadpool
正如我们所知,线程池是用于处理很多除了文件 I/O 以外的任务的,它有可能会变成应用的性能瓶颈。如果你觉得应用在处理文件 I/O 或者像 crypto 这样的操作耗时比较多的时候,你可以通过设置 UV_THREADPOOL_SIZE 环境变量来提高性能。
Event loop monitoring
监控事件循环的延迟时间对于防止事件循环中断是非常重要的,它也可以提供预警,强制重启或者执行一些拓展服务。
监控事件循环的延迟时间最简单的方式就是检查一个定时器执行它的回调函数的时候额外花费了多少时间。假设我们设定了一个 500 ms 的定时器,如果它执行回调的时候花费了 550 ms,那么我们可以推断这个事件循环大概延迟了 50 ms。这个 50 ms 可能是执行事件循环其他阶段所花费的时间,下面我们会通过 loopbench 这个工具来进行监控:
你可以在健康检查的接口中返回以上代码的结果,这可以用于监控应用的性能。
健康检查的接口的返回例子如下:
如果检测到接口返回中显示事件循环已经超过负载了,那么你可以返回一个 503 的错误码。这也可以帮助你实现高可用的负载均衡。
References:
The text was updated successfully, but these errors were encountered: