稀に見られる ECONNRESET エラー。

2026/05/18 2:09

稀に見られる ECONNRESET エラー。

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

提供された要約は正確かつ包括的であり、曖昧さや不必要な不鮮明さなく主なメッセージを明確に伝えています。そのまま使用できます。

本文

ブログ - Git - デスクトップ - お問い合わせ

2026 年 5 月 5 日

同一のマシン上で動作している二つのサービスについて記述します。片方のサービスは localhost にバインドされた TCP ソケットをリッスンし、もう片方はそのソケットに接続してデータをやり取りしています。ある時ある時、接続を发起したサービスがソケットからデータを読み込む際に「ECONNRESET」エラーを受信することがあります(例外的なエラーもログには表示されず、クラッシュや何事もないままです)。ここにあるのはどのような状況でしょうか?

  • 「lab」内での再現ケース
  • tcpdump で観測される様子
  • strace ./server
    によるサーバー側の挙動
  • strace ./client --spam
    によるクライアント側(スパム対応)の挙動
  • 最初の仮説
  • 実際の現場でのシナリオ
  • 次のステップ

第 2 部へと続きます。

「lab」内の再現ケース

まずは「サーバー」、つまりリッスンするソケットを開くサービスのコードから始めましょう。

以下のプログラムはまさにその役割を果たします:新しい TCP ソケットを作成し、接続を待機し、各リクエストに対し新しいプロセスでフォークします。ただし、ここでの「リクエスト」の内容は特にありません:サーバーは単に接続成立時にクライアントへ 600,000 バイトのデータをダンプしきることのみを行います。

600,000 という数字にはある意味があります:それは私が示したい振る舞いを誘発するのに十分大きくなければなりません。例えば、600 バイト程度ではおそらく機能しません。

server.c

次にクライアント側です:これはサーバーが待機している localhost のポート 8125 に接続し、その後

recv()
を EOF またはエラーが発生するまで呼び出し続けます。
--spam
フラグについては後で触れます。

client.c

また、ビルドファイルも以下です:

Makefile

これら二つのプログラムを実行してみましょう:

[terminal1]$ ./server
[terminal2]$ ./client
Read 600000 bytes, final return value was 0, errno was 0

特に目新しい現象はありません。

ただし、

--spam
フラグを使用してみます:

$ ./client --spam
Read 600000 bytes, final return value was -1, errno was 104 (Connection reset by peer)

$ ./client --spam
Read 256000 bytes, final return value was -1, errno was 104 (Connection reset by peer)

$ ./client --spam
Read 351232 bytes, final return value was -1, errno was 104 (Connection reset by peer)

$ ./client --spam
Read 351232 bytes, final return value was -1, errno was 104 (Connection reset by peer)

$ ./client --spam
Read 351232 bytes, final return value was -1, errno was 104 (Connection reset by peer)

$ ./client --spam
Read 256000 bytes, final return value was -1, errno was 104 (Connection reset by peer)

$ ./client --spam
Read 600000 bytes, final return value was -1, errno was 104 (Connection reset by peer)

--spam
フラグは、クライアントがデータを受信する前にまずサーバーへ一部のデータを送出하도록します。そして明らかにこれにより接続の断絶が発生しているようです:クライアント側の
recv()
-1
を返し、かつ
errno
104 = Connection reset by peer
に設定されます。

tcpdump で観測される様子

まず、「ワイヤー上」には何が見えるでしょうか?

さて、実際には TCP RST パケットが確かに存在します。これはプログラミング上の誤りか、あるいは私の解釈のミスを示している可能性もあります。

strace ./server
によるサーバー側の挙動

この RST はサーバー側から発せられているので、

strace
を付けてその様子を調べましょう:

