Cloudflare Gen 13 서버 도입기: 캐시를 포기하고 코어를 취하다 (그리고 Rust가 이를 구원한 방법)
엔지니어링 씬에서 하드웨어 업그레이드는 보통 ‘공짜 점심’으로 여겨집니다. 새로운 CPU를 서버 랙에 꽂기만 하면 Latency는 줄어들고 Throughput은 늘어나니까요. 하지만 인프라 규모가 글로벌 엣지 수준으로 커지면 이야기가 달라집니다.
최근 Cloudflare가 발표한 Gen 13 서버 도입 블로그 포스트는 시니어 엔지니어라면 누구나 흥미로워할 만한 딜레마를 다루고 있습니다. 바로 코어 수를 늘리기 위해 L3 캐시를 포기해야 하는 상황입니다. 오늘은 이들이 직면했던 하드웨어 병목 현상과, 이를 소프트웨어 아키텍처(Rust 재작성)로 어떻게 극복했는지 깊게 파헤쳐 보겠습니다.
하드웨어의 딜레마: 코어 수 vs L3 캐시 용량
Cloudflare의 이전 세대인 Gen 12 서버는 AMD EPYC Genoa-X 프로세서를 사용했습니다. 이 칩의 핵심은 거대한 3D V-Cache였습니다. 하지만 트래픽이 폭증하면서 더 높은 Throughput이 필요해졌고, 이들은 차세대 AMD Turin 프로세서(Gen 13)를 평가하게 됩니다.
여기서 문제가 발생합니다. 최상위 밀도를 자랑하는 Turin 9965 모델은 무려 192개의 코어를 제공하지만, 코어당 L3 캐시 할당량은 Gen 12의 12MB에서 2MB로 1/6 토막이 나버렸습니다.
- Gen 12 (Genoa-X): 96코어, 코어당 12MB L3 캐시
- Gen 13 (Turin 9965): 192코어, 코어당 2MB L3 캐시
캐시 용량이 줄어들면 어떤 일이 벌어질까요? Cloudflare의 프로파일링 데이터에 따르면, L3 캐시 적중 시 약 50 사이클이 소요되지만, 캐시 미스로 인해 메인 메모리(DRAM)까지 다녀오면 350 사이클 이상이 소요됩니다. 기존 NGINX 및 LuaJIT 기반의 레거시 스택인 FL1은 캐시 지역성(Cache locality)에 크게 의존하고 있었기 때문에, CPU 사용률이 높아질수록 캐시 경합이 심해져 Latency가 50% 이상 치솟는 치명적인 성능 저하를 겪었습니다.
하드웨어 튜닝의 한계와 기시감
이 문제를 해결하기 위해 Cloudflare와 AMD 엔지니어들은 온갖 하드웨어 레벨의 튜닝을 시도했습니다. Hardware prefetcher 조정, NUMA 노드 피닝, 그리고 PQOS(Platform Quality of Service)를 이용한 L3 캐시 파티셔닝까지 말이죠.

