경로별 CDN 라우팅 메타데이터를 크기가 제한된 인덱스 샤드로 대체해 P99 경로 메타데이터 조회 지연을 91% 줄이고 배포 속도까지 개선했습니다. 그 과정을 자세히 소개합니다.
Vercel로 들어오는 모든 요청은 CDN을 거칩니다. 이 CDN은 평균적으로 초당 8천만 건 이상의 라우팅 명령을 처리합니다. 그 중 하나가 경로의 존재 여부와 처리 방식을 파악하기 위한 메타데이터 조회입니다. 메타데이터가 캐시에 없으면 CDN은 응답을 반환하기 전에 먼저 메타데이터를 가져와야 합니다.
기존에는 메타데이터를 경로 단위로 가져와 캐시했습니다. 조회에 필요한 데이터만 딱 가져오는 방식이라 당시에는 효율적으로 보였지만, 대규모 배포에는 수십만 개의 경로가 존재하고 각각 별도의 캐시 항목을 가집니다. 새 배포가 이루어질 때마다 메타데이터가 새로 생성되므로, 배포 빈도가 높은 대형 사이트에서는 캐시 미스가 반복적으로 발생할 수밖에 없었습니다.
한 번에 더 많은 메타데이터를 가져오면 조회가 빨라집니다. 여러 경로를 묶어 한 번에 가져오면, 이후 해당 경로들을 조회할 때 캐시를 재활용할 수 있기 때문입니다. 그러나 너무 많이 가져오면 그것대로 비용이 발생했습니다. 프로덕션 실험을 거쳐 균형점을 찾은 결과, P99 메타데이터 조회 지연을 91% 단축하고 배포 속도까지 개선할 수 있었습니다.
요청의 경로가 실제로 콘텐츠나 함수를 제공하는 경로와 항상 일치하지는 않습니다. 프레임워크의 라우팅 규칙이 이 둘을 연결합니다. 예를 들어 /blog/hello-world에 대한 요청은 동적 라우트 /blog/[slug]로 해석될 수 있고, 해당 페이지의 React 서버 컴포넌트 페이로드 요청은 /blog/[slug].rsc로 해석될 수 있습니다. CDN이 이러한 규칙을 적용하는 과정에서 올바른 응답을 찾기 위해 여러 대상 경로를 확인해야 할 수도 있습니다.
프레임워크는 빌드 과정에서 이러한 라우트를 정의합니다. 프레임워크 정의 인프라(framework-defined infrastructure)를 통해 애플리케이션 코드는 어떤 출력이 정적인지, 어떤 것이 Functions를 필요로 하는지, 어떤 응답을 캐시하거나 재생성할 수 있는지를 선언합니다. 프레임워크는 이 의도를 Build Output API 출력으로 변환하고, Vercel은 CDN이 읽는 라우팅 메타데이터를 생성합니다.
요청이 들어오면 CDN은 대상 경로의 존재 여부를 판단하고 해당 메타데이터를 가져와야 합니다. 글로벌 라우팅에 추가한 블룸 필터(Bloom filter)가 존재하지 않는 경로를 걸러내고, 나머지 경로는 정확한 메타데이터 조회를 거칩니다.
기존에는 대상 경로마다 별도의 객체로 메타데이터를 저장했습니다. CDN은 각 객체를 독립적으로 가져와 캐시했는데, 소규모 프로젝트나 배포 빈도가 낮은 경우에는 잘 작동했습니다. 하지만 새 배포마다 캐시 키가 새로 생성되므로, 각 경로에 대한 첫 번째 조회는 반드시 캐시 미스가 발생했습니다.
캐시 미스를 줄이기 위해 여러 경로의 메타데이터를 샤드(shard)라는 파일로 묶었습니다. 조회 시 가져오는 데이터 양을 제어하기 위해 각 샤드의 크기를 제한했습니다. 샤드 하나를 가져오면 포함된 모든 경로의 메타데이터가 캐시에 올라가므로, 이후 해당 경로들을 조회할 때 캐시를 재활용할 수 있습니다.
각 샤드에는 인덱스도 포함되어, 다른 항목을 디코딩·압축 해제·파싱하지 않고도 특정 경로의 메타데이터를 바로 찾을 수 있습니다. 샤드 하나를 가져오면 많은 경로가 캐시에 올라가지만, 실제 조회 시에는 필요한 레코드만 파싱합니다.
샤드는 Vercel의 다른 대규모 라우팅 데이터셋에서 이미 사용 중인 JSONL 구조로 만들었습니다. JSONL은 한 줄에 JSON 값 하나를 저장하는 형식입니다. 대량 리다이렉트(Bulk Redirects)의 정렬된 키-값 레코드 교차 방식과, 블룸 필터를 위해 처음 개발한 직접 주소 지정 가능한 Base64 데이터 구조를 차용했습니다.
데이터 레이아웃과 그 위의 조회 구조를 분리해, 각 워크로드가 필요한 속성을 선택할 수 있도록 했습니다.
랜덤 접근이 가능한 검사 가능한 정렬된 키-값 JSONL 레코드
오버헤드가 낮은 이진 탐색을 위한 선택적 인라인 인덱스
오프셋 기반 디코딩을 지원하는 임베디드 Base64 데이터
전송 및 캐시 비용을 제어하는 크기 제한 샤드
각 샤드는 정렬된 대상 경로와 해당 메타데이터를 JSONL 레코드로 교차 저장합니다. 인라인 인덱스에는 각 항목의 시작 위치가 기록되어 있어, 라우팅 프로세스가 원하는 항목으로 바로 이동할 수 있습니다. 이 위치는 고정 너비 포인터로 저장됩니다. 포인터 하나는 6비트 Base64 문자로 정확히 나누어지는 단위로 구성되므로, 인덱스 줄을 JSON으로 파싱하거나 전체 Base64 문자열을 디코딩하지 않고도 포인터 하나만 제자리에서 디코딩할 수 있습니다.
대량 리다이렉트와 마찬가지로, 배포 후 첫 번째 메타데이터 요청에서 이미 해당 경로가 속한 샤드를 파악합니다. 따라서 샤드를 선택하는 데 추가 왕복이 필요하지 않습니다.
샤드 내에서 라우팅 프로세스는 인덱스 포인터를 이용해 인코딩된 경로에 대해 이진 탐색을 수행합니다. 경로를 찾는 데 O(log n)번의 포인터 읽기와 문자열 비교가 필요합니다. 일치하는 경로를 찾으면 다음 줄의 메타데이터 값만 파싱하고, 샤드의 나머지 부분은 파싱하지 않습니다.
처음에는 대부분의 배포에서 경로 메타데이터 전체가 샤드 하나에 들어가길 기대했습니다. 인덱스와 이진 탐색 덕분에 파싱 비용이 낮았기 때문에 처음에는 수 메가바이트 크기의 샤드로 시작했습니다.
대용량 샤드가 효과를 발휘하려면 요청 근처에 캐시되어 있어야 했습니다. 각 라우팅 프로세스는 최근 샤드를 메모리에 소규모 LRU(최근 미사용 항목 우선 교체) 캐시로 유지하고, 그 앞단에 리전 내 모든 프로세스가 공유하는 더 큰 캐시가 있습니다. 배포당 샤드 수가 적으면 대부분 LRU 캐시에서 히트할 것으로 예상했고, 이를 통해 대용량 샤드 전송 비용을 상쇄할 수 있을 거라 봤습니다.
테스트 결과, 리전 캐시 히트율은 높았지만 LRU 히트율은 낮았습니다. 리전 내 요청이 여러 프로세스에 분산되기 때문이었습니다. 또한 수 메가바이트 샤드를 전송하는 비용도 예상보다 컸습니다.
최종적으로 약 200KB 크기의 샤드를 선택했습니다. 리전 캐시 히트율을 높게 유지하면서 LRU 미스 시에도 빠르게 채울 수 있는 크기였습니다.
프로덕션 측정 결과, 평균 및 P99 조회 지연이 모두 감소했습니다.
메타데이터 조회 지표 | 이전: 경로별 메타데이터 | 이후: 인덱스 샤드 | 개선율 |
|---|---|---|---|
P99 지연 | 215.8 ms | 19.1 ms | 91% 감소 |
평균 지연 | 8.59 ms | 1.81 ms | 79% 감소 |
표준 편차 | 44.9 ms | 19.0 ms | 58% 감소 |
샤드당 항목 수를 줄이면 전송 비용은 낮아지지만 샤드 수가 늘어납니다. 샤드 수는 유지하면서 항목을 더 압축해 샤드 크기를 줄이는 방법도 검토했습니다. 세 가지 방식을 테스트했습니다.
정렬된 경로를 프론트 코딩(front-coding)해 이전 항목과 다른 부분만 저장하는 방식
메타데이터를 중복 제거하는 JSONL 문서로 샤드를 분할하는 방식
더 간결한 커스텀 직렬화 포맷을 사용하는 방식
오프라인 시뮬레이션으로 인코딩 크기와 조회 비용을 모두 측정했습니다.
각 방식 모두 샤드 크기를 크게 줄였지만, 시뮬레이션에서 예측된 지연 개선 폭은 미미했습니다. 이번 마이그레이션에서는 추가적인 인코딩 작업, 호환성 처리, 롤아웃 작업을 감수할 만큼의 가치가 없다고 판단했습니다. 향후 다른 워크로드에서 인덱싱이나 압축의 가치가 더 높아지면, 공유 라이브러리에 추가할 예정입니다.
CDN은 모든 배포에 대한 요청마다 이 조회를 수행합니다. 샤드 메타데이터와 경로별 메타데이터가 일치하지 않으면 오래된 라우트, 잘못된 상태 코드, 또는 실제로 존재하는 경로에 대한 404를 반환할 수 있습니다.
이런 불일치를 먼저 오프라인에서 확인한 뒤, 프로덕션에서도 검증했습니다. 오프라인에서는 테스트 배포를 만들고, 모든 경로를 두 방식으로 조회해 결과를 비교하는 테스트 하네스를 실행했습니다.
프로덕션에서는 피처 플래그 뒤에서 라우팅 시스템이 무작위 샘플 요청에 대해 두 방식의 조회를 모두 수행하되, 응답은 기존 방식의 결과를 그대로 사용했습니다. 백그라운드에서 새 결과와 기존 결과를 비교했고, 프로덕션 요청 속도에 영향을 주지 않으면서 수 주에 걸쳐 불일치 여부를 모니터링했습니다. 이 방식을 섀도 모드(shadow mode)라고 불렀습니다.
섀도 모드에서 차이가 발견됐지만 발생 빈도는 극히 드물었습니다. 그 중 하나는 기존 인코딩의 버그였습니다. 비 ASCII 텍스트를 ASCII 전용 필드에 맞추기 위해 RFC 2047 인코딩 워드를 사용하다가, 이모지가 두 워드로 나뉘는 경우에만 문제가 나타났습니다. 새 포맷은 경로를 일반 UTF-8로 저장하므로 이 버그가 발생할 수 없습니다. 이런 엣지 케이스를 발견한 덕분에 비교 결과에 대한 신뢰도가 높아졌고, 샤드로 프로덕션 트래픽을 처리하기 시작했습니다.
샤드가 프로덕션을 처리하기 시작한 뒤, 빌드 파이프라인으로 돌아가 샤드 도입으로 불필요해진 작업을 제거했습니다.
경로별 메타데이터 업로드를 생략해 약 9.7초를 절약했습니다.
라우트 그룹 메타데이터를 매니페스트에 직접 기록해 약 4.5초를 절약했습니다.
이 두 변경으로 비게 된 파일을 업로드하지 않아 약 2.4초를 추가로 절약했습니다.
합산하면 약 16.6초 단축입니다. 전체 배포 기준으로 배포 단계가 약 10% 빨라졌으며, 이러한 단계가 대부분을 차지하는 메타데이터 집약적 배포에서는 약 25%에 가까운 개선 효과가 있을 것으로 추정합니다.
메타데이터 조회 속도가 빨라지면서 라우트 결정 전체가 개선됐습니다. 대형 사이트의 경우 P99 라우트 결정 속도가 약 2배 빨라졌습니다. Vercel 자체 마케팅 사이트와 문서 사이트에서는 P99 메타데이터 조회 지연이 203ms에서 31ms로 줄었습니다. 중간값(median) 메타데이터 조회 지연은 약 0.7ms를 유지했습니다.
애플리케이션의 Build Output API 계약은 그대로입니다. 프레임워크는 여전히 애플리케이션의 요구 사항을 정의하고, CDN이 이를 처리하는 방식은 Vercel이 개선합니다.
2026년 7월 17일 이후 빌드된 배포는 이미 새 메타데이터 샤드를 사용합니다. 이전 배포라면 프로젝트를 재배포해 더 빠른 조회를 적용받으세요.