第三章 - 其它关于 Node.JS 应用运行效率和性能的优秀实践
本系列的头两篇文章中我们看到如何扩展一个 Node.JS 应用以及在应用的代码部分应该考虑什么才能使其在这个过程中运行如我们所愿. 在这最后一篇文章中, 我们将介绍一些其它实践, 以进一步提高应用运行效率和性能.
web 和 Worker 进程
就像你可能知道的那样, Node.JS 在实际运行中是单线程的, 因此一个进程实例在同一时间只能执行一个操作. 在 Web 应用的运行生命周期中, 会执行很多不同类型的任务: 包括管理 API 调用, 读 / 写数据库, 与外部网络服务通信, 以及不可避免地执行某些 CPU 密集型工作等.
尽管你使用的是异步编程, 但是将所有这些操作都指派给同一个用于响应 API 调用的进程真的是一种效率很低的方式.
一种常见的模式是基于组成你应用不同类型进程之间的责任分离, 这种情况下进程通常被分为 Web 进程和 worker 进程.
Web 进程主要的任务是管理传入的网络调用并尽快将它们分发出去. 每当一个非阻塞任务需要被执行时, 例如发送电子邮件 / 通知, 写日志, 执行一个触发操作, 它们都不需要马上响应 API 调用返回结果, Web 进程会把这些操作委派给 worker 进程.
Web 和 worker 进程之间的通信可以通过不同的方式实现. 一种常见且有效的解决方案是优先级队列, 就像我们将在下一段描述的 Kue 所实现的那样.
这种方式有一个很大的优点, 无论在同一台还是不同机器上其都可以分别独立扩展 Web 和 worker 进程.
例如, 如果你的应用请求量很大, 相较于 worker 进程你可以部署更多的 Web 进程而几乎不会产生任何副作用. 而如果请求量不是很大但是有很多的工作需要 worker 进程去处理, 你可以据此重新分配相应的资源.
Kue
为了使 Web 进程和 worker 进程可以相互通信, 使用队列是一种灵活的方式, 它可以使你不需要担心进程之间的通信.
Kue http://automattic.github.io/kue/ 是 Node.JS 中常用的队列库, 它基于 Redis 并且让你可以用完全一致的方式让运行在同一台或不同机器上的进程间相互通信.
任何类型的进程都可以创建一个工作并将之放入队列, 然后被配置的相应 worker 进程就会从队列中提取并执行它. 每个工作都提供了大量的可配置选项, 如优先级, TTL, 延迟等.
你创建的 worker 进程越多, 执行这些作业的并行吞吐量也就越大.
Cron
应用程序通常需要定期执行一些任务. 通常这种类型的操作, 是通过操作系统级别的 cron 工作进行管理, 也就是会调用你应用程序之外的一个单独脚本.
当需要把你的应用部署到新的机器上时, 这种方式会需要额外的配置工作, 如果你想要自动化部署应用时, 它会让人对其感到不舒服.
我们可以使用 NPM 上的 cron 模块 https://www.npmjs.com/package/cron 从而更轻松地实现同样的效果. 它允许你在 Node.JS 代码中定义 cron 工作, 从而使其免于操作系统的配置.
根据上面所描述的 Web/worker 进程模式, worker 进程可以通过定期调用一个函数把工作放到队列从而实现创建 cron.
使用队列可以使 cron 的实现更加清晰并且还可以利用 Kue 所提供的所有功能, 如优先级, 重试等.
当你的应用有多个 worker 进程时就会出现一个问题, 因为同一时间所有 worker 进程的 cron 函数都会唤醒应用把多个同样重复的工作放入队列, 从而导致同一个工作将会被执行多次.
为了解决这个问题, 有必要识别将要执行 cron 操作的单个 worker 进程.
Leader 选举和 cron-cluster
这种类型的问题被称为 "leader 选举",NPM 为我们提供了这种特定情况下的处理方案, 有一个叫做 https://www.npmjs.com/package/cron-cluster 的包.
它在维持和 cron 模块一致 API 的同时增强了模块, 但是在启动过程中它需要有 Redis 连接, 用于和其它进程间通信和执行 leader 选举算法.
使用 Redis 作为单一事实的来源, 所有进程最终都会同意谁将执行 cron, 并且只有一个工作副本会被放入队列中. 在这之后, 所有的 worker 进程都可以像往常一样选择是否执行这个工作.
缓存 API 调用
服务端缓存是提高你 API 调用性能和反馈性一种常用的方式, 但这是一个非常广泛的主题, 有很多可能的实现.
在像我们在这个系列所描述的分布式环境中, 如果想要所有的节点在处理缓存时表现一致, 最好的办法或许是使用 Redis 来缓存需要的值.
缓存所需要考虑最困难的方面就是缓存失效. 一种快捷实用的解决方案是只考虑缓存时间, 这样缓存中的值就会在固定的 TTL 时间后刷新, 这样做的缺点是我们不得不等到下一次缓存刷新才能看到响应中的更新.
如果你能有更多的时间, 最好在应用级别实现失效, 即当数据库中的值更改时手动刷新 Redis 缓存中的相关记录.
来源: https://juejin.im/post/5bcf14cee51d451cb039d13d