Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
GoでTCPサーバーの GracefulShutdownをシンプルにやる
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
Masato Inoue
September 07, 2024
85
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
GoでTCPサーバーの GracefulShutdownをシンプルにやる
Masato Inoue
September 07, 2024
More Decks by Masato Inoue
See All by Masato Inoue
10,000店舗の飲食店を支えるGoと バックエンドアーキテクチャの変遷 🌮
masaygggg
3
1k
事業フェーズの変化に対応する 開発生産性向上のゼロイチ
masaygggg
0
750
CTOとしてどのように役割を変えてきたか / 4年間を振り返ってみた
masaygggg
0
4.3k
Startup CTO of The Year 2023 株式会社tacoms井上将斗
masaygggg
0
210
Featured
See All Featured
Reflections from 52 weeks, 52 projects
jeffersonlam
356
21k
HDC tutorial
michielstock
2
720
The SEO identity crisis: Don't let AI make you average
varn
0
490
Game over? The fight for quality and originality in the time of robots
wayneb77
1
200
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
130k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
25
2k
Visual Storytelling: How to be a Superhuman Communicator
reverentgeek
2
560
Building Experiences: Design Systems, User Experience, and Full Site Editing
marktimemedia
0
530
A Modern Web Designer's Workflow
chriscoyier
698
190k
The innovator’s Mindset - Leading Through an Era of Exponential Change - McGill University 2025
jdejongh
PRO
1
200
What’s in a name? Adding method to the madness
productmarketing
PRO
24
4.1k
The Anti-SEO Checklist Checklist. Pubcon Cyber Week
ryanjones
0
160
Transcript
2024/09/13 Asakusa.go #3 株式会社tacoms 井上将⽃ GoでTCPサーバーの GracefulShutdownをシンプルにやる
井上将⽃ 株式会社tacoms 飲⾷店向けSaaSのCamelというプロダクトを運営してます! ⾃⼰紹介 ps 去年の秋から浅草寺の近くに住んでるのですが居⼼地が良くてお気 に⼊りの街です🏠 マイブームは周りの⼈に銀座線の便利さを語ることです (浅草始発だし主要な駅全部⾏けて便利! 笑)
@masa1934yg
本⽇お話しすること TCPサーバー実装の背景 GracefulShutdownの話 実装イメージ
TCPサーバー実装の背景
外部サービスとの連携が多いプロダクト特性 連携 連携 フードデリバリー各社 オペレーションの⼀元管理という性質上 デリバリーサービス側とPOSの仲介役となっており外部連携が多いプロダクト POS / 基幹システム
HTTPではなくTCPレイヤーでデータ受信する要件が... ‧連携先の会社は⼤⼿からスタートアップまで様々 ‧tacomsのPublicAPIで連携することもあるが連携先APIに合わせて実装することも多い ‧連携⽅法は懐かしいものから新しいものまで様々🥹 (REST / GraphQL / SOAP /
TCP) ‧今回はTCPレイヤーでデータを受信するサーバーが必要に.....!
GracefulShutdownの話
GracefulShutdownとは ‧APIサーバー‧ジョブなどある特定プロセスを停⽌する際に安全に正常終了するための仕組み ‧特定プロセスが終了するタイミングにおいて、いきなり全てのプロセスを終了させるのではな く、処理中プロセスの完了を待機してから終了する ‧特定プロセスの終了を検知するための⼿段としては各osのカーネルが持つシグナルという機能 がある
シグナルとは $ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT
4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGEMT 8) SIGFPE 9) SIGKILL 10) SIGBUS 11) SIGSEGV 12) SIGSYS 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGURG 17) SIGSTOP 18) SIGTSTP 19) SIGCONT 20) SIGCHLD 21) SIGTTIN 22) SIGTTOU 23) SIGIO 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGINFO 30) SIGUSR1 31) SIGUSR2 $ kill -15 PID(プロセスID) ‧シグナルとは各osのカーネルに備わる機能でプロセスへ様々なイベントを通知することが出 来る機能 ‧イベントの通知はコマンドでも他プロセスからでも可能 ‧シグナルの⼀覧及び送信は以下のコマンドで実現出来る ※シグナルに関しての詳しい説明は本題から外れるので割愛します
TCPサーバーにおけるGracefulShutdownの重要性 シグナルを検知して処理中のTCPコネクションの終了を待機してから TCPサーバーのプロセスを終了させる必要がある 本題のTCP サーバーにおいてプロセスが終了するタイミングはいくつか考えられる ‧実装ミスによるPanic及びRecover漏れによる強制終了 ‧デプロイなどによりサーバーが切り替わるタイミングでのプロセス終了 ‧インシデント起因のサーバープロセス終了 ‧ローカル開発におけるcmd+C TCP
サーバーが終了する際に処理中のTCPコネクションがある時データ整合成やUXで問題が⽣じる可能性が ex)DBへの登録中にプロセスが強制終了してしまいデータが登録されなかった (レスポンスも返せないためリトライもされない) ex)デプロイ時プロセスが終了した時、商品⼀覧のAPIから⼀瞬502が返されユーザー側にエラーが表⽰される
実装イメージ
GracefulShutdownなTCPサーバー実装 func main() { // シグナル以外でプロセス全体を終了させたい場合は WithCancelを使う ctx := context.Background()
// シグナル起点で Done()が呼ばれる ctxを生成 ctx, cancel := signal.NotifyContext( ctx, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP, ) defer cancel() srv, err := newServer() if err != nil { panic(err) } // サーバの起動 go srv.serve(ctx) // シグナルごとに処理が変わる場合は switch // シグナル起点で Done()が呼ばれたら処理中のプロセスを待機して終了とする <-ctx.Done() fmt.Println("server shutdown start") // シャットダウン処理 srv.Shutdown() fmt.Println("server shutdown complete") } ① シグナル受信⽤の Context作成 ③ 処理中プロセスの待機 プロセスのクローズ ② TCPサーバーの起動 コネクションの管理
① シグナル受信⽤のContext作成 func main() { // シグナル以外でプロセス全体を終了させたい場合は WithCancelを使う ctx :=
context.Background() // シグナル起点で Done()が呼ばれる ctxを生成 ctx, cancel := signal.NotifyContext( ctx, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP, ) defer cancel() os/signal packageのNotifyContextでシグ ナル受信のタイミングでDone()される Contextを⽣成 もしシグナルごとにハンドリングを変えたい 場合はチャネルを作ってselectで待ち受けて 分岐する sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP) select { case signal := <-sigChan: switch signal { case syscall.SIGINT, syscall.SIGQUIT: fmt.Println("SIGINT, SIGQUIT server shutdown") // something fmt.Println("SIGINT, SIGQUIT server shutdown") return case syscall.SIGTERM: fmt.Println("SIGTERM server shutdown") // something fmt.Println("SIGTERM server shutdown") default: panic("unexpected signal has been received") } default: }
server構造体にListenerとWaitGroupのフィールドを 持つ newServerでIPとPORTを指定してプロセスを起動す る WaitGroupの⽤途は後述 ② TCPサーバーの起動 & コネクションの管理 type
server struct { listener *net.TCPListener wg sync.WaitGroup } func newServer() (*server, error) { tcpAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080") if err != nil { return nil, err } l, err := net.ListenTCP("tcp", tcpAddr) if err != nil { return nil, err } return &server{ listener: l, }, nil }
signalのctx.Done()のハンドリングをしつつ、無 限ループでTCPコネクションを貼り続ける TCPコネクションでエラーになることは滅多にな いが念の為リトライ処理 server構造体のwgをAdd/Doneすることで main.goからshutdownが呼ばれた時にwg.Wait で処理をストップできるようにしておく Goのnet packageを⾒るとKeepAliveのデフォル トが15sのため、それ以上の時間でコネクション
タイムアウトを設定する ② TCPサーバーの起動 & コネクションの管理 func (s *server) serve(ctx context.Context) { defer func() { if r := recover(); r != nil { fmt.Printf("Recovered from:%v\n", r) } s.listener.Close() }() var tempDelay time.Duration LOOP: for { select { case <-ctx.Done(): // シグナルのctx.Done()が来たらreturnして終了 return default: conn, err := s.listener.AcceptTCP() if err != nil { // タイムアウトエラーの場合は時間を遅延しながらリトライ if netErr, ok := err.(net.Error); ok && netErr.Timeout() { tempDelay *= 5 time.Sleep(tempDelay) if tempDelay > 1*time.Second { // 1秒以上の場合は return error return } continue LOOP } return } conn.SetDeadline(time.Now().Add(30 * time.Second)) // TCPコネクションのタイムアウト設定 s.wg.Add(1) // wg.Add(1)でgoroutineの数をカウント go func() { s.handleConnection(ctx, conn) s.wg.Done() // wg.Done()でgoroutineの数をデクリメント }() } } } 参考 https://github.com/golang/go/blob/807e01db4840e25e4d98911b28a8fa54 244b8dfa/src/net/dial.go#L19 // defaultTCPKeepAlive is a default constant value for TCPKeepAlive times // See go.dev/issue/31510 defaultTCPKeepAlive = 15 * time.Second
③ 処理中プロセスの待機 & プロセスのクローズ main.goでシグナルのctx.Done()を待機 ctx.Done()のタイミングでshutdownを呼び出し ListnerのCloseとwg.Waitを⾏う ②のTCPコネクションのハンドリングにて goroutineの処理をwgで囲っていたため、 wg.Waitで処理中のプロセスが完了するまで待機
できる func (s *server) shutdown() { s.listener.Close() s.wg.Wait() } // シグナルごとに処理が変わる場合は switch // シグナル起点で Done()が呼ばれたら処理中のプロセスを待機して終了とする <-ctx.Done() fmt.Println("server shutdown start") // シャットダウン処理 srv.Shutdown() fmt.Println("server shutdown complete") func main()
TCPサーバーを起動した上でGoのスクリプトからTCPサーバーへ リクエスト クライアント側で TCPコネクション後にスリープ TCPコネクションのスリープ中にシグナルイベントを発⾏ 処理中のプロセスが完了してからサーバーが終了することを確認 する 動作確認 $ go
run main.go start to tcp server :8080 conn read start: 127.0.0.1:59750 conn read start: 127.0.0.1:59745 conn read start: 127.0.0.1:59747 conn read start: 127.0.0.1:59746 conn read start: 127.0.0.1:59749 server shutdown start conn read end: 127.0.0.1:59750 conn read end: 127.0.0.1:59745 conn read end: 127.0.0.1:59749 conn read end: 127.0.0.1:59746 conn read end: 127.0.0.1:59747 server shutdown complete func main() { wg := &sync.WaitGroup{} for i := 0; i < 5; i++ { wg.Add(1) fmt.Println("request_count: ", i) go sendSocketWithWG(wg) } wg.Wait() } func sendSocketWithWG(wg *sync.WaitGroup) { defer wg.Done() message := "HELLO SOCKET SERVER !!!" conn, err := net.Dial("tcp", "127.0.0.1:8080") if err != nil { panic(err) } defer conn.Close() time.Sleep(7 * time.Second) conn.Write([]byte(message)) response := make([]byte, 1000_000) readLen, err := conn.Read(response) if err != nil { log.Println("read error") } fmt.Println(string(response)) } client.go
おまけ: HTTPサーバーの場合は既存の仕組みを使おう HTTPでサーバーを実装出来る場合は迷わず既存の仕組みを使いましょう。。! 標準のhttp packageでもありますし、Echoなどの有名どころのフレームワークでも実装できま す package http package echo
func (srv *Server) Shutdown(ctx context.Context) error {.....} func (e *Echo) Shutdown(ctx stdContext.Context) error {.....}
おまけ: wg.Wait後のタイマー処理までやると丁寧 シグナルをハンドリングするときはインフラ環境の仕様に合わせて実装を調整する → 例えばAWSのECS Fargateはタスクの停⽌をトリガーにコンテナプロセスへSIGTERMを投げる。 SIGTERMの30秒後にSIGKILLが発⾏される 処理に時間のかかるプロセスがあった場合、wg.Waitのみだとサーバー停⽌までに時間がかかる問題が⽣じ るため、待機時間を設定するやり⽅があるが今回はECSの仕様でどのみちKillされるため考慮してない なお、http
packageのShutdownメソッドではタイマーが設定されてました 参考 ECS のアプリケーションを正常にシャットダウンする⽅法 https://aws.amazon.com/jp/blogs/news/graceful-shutdowns-with-ecs/ http packageのShutdownメソッド https://github.com/golang/go/blob/807e01db4840e25e4d98911b28a8fa54244b8dfa/src/net/http/server.go#L3055
ご清聴ありがとうございました tacomsは Goエンジニア 募集中です👏👏👏 採⽤HP テックブログ https://zenn.dev/p/tacoms https://tacoms-inc.com/#block-cddb 337d21b847408a45f9ee69b14077