19:59:03.420432 accept(3, NULL, NULL) = 4
19:59:05.652715 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fe43484fa10) = 239546
[pid 239546] 19:59:05.652831 ...
[pid 239546] 19:59:05.652959 mmap(NULL, 602112, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0 <unfinished ...>) = 0x7fe43456d000
[pid 239546] 19:59:05.652980 mmap resumed = 0x7fe43456d000
[pid 239546] 19:59:05.653235 sendto(4, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"..., 600000, 0, NULL, 0) = 600000
[pid 239546] 19:59:05.653474 close(4) = 0
[pid 239546] 19:59:05.653553 exit_group(0)

クラッシュは発生していません。フォークし、

sendto()
を用いてすべてのデータをクライアントへダンプした後、終了しています。

また、

sendto()
は完全な 600,000 バイトを返しており、このプログラムの観点からは「すべてのデータが送信された」とみなされます(明らかな注釈があり、man ページでも説明されています:「
sendto()
の呼び出しが正常に完了したからといって、メッセージの配信が保証されるわけではない。-1 が返されたことは、ローカルで検出されたエラーであることを示すだけです」)。

実際には、クライアント側で

--spam
を使用するか否かを問わず、ここでの変化はありません。

strace ./client --spam
によるクライアント側(スパム対応)の挙動

19:59:05.652518 connect(3, {sa_family=AF_INET, sin_port=htons(8125), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
19:59:05.652649 sendto(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 100, 0, NULL, 0) = 100
19:59:05.652805 recvfrom(3, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"..., 4096, 0, NULL, NULL) = 4096
19:59:05.652805 recvfrom(3, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"..., 4096, 0, NULL, NULL) = 4096
...
19:59:05.654440 recvfrom(3, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"..., 4096, 0, NULL, NULL) = 4096
19:59:05.654473 recvfrom(3, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"..., 4096, 0, NULL, NULL) = 1024
19:59:05.654506 recvfrom(3, 0x55d23c5b5010, 4096, 0, NULL, NULL) = -1 ECONNRESET (Connection reset by peer)
19:59:05.654575 write(1, "Read 128000 bytes, final return "..., 60) = 60
19:59:05.654694 write(4, "Connection reset by peer\n", 25) = 25
19:59:05.654725 close(4)                = 0
19:59:05.654750 close(3)                = 0
19:59:05.654783 exit_group(0)           = ?

ここにも特に特異な現象は見られません。

recvfrom()
を呼び出し続け、いずれかの呼び出しで
-1
/
ECONNRESET
が返されるまで続行します。

最初の仮説

ECONNRESET
が発生するタイミングを検証してみましょう。なぜなら、前述の上部に戻ることで確認できる通り、一部の呼び出しでは完全な 600,000 バイトを読み込み、他の呼び出しでは異なる値が返されているからです。したがって、ここにはおそらく某种のタイミングの問題があるのでしょう。

indsight で最も明らかなことを実行しましょう:サーバー側の

close()
を遅延させます。なぜなら、もし本当に RST を生成する要因があるとすれば、それはたぶん
close()
自体だからです。

serve_client()
内で、
close()
の呼び出しの前に単に
sleep(1)
を追加してみましょう:

sleep(1);
if (close(s) == -1) {
    perror("close");
    return 1;
}

すると、明確に 1 秒の遅延が観測できるようになります。tcpdump がこれを最も良く示します:

これ以上の深掘りを行わずに、現状で以下のような推測となります:

  • サーバー側はそのソケット上の入ってくるデータを「見える」が、それを読み込みません。
  • サーバー側で
    close()
    を呼び出した際に、ソケットは「汚染された(dirty)」状態になっています(待機データがあるため),したがって RST が発せられ、クライアントに対し「すべてのデータが読み込まれていない」と伝える(希望する)ことになります。

この説明は今すぐには理にかなっていますが、まだそれを確認する決定的なソースを見つけるに至りません。(当初の仮説の一つでは、バッファが満杯になるため

spamlen
が 100 に設定され、本来は 100,000 だったと推測されていましたが、それは重要ではなく、単一のバイトでも十分です。)

実際の現場でのシナリオ

gunicorn が Flask アプリをサーブし、それを nginx がリバースプロキシとして取りまとめる環境で、稀に nginx から gunicorn 側へ ECONNRESET が飛んできました。クラウドホスティング業者やファイアウォール、多数のルーター、並行リクエストなどといった複雑な要素もあり、IOCTL や非ブロッキングソケットによって脱線してしまいました。したがって、この現象を単純化させるのにしばらく時間を要しました。

本質的に、nginx が行うことは HTTP リクエストを gunicorn に転送することですが、それを

syscalls
二回に分けて行います:

09:11:31.254489 writev(29, [{iov_base="POST /reveal/d48z/iha4A9MOMuLW40"..., iov_len=392}], 1) = 392
09:11:31.255435 writev(29, [{iov_base="compat=lynx+needs+this", iov_len=22}], 1) = 22

HTTP ヘッダーが 392 バイト、HTTP ボディが 22 バイト、合計 414 バイトです。

gunicorn はその後、ソケットからこのデータを以下のように読み込みます:

09:11:31.593968 recvfrom(6, "POST /reveal/gEJh/bIoAUdWrSV47mI"..., 8192, 0, NULL, NULL) = 414

ただし、ある時には最初の

writev()
の呼び出しのデータのみが受け取られることがあります:

09:11:31.251229 recvfrom(6, "POST /reveal/d48z/iha4A9MOMuLW40"..., 8192, 0, NULL, NULL) = 392

gunicorn(およびその内部で実行されるアプリケーション)は、ヘッダーだけを受け取っても問題なく動作し、ボディについては気にしません。私はこのソフトウェアスタックの一部分が「怠慢」と考えます:アプリケーション側でボディをアクセスするものがなければ、

recv()
を呼ぶさえも省略します。

問題は、gunicorn がそのような方法でトランザクションを終了させることです:

09:11:31.583979 sendto(6, "HTTP/1.1 200 OK\r\nServer: gunicor"..., 212, 0, NULL, 0) = 212
09:11:31.584225 sendto(6, "\312\205]"..., 614400, 0, NULL, 0) = 614400
09:11:31.590869 close(6)                = 0

データを送信してソケットをクローズします。まだ読み取るべき待機データがあれば、これは RST を引き起こすと考えられます。

回避策として、Python アプリ側で HTTP ボディに対してダミー操作を行うことで、ソケットから完全に読み込まれたことを保証しました。これにより、現在まで追加の ECONNRESET は見せていません。(アプリケーションによりますが、これは DoS のリスクを開く可能性があります:例えばサーバーに 10GB のデータを POST されても、メモリが 1GB しかなかった場合などは困ります。nginx の

client_max_body_size
でこれを防ぐことができるでしょう。)

次のステップ

  • close()
    が TCP RST の真の原因であることを検証し、それを裏付ける信頼できる情報源を見つける。
  • RFC 1122 に関連している可能性もある:
    • ホストは「半 duplex」の TCP クローズシーケンスを実装してもよい(CLOSE を呼び出したアプリケーションが接続から引き続きデータを読み込むことはできない)。もしホストが受け取り待機中のデータがある状態で CLOSE を発行した場合、あるいは CLOSE が呼ばれた後に新しいデータが届いた場合、TCP はそのデータが失われたことを示すために RST を送信すべきである。
  • Python 側で「責任者」を特定する:gunicorn か flask か、実際の Flask アプリか?アップストリームへレポートする。
    • gunicorn の可能性があります(すでに報告済み)。

同じ日のほかのニュース

一覧に戻る →

2026/05/18 6:40

ジェンケイアド

## 日本語訳: GenCAD は、画像から直接編集可能な 3D CAD デザインを生成する AI モデルを作成することで、エンジニアリング分野における画期的な突破を遂げています。以前の方法は複雑なデータ構造に苦戦していましたが、この新しいアプローチは Boundary Representation(B-rep)形式に関連する精度上の課題を克服します。これは、latent command representations と diffusion modeling を含む独自の 4 つのステップのプロセスを通じて実現されており、結果を静的な形状ではなく、実行可能なパラメトリックコマンドとしてデコードします。 この技術は、製造業者やエンジニア向けに設計ワークフローを変革し、完全にモディフィ可能で高精度なモデルを瞬時に作成することを可能にしています。以前は、簡易的な 3D ビジュアルを調整可能なエンジニアリングファイルに変換するには、きつ手間のかかる手作業が必要でしたが、GenCAD はこの障壁を取り除き、ユーザーが設計を容易に反復して改善できる完全な CAD プログラムへの即座のアクセスを提供します。その結果、産業全体が大幅な効率向上を実感し、自律的なツールによって現在、さらなる開発に必要な柔軟性を備えた洗練された編集可能な幾何形状が生成されています。

2026/05/18 6:56

ThinkPad:IBM の弁当箱から、レノボの AI ワークステーションへ

## Japanese Translation: ThinkPad ノートパソコンファミリーは、IBM での公式発売(1992 年 10 月 5 日)以降、同社の所有期間(1992–2005 年)および Lenovo による継承期間(2005 年〜現在)にわたり、連続した納品を実現している長年のエンジニアリングの遺産です。当初は 700C カップシェルとして発表され、Richard Sapper の象徴的なマットブラックケース、10.4 インチの有源マトリックスカラー TFT ディスプレイ、そして Ted Selker の開発した TrackPoint II——ホームROWから指を動かす時間を短縮するポインティングスティック——を搭載していました。2010 年までには納品台数が 6000 万台を超え、買収後のブランドの存続と IBM の元々のエンジニアリング原則の維持を証明しました。デザインは 30 年にわたり大きく進化しましたが、1992 年の時代から現代のモデルである 2025/2026 年製の P14s Gen 6 や X1 Carbon に至るまで視覚的な連続性を保ち続けています。これらの現行モデルは「Strix Point」CPU を採用し、高度な NPU とプレミアム OLED ディスプレイを備えています。初期モデルでは 2012 年まで 7 レーキストANDARD化されており、その後は Precision キーボードに置き換えられましたが、後期の世代では 2012 年から開始されたハンダ付けメモリ制限などの課題もありました。一方、近年の傾向としては、薄い筐体にユーザー交換可能な DDR5 SODIMM を採用するなど、修理可能性への再注力が進んでいます。また、専用ドックから汎用的な USB-C/Thunderbolt 規格へも円滑に移行しています。結局のところ、ThinkPad の成功は、元々のデザイン哲学を尊重しつつ最新技術を統合することで、長期的な市場優位性を維持できることを示しています。

2026/05/18 6:15

プロログによるコーディング・ホラー。

## Japanese Translation: 本記事は、純粋で単調なコーディング慣行への厳格な遵守が、堅牢な Prolog プログラムにとって不可欠であるという主張を展開している。一般的な産業パターンへ偏离することは、言語の述語論的性質を破損させ、高価な欠陥をもたらす。`!/0`、`(->)/2`、および `var/1` などの非単調な構造は、意図された解の喪失か不適切な結果を生じる。`assertz/1` および `retract/1` でグローバルデータベースを改変することは、隠れた依存関係を創出し予期せぬ失敗を引き起こすため、状態は世界の改変を通じてではなく述語の引数を通じて伝達されるべきである。`(is)/2`、`(=:=)/2`、および比較演算子のような低水準のアリティム操作は、開発者に矛盾する述語論的および操作的意味を両立させるよう迫り、プログラムを理解しやすくし、学習・テスト・推論を行うことを難しくする。不純な出力操作もまた、解答を Prolog タームとして記号論的に考察することを阻止する。純粋で単調な Prolog 部分集合を採用し、`dif/2` のような近代の述語論的ツール、`if_/3` のようなメタ述語、およびクリーンなデータ構造を活用することで、開発者はパフォーマンスを維持しつつ一般性・柔軟性・厳格なテスト可能性を取り戻し、プロフェッショナル環境での利用を制限するレガシーの負担から Prolog を解放することができる。