릴리즈아티클커뮤니티
아티클 목록
VercelVercel기술 블로그engineering

Vercel에서 리다이렉트를 무한대로 확장하기

리다이렉트는 소규모에서는 간단한 문제지만, 수백만 건 규모가 되면 지연 시간과 비용이 실질적인 시스템 과제로 떠오릅니다. Vercel이 대량 리다이렉트를 어떻게 구현했는지 그 과정을 다룹니다.

원본 링크

리다이렉트는 소규모에서는 간단한 문제지만, 수백만 건 규모가 되면 지연 시간과 비용이 실질적인 시스템 과제로 떠오릅니다.

기존 Vercel에서는 라우팅 규칙과 미들웨어로 리다이렉트를 처리했습니다. 라우팅 규칙은 와일드카드를 포함해 최대 2,000개의 복합 리다이렉트를 지원하며, 순서대로 평가되는 목록 방식으로 동작합니다. 각 규칙에 정규식 매칭이 포함될 수 있어, 단일 요청에도 비용이 큰 평가가 여러 차례 실행될 수 있습니다. 수천 개 수준에서는 문제가 없지만, 규칙이 늘어날수록 요청당 처리 비용이 선형으로 증가합니다.

미들웨어는 유연성이 더 높지만, 모든 요청에 추가 코드를 실행하므로 지연 시간이 늘어납니다. 수백만 건의 리다이렉트를 낮은 지연 시간으로 처리하려면, 요청당 상수 시간 또는 로그 시간에 가까운 전용 조회 경로가 필요했습니다. Bloom 필터를 활용해 글로벌 라우팅을 고속화한 기존 작업을 기반으로, 수백만 건의 리다이렉트까지 확장할 수 있는 방법을 찾았습니다.

최적화 목표

  • 확장성:

    • 프로젝트당 수백만 건의 정적 리다이렉트 지원

  • 런타임 동작:

    • 리다이렉트를 설정하지 않은 프로젝트에는 추가 지연 시간이 발생하지 않을 것

    • 대부분의 요청은 리다이렉트 대상이 아니므로, "리다이렉트 없음" 경로를 빠르게 처리할 것

    • 프로세스 메모리 사용량을 낮게 유지하고, 외부 스토리지와 캐싱 레이어에 의존할 것

  • 엔지니어링 원칙:

    • 과도한 최적화보다 단순함과 디버깅 용이성을 우선할 것

    • 처음부터 완벽하게 만들기보다 점진적으로 개선해 나갈 것

이러한 목표를 염두에 두고, 가장 단순한 설계부터 출발했습니다. 리다이렉트 데이터와 Bloom 필터를 하나의 파일에 합치는 방식이었습니다. 리다이렉트 데이터는 이미 JSON이었고, Bloom 필터도 JSON 내보내기를 지원하고 있었기 때문에, JSONL 파일 형식을 사용해 이 정보를 저장하기로 했습니다.

JSON과 Bloom 필터, 그리고 대략적인 계산

Bloom 필터는 특정 요소가 집합에 포함되어 있는지 검사하는 확률적 자료 구조입니다. 거짓 양성(false positive)은 발생할 수 있지만 거짓 음성(false negative)은 절대 발생하지 않아, "집합에 확실히 없음" 또는 "집합에 있을 수도 있음"이라는 답을 줍니다. 크기가 작은 Bloom 필터를 캐시에 올려 먼저 확인하면, 매칭되지 않는 요청에 대해서는 리다이렉트 조회를 완전히 건너뛸 수 있으므로, "리다이렉트 없음" 경로를 극도로 저렴하게 유지할 수 있습니다. JSON 파일 파싱은 양성 판정이 나온 경우에만 수행합니다.

단순하긴 하지만, 과연 확장이 가능할까? 대략적인 계산 결과는 부정적이었습니다. 리다이렉트가 100만 건이면 파일 크기가 수백 MB에 달할 수 있고, 그 정도 크기의 파일을 가져와 파싱하면 지연 시간과 메모리 한도를 훌쩍 초과하게 됩니다. 전체 데이터셋을 한 번에 로드하지 않는 방법이 필요했습니다.

샤딩과 Bloom 필터로 메모리를 줄이고 조회를 빠르게

해결책은 샤딩이었습니다. 하나의 거대한 JSONL 파일 대신, 리다이렉트 경로를 해싱해 여러 개의 작은 샤드로 분산 저장했습니다. 이렇게 하면 특정 요청에 필요한 소량의 데이터만 로드할 수 있어, 부하가 프로세스 메모리에서 외부 스토리지와 파일 시스템 캐시로 이동합니다. Bloom 필터는 여전히 앞단에서 대부분의 트래픽에 대해 조회를 차단합니다. 다만 이제는, 요청이 Bloom 필터를 통과하더라도 전체 리다이렉트 데이터가 아닌 작은 샤드 하나만 가져와 파싱하면 됩니다.

샤드 구조

각 샤드는 3개 부분으로 구성됩니다:

  • Bloom 필터의 속성을 인코딩한 헤더 라인

  • base64로 인코딩된 Bloom 필터

  • src 경로를 키로 하는 리다이렉트 JSON 객체

다음은 샘플입니다:

빌드 시점에 모든 샤드와 해당 Bloom 필터를 생성해 외부 스토리지에 업로드합니다. 런타임에는 서버가 요청을 받았을 때 해당 프로젝트 또는 배포에 적용되는 데이터셋과 샤드 수만 알면 됩니다.

JSON 파싱 전에 Bloom 필터를 먼저 확인하는 조회 경로

요청 시점의 대량 리다이렉트 조회는 다음과 같이 동작합니다:

  • 프로젝트 또는 배포에 대량 리다이렉트가 설정되어 있는지 확인합니다. 설정되어 있지 않으면 모든 과정을 건너뛰고 일반적인 처리를 진행합니다.

  • 수신된 요청으로부터 리다이렉트 키를 계산하고, 해싱하여 대상 샤드를 결정합니다.

  • 캐시 또는 원본 스토리지에서 샤드를 가져온 뒤, Bloom 필터를 확인합니다.

    • Bloom 필터에 해당 키가 없으면, 샤드의 JSON 본문을 파싱하지 않습니다.

    • Bloom 필터에 해당 키가 있을 수 있다는 결과가 나오면, 샤드의 JSON 본문을 로드하고 해당 객체 내에서 정확한 리다이렉트를 조회합니다.

이 설계에는 몇 가지 좋은 특성이 있습니다:

  • 빠른 음성 조회: Bloom 필터는 매우 빠르고, 거짓 양성률을 매우 낮게 조정할 수 있습니다

  • 사람이 읽을 수 있는 샤드: 샤드는 단순한 JSONL 파일이므로, 문제가 생기면 샤드를 덤프해서 내용을 바로 확인할 수 있습니다

  • 낮은 구현 리스크: JSON 파싱과 Bloom 필터는 단순한 기술이므로 빠르게 배포할 수 있고, 실제 운영 데이터를 수집할 수 있습니다

양성 조회에서 JSON 파싱이 병목이 되다

JSON 파싱이 병목이 될 수 있다고 예상했는데, dogfooding 과정에서 이를 확인했습니다. Bloom 필터가 리다이렉트가 존재할 수 있다고 판정하면, 해당 샤드의 전체 JSON 본문을 파싱하는 데 상당한 시간이 소요되었습니다. 또한 CPU 부하가 높은 상황에서 지연 시간이 급격히 치솟는 현상도 관찰되었는데, JSON 파싱은 CPU 집약적인 작업이라 노드의 다른 모든 작업과 리소스를 두고 경쟁하기 때문이었습니다.

샤드 크기를 줄이면 파싱 속도는 개선되지만, 샤드 수(카디널리티)가 늘어나면서 캐시 미스율이 높아집니다. 여기서 트레이드오프가 발생했습니다. 샤드가 크면 파싱으로 인한 CPU 오버헤드가 높아지고, 샤드가 작으면 캐시 미스로 인한 I/O 지연 시간이 늘어납니다. 전체 샤드를 파싱하지 않고도 단일 값을 조회할 수 있는 데이터 형식이 필요했습니다.

정렬된 키에 대한 이진 탐색으로 전체 샤드 파싱 회피

리다이렉트를 JSON 블롭에 저장하는 대신, 리다이렉트 경로를 키로 한 이진 탐색을 구현했습니다. 각 샤드에는 리다이렉트 키가 정렬된 순서로 저장되어 있어, 로그 시간 탐색이 가능합니다. 키를 찾은 후에는 해당 리다이렉트에 대한 JSON만 파싱하면 됩니다. 이 방식으로 샤드 크기 문제를 근본적으로 해결했습니다. 조회 비용이 샤드 내 전체 데이터 양에 비례하지 않으므로, 전체 JSON 파싱 비용 없이도 캐시 적중률을 높일 만큼 샤드를 충분히 크게 유지할 수 있습니다.

지연 시간이 감소하고 급격한 스파이크가 사라지다

양성 조회의 핫 패스에서 JSON 파싱을 제거하자, 실제로 존재하는 리다이렉트에 대한 요청이 더 빨라지고 예측 가능해졌습니다.

가장 눈에 띄는 개선은 CPU 부하가 높을 때 나타나던 지연 시간 스파이크가 사라진 것입니다. 전체 JSON 샤드를 파싱할 때는 리다이렉트 조회가 노드에서 실행 중인 다른 모든 작업과 CPU 시간을 두고 경쟁했습니다. 이진 탐색으로 전환한 후에는 요청당 CPU 비용이 충분히 낮아져 리소스 경합이 더 이상 문제가 되지 않았습니다.

일반적인 경우에 맞춘 설계

리다이렉트 자체는 단순합니다. 문제는 이 단순한 추상화를 대규모의 대부분 콜드(cold) 상태인 데이터셋, 그리고 엣지에서의 엄격한 지연 시간 요구 사항과 결합할 때 발생합니다. 라우팅 규칙은 이 문제에 적합한 도구가 아니었습니다.

대신, 대량 리다이렉트를 위한 전용 경로를 구축했습니다:

  • 리다이렉트 데이터를 샤딩하여 각 조각을 작게 유지

  • Bloom 필터를 활용하여 "리다이렉트 없음"이라는 일반적인 경우를 저비용으로 처리

  • 키에 대한 이진 탐색이 가능한 레이아웃으로 리다이렉트 저장

이번 개발 과정은 우리가 늘 되돌아오는 원칙을 다시 한번 확인시켜 주었습니다. 과도한 최적화를 피하라는 것입니다. 단순하고 디버깅이 쉬운 구현에서 출발해 계측 데이터를 수집한 덕분에, 실제 운영 데이터가 어디에 복잡성이 진짜 필요한지 알려줄 수 있었습니다.

대량 리다이렉트 시작하기

대량 리다이렉트는 Pro 및 Enterprise 고객에게 제공되며, 프로젝트 설정 파일, 대시보드, API, CLI를 통해 구성할 수 있습니다. 현재 프로젝트당 최대 100만 건의 리다이렉트를 지원합니다. 더 큰 용량이 필요하다면 문의해 주세요.

플랜

기본 제공 리다이렉트

추가 용량

Pro

프로젝트당 1,000건

25,000건당 월 $50

Enterprise

프로젝트당 10,000건

25,000건당 월 $50

대량 리다이렉트를 활용하면 대규모 마이그레이션 관리, 깨진 링크 수정, 만료된 페이지 처리 등 다양한 작업이 가능합니다. 자세한 내용은 대량 리다이렉트 문서 또는 시작 가이드를 참고하세요.