このコンテンツは自動機械翻訳サービスによる翻訳版であり、皆さまの便宜のために提供しています。原本の英語版と異なる誤り、省略、解釈の微妙な違いが含まれる場合があります。ご不明な点がある場合は、英語版原本をご確認ください。
ecdysis | ˈekdəsəs |
noun
古い表請求書を移行したり、虫組やを見るような説明行為を移行するために、外請求ケースに移行することです。
1つの接続も中断させることなく、世界中で1秒あたり数百万のリクエストを処理するネットワークサービスをアップグレードするにはどうすればよいでしょうか?
この大きな課題に対するCloudflareのソリューションの1つが、長年にわたりecdysisです。これは、ライブ接続がドロップされず、新しい接続が拒否されないグレースフルプロセスリスタートを実装するRustライブラリです。
先月、私たちはECサイトをオープンソース化し、誰でも利用できるようにしました。Cloudflareで5年間本番運用されているecdysisは、当社の重要なRustインフラストラクチャ全体でダウンタイムゼロのアップグレードを可能にし、Cloudflareのグローバルネットワーク全体で、再起動のたびに数百万のリクエストを節約することで、その有効性を実証しました。
こうしたアップグレードを正しく行うことの重要性は、特にCloudflareのネットワークの規模では、強調することの難しさです。当社のサービスの多くは、トラフィックルーティング、TLSライフサイクル管理、ファイアウォールルールの適用など、重要なタスクを実行し、継続的に稼働しなければなりません。それらのサービスの1つが一時的にもダウンすれば、壊滅的な影響を受ける可能性があります。接続の切断やリクエストの失敗は、すぐに顧客のパフォーマンス低下とビジネスへの影響につながります。
これらのサービスの更新が必要になった場合、セキュリティパッチの適用を待つことはできません。バグ修正にはデプロイが必要で、新機能をロールアウトする必要があります。
古いプロセスが停止するのを待ってから新しいプロセスを起動する必要がありますが、これにより、接続が拒否され、リクエストがドロップされる時間枠ができます。単一の場所で毎秒数千のリクエストを処理するサービスが、数百のデータセンター全体で処理されるリクエスト数を掛け合わせると、簡単な再起動で世界中で何百万ものリクエストが失敗します。
問題を掘り下げ、ECdysisが私たちにとってどのようにソリューションとなったか、そして貴社にも役立つかもしれません。
リンク: GitHub | crates.io | docs.rs
前述したように、サービスを再起動する際の単純なアプローチは、古いプロセスを止めて新しいプロセスを開始することです。これは、リアルタイム要求を処理しないシンプルなサービスであれば問題なく機能しますが、ライブ接続を処理するネットワークサービスでは、このアプローチには重大な制限があります。
まず、単純なアプローチでは、着信接続を待機するプロセスが作成されます。古いプロセスが停止すると、リッスンソケットを閉じ、OSは ECONNREFUSED との新しい接続を即座に拒否します。新しいプロセスがすぐに開始されたとしても、ミリ秒か数秒かにかかわらず、接続を受け入れないギャップが常にあります。1秒あたり数千のリクエストを処理するサービスでは、100ミリ秒のギャップでさえ、何百もの接続が切断されることを意味します。
第二に、古いプロセスを停止すると、すでに確立された接続がすべて消失します。大きなファイルをアップロードしたり、動画をストリーミングしたりするクライアントが突然切断されます。WebSocketsやgRPCストリームのような長期間の接続は、運用の途中で終了します。クライアントから見れば、サービスは失われただけです。
古いプロセスをシャットダウンする前に新しいプロセスをバインディングすると、これは解決するように見えますが、さらなる問題も引き起こします。カーネルは通常、1つのプロセスのみを1つのアドレス:ポートの組み合わせにバインドできますが、SO_REUSEPORTソケットオプションでは複数のバインドが可能です。しかし、これはプロセスの移行中に問題が発生するため、グレースフルリスタートには適していません。
SO_REUSEPORTを使用すると、カーネルは各プロセスに対して個別のリッスンソケットを作成し、これらのソケット間で新しい接続を負荷分散します。接続の最初のSYNパケットが受信されると、カーネルはそれをリッスンプロセスの1つに割り当てます。最初のハンドシェイクが完了すると、接続はプロセスが受け入れるまでプロセスのaccept()キューにあります。その後、プロセスがこの接続を受け入れる前に終了すると、オーファン、カーネルによって終了されます。GitHubのエンジニアリングチームは、GLB Directorロードバランサーを構築する際に、この問題を広範囲に文書化しました。
Equinixのデザインと構築に着手した際、私たちはライブラリの4つの主要目標を特定しました。
古いコードはアップグレード後に完全にシャットダウンできます。
新しいプロセスには、初期化の猶予期間があります。
初期化中に新しいコードがクラッシュしても許容されます。実行中のサービスに影響を与えるべきではありません。
カスケード障害を回避するために、一度のアップグレードだけが並行で実行されます。
ecdysisは、初期の頃からグレースフルアップグレードをサポートしてきたNGINXが開拓したアプローチに従って、これらの要件を満たしています。アプローチは簡単です。
親プロセスは新しい子プロセスを fork() します。
子プロセスは、execve() を使用して、コードの新しいバージョンに置き換わります。
子プロセスは、親と共有される名前付きパイプを介してソケットファイル記述子を引き継ぎます。
親プロセスは、子プロセスが準備完了を知らせるのを待ち、シャットダウンします。
重要なのは、移行中もソケットがオープンのままであることです。子プロセスは、名前付きパイプを介して共有されるファイル記述子として、親から待機中のソケットを引き継ぎます。子のプロセスの初期化中、両方のプロセスが同じ基盤となるカーネルデータ構造を共有するため、親は新規および既存の接続を引き続き受け入れて処理します。子が初期化を完了すると、親に通知し、接続の受け入れを開始します。この準備完了通知を受け取ると、親はリッスンソケットのコピーを即座にクローズし、既存の接続のみの処理を続けます。
このプロセスにより、子供に安全な初期化期間を提供しながら、適用範囲のギャップを排除します。親と子が同時に接続を受け入れることがあります。これは意図的なものであり、親によって受け入れられた接続は、枯渇プロセスの一環として完了するまで単純に処理されます。
このモデルは、必要な衝突安全性も提供します。子プロセスが初期化中に失敗した場合(例えば、設定エラーが原因で)は、単に終了するだけです。親が待機を停止したことはないため、接続が切断されることはなく、問題が修正されればアップグレードを再度試行することができます。
ecdysisは、Tokioとsystemd統合による非同期プログラミングの一流のサポートを通じて、フォークモデルを実装します。
Tokioの統合:Tokioのネイティブ非同期ストリームラッパー。引き出されたソケットは、グルーコードを追加することなくリスナーになります。同期サービスの場合、ecdysisは非同期ランタイム要件なしの操作をサポートします。
systemd-notify サポート: systemd_notify 機能を有効にすると、ecdysis は systemd のプロセスライフサイクル通知と自動的に統合されます。サービスユニットファイルでType=notify-reloadを設定すると、systemdがアップグレードを正しく追跡できるようになります。
systemd named sockets: systemd_sockets機能によって、ecdysisはsystemdでアクティブ化されたソケットを管理することができます。サービスはソケットでアクティブ化し、グレースフルリスタートを同時にサポートできます。
プラットフォームの注意:ecdysisは、ソケットの継承とプロセス管理のためにUnix固有のsyscallに依存しています。Windowsでは動作しません。これは、フォーク手法の基本的な制限です。
グレースフルリスタートには、セキュリティ上の考慮事項があります。フォークモデルでは、2つのプロセス世代が共存する短いウィンドウが作成されます。両方が同じリッスンソケットと潜在的に機密性のあるファイル記述子にアクセスできます。
ecdysisは、設計を通じてこれらの懸念に対処しています。
Fork-then-exec: ecdysisは、fork()の後にexecve()が続く従来のUnixパターンに従います。これにより、子プロセスは、新しいアドレス空間、新しいコード、およびメモリを引き継ぎない状態で開始されます。明示的に渡されたファイル記述子のみが境界を越えます。
明示的な引き継ぎ:リッスンソケットと通信パイプのみが引き継がれます。その他のファイル記述子は、CLOEXECフラグを介してクローズされます。これにより、機密性の高いアドレスの偶発的な漏洩を防ぐことができます。
seccompの互換性: seccompフィルタを使用するサービスは、fork()とexecve()を許可しなければなりません。グレースフルリスタートにはシステムコールが必要なため、ブロックすることはできません。
ほとんどのネットワークサービスでは、こうしたトレードオフは許容されます。フォーク実行モデルのセキュリティはよく理解されており、NGINXやApacheなどのソフトウェアで数十年にわたって実地テストが行われています。
実際の例を見てみましょう。以下は、グレースフルリスタートをサポートする簡略化されたTCPエコーサーバーです。
use ecdysis::tokio_ecdysis::{SignalKind, StopOnShutdown, TokioEcdysisBuilder};
use tokio::{net::TcpStream, task::JoinSet};
use futures::StreamExt;
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
// Create the ecdysis builder
let mut ecdysis_builder = TokioEcdysisBuilder::new(
SignalKind::hangup() // Trigger upgrade/reload on SIGHUP
).unwrap();
// Trigger stop on SIGUSR1
ecdysis_builder
.stop_on_signal(SignalKind::user_defined1())
.unwrap();
// Create listening socket - will be inherited by children
let addr: SocketAddr = "0.0.0.0:8080".parse().unwrap();
let stream = ecdysis_builder
.build_listen_tcp(StopOnShutdown::Yes, addr, |builder, addr| {
builder.set_reuse_address(true)?;
builder.bind(&addr.into())?;
builder.listen(128)?;
Ok(builder.into())
})
.unwrap();
// Spawn task to handle connections
let server_handle = tokio::spawn(async move {
let mut stream = stream;
let mut set = JoinSet::new();
while let Some(Ok(socket)) = stream.next().await {
set.spawn(handle_connection(socket));
}
set.join_all().await;
});
// Signal readiness and wait for shutdown
let (_ecdysis, shutdown_fut) = ecdysis_builder.ready().unwrap();
let shutdown_reason = shutdown_fut.await;
log::info!("Shutting down: {:?}", shutdown_reason);
// Gracefully drain connections
server_handle.await.unwrap();
}
async fn handle_connection(mut socket: TcpStream) {
// Echo connection logic here
}
主要ポイント:
build_listen_tcpは、子プロセスに引き継がれるリスナーを作成します。
ready() は、初期化が完了し、安全に終了できることを親プロセスに通知します。
shutdown_fut.await は、アップグレードまたは停止が要求されるまでブロックします。この未来は、アップグレード/リロードが正常に実行された、またはシャットダウンシグナルを受信したために、プロセスをシャットダウンする必要がある場合にのみ発生します。
このプロセスにSIGHUPを送信すると、ecdysisは次のような動作をします。
…親プロセス上:
...子プロセス上:
親と同じ実行フローに従って自身を初期化しますが、ecdysisが所有するソケットは継承され、子にバインドされません。
ready()を呼び出して、親に準備完了を知らせます。
シャットダウンまたはアップグレードシグナル待ちのブロック。
ECdysisはCloudflareで2021年から稼働しています。120か国以上、330以上のデータセンターにデプロイされた重要なRustのインフラストラクチャサービスを支えています。これらのサービスは、1日あたり数十億件のリクエストを処理し、セキュリティパッチ、機能リリース、設定変更のための頻繁な更新を必要としています。
Ecdysisを使用して再起動を行うたびに、単純なStop/Startサイクルではドロップされる可能性がある何十万ものリクエストが保存されます。グローバルフットプリント全体で、これは何百万もの接続の維持とお客様のための信頼性の向上につながります。
いくつかのエコシステム用のグレースフルリスタートライブラリが存在します。適切なツールを選択するためには、ECサイトを使用するタイミングと代替ツールを理解することが重要です。
tableflip は、ecdysisの着想元となった当社のGoライブラリです。Goサービスと同じフォークとインジェクションモデルを実装しています。Goが必要なら、tableflipをお勧めします!
shellflip は、CloudflareのRustベースのプロキシであるOxy用に特別に設計された、Cloudflareの他のRustグレースフルリスタートライブラリです。Shellflipは、システム化と時発生を想定し、親と子の間で任意のアプリケーション状態を転送することに焦点を当てています。これは、複雑なステートフルサービスや、独自のソケットを開くことさえできない積極的なサンドボックスを適用したいサービスに優れていますが、より単純なケースではオーバーヘッドが追加されます。
ecdysisは、Rustのエコシステムに5年間にわたる本番ハードウェアとしてのグレースフルリスタート機能を提供します。これは、Cloudflareのグローバルネットワーク全体の何百万もの接続を保護しているのと同じ技術で、現在オープンソース化され、誰でも利用できるようになっています。
完全なドキュメントは、docs.rs/ecdysisでご覧いただけます。API参照、一般的なユースケースの例、systemdとの統合手順など。
リポジトリのサンプルディレクトリには、TCPリスナー、Unixソケットリスナー、およびsystemd統合を示す動作コードが含まれています。
このライブラリは、Argo Smart Routing & Orpheusチームによって積極的に保守されており、Cloudflare全体のチームの貢献も受けています。GitHubでの貢献、バグ報告、機能リクエストを歓迎します。
高性能プロキシを構築する場合でも、長期間維持されるAPIサーバーでも、稼働率が重要なネットワークサービスであっても、ECdysisはダウンタイムゼロの運用基盤を提供できます。
構築を開始する: github.com/cloudflare/ecdysis