Node.js完成Ws断开后等待2分10秒才退出的原因排查
问题:基于polkadot.js WsProvider的RxJS Observable执行后Node.js进程延迟退出
我尝试编写一个Creation Operator,基于polkadot.js的WsProvider创建Observable实例,用于获取polkadot事件。
以下是实现代码:
import {from, fromEvent, of,Observable} from 'rxjs'; import {tap,mergeMap} from "rxjs/operators" import { WsProvider } from '@polkadot/api'; console.time("main") const urls = of("wss://192") const fromWs = (url:string) => { const ws = new WsProvider(url,0); ws.connect().then(val => console.timeLog("main",val)).catch(console.error) return new Observable((subscriber) => { ws.on("disconnected",()=>{ console.timeLog("main","wss disconnected") subscriber.next(url); subscriber.complete() }) ws.on('error', () =>{ console.timeLog("main","wss error") subscriber.complete() }) ws.on('connected' ,() => { console.timeLog("main", `${url} connected`) }) setTimeout(() => { console.timeLog("main", `${url} timeout`) ws.disconnect().then(val=> console.timeLog("main",'ws disconnect')).catch(err => console.error("disconnect error")) subscriber.complete() },2000) }) } urls.pipe( tap(console.log), mergeMap( (url:string) =>{ return fromWs(url) }), ).subscribe({ next: val => console.timeLog("main", 'subcrnext'), error: err=> console.timeLog("main", "sub error"), complete: () => console.timeLog("main", "sub complete") }) process.on('exit', (code) => { console.timeLog("main",'System exit'); });
使用ts-node执行该脚本,执行命令及输出如下:
➜ rxjsexample ts-node src/main.ts wss://192 main: 11.959ms undefined main: 2.013s wss://192 timeout main: 2.015s wss error main: 2.016s sub complete main: 2.017s wss disconnected main: 2.018s ws disconnect main: 2:10.176 (m:ss.mmm) System exit
从日志可以看到,程序在2.018s时就已经完成网络连接断开操作,但Node.js进程直到2分10秒才正式退出。
原因分析
2分10秒的等待时长是典型的TCP SYN包默认重传超时总时长:主流操作系统默认配置下,TCP第一次握手发送SYN包后未收到响应,会按1s、2s、4s、8s、16s、32s的间隔重传,最后一次重传后等待64s仍无响应才会判定连接失败,总耗时约127秒,和日志中的2分10秒完全吻合,差值为系统调度、运行环境的正常开销。
等待期间Node.js没有执行任何业务层代码,事件循环中没有待处理的JS回调,只是被残留的底层TCP socket句柄阻塞了退出流程,具体根因有两点:
- 传入的
wss://192不是合法可访问的WebSocket端点,WsProvider初始化后发起的TCP连接一直处于三次握手的SYN_SENT状态,内核按规则持续重传SYN包。 - 调用的
ws.disconnect()方法只会清理已经建立成功的WebSocket连接的状态和事件监听,不会终止还在三次握手阶段的底层TCP连接请求。这个未完成的TCP socket属于事件循环追踪的活跃异步资源,Node.js默认会等待所有活跃资源全部释放后才退出进程,因此一直空等到TCP连接超时、内核自动回收socket后才终止。
修复方案
- 优先传入合法可访问的WsProvider端点地址,从源头避免无效连接请求。
- 不要仅调用
disconnect()方法终止连接:需要在连接终止逻辑中额外调用ws.destroy()方法,彻底销毁所有底层资源,包括未完成握手的TCP socket。 - 补全Observable的标准清理逻辑:在Observable构造函数的回调中返回teardown清理函数,在订阅终止、超时触发时主动移除注册的所有事件监听、清除定时器、彻底销毁WsProvider实例,避免残留资源持有事件循环引用。
修正后的核心实现片段:
const fromWs = (url:string) => { const ws = new WsProvider(url,0); ws.connect().then(val => console.timeLog("main",val)).catch(console.error) // 提前定义事件回调,方便后续移除 const onDisconnected = () => { console.timeLog("main","wss disconnected") subscriber.next(url); subscriber.complete() } const onError = () =>{ console.timeLog("main","wss error") subscriber.complete() } const onConnected = () => { console.timeLog("main", `${url} connected`) } return new Observable((subscriber) => { ws.on("disconnected", onDisconnected) ws.on('error', onError) ws.on('connected' ,onConnected) const timer = setTimeout(() => { console.timeLog("main", `${url} timeout`) ws.disconnect() .finally(() => { ws.destroy() console.timeLog("main",'ws disconnect') }) subscriber.complete() },2000) // Teardown逻辑:订阅结束时自动执行清理 return () => { clearTimeout(timer) ws.off("disconnected", onDisconnected) ws.off("error", onError) ws.off("connected", onConnected) ws.disconnect().finally(() => ws.destroy()) } }) }
补上资源销毁逻辑后,无论连接处于什么状态,所有底层socket、定时器、事件监听器都会被立即释放,Node.js进程会在业务逻辑执行完成后立刻退出,不会等待TCP超时。
内容的提问来源于stack exchange,提问作者kula
相关产品推荐
相关产品推荐

