Rust가 Cloudflare를 어떻게 다운시켰는가

이미지

서론

11월에 굵직한 IT 서비스(롤, 디스코드 등)에 접속이 불가한 현상이 발생했다. Cloudflare가 장애의 원인이었다.

나도 Cloudflare의 Pages 서비스를 활용해서 서비스를 하나 배포하고 있었는데 접속을 시도해보니 실패하고 다음과 비슷한 창이 나왔다.

alt text

이 글에서는 웹 개발자라면 모를 수가 없는 글로벌 IT 기업 Cloudflare가 이렇게 광범위한 장애를 일으킨 원인과 예방방법에 대해 공부하며 정리해본 내용을 공유하려한다.

Cloudflare

Cloudflare는 많은 웹사이트의 현관문 역할을 하는 '리버스 프록시' 서비스다.

사용자가 특정 웹사이트에 접속을 시도하면, 그 요청은 먼저 Cloudflare를 거친다. Cloudflare에서는 이 요청들이 봇이나 악성 트래픽인지 등을 거르고 기본적 보안 검사를 한 후 검증된 요청만 실제 뒷단 서버로 전달한다.

이번 장애에서 중점적으로 볼 부분중 하나는 타 웹사이트의 접속 장애 여부를 확인 하는 Downdetector.com 같은 사이트도 먹통이 된것이다.

Cloudflare의 영향력을 알 수 있으면서도 Downdetector.com 같은 접속 장애 여부를 알려주는 사이트도 먹통이 되는 상황이 우습기도하다.

Cloudflare 봇 관리

클라우드플레이어의 핵심 서비스 중 하나는 정교한 봇 관리 시스템이다.

봇 관리 시스템이란?

'CAPTCHA 없이 악성 봇 차단' Cloudflare의 수백만 개 인터넷 자산 데이터를 이용해 실시간으로 신속 정확하게 양호한 봇과 악성 봇을 관리

  • 봇 공격 차단
  • 여러 감지 방법 제공
  • CAPTCHA 피하기
  • 간단한 배포

Cloudflare Bot Management

이 시스템의 특징은 매우 동적이라는 점이다. 새로운 공격 패턴에 신속히 대응키위해, 약 5분마다 새로운 기능(feature)가 시스템에 배포된다.

이런 동적인 환경에서 최고의 성능을 유지하기위해 '메모리 사전 할당(pre-allocation)' 최적화 기법을 사용했다한다. 미리 애플리케이션에 필요한 메모리 공간을 고정적으로 확보하여, 실행 중 메모리 스왑등으로 발생할 수 있는 속도 저하를 원천 차단하겠다는 것이다.

안정적 성능을 보장하기 위한 이 설계 원칙은 NASA가 안전이 중요한 소프트웨어에 적용하는 'rule of 10 power rule(10의 거듭제곱 규칙)'에서 영감 받은 설계였다.

하지만 여기서 Cloudflare와 NASA 시스템의 맥락 불일치가 있다. NASA의 원칙은 그 기반이 되는 세상이 불변하는 하드웨어처럼 정적이고 예측가능한 환경을 위해 만들어 졌다. 반면, 클라우드플레이는 이 원칙을 5분 마다 데이터가 바뀌는 동적인 환경에 적용했다.

이런 불일치는 시스템 붕괴를 초래했다. 클라우드플레이서 시스템에는 최대 200개의 기능만 처리하도록 하드코딩된 제한이 있었다. 그런데 Cloudflare 내부의 변경 사항(DB 권한 수정)인해 발생한 버그가 서버로 전송되는 기능 파일에 중복된 레코드를 생성했고, 기능 목록이 200개를 넘어섰다.

이때 시스템은 속도가 느려지는 대신, 아예 전체 서비스를 중단시키도록 설계되있었던 것이다.

최초 원인: DB(ClickHouse) 권한 설정 변경

Cloudflare는 기능 목록을 가져오기 위해 특정 테이블에서 이름과 타입을 가져오라는 쿼리를 실행했다. 그런데 이 쿼리에는 어느 DB에서 가져올지 명시하는게 없었다.

원래는 default라는 데이터베이스만 봤지만, 권한 설정이 변경되면서 하위 데이터베이스인 r0의 메타데이터 까지 함께 보이게 됬다.

이런 상태에서 쿼리시 두 곳의 데이터베이스를 함께 읽게 됬는데 새롭게 보이는 r0같은 데이터베이스에도 똑같은 레코드들이 읽어져 오는것이다.