개인적으로 이 대목에서 깊은 공감을 했습니다. 저 역시 과거 대규모 트래픽을 처리하는 API 게이트웨이를 최적화할 때, 캐시 경합 문제를 해결하겠다고 CPU isolation과 NUMA 튜닝으로 밤을 새운 적이 있거든요. 하지만 결론은 항상 비슷합니다. 하드웨어 튜닝으로 얻을 수 있는 이득은 기껏해야 10~15% 내외이며, 근본적인 소프트웨어의 메모리 접근 패턴을 바꾸지 않으면 한계에 부딪힌다는 것입니다.
진정한 해결책: Rust를 통한 메모리 레이아웃 통제
Hacker News의 한 유저는 Cloudflare의 글을 읽고 이렇게 비판했습니다. “Rust로 재작성해서 캐시 문제를 해결했다고 하는데, 너무 모호하다. 동적 언어에서 정적 언어로 넘어오면서 메모리 레이아웃 제어권이 생겨서 그런 것 아닌가?”
정확한 지적입니다. Cloudflare는 Pingora와 Oxy 프레임워크를 기반으로 한 Rust 기반의 새로운 요청 처리 레이어인 FL2를 개발 중이었습니다. 이 FL2가 Gen 13 서버에 올라가자 기적처럼 캐시 병목이 사라졌습니다.
단순히 ‘Rust가 빠르기 때문’이 아닙니다. 핵심은 메모리 레이아웃과 데이터 구조에 있습니다. Lua와 같은 동적 타입 언어는 데이터 구조가 포인터로 연결된 형태(Pointer-heavy)를 띠기 쉽습니다. 힙(Heap) 메모리 곳곳에 흩어진 데이터를 쫓아다니는 Pointer chasing은 CPU 캐시를 무용지물로 만듭니다.
반면 Rust는 개발자가 데이터의 메모리 배치를 엄격하게 제어할 수 있습니다.
// Rust에서는 데이터가 메모리상에 연속적으로 배치되도록 강제할 수 있습니다.
#[repr(C)]
struct RequestContext {
headers_ptr: *const u8,
body_len: usize,
is_authenticated: bool,
// ...
}
// 힙 할당을 최소화하고 스택이나 연속된 메모리 풀을 사용
let contexts: Vec<RequestContext> = Vec::with_capacity(1000);
값 타입(Value types)을 사용하고 동적 할당을 최소화하면, CPU가 메모리를 읽을 때 공간적 지역성(Spatial locality)이 극대화됩니다. 즉, 한 번 캐시에 올라온 데이터 블록 안에 필요한 정보가 모두 들어있게 되는 것이죠. FL2의 이러한 깔끔한 아키텍처 덕분에 거대한 L3 캐시에 의존할 필요가 없어졌고, 코어 수가 늘어나는 만큼 Throughput이 선형적으로 증가할 수 있었습니다.
마케팅의 함정과 숨겨진 영웅, LuaJIT
물론 이 블로그 포스트에 아쉬운 점도 있습니다. Hacker News 커뮤니티에서 예리하게 지적했듯, Cloudflare는 FL2 + Gen 13의 결합된 벤치마크만 보여주었습니다.
- 아쉬운 점: Gen 12에서 FL1과 FL2를 비교한 데이터가 빠져 있습니다. 소프트웨어 아키텍처 개선으로 얻은 이득과 하드웨어 업그레이드로 얻은 이득이 혼재되어 있어, 순수한 하드웨어의 발전량을 가늠하기 어렵습니다. 시니어 엔지니어로서 이런 식의 마케팅 섞인 지표는 조금 불편하게 다가옵니다.
- 숨겨진 영웅: 또 하나 짚고 넘어갈 점은 LuaJIT의 경이로움입니다. 15년 동안 전 세계 인터넷 트래픽의 상당 부분을 처리해 온 FL1 스택의 기반이 바로 NGINX와 LuaJIT이었습니다. 캐시 용량이 1/6로 줄어드는 극한의 하드웨어 변화가 오기 전까지는 Rust로 짠 최신 스택과 비견될 만큼 버텨주었다는 뜻이죠. LuaJIT이 얼마나 시대를 앞서간 괴물 같은 프로젝트였는지 새삼 깨닫게 됩니다.
결론: Hardware-Software Co-design의 시대
Cloudflare의 Gen 13 도입기는 우리에게 중요한 교훈을 남깁니다. 무어의 법칙이 둔화되고 멀티코어 밀도가 극단적으로 높아지는 현대의 서버 아키텍처에서는, 더 이상 하드웨어 스펙만으로 성능을 보장할 수 없습니다.
결국 소프트웨어가 하드웨어의 특성(적은 캐시, 많은 코어, NUMA 아키텍처 등)을 이해하고 그에 맞춰 진화해야 합니다. 메모리 안전성뿐만 아니라 예측 가능한 메모리 접근 패턴을 강제할 수 있는 Rust 같은 언어가 인프라 레벨에서 각광받는 것은 우연이 아닙니다.
단순히 코드를 짜는 것을 넘어, CPU 캐시 라인과 메모리 대역폭까지 고민할 수 있는 엔지니어의 가치는 앞으로 더욱 높아질 것입니다.
References: