エラーの概要おそらく、本番環境のログに次のような不可解なスタックトレースが出力されているのを目にしていることでしょう。
Error: read ECONNRESET
at TCP.onStreamRead (node:internal/stream_base_commons:217:20)
at errnoException (node:internal/errors:523:12)
技術的な観点で見ると、Node.jsアプリケーションがTCPソケットからデータを取得しようとした際に、リモートピアが突然接続を強制終了したことを意味します。標準的な4ウェイ・ハンドシェイク(FIN)による終了ではなく、相手側から**RST (Reset)**パケットが送信されました。ソケットは無効になり、パイプは切断されます。もしエラーをキャッチしていなければ、アプリケーションはクラッシュしてしまいます。
本番環境で発生する理由ほとんどの本番環境クラスターにおいて、ECONNRESETはコードのバグではありません。これは、アプリケーションとインフラストラクチャ間のタイミングの競合によって発生します。
1. Keep-Aliveタイムアウトの競合Node.jsは、接続を再利用してレイテンシを減らすためにKeep-Aliveを使用します。しかし、AWS ALBからNginxに至るまで、あらゆるインフラコンポーネントにはアイドルタイムアウトが設定されています。例えば、AWSロードバランサーのデフォルトのアイドルタイムアウトが60秒で、Node.jsサーバー側が65秒間アイドル状態を維持するように設定されている場合、バランサー側が先に接続を切断します。アプリがその「ゾンビ」ソケットを使用しようとすると、バランサーはリセットを返します。
2. アップストリームのリソース枯渇過負荷状態のサーバーは、稼働を維持するために接続を切断することがあります。アップストリームのサービスがmax_connections制限に達したり、カーネルレベルのクラッシュが発生したりすると、すべてのアクティブなクライアントに即座にRSTパケットを送信します。Kubernetes環境では、ポッドがOOMKilled (Out Of Memory)された際によく見られる現象です。
3. ステルス・ミドルボックスステートフル・ファイアウォールやWebアプリケーション・ファイアウォール(WAF)は、エンドポイントに通知することなく、ステートテーブルから「非アクティブ」な接続エントリを削除することがあります。アプリは経路が確保されていると考えますが、次にパケットを送信した際に「壁」に突き当たり、リセットがトリガーされます。
段階的な解決策### ステップ1:Keep-Aliveタイマーを合わせるNginxやHAProxyなどのプロキシの背後でNode.jsを実行する場合、サーバーのkeepAliveTimeoutはプロキシのタイムアウト値よりも大きく設定する必要があります。これにより、Node.jsプロセスではなく、プロキシ側が正常なクローズを開始するように保証されます。
const server = http.createServer(app);
// ロードバランサーのアイドルタイムアウト(例:60秒)よりも高く設定します
server.keepAliveTimeout = 65000;
// headersTimeoutはkeepAliveTimeoutよりもわずかに高く設定する必要があります
server.headersTimeout = 66000;
ステップ2:ソケットリスナーを保護する生のnetモジュールや古いデータベースドライバを使用している場合、キャッチされないソケットエラーが1つ発生するだけでプロセスが終了してしまいます。常にソケットレベルでエラーリスナーを設定してください。
const client = net.connect({ port: 8080 }, () => {
console.log('Successfully connected');
});
client.on('error', (err) => {
if (err.code === 'ECONNRESET') {
console.error('Remote server reset the connection. Initiating backoff...');
return;
}
throw err;
});
ステップ3:堅牢なリトライロジックの実装分散システムに失敗はつきものです。冪等(べきとう)な操作(GET, PUT, DELETE)において、一度のECONNRESETでユーザーのリクエストを失敗させるべきではありません。axios-retryを使用することで、これらを透過的に処理できます。
import axios from 'axios';
import axiosRetry from 'axios-retry';
const http = axios.create();
axiosRetry(http, {
retries: 3,
retryCondition: (error) => {
// ネットワーク障害や特定のリセットコードの場合にリトライします
return axiosRetry.isNetworkError(error) || error.code === 'ECONNRESET';
},
retryDelay: axiosRetry.exponentialDelay // 小さく始まり、1s, 2s, 4s...と増加します
});
ステップ4:アップストリームの安定性を調査するアップストリームのサービスのクラッシュを監視してください。リセットが急増している場合は、対向サービスのSIGKILLイベントやCPUのスパイクを確認してください。プロセスのクラッシュは、不完全なソケットクローズが発生する最も一般的な原因です。
検証:修正のテスト本番環境で発生する前に失敗をシミュレートしましょう。エラー処理を運任せにしてはいけません。
- 強制終了のシミュレーション:
tcpkill -i eth0 port 8080を使用して強制的に接続を切断し、リトライロジックが動作することを確認します。- トラフィック分析: 負荷テスト中にss -antまたはnetstatを実行します。CLOSE_WAITやLAST_ACK状態の接続がないか監視してください。- 負荷テスト:Autocannonを使用して10,000リクエストを送信します。テストの速度が落ちた時にのみリセットが発生する場合、Keep-Aliveタイムアウトの不一致が発生しています。## アーキテクチャと予防策現代のクラウドネットワークでは、正確なIPとサブネットの計画が必要です。CIDRブロックの重複やNATゲートウェイの設定ミスは、パケットの「ブラックホール化」を引き起こし、最終的にタイムアウトが発生した際にリセットを招く原因となります。 複雑なVPCを設計したり、リージョンを跨ぐトラフィックをデバッグしたりする際、私はネットワークマスクを確認するためにブラウザベースのツールを利用しています。ToolCraftのサブネット計算機は非常に優れています。計算をローカルで行うため、機密性の高い内部インフラの詳細がマシン外に出ることはありません。 最後に、環境を最新の状態に保ってください。Node.js 18以降、keepAliveTimeoutとheadersTimeoutのデフォルト値はクラウドロードバランサーとの親和性を高めるように調整されており、これらのエラーの発生頻度は大幅に減少しています。

