事象の概要
Go 1.21.0〜1.21.4 でコンパイルした TCP プロキシが CentOS 7.9(カーネル 3.10.0)上で動作中、io.Copy が永遠に戻らなくなるケースが発生しました。
原因は net.TCPConn.ReadFrom が内部的に使う splice が SPLICE_F_NONBLOCK フラグを失ったことで、ノンブロッキング動作が無効になり、古いカーネルでブロックしていたためです。
Go 1.21.5 で該当フラグが復活し事象は解消しています。
環境
- ホスト: Debian 12(6.1.0-10-amd64)
- ゲスト VM: CentOS 7.9(3.10.0-1160.62.1.el7.x86_64)
- 言語ランタイム: Go 1.21.1(問題が出たバージョン)
- ハイパーバイザ: QEMU 7.2.4 + virtio-net
プロキシ実装例
package main
import (
"flag"
"io"
"log"
"net"
"sync"
)
func main() {
from := flag.String("l", "", "listen addr")
to := flag.String("t", "", "target addr")
flag.Parse()
ln, err := net.Listen("tcp", *from)
if err != nil { log.Fatal(err) }
for {
c1, _ := ln.Accept()
go func() {
c2, err := net.Dial("tcp", *to)
if err != nil { log.Print(err); c1.Close(); return }
var wg sync.WaitGroup
wg.Add(2)
go func() { io.Copy(c2, c1); c2.Close(); wg.Done() }()
go func() { io.Copy(c1, c2); c1.Close(); wg.Done() }()
wg.Wait()
}()
}
}
問題の切り分け
strace -p <pid>で goroutine がsplice(…, 0)で止まっていることを確認。netstat -ntpで受信バッファに 1.6 MB 以上のデータが滞留。kill -3でスタックトレースを取得するとnet.splice→poll.Splice→syscall.Spliceのコールパスが残る。- Go 1.18 で同じコードをビルドすると正常に動作。
- Go 1.21.0〜1.21.4 の
poll/splice_linux.goを確認するとSPLICE_F_NONBLOCKが削除されていた。
最小再現 C コード
#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>
static ssize_t relay(int out, int in, size_t max) {
int p[2];
pipe2(p, 0); /* ノンブロッキングではないパイプ */
ssize_t n = splice(in, 0, p[1], 0, max, 0);
if (n > 0) splice(p[0], 0, out, 0, n, 0);
close(p[0]); close(p[1]);
return n;
}
このコードはカーネル 3.10でパイプバッファが一杯になると splice が返らなくなります。
カーネル実装の違い
| カーネル | pipe 残容量チェック | 一度に転送可能サイズ限界 | 結果 |
|---|---|---|---|
| 3.10.0 | 転送ループ内で判定 | 無制限(要求サイズ丸ごと) | ブロック |
| 6.1.0 | 事前に wait_for_space | min(要求, 残容量) | 非ブロック |
回避策・対処法
- Go 1.21.5 以降を利用する(推奨)
- 環境変数
GODEBUG=netpoll=0を設定して splice を無効化 - プロキシ内部で自前の
read/writeループを実装しio.CopyBufferを使用 - カーネルアップグレード(5.x 以降)
まとめ
Go 1.21.0〜1.21.4 のゼロコピー実装が Linux 3.10 の pipe 実装と相性を起こし、splice がブロックするケースがありました。
Go 1.21.5 で SPLICE_F_NONBLOCK が復活したため、CentOS 7 など古いカーネルを使う場合は必ず 1.21.5 以降を利用してください。