Open3
Go Memo

Graceful Shutdown付きTCPプロキシサーバの実装
この記事の中にある「Graceful Shutdown付きTCPプロキシサーバの実装」の理解
🔹 1. 接続制御と終了処理の準備
shutdown := make(chan struct{}, 1)
semaphore := make(chan struct{}, a.MaxConnections)
wg := sync.WaitGroup{}
ここでは以下を準備しています:
-
shutdown
: 終了指示用チャンネル。これを閉じると全体の終了が始まる。 -
semaphore
: 同時接続数の制限をするためのチャンネル。バッファサイズは最大接続数。 -
wg
: 各クライアント処理の終了を待ち合わせるための仕組み。
🔹 2. 接続受付ループの開始
go func() {
for {
local, err := listener.(*net.TCPListener).AcceptTCP()
if err != nil {
select {
case <-shutdown:
return
default:
continue
}
}
...
}
}()
- 新しいTCP接続を受け付け(例:クライアントが
curl localhost:8080
)。 - エラーが起きたら
shutdown
をチェック。そうでなければ次へ。
🔹 3. 接続処理用のGoroutineを起動
semaphore <- struct{}{}
wg.Add(1)
go func() {
defer func() {
<-semaphore
wg.Done()
}()
- セマフォを使って接続数を制限。
- 接続処理が終わったら
semaphore
を戻し、wg.Done()
で終了を通知。
🔹 4. ターゲット(転送先)への接続を確立
remoteAddress := target.Host
if target.Port() == "" {
switch target.Scheme {
case "http":
remoteAddress = net.JoinHostPort(target.Hostname(), "80")
case "https":
remoteAddress = net.JoinHostPort(target.Hostname(), "443")
}
}
remote, err := net.DialTimeout("tcp", remoteAddress, 10*time.Second)
-
target
にポートがない場合、プロトコルに応じてデフォルトポートを補完。 -
remote
には、ターゲットサーバとのTCP接続が確立される。
🔹 5. 双方向転送の実行
c := make(chan struct{}, 2)
f := func(c chan struct{}, dst io.Writer, src io.Reader) {
_, _ = io.Copy(dst, src)
c <- struct{}{}
}
go f(c, remote, local)
go f(c, local, remote)
- クライアント → ターゲット、ターゲット → クライアントの2方向を
io.Copy
で並行実行。 - いずれかが完了すると
c
に通知。
例:
クライアント: GET /hello\n
↓
リモートサーバ: HTTP/1.1 200 OK\nHello!
↓
クライアントへ返送
🔹 6. 終了 or 転送完了の検出
select {
case <-c:
case <-shutdown:
local.CloseWrite()
}
- どちらかの転送が終了 or シャットダウンが来たら
select
が進む。 - シャットダウンなら
local.CloseWrite()
によって書き込み側を閉じ、片方向終了を伝える。
🔹 7. シャットダウン信号の待機と処理
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM)
<-quit
time.Sleep(time.Duration(a.Lameduck) * time.Second)
- プロセスに
SIGTERM
(例:kill
やdocker stop
)が来るまで待機。 - 来たら
Lameduck
(猶予)期間だけ待つ。ここで新しい接続は停止すべきだが、既存接続は継続する。
🔹 8. サーバ停止処理
close(shutdown)
listener.Close()
wg.Wait()
-
shutdown
を閉じて、すべてのGoroutineに終了を促す。 -
listener.Close()
によりAccept()
が失敗してループが抜ける。 -
wg.Wait()
によって、すべての接続処理が終わるのを待つ。
✅ まとめ図解
┌────────────┐ ┌────────────┐
│ クライアント │<──→│ プロキシ │<──→ ターゲット
└────────────┘ └────────────┘
│ │
SIGTERM io.Copyで双方向中継
▼ │
Lameduckタイマー → listener.Close()
▼
既存処理が終わるまで wg.Wait()
▼
プロセス終了