ecdysisによる古いコードの脱皮:CloudflareのRustサービスにおける優雅な再起動
ecdysisはRustライブラリで、ネットワークサービスのダウンタイムゼロアップグレードを実現。Cloudflareで5年間使用後、オープンソース化。
キーポイント
Cloudflareが5年間の実運用を経て、ゼロダウンタイムでRustサービスをアップグレードするライブラリ「ecdysis」をオープンソース化
大規模ネットワークサービスにおけるグレースフルリスタートの技術的課題(接続拒否・切断)を解決する実用的なソリューション
Rustエコシステムにおける本番環境での信頼性と可用性向上に貢献する重要なインフラツール
影響分析・編集コメントを表示
影響分析
この記事は、大規模分散システムにおけるサービスアップグレードの根本的な課題に対する実用的な解決策を提供しており、特に高可用性が求められるクラウドインフラ業界に大きな影響を与える。Cloudflareの実績に裏打ちされた技術がオープンソース化されることで、Rustコミュニティ全体の信頼性向上と、ゼロダウンタイムデプロイメントの実現可能性を高める。
編集コメント
実運用で5年間検証された技術のオープンソース化は、Rustエコシステムの成熟度を示す重要なマイルストーン。大規模システムの運用ノウハウが広く共有されることで、業界全体の信頼性向上に寄与する。
ecdysisによる古いコードの脱皮:CloudflareのRustサービスにおけるグレースフルな再起動
Manuel Olguín Muñoz
ecdysis | ˈekdəsəs |
(爬虫類における)古い皮を脱ぐ、または(昆虫やその他の節足動物における)外殻を脱ぎ捨てるプロセス。
世界中で毎秒数百万リクエストを処理するネットワークサービスを、たった一つの接続も切断することなく、どのようにアップグレードすればよいでしょうか?
この巨大な課題に対するCloudflareでの解決策の一つが、長らくecdysisでした。これは、稼働中の接続が一つも切断されず、新しい接続も拒否されないグレースフルなプロセス再起動を実装するRustライブラリです。
先月、私たちはecdysisをオープンソース化しました。これで誰でも利用できるようになりました。Cloudflareでの5年間の本番運用を経て、ecdysisは、当社の重要なRustインフラストラクチャ全体でダウンタイムゼロのアップグレードを可能にし、Cloudflareのグローバルネットワークにおける各再起動ごとに数百万のリクエストを救うことで、その実力を証明しています。
特にCloudflareネットワークの規模では、これらのアップグレードを正しく行うことの重要性はいくら強調してもしすぎることはありません。当社のサービスの多くは、トラフィックルーティング、TLSライフサイクル管理、ファイアウォールルールの適用などの重要なタスクを実行しており、継続的に動作しなければなりません。これらのサービスの一つがダウンした場合、ほんの一瞬であっても、連鎖的な影響は壊滅的になり得ます。切断された接続と失敗したリクエストは、すぐに顧客のパフォーマンス低下とビジネスへの影響につながります。
これらのサービスに更新が必要な場合、セキュリティパッチは待てません。バグ修正はデプロイする必要があり、新機能はロールアウトされなければなりません。
単純なアプローチでは、古いプロセスが停止するのを待ってから新しいプロセスを起動しますが、これでは接続が拒否されリクエストがドロップされる時間の窓が生じてしまいます。一箇所で毎秒数千のリクエストを処理するサービスを、数百のデータセンターで掛け合わせれば、短い再起動が世界中で数百万の失敗したリクエストになるのです。
問題と、ecdysisがどのように私たちの解決策となったか(そしておそらくあなたにとってもそうなるかもしれないか)を掘り下げてみましょう。
リンク: GitHub | crates.io | docs.rs
なぜグレースフルな再起動が難しいのか
前述のように、サービスを再起動する単純なアプローチは、古いプロセスを停止して新しいプロセスを起動することです。これはリアルタイムリクエストを処理しない単純なサービスでは許容範囲で機能しますが、ライブ接続を処理するネットワークサービスでは、このアプローチには重大な制限があります。
第一に、単純なアプローチでは、着信接続をリッスンするプロセスが存在しない時間の窓が生じます。古いプロセスが停止すると、そのリッスンソケットを閉じ、これによりOSは新しい接続をECONNREFUSEDですぐに拒否します。
第二に、古いプロセスを停止すると、すでに確立されたすべての接続が切断されます。大きなファイルをアップロードしているクライアントやビデオをストリーミングしているクライアントは、突然切断されます。WebSocketやgRPCストリームのような長寿命の接続は、操作の途中で終了します。クライアントの視点では、サービスが単に消えてしまいます。
古いプロセスをシャットダウンする前に新しいプロセスをバインドすれば、これは解決するように見えますが、同時に追加の問題も引き起こします。カーネルは通常、一つのプロセスだけがアドレス:ポートの組み合わせにバインドすることを許可しますが、SO_REUSEPORTソケットオプションは複数のバインドを許可します。しかし、これによりプロセス遷移時に問題が生じ、グレースフルな再起動には適さなくなります。
SO_REUSEPORTが有効な場合、複数のプロセスが同じアドレスとポートにバインドできます。しかし、カーネルはこれらのプロセス間で接続を分配します。古いプロセスが停止すると、そのプロセスに割り当てられていた接続はすべて切断されます。新しいプロセスは新しい接続を受け付けますが、古いプロセスに割り当てられていた既存の接続は失われます。これではグレースフルな再起動にはなりません。
ecdysisの仕組み
ecdysisの設計と構築に着手した際、私たちはこのライブラリに対して4つの主要な目標を設定しました:
アップグレード後、古いコードを完全にシャットダウンできること。
新しいプロセスには初期化のための猶予期間があること。
初期化中に新しいコードがクラッシュしても許容され、稼働中のサービスに影響を与えないこと。
連鎖的な障害を避けるため、並行して実行されるアップグレードは一つだけであること。
ecdysisは、初期の頃からグレースフルなアップグレードをサポートしてきたNGINXによって開拓されたアプローチに従い、これらの要件を満たします。そのアプローチは単純明快です:
親プロセスがfork()を呼び出して子プロセスを作成します。
子プロセスはexecve()で自身を新しいバージョンのコードに置き換えます。
子プロセスは、親と共有する名前付きパイプを介してソケットファイル記述子を継承します。
親プロセスは、シャットダウンする前に子プロセスが準備完了のシグナルを送るのを待ちます。
重要な点は、ソケットが遷移全体を通じて開いたままであることです。子プロセスは、名前付きパイプを介して共有されたファイル記述子として、親からリッスンソケットを継承します。子の初期化中、両プロセスは同じ基盤となるカーネルデータ構造を共有し、親は新しい接続と既存の接続の受け付けと処理を継続できます。子が初期化を完了すると、親に通知し、接続の受け付けを開始します。この準備完了通知を受け取ると、親は直ちに自身のリッスンソケットのコピーを閉じ、既存の接続の処理のみを継続します。
このプロセスにより、カバレッジのギャップを排除しつつ、子に安全な初期化ウィンドウを提供します。親と子の両方が同時に接続を受け付ける可能性がある短い時間の窓があります。これは意図的なものです。親によって受け付けられた接続は、ドレイン処理の一部として単純に完了まで処理されます。
このモデルはまた、必要なクラッシュ安全性も提供します。もし子プロセスが初期化中に(例えば設定エラーにより)失敗した場合、単に終了します。親はリッスンを止めていないため、接続は切断されず、問題が修正されればアップグレードを再試行できます。
ecdysisは、Tokioとsystemdを通じた非同期プログラミングのファーストクラスサポートを備えたフォークモデルを実装しています。
Tokio統合: Tokioのためのネイティブな非同期ストリームラッパー。継承されたソケットは追加のグルーコードなしでリスナーになります。同期サービスの場合、ecdysisは非同期ランタイム要件なしでの動作をサポートします。
systemd-notifyサポート: systemd_notify機能が有効な場合、ecdysisは親プロセスと子プロセスの両方でsd_notify(3)呼び出しを管理し、systemdに状態を通知します。これにより、サービスユニットでType=notify-reloadを使用できるようになり、systemdがアップグレードを追跡し、失敗した場合にサービスを再起動できるようになります。
systemd名前付きソケット: systemd_sockets機能が有効な場合、ecdysisはsystemdから継承されたソケットを自動的に検出し、それらを新しいプロセスに渡します。これにより、systemdのソケットアクティベーションメカニズムとシームレスに統合できます。
プラットフォームに関する注意: ecdysisは、ソケット継承とプロセス管理のためにUnix固有のシステムコールに依存しています。Windowsでは動作しません。これはフォーキングアプローチの根本的な制限です。
セキュリティ上の考慮事項
グレースフルな再起動はセキュリティ上の考慮事項を導入します。フォークモデルでは、2つのプロセス世代が共存する短い時間の窓が生じ、両方が同じリッスンソケットと潜在的に機密性の高いファイル記述子にアクセスできる状態になります。
ecdysisはその設計を通じてこれらの懸念に対処します:
フォーク後実行: ecdysisは、fork()の直後にexecve()を呼び出すという伝統的なUnixパターンに従います。これにより、子プロセスは親のメモリ空間や実行状態を継承しません。子は新しいバイナリをゼロからロードします。
明示的な継承: リッスンソケットと通信パイプのみが継承されます。他のファイル記述子はCLOEXEC(exec時に閉じる)フラグによって閉じられます。これにより、子が意図せず機密性の高いファイル記述子にアクセスすることを防ぎます。
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() { // ecdysisビルダーを作成 let mut ecdysis_builder = TokioEcdysisBuilder::new( SignalKind::hangup() // SIGHUPでアップグレード/リロードをトリガー ).unwrap(); // SIGUSR1で停止をトリガー ecdysis_builder .stop_on_signal(SignalKind::user_defined1()) .unwrap(); // リッスンソケットを作成 - 子プロセスに継承される 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(); // 接続を処理するタスクをスポーン let server_handle = tokio::spawn(async move { l
原文を表示
Shedding old code with ecdysis: graceful restarts for Rust services at Cloudflare
Manuel Olguín Muñoz
ecdysis | ˈekdəsəs |
the process of shedding the old skin (in reptiles) or casting off the outer cuticle (in insects and other arthropods).
How do you upgrade a network service, handling millions of requests per second around the globe, without disrupting even a single connection?
One of our solutions at Cloudflare to this massive challenge has long been ecdysis, a Rust library that implements graceful process restarts where no live connections are dropped, and no new connections are refused.
Last month, we open-sourced ecdysis, so now anyone can use it. After five years of production use at Cloudflare, ecdysis has proven itself by enabling zero-downtime upgrades across our critical Rust infrastructure, saving millions of requests with every restart across Cloudflare’s global network.
It’s hard to overstate the importance of getting these upgrades right, especially at the scale of Cloudflare’s network. Many of our services perform critical tasks such as traffic routing, TLS lifecycle management, or firewall rules enforcement, and must operate continuously. If one of these services goes down, even for an instant, the cascading impact can be catastrophic. Dropped connections and failed requests quickly lead to degraded customer performance and business impact.
When these services need updates, security patches can’t wait. Bug fixes need deployment and new features must roll out.
The naive approach involves waiting for the old process to be stopped before spinning up the new one, but this creates a window of time where connections are refused and requests are dropped. For a service handling thousands of requests per second in a single location, multiply that across hundreds of data centers, and a brief restart becomes millions of failed requests globally.
Let’s dig into the problem, and how ecdysis has been the solution for us — and maybe will be for you.
Links: GitHub | crates.io | docs.rs
Why graceful restarts are hard
The naive approach to restarting a service, as we mentioned, is to stop the old process and start a new one. This works acceptably for simple services that don’t handle real-time requests, but for network services processing live connections, this approach has critical limitations.
First, the naive approach creates a window during which no process is listening for incoming connections. When the old process stops, it closes its listening sockets, which causes the OS to immediately refuse new connections with ECONNREFUSED
Second, stopping the old process kills all already-established connections. A client uploading a large file or streaming video gets abruptly disconnected. Long-lived connections like WebSockets or gRPC streams are terminated mid-operation. From the client’s perspective, the service simply vanishes.
Binding the new process before shutting down the old one appears to solve this, but also introduces additional issues. The kernel normally allows only one process to bind to an address:port combination, but the SO_REUSEPORT socket option permits multiple binds. However, this creates a problem during process transitions that makes it unsuitable for graceful restarts.
When SO_REUSEPORT
How ecdysis works
When we set out to design and build ecdysis, we identified four key goals for the library:
Old code can be completely shut down post-upgrade.
The new process has a grace period for initialization.
New code crashing during initialization is acceptable and shouldn’t affect the running service.
Only a single upgrade runs in parallel to avoid cascading failures.
ecdysis satisfies these requirements following an approach pioneered by NGINX, which has supported graceful upgrades since its early days. The approach is straightforward:
The parent process fork()
The child process replaces itself with a new version of the code with execve()
The child process inherits the socket file descriptors via a named pipe shared with the parent.
The parent process waits for the child process to signal readiness before shutting down.
Crucially, the socket remains open throughout the transition. The child process inherits the listening socket from the parent as a file descriptor shared via a named pipe. During the child's initialization, both processes share the same underlying kernel data structure, allowing the parent to continue accepting and processing new and existing connections. Once the child completes initialization, it notifies the parent and begins accepting connections. Upon receiving this ready notification, the parent immediately closes its copy of the listening socket and continues handling only existing connections.
This process eliminates coverage gaps while providing the child a safe initialization window. There is a brief window of time when both the parent and child may accept connections concurrently. This is intentional; any connections accepted by the parent are simply handled until completion as part of the draining process.
This model also provides the required crash safety. If the child process fails during initialization (e.g., due to a configuration error), it simply exits. Since the parent never stopped listening, no connections are dropped, and the upgrade can be retried once the problem is fixed.
ecdysis implements the forking model with first-class support for asynchronous programming through Tokio and systemd
Tokio integration: Native async stream wrappers for Tokio. Inherited sockets become listeners without additional glue code. For synchronous services, ecdysis supports operation without async runtime requirements.
systemd-notify support: When the systemd_notify
Type=notify-reload
systemd named sockets: The systemd_sockets
Platform note: ecdysis relies on Unix-specific syscalls for socket inheritance and process management. It does not work on Windows. This is a fundamental limitation of the forking approach.
Security considerations
Graceful restarts introduce security considerations. The forking model creates a brief window where two process generations coexist, both with access to the same listening sockets and potentially sensitive file descriptors.
ecdysis addresses these concerns through its design:
Fork-then-exec: ecdysis follows the traditional Unix pattern of fork()
Explicit inheritance: Only listening sockets and communication pipes are inherited. Other file descriptors are closed via CLOEXEC
seccomp compatibility: Services using seccomp filters must allow fork()
For most network services, these tradeoffs are acceptable. The security of the fork-exec model is well understood and has been battle-tested for decades in software like NGINX and Apache.
Let’s look at a practical example. Here’s a simplified TCP echo server that supports graceful restarts:
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 }
The key points:
build_listen_tcp
shutdown_fut.await
When you send SIGHUP
…on the parent process:
Forks and execs a new instance of your binary.
Passes the listening socket to the child.
Waits for the child to call ready()
Drains existing connections, then exits.
…on the child process:
Initializes itself following the same execution flow as the parent, except any sockets owned by ecdysis are inherited and not bound by the child.
Signals readiness to the parent by calling ready()
Blocks waiting for a shutdown or upgrade signal.
Production at scale
ecdysis has been running in production at Cloudflare since 2021. It powers critical Rust infrastructure services deployed across 330+ data centers in 120+ countries. These services handle billions of requests per day and require frequent updates for security patches, feature releases, and configuration changes.
Every restart using ecdysis saves hundreds of thousands of requests that would otherwise be dropped during a naive stop/start cycle. Across our global footprint, this translates to millions of preserved connections and improved reliability for customers.
ecdysis vs alternatives
Graceful restart libraries exist for several ecosystems. Understanding when to use ecdysis versus alternatives is critical to choosing the right tool.
tableflip is our Go library that inspired ecdysis. It implements the same fork-and-inherit model for Go services. If you need Go, tableflip is a great option!
shellflip is Cloudflare’s other Rust graceful restart library, designed specifically for Oxy, our Rust-based proxy. shellflip is more opinionated: it assumes systemd and Tokio, and focuses on transferring arbitrary application state between parent and child. This makes it excellent for complex stateful services, or services that want to apply such aggressive sandboxing that they can’t even open their own sockets, but adds overhead for simpler cases.
ecdysis brings five years of production-hardened graceful restart capabilities to the Rust ecosystem. It’s the same technology protecting millions of connections across Cloudflare’s global network, now open-sourced and available for anyone!
Full documentation is available at docs.rs/ecdysis, including API reference, examples for common use cases, and steps for integrating with systemd
The examples directory in the repository contains working code demonstrating TCP listeners, Unix socket listeners, and systemd integration.
The library is actively maintained by the Argo Smart Routing & Orpheus team, with contributions from teams across Cloudflare. We welcome contributions, bug reports, and feature requests on GitHub.
Whether you’re building a high-performance proxy, a long-lived API server, or any network service where uptime matters, ecdysis can provide a foundation for zero-downtime operations.
Start building: github.com/cloudflare/ecdysis

関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み