결과적으로 읽어 오는 데이터 양이 2배 이상 커지게 되었다. 이 2배가 왜 문제였을까?

이유는 앞서 말했던 메모리 사전 할당 때문이다. 성능 최적화를 위해 메모리 사정 할당을 하지만 한도는 정해놓은 상태였는데 그 한도가 200개였던것.

중복된 레코드때문에 한도 200개를 넘어섰고 Rust로 개발된 시스템이 이 파일을 읽으려 할때 설정된 한도를 넘었다는 에러를 발생시켰다.

이때 해당 에러를 안전하게 처리하지 않고 unwrap()이라는 함수를 사용한 것이 문제였다. Rust에서 이 함수는 에러가 발생하면 프로그램을 즉시 강제로 종료(Panic) 시켜 버리기 때문.

원인 코드: Rust의 unwrap()

이제 코드단에서 원인을 더 자세히보겠다. Rust 코드의 unwrap()이 한줄이 핵심 원인이라 볼 수 있다.

다음 코드는 Cloudeflare 블로그에 첨부된 코드다.

/// Fetch edge features based on 'input' struct into ['Features'] buffer.
pub fn fetch_features (
    smut selt,
    input: &dyn BotsInput,
    features: &mut Features,
) -> Result<(), (ErrorFlags, i32) > {
    //update features checksum (lower 32 bits) and copy edge feature names
    features.checksum &= 0xFFFF_FFFF_0000_0000;
    features.checksum |= u64::from(self.config.checksum);
    let (feature_values, _ ) = features
        .append_with_names(&self.config.feature_names)
        .unwrap(); // UNWRAP!
  • Rust의 Result 타입은 성공 또는 실패를 우아하게 처리해 주는 타입이다. try-cahtch 없이도 오류 상황을 다룰 수 있게해준다.

Java와 비교해서 알아보겠다.

1. Java의 Optional.get()과 유사한 동작

JPA를 쓰다보면 자주 마주치는, Java 8 부터 도입된 Optional<T> 클래스에는 get()이란 메서드가 있다. 이를 Rust의 Resultunwrap()과 비교해보자면

  • Java: Optional 객체가 비어있는데 get()을 호출하면 NoSuchElementException이라는 런타임 예외가 발생한다.
  • Rust: unwrap()도 마찬가지인데 값이 있을 것이라 가정하고 꺼내려 하지만, 만약 값이 없거나 에러 상태라면 즉시 패닉을 일으키며 프로그램을 중단시킨다.

2. 예외 처리 방식 차이: try-catch vs Result

  • Java(try-catch): Java는 에러가 발생할 것 같은 코드를 try로 감싸고 catch로 예외를 잡아서 처리한다. 예외가 발생해도 잘 catch하면 애플리케이션은 계속 동작한다.
  • Rust(Result 타입): Rust에는 try-catch가 없는 대신 함수가 Ok 또는 Err를 담는 Result 객체를 반환한다. 개발자는 이 객체를 열어 에러를 확인하면 된다.

Cloudflare의 개발자는 이 Result 객체를 체크하는 대신, 절대 에러는 없다라는 가정을 하고 unwrap()을 사용했다. 이러니 기능 개수가 200개 넘어가서 예외가 발생시 애플리케이션이 죽어버리는 것이었다.

Rust에서는 unwrap()으로 인한 Panic은 시스템이 더 이상 정상적인 상태를 유지할 수 없다고 판단하여 안전하게 종료하는 복구 불가능한 에러로 취급된다.

3. 런타임 예외(runtime exception) vs 프로그램 종료(Panic)

Java에서는 예외가 발생해도 WAS가 해당 스레드만 종료시키고 서버는 살려둔다.

하지만 Rust에서는 unwrap()시 에러가 있다면 Panic을 발생시키며 프로그램을 죽여버린다.

Cloudflare는 이 Panic 때문에 트래픽을 처리하는 애플리케이션이 종료되었기에 이런 장애가 발생하게 된 것이다.

요약

결론적으로,

  • 데이터베이스 권한 변경으로 인해 똑같은 데이터가 여러 번 읽힘(중복된 레코드)
  • 이로 인해 전체 기능 개수가 시스템이 감당하기로 약속한 수치(200개)를 넘김
  • 이를 발견한 코드가 unwrap()에 의해 서버 전체를 다운시켜버린 사건

참고 자료