Goエンジニア必見!グレースフルシャットダウンの現場パターン
引用元:https://news.ycombinator.com/item?id=43889610
構成によっては Kubernetes
がロードバランサーのターゲット IP
を更新するのにびっくりするくらい時間がかかることに悩まされてきたよ。俺の場合、グレースフルシャットダウンの9割は、Podが終了する前にちゃんとトラフィックが捌けてるか確認することだった。
グローバルな preStop
フックで15秒スリープ入れたら、 HTTP 503
の発生率が劇的に減ったんだ。これのおかげで、ロードバランサーの登録解除が始まってから、実際にアプリケーションに SIGTERM
が送られるまでの間に時間ができて、アプリケーション側の処理がだいぶ簡単になったよ。
そうそう。 Prestop sleep
は高品質なローリングデプロイのための魔法の SLO
ソリューションだよ。
個人的には、 Kubernetes
が改善できる点は2つあると思うんだ。
1. Podはシャットダウンシーケンスを開始する 前に Endpointsから削除されるべき。termination graceみたいに、termination delayのオプションがあるべきだね。
2. PDBはevictionの 前に recreationを許可すべき。
考慮すべきもう一つの要素は、もしあなたが典型的な Prometheus
の /metrics
エンドポイントを持っていて、それがN秒ごとにスクレイピングされるなら、最後のスクレイピングから実際のプロセス終了までの間に、記録されたメトリクスが伝播されない期間があるってこと。これはシャットダウンシーケンス中にエラーが発生してるかどうかについて、誤った印象を与える可能性があるよ。
注意しないと、サービスがシャットダウンしている最後の数秒間のログを失う可能性もある。例えば、 Promtail
や Vector
のようなサイドカープロセスが見ているログファイルに書き込んでいて、起動時にサービスがその同じパスを切り詰めて書き込みを開始する場合、シャットダウンからのログを失う可能性のあるレースコンディションがあるんだ。
俺だけかな、オブザーバビリティスタックってなんかバカバカしくない?ログ、メトリクス、トレースがそれぞれ独自のデータベース、サイドカー、可視化スタックを持ってさ。誰かが気が向いたときに書いた言語固有の連携ライブラリ。とてつもないクラウド料金。
で、そんな苦労をした後でも、データのほとんどは完全に無視されて、ビジネス上の洞察も、SSHで箱に入ってログファイルをgrepしてエラー出力を探すって「トレーラーパーク版」と比べて、ほとんど変わらない。
なんていうか、このエコシステムにものすごく労力をかけてるけど、稼働時間、パフォーマンス、エルゴノミクスに関して、それに見合うだけの significant な増加で報われてるとは思えないんだ。
全部のオブザーバビリティツールが揃ってる場所から、「SSHで箱に入ってログgrep」の段階だった場所に移った経験から言うと、前の会社 A
が死ぬほど恋しかったと断言できるね。どの箱にSSHするか、どのログファイルをgrepするか、どんな魔法の言葉で検索するか、なんて、そのマシンをセットアップしてバグを書いた開発者じゃないと、ほとんど不可能だったんだ。
君には完全に同意だけど、同時に、「tech」の多くの側面と同じように、その特定のセグメントが特定の組織にとって独占されて利益生成装置に変えられてしまったと思うんだ。 DevOps
、 Agile/Scrum
、 Observability
、 Kubernetes
なんかが全部その例だね。
これが良い部分や役立つ部分を、マーケティングのでたらめで薄めているんだ。Grafana
が数ヶ月ごとに新しいタイムシリーズデータベースやエンジンを「発明」しているらしいのは、情報通でいようとするのが本当に苦痛だよ。
だから、また rrdtool/smokeping
を使い始めたくらいさ。
openobserve.ai
を見てみたらどうかな?自分でホストできるし、ログ/メトリクス/トレースを取り込む単一のバイナリだよ。俺のサイドプロジェクトでは役立ってる。
「SSHで箱に入ってログファイルgrep」で済むくらいシンプルなシステムで作業してるなら、どうぞそうしてください。
でも多くのシステムはそれより複雑だ。オブザーバビリティのエコシステムが存在するのは理由があって、それが解決している本当の問題があるんだ。
例えば、あなたのアプリが単一の箱で動くのをやめて、成長したとする。今度はN個の異なるホストにSSHして、全部からログファイルをgrepする必要がある。あるいは、SCPをループで実行するシェルスクリプトで独自のログ転送バージョンを発明する。
さらに一歩進んで、それらの箱をオートスケーリンググループに入れて、需要に基づいて自動的にスケールアップダウンするようにしたとしよう。今度は本当に何らかの自動ログ転送が必要だ、さもないと ASG
のホストが終了するたびに、それが稼働中に処理したトラフィックのログを捨ててしまうことになる。
あるいは、パフォーマンスの低下に気づいて、特定の API
エンドポイントが遅いせいだと特定したとしよう。そのエンドポイントの応答時間を時間経過でグラフ化できると役立つことが多い。徐々に遅くなっていたのか、それとも応答時間が突然増加したのか?もし突然の増加なら、同じくらいの時間に何が起こった?コードデプロイかもしれないし、データベースの設定変更かもしれない、などなど。
たぶん、あなたが運用しているサービスはスタンドアロンではなく、会社の他のチームが書いたサービスと連携している。システム全体で何かうまくいかないとき、どうやって問題の根本原因を突き止める?すべての異なるシステムを通して、リクエストや操作のライフサイクルをどうやってトレースする?
何かうまくいかないとき、箱にSSHしてログファイルを見る...でも、そもそも何かうまくいかないとどうやって知るの?サポートのメールに届くユーザーからの苦情だけに頼る?それとも、「うーん、そんなことは決して起こるはずがない」ということが起こっている場合に、積極的に通知してくれる監視ルールがある?
全体として、集中ログとメトリクスは超価値があると思うよ。でも今のスタックは全部的を外してる。
例えば、どんなログメッセージも何百個ものフィールドを持ってて、そのほとんどは決して変わらない。なんでこの情報をサービス起動時に一度だけプッシュして、すべてのログメッセージと一緒に送らないんだ?
OK、明らかに今のシステムが高額なのは、これらのサービスを提供している会社の利益のためだよね。
例えば、どんなログメッセージにも何百ものフィールドがあるけど、ほとんど変わらないじゃん。サービス起動時に一回だけ送って、ログメッセージごとには送らなくていいんじゃない?
ログフィールドが各ログエントリで変わらないなら、VictoriaLogsみたいなログに良いデータベースはそういうフィールドを1000倍以上圧縮してくれるから、ストレージ容量は無視できるし、クエリ性能にも全く影響しないんだ。
各ログエントリにたくさんのフィールドを持たせることで、後で分析するのが楽になるんだ。相互に関連する大量のログを飛び回る代わりに、単一のログエントリから必要な情報を全て得られるからね。これにより、数多くのフィールドのどんなサブセットでもフィルタリングやグループ化して、スケールでのログ分析も改善されるんだ。こういうたくさんのフィールドを持つログは”wide events”って呼ばれてるよ。このタイプのログに関するこの素晴らしい記事を見てみて - https://jeremymorrell.dev/blog/a-practitioners-guide-to-wide…
プログラムって人のためにあるんだよ。だからJSONとかデバッガとかPythonとかがあるわけ。プログラミング自体はプログラミングの1割くらいしか占めてないんだぜ。
君だけじゃないよ - OSSツールスタックは広範囲に及んで手作業の長いプロセスが必要だったりする一方で、ほとんどのエンタープライズベンダーのコストはフルマップされた可観測性には高すぎるんだ。
Corootは俺が取り組んでるオープンソースプロジェクトで、これを解決しようとしてるんだ。eBPFが自動的にデータを集めて中央のサービスマップにしてくれて、ツールがRCA(マッピングされたインシデント時間枠とか)の洞察を提供してくれるから、修正を早く実装して稼働時間を改善できるんだ。
GitHubはこちらで、もし役に立つと思ったらフィードバック大歓迎だよ: https://github.com/coroot/coroot
参考までに、俺はまさにこれを(もっと色々)プラットフォームライブラリでやってるんだ。Goで高負荷アプリを8年以上やってきて遭遇した問題を網羅してるよ。この間、プラットフォームの開発・改善は、どの会社でも趣味だったんだ :)
これは”ログを同期する”とか”livenessハンドラにイングレスが追いつくのを待つ”みたいなことを(将来的に)網羅する予定だよ。
https://github.com/utrack/caisson-go/blob/main/caiapp/caiapp…
https://github.com/utrack/caisson-go/tree/main/closer
ドキュメントは少ないし、まだカバーされてないものもあるけど、休暇から戻ったら最初のリリースをする予定なんだ。
最終的には、これはメタプラットフォーム(慎重に作られたビルディングブロック)と、典型的なk8s/otel/grpc+httpインフラを網羅するリファレンスプラットフォームライブラリになるだろうね。
これ見てみるよ、共有ありがとう!俺たちのgolangインフラ/プラットフォーム担当はみんな自分たちで似たようなライブラリ書かないといけなかったと思うんだ。君のを共有してくれてありがとう!
なんでPrometheusとか関連ツールがデータの”pull”モデルを使うのか、全然わからなかったんだよな。ほとんどのものは”push”モデルなのにさ。
それは元々のGoogleのborgmon設計の名残なんだよ。ちなみに、Googleの”v2”システムではpush-onlyに切り替えようとしたけどうまくいかなくて、ハイブリッドなpull-pushストリーミングAPIに落ち着いたらしいね。
”v2”ってMonarchに関する彼らの論文が元になってるの?
ああ、なら君が言及した新しいハイブリッドシステムに関する資料って何かある?教えてくれると助かる!
Prometheusはデフォルト”pull”だけどメリットあるよ.”push”だとメトリクススレッド落ちたら検知できない.Prometheusはサービスディスカバリ連携で勝手にインスタンス見つけてscrapeするから、本体生きてるか(”up”)は検知できるのが良い.クライアント側実装も楽.Erlangの”supervision trees”みたいに親がpullで子を監視する感じ.詳細はリンク見てね.
最高のアンサーだね.昔Cacti,Nagios,Graphite,KairosDB使ってた頃、pushベースで大変だったのはソースが制御不能なデータ量.スケーリングがheadacheだった.”Scraping”は多数の”scraper”でソースを自動発見して、ソースごとのscrape量制限でシステムを過負荷から守れる.うるさいソースのメトリクス落ちても「君のせい」って言える.昔はfire hose状態だったからね.
書いてくれてありがとう;すごく洞察に満ちてたよ!
>典型的なPrometheusの”/metrics”エンドポイントだと、”最後の”scrapeとプロセス終了の間にメトリクスが伝播しない期間があるって考慮点.シャットダウン中のエラーについて誤解を招くかも.これに良い解決策ある?scrape間隔15秒だと30秒はない.この挙動がStatsd使ってる理由の一つ.pushだとこの問題ないからね.
注意点:Goで”log.Fatal”は”defer”を実行しない!os.Exit呼ぶから即閉じちゃうんだ.”panic”なら実行されるよ.例:<br> package main; import ( ”fmt” ”log” ); func main() { defer fmt.Println(”in defer”) ; log.Fatal(”fatal”) } <br>←”fatal”だけ.<br> package main; import ( ”fmt” ”log” ); func main() { defer fmt.Println(”in defer”) ; panic(”fatal”) } <br>←両方出る.
分散システムが、うまく動くためにクライアントがきれいに終了することに依存してると、そのシステムはいずれひどく壊れるよ.
僕はそれをすごく信じてるから、設計でグレースフルシャットダウンは考えない.コンポーネントは安全にハードクラッシュできるべきだし、重要部分が生きてれば全体システムに影響しないはず.システムがハードクラッシュを扱える唯一の方法は、それが常に起きる普通のことである場合だよ.Chaos Monkey万歳!
クライアントやワークフローにとって”nice”であるためのグレースフルシャットダウンと、クライアントがそれが動くことに”rely”(依存)することの間には大きなギャップがあるよ.
ずっと昔、物理環境でね − それにはSTONITHを使ったよ!https://smcleod.net/2015/07/delayed−serial−stonith/
回復可能でも、典型的な終了が壊滅的に見えないようにしたい妥当な理由はあるよ.アプリがSIGINTで落ちたのとKillで落ちたのは全然違うんだ.Blue-Green移行とかは例えば、グレースフルな終了が必要なんだよね.
> Blue-Green migrations とかだとグレースフルな終了動作が必要になることもあるけど、常に必要ってわけじゃないんだ。例えば、ステートレスなバックエンドサービスの新しいバージョンをデプロイする時、load balancer が今のバージョンと新しいバージョン両方にトラフィックを流してる場合、load balancer が切り替えの責任を持てる。
そうすれば、処理中のリクエストは今のバージョンのバックエンドで処理させて、新しいリクエストだけ新しいバックエンドに転送できる。
で、LB がもうリクエスト処理してないって言ったら、古いバックエンドは ungraceful に terminate しても大丈夫。
ああ、そうだね。でも、たとえソフトウェアがそういうシャットダウンに耐えられる設計だとしても、わざわざプラグを抜くみたいに止めなくてもいいと思うんだ。
でも、ちょっと思ったけど、やっぱりそうするべきかも。それが想定通りの動きをするかを確かめる唯一の方法かもしれない。Netflix の chaos monkey みたいにね。
もっとコメントを表示(1)
グレースフルな終了に頼ることと、それをサポートすることは別の話だよ。
サポートしておけば、クライアントに嫌な5xxエラーを見せることなくサービスを停止できるからね。
記事で、新しいサービスインスタンスが古いインスタンスからリスニングソケットを受け取って、 incoming のコネクションを一切落とさずにアプリケーションを再起動する方法を解説してくれたらなって期待してたんだ。
systemd だと比較的簡単に実装できるし、nginx なんかは20年以上前からサポートしてる。
残念ながら Kuberenets とか Docker は、load balancer か reverse proxy でやるもんだと思ってて、そういうサポートはないんだよね。
君が探してるのは、たぶん Cloudflare の tableflip だよ。
https://github.com/cloudflare/tableflip
俺の同僚でいつも言ってたやつがいてさ、もしプログラムが ctrl c とかの終了コマンドできれいに閉じられなかったら、それはコードがダメだってね。
Elixirがマジ賢いって思う点の一つなんだよね。あんま経験ないけど、小さなVMプロセスとして設計されてて、パニックしたり終わったりしても勝手に再起動されるみたいで、意図的にグレースフルシャットダウンの処理作る必要がないっぽい。アプリのアーキテクチャに最初から入ってるからさ。
それって著者が話してるグレースフルシャットダウンの必要性をどうなくすわけ?
GCが手動メモリ管理の必要なくすのと一緒だよ。たまにそれだけじゃ足りなくて手動でやる必要あるけど、大体GCあるシステムでメモリ解放のことしょっちゅう考えないじゃん。BEAMは分散とか障害に強いシステム作るように設計されてて、そういう心配事がさ、外部ライブラリ(例えばKafkaとか)とか完全に外側(例えばKubernetesとか)にあるんじゃなくて、システム内で第一級オブジェクトとして扱われてるんだ。記事の最初に著者が挙げてる3つのポイントはもう組み込まれてて、自分で実装するんじゃなくて、振る舞いを記述する感じなんだよね。それがさ、OPが「意図的にグレースフルシャットダウンの処理を作る必要がない」って言いたかったことだと思うよ。
Elixirの話が記事のグレースフルシャットダウンとどう関係あるか不明。アプリによって即終了かハンドリングか選ぶし、HTTPサーバーは現在のリクエストを待つのが普通。記事のtime.Sleepは微妙でsync.WaitGroupが一般的だよ。GC関係なくコネクションは手動で閉じてエラー処理したいもんね。
Elixir/BEAMはOTPでグレースフルシャットダウンを含む色んな機能が組み込まれてるから、手動で実装する必要ない。著者がtime.Sleepを選んだみたいに手作りは大変。GC比喩は、BEAMには障害回復や監視、トレーシングなどシステムレベルの機能が最初からあるって意味。他のランタイムは外部ツールやライブラリに頼るけど、BEAMは分散・耐障害性特化で約35年かけて進化してるから違うんだ。
自分でgraceful shutdown用の小さいライブラリ作ったよ(https://github.com/eberkund/graceful)。サービスごとに違う開始・終了処理(インスタンス化、contextキャンセル、”Stop”メソッド呼び出しとか)を、統一APIで一元管理できるようにしたんだ。
あはは、俺も全く同じアイデアだったわ(https://pkg.go.dev/git.sr.ht/~mariusor/wrapper#example-Regis…)。俺のはAPIがちょっと劣るかもだけど、複数のシグナルをどう処理するか設定できるんだ。
俺も似たようなの作ったよ!
https://github.com/pseidemann/finish
記事の「Readiness Probeを失敗させてから待つ」ってのは違うと思うな。Podが終了状態になったら自動でNot Readyになるんだよ。SIGTERMの直後は窓があるかもだけど、大したことないってば。
クラスタ管理者としては、新しい接続止めて既存のをちゃんと閉じて、すぐ終わるのが一番。なんかSIGTERM処理しても終了が遅いアプリ多すぎ!
JustWatchでGoogle Wire(https://go.dev/blog/wire
https://github.com/google/wire)を使い始めたんだけど、マジでヤバい!Kubernetesでの面倒なシャットダウン処理がスッキリしたよ。Wireが依存性注入をキッチリやるから、全部順番に終わるんだ…きっとね :-D
livenessについても記事で触れて欲しかったな。livenessとreadinessを同じエンドポイントにしてるアプリ、あれ違和感あるんだよね。
俺はWaitGroupとContextを使ったパターンをよく使うよ。シャットダウン対象のサービスには、終了を知らせるContextと、完了を待つWaitGroupを持たせるんだ。
メインの処理でContextを閉じれば各サービスがシャットダウンを始めて、WaitGroupで待てば全部終わるまで待てるってわけ。
記事には、シャットダウン中にミドルウェアが気を利かせてくれる点があるね。WithCancellation()の詳細はないけど、SIGINT/SIGTERMで停止中に来たリクエストに対して、変なエラーじゃなくて設定した「not in service」エラーを返せるみたい。
正直さ、ログとかシャットダウンっていつも大変なんだよね。シンプルだって思ったことないよ。その場しのぎの対応が必要な感じ。