暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

谈谈 Node 中错误处理(二)

浮生知记 2021-09-27
1692

这一篇,我们开始讲 Node 社区中关于错误处理的一些具体的最佳实践。文章会涉及到 Node 进程的知识点(例如,生命周期);一些正确处理进程意外崩溃的策略。

一、Node 进程的生命周期

Node 的进程非常轻量,内存占用极小。在线上,Node 的进程崩溃基本不可避免,在设计技术方案时,要尽量保证启动过程的简洁、轻量。如果你的程序启动中包含 CPU 密集型或者同步操作,这会影响你的 Node 服务启动的速度。

一个可行的方案是,尽量预编译你的 Node 服务。可行的一个操作是,在构建时提前准备服务的一些固定数据或者静态资源文件。这可能会延长你服务部署的时间,但这样可以提高你服务启动的效率。当 crash 发生的时候,你可以退出当前进程并且快速的重新启动一个新的。

1. Node 中的 exit 方法

下面我们聊聊中断 Node 进程的几种方法,以及它们之间的区别。

最常见的方法就是使用process.exit()
它接受一个整数作为参数,参数值为0,代表成功退出;参数值 >0,代表这进程中存在错误,1
 是一个常见进程错误退出码。

另一个方法是使用process.abort()
。当这个方法被调用的时候,这个 Node 进程将立即退出。更重要的是,在操作系统允许的情况下,Node 会生成一个文件,里面包含大量的关于这个进程的有用信息,你可以使用llnode之类的工具搭配这个文件来进行调试。

2. Node 中的 exit 事件

得益于 Node 中的事件循环机制,在 Node 进程退出时,它同时触发了好几种事件。

其一是beforeExit
,正如它的名字一样,它会在 Node 进程退出之前调用。你可以在这个事件里面做一些异步操作,事件循环机制会在进程退出之前执行完里面的所有操作。要注意,这个事件不是在 process.exit()
或者uncaughtExceptions
事件里面触发的,我们下文会提到这一点。

另一个事件就是exit
,只有在process.exit()
调用的时候才会触发。它被触发之后,事件循环就停止了,在这个事件中,不可以进行异步操作。

下面一段代码对比了这两个方法:

3. OS 信号事件

操作系统也会触发 Node 进程发送事件,这个行为取决于 Node 服务外部的情况,这些被称为信号(IPC)。两个最常见的信号是SIGTERM
SIGINT

SIGTERM
通常由进程监视器发送,告知 Node 服务可以成功退出进程了。如果你是用systemd
upstart
管理 Node 服务,一旦你停止 Node 服务,它会发送SIGTERM
事件告知你进程可以关闭了。

如果你在键盘上按下了 control+c
, Node 进程被手动中断,SIGINT
事件将被触发,你可以捕获到这一事件,并且做一些事件操作。

下面是有关这两个信号的代码示例:

一般而言,这两个信号代表着进程可以成功退出了。

4. javascript 中的错误事件

当一个 js 的 error 在代码中没有处理的时候,uncaughtException
将会被触发,告知开发者有一个bug 需要尽快处理,例如,在null
上调用一个方法。

unhandledRejection
错误是一个比较新的概念,它代表着一个 promise 操作出错了,简单来说,就是Promise.reject
 触发了,但是没做处理。这个错误同样需要开发者尽快处理。在这两种情况下,作为开发者,你需要的是让你的 Node 进程崩溃,而不是让它继续运行。不要在这两种事件触发之后,仍然让你的程序运行,这可能会让你的程序出现内存泄露或者 socket 被挂起。最简单的方法是让你的进程重启。

代码示例如下:

不要害怕让你的进程重启,这两个事件已经告知你进程不宜继续运行下去了。Node 官方文档是这么说的:

未处理的异常本质上意味着应用程序处于未定义状态…正确的做法是在“uncaughtException”事件发生时同步清理之前进程分配的资源(如文件描述符、句柄等)。在“uncaughtException”之后恢复正常操作是不安全的。

unhandledRejection
是一个比较常见的错误,Node.js官方维护人员建议开发者在事件发生时关闭进程。在 Node.js 未来版本中,unhandledRejection
将默认导致进程崩溃。

[DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

在线上尽量使用多线程

即使进程启动时间非常快,但仅运行一个进程是不安全的,如果你需要对应用程序进行线上操作,更会降低服务的稳定性。建议在线上运行多个进程,并利用负载平衡来进行调度。如果其中一个进程崩溃,另一个进程仍处于健康状态并且能够接收新请求。

你可以利用 Nginx 或者 HAProxy 配置反向代理,实现负载均衡。如果你的服务部署在 K8S 上,你可以使用 Ingress 或者其它的负载均衡策略

线上监控

在 Node 程序的线上环境中,最好设置进程监控,不断是检查 Node 服务的进程是否可用。在进程出现意外崩溃,能尽快的重启相关进程。

Node 社区推荐的做法是使用操作系统提供的进程监控功能。比如,如果你的线上环境使用 Unix 或 Linux,那么可以使用 systemd 或 upstart 命令,如果使用的是容器,Docker 提供了一个标志:--restart,K8S 自带restartPolicy,这两个都很有用。

如果无法使用以上的工具,那么可以使用 Node 进程监控,如 PM2 或者 forever 最为兜底方案,这些工具也能实现类似的功能。

如何优雅的“关机”

假设我们有一个服务器正在运行。它接收请求并与客户端建立联系。但是如果进程崩溃了怎么办?如果我们没有执行正常的关闭,那么其中一些 socket 连接将被挂起并一直等待响应,直到达到超时。中间存在不必要的时间消耗,同时会影响资源的占用,最终导致服务器停机,用户体验被降低。

最佳的方式是能显式控制停止接收连接,这样服务器可以在恢复时断开连接。新的连接都将通过负载平衡转到其他运行中的 Node.js 进程。

你可以在代码中使用 server.close()
,这个方法接受回调函数作为参数,它的作用是告诉服务器停止接受新的请求。

现在,假设您的服务器存在很多客户端链接,并且大多数客户端都没有遇到错误返回。如何在不突然断开有效客户端连接的情况下关闭服务器?我们需要使用到 timeout,如果所有连接都没有在某个限制时间内关闭,我们可以完全关闭服务器。这样做是因为我们希望留给健康客户端时间来完成任务,但不希望服务器需要等待太长时间才能关闭连接。

代码示例如下:

日志

很有可能你的公司内已经有一套日志系统,直接接入即可。但需记住,当服务关闭时,记下详尽的信息以供问题排查。

服务可用性

最重要的,建议在服务中添加健康检查路由,它主要是用来确认服务可用性,只需要简单的返回 200 状态码就行。

线上可以有一个单独的服务来监控该路由。实现方式有很多,反向代理(如 nginx 或 HAProxy)或负载平衡(如ELB或ALB)。

任何服务都可以用来持续监视运行状况检查是否返回。这些可以让你更清楚地了解当前线上 Node.js 服务的运行状况。

总结

在 Node 服务中,可以使用类似以下的代码来确保我的服务可用性,代码示例:

上面的代码可以用来监听错误事件:

番外

社区有大神提供了一些 npm 包,用来解决上文描述的问题,你可以查看一下:

@godaddy/terminusstoppablehttp-graceful-shutdown


文章转载自浮生知记,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论