뱀의 탈피에서 배운 서버 재시작 — Rust로 커넥션 제로 로스를 구현하는 ecdysis

9 min read
RustCloudflaregraceful-restartecdysisopen-source
뱀의 탈피에서 배운 서버 재시작 — Rust로 커넥션 제로 로스를 구현하는 ecdysis

달리는 기차의 바퀴를 멈추지 않고 교체할 수 있을까? 불가능하게 들리지만, 서버 세계에서는 이것과 비슷한 일을 매일 해야 한다. 프로세스를 새 버전으로 교체하면서 수십만 개의 활성 커넥션을 단 하나도 끊지 않는 것. Cloudflare는 이 문제를 5년 전부터 Rust로 풀어왔고, 그 결과물인 ecdysis를 오픈소스로 공개했다.

ecdysis — 허물을 벗는 서버

ecdysis는 생물학에서 뱀이나 곤충이 허물을 벗는 탈피를 뜻한다. 낡은 바이너리를 벗어던지고 새 바이너리로 갈아입는다는 의미를 이름에 담았다. 핵심 기능은 단순하다. 라이브 커넥션을 하나도 끊지 않고 프로세스를 교체하는 것이다.

2021년부터 Cloudflare 프로덕션에 투입되어 330개 이상의 데이터센터, 120개국 이상에서 매일 수십억 건의 요청을 처리하는 핵심 인프라에 사용되어 왔다. "실전에서 검증됐다"는 말이 가장 잘 어울리는 라이브러리다.

[💡 잠깐! 이 용어는?] Graceful Restart(무중단 재시작): 실행 중인 프로세스를 새 버전으로 교체하면서 기존 커넥션을 유지하는 기법이다. 클라이언트 입장에서는 서버가 재시작됐는지 알 수 없다. NGINX, HAProxy 등이 이 방식을 사용한다.


동작 원리 — 교대 근무의 인수인계

ecdysis의 재시작 흐름은 야간 교대 근무에 비유할 수 있다. 퇴근하는 직원이 출근한 직원에게 진행 중인 업무 파일을 전부 넘기고, 새 직원이 "준비 됐다"고 말한 뒤에야 퇴근하는 프로세스다.

1단계 — fork()로 자식 생성

부모 프로세스가 fork()를 호출해 자식 프로세스를 만든다. 이 시점에서 자식은 부모의 복제품이다.

2단계 — execve()로 새 바이너리 적재

자식 프로세스가 즉시 execve()를 실행해 자기 자신을 새 바이너리로 교체한다. 새로운 주소 공간, 새로운 코드, 부모로부터 상속된 메모리는 전부 사라진다. 깨끗한 상태에서 시작한다.

3단계 — Named Pipe로 소켓 전달

부모가 보유한 소켓 파일 디스크립터를 Named Pipe를 통해 자식에게 넘긴다. 자식이 소켓을 물려받는 순간부터 새 커넥션을 수락할 수 있다.

4단계 — 부모 종료

자식이 "준비 완료" 시그널을 보내면 부모는 기존 커넥션의 처리를 마무리한 뒤 조용히 종료된다.

ecdysis-기본-사용.rs
use ecdysis::GracefulRestart;
 
#[tokio::main]
async fn main() {
    let restart = GracefulRestart::new();
 
    let listener = restart
        .tcp_listener("0.0.0.0:8080")
        .await
        .expect("리스너 바인딩 실패");
 
    restart.ready();
 
    loop {
        let (stream, addr) = listener.accept().await.unwrap();
        tokio::spawn(async move {
            handle_connection(stream, addr).await;
        });
    }
}

[💡 잠깐! 이 용어는?] Named Pipe: 프로세스 간 통신(IPC)에 쓰이는 Unix 파일 시스템 객체다. 일반 파이프는 부모-자식 관계에서만 동작하지만, Named Pipe는 이름이 있어 관계 없는 프로세스끼리도 데이터를 주고받을 수 있다.


재시작 도중 커넥션은 어디로 가는가

무중단 재시작에서 가장 까다로운 구간은 부모와 자식이 동시에 존재하는 짧은 시간이다. 이 구간에서 ecdysis는 양쪽 모두 커넥션을 수락하도록 설계되어 있다. 의도적인 오버랩이다.

재시작-타임라인.txt
시간 →
[부모 프로세스]  ████████████████████░░░░ (기존 커넥션 마무리 후 종료)
[자식 프로세스]            ░░░████████████████████ (새 커넥션 수락 시작)

                    소켓 전달 완료 지점
                    양쪽 모두 수락 가능 구간

부모가 이미 수락한 커넥션은 부모가 끝까지 처리한다. 새로 들어오는 커넥션은 자식이 받는다. 커넥션 유실 구간이 제로다. 이걸 비유하면 릴레이 경주에서 바통 터치 구간과 같다. 주자 두 명이 동시에 달리는 짧은 구간이 있고, 바통이 넘어간 뒤에야 앞 주자가 멈춘다.


ecdysis vs shellflip — 언제 뭘 쓰는가

Cloudflare는 사실 무중단 재시작 라이브러리를 두 개 보유하고 있다. 상황에 따라 적합한 도구가 다르다.

기준ecdysisshellflip
설계 철학범용 경량상태 전송 특화
의존성최소한systemd + Tokio 필수
소켓 전달 방식Named PipeUnix Domain Socket
상태 전달소켓만임의의 애플리케이션 상태
코드 복잡도낮음높음
적합한 서비스단순 TCP/UDP 서버Oxy 같은 복잡한 프록시

단순한 네트워크 서비스라면 ecdysis가 낫다. 세션 상태, 인메모리 캐시 같은 애플리케이션 레벨 데이터까지 새 프로세스에 넘겨야 하는 복잡한 프록시라면 shellflip이 적합하다.


systemd와의 통합

ecdysis는 systemd 소켓 활성화(Socket Activation)와도 매끄럽게 연동된다. systemd_sockets 피처 플래그를 켜면 된다.

Cargo.toml
[dependencies]
ecdysis = { version = "0.1", features = ["systemd_sockets"] }
systemd-소켓-활성화.rs
use ecdysis::GracefulRestart;
 
#[tokio::main]
async fn main() {
    let restart = GracefulRestart::new()
        .with_systemd_sockets();
 
    let listener = restart
        .systemd_tcp_listener("my-service.socket")
        .await
        .expect("systemd 소켓 바인딩 실패");
 
    restart.notify_ready();
 
    loop {
        let (stream, _) = listener.accept().await.unwrap();
        tokio::spawn(handle_connection(stream));
    }
}

서비스가 처음 시작될 때는 systemd가 소켓을 넘겨주고, 이후 재시작 시에는 ecdysis가 소켓을 넘겨받는다. 두 메커니즘이 충돌 없이 공존한다.

[💡 잠깐! 이 용어는?] Socket Activation: systemd가 서비스 대신 소켓을 미리 열어두고, 첫 번째 커넥션이 들어올 때 서비스를 깨우는 방식이다. 서비스가 죽어도 소켓은 살아있어 커넥션 유실을 방지하고, 부팅 시 서비스 시작 순서 문제도 해결한다.


마무리

서버를 재시작하면서 커넥션을 하나도 잃지 않는 것은 인프라 엔지니어링의 오래된 숙제다. ecdysis는 이 숙제를 330개 데이터센터에서 5년간 증명한 방식으로 풀어냈다. API 표면이 작고 의존성이 적어, 기존 Rust 서비스에 빠르게 통합할 수 있다. 커넥션 유실 없는 재시작이 필요한 네트워크 서비스를 운영한다면, 가장 먼저 살펴볼 만한 선택지다.


참고:

관심 있을 만한 포스트

Docfind — Rust와 WebAssembly로 만든 서버 없는 브라우저 검색 엔진

Microsoft VS Code 문서 팀이 Rust와 WebAssembly로 구현한 클라이언트 사이드 검색 엔진 Docfind의 내부 설계를 파헤친다.

RustWebAssembly

Cloudflare Code Mode — 2,500개 API를 1,000 토큰에 담는 MCP의 새로운 패턴

Cloudflare가 공개한 Code Mode는 AI 에이전트에게 수천 개의 API 엔드포인트를 단 2개 도구로 제공하는 MCP 서버 설계 패턴이다.

CloudflareMCP

Rolldown의 코드 스플리팅 — 비트셋 한 줄로 모듈의 소속을 결정하는 법

Vite의 차세대 번들러 Rolldown이 비트셋 기반 알고리즘으로 코드 스플리팅을 수행하는 원리를 분석한다.

RolldownVite

Accept: text/markdown — AI 에이전트가 HTML 대신 마크다운을 받는 시대

Cloudflare가 AI 에이전트를 위해 HTML을 마크다운으로 자동 변환하는 기능의 동작 원리와 의미를 살펴본다.

CloudflareAI Agent

좀비 TV 400만 대의 35초 — 2025년 DDoS 공격은 어떻게 역대 기록을 갈아치웠나

2025년 DDoS 공격이 전년 대비 두 배 이상 증가하고, 역대 최대 31.4 Tbps 공격이 기록된 Cloudflare Q4 리포트 분석.

DDoSCloudflare

Next.js 블로그 만들기 — GitHub Pages에서 Cloudflare Pages로 이전하기

GitHub Pages의 한계를 넘어 Cloudflare Pages로 블로그를 이전한 과정. 비교, 설정, SEO까지 한 번에 정리.

Cloudflare배포

38년 된 RFC의 복수 — DNS 레코드 순서가 뒤집히자 리눅스가 멈췄다

1987년 RFC 문서의 모호한 한 문장이 2025년 Cloudflare 1.1.1.1 장애로 이어진 과정과 그 기술적 원인을 파헤친다.

DNSCloudflare

AI 코딩의 맹점 — Artifacts 없이 에이전트는 기억을 잃는다

PRD, ADR, TDD가 AI 코딩 워크플로우에서 왜 선택이 아닌 필수인지, 실전 구조와 함께 살펴본다.

AI 코딩Artifacts