We just shipped support for the ugliest part of HTTP: Vary
Cloudflare, HTTP의 골칫거리 Vary 지원 시작
Cloudflare가 모든 요금제의 Cache Rules에 HTTP Vary 지원을 추가했습니다. 요청 헤더를 정규화하거나 원본 값을 그대로 비교하고, 변동이 너무 큰 헤더가 포함된 응답은 캐시에서 제외할 수 있습니다. 잘못된 응답 제공을 막으면서 캐시 조각화를 줄이는 것이 목표입니다.
- 주제
AI 요약
하나의 URL이 요청에 따라 HTML이나 JSON, 여러 언어와 이미지 형식처럼 서로 다른 응답을 돌려줄 수 있습니다. HTTP의 Vary 헤더는 어떤 요청 헤더가 응답에 영향을 주는지 캐시에 알려줍니다. 하지만 헤더 값이 조금만 달라도 별개의 응답으로 취급하면, 사실상 같은 응답이 수많은 캐시 항목으로 흩어질 수 있습니다. Cloudflare는 Cache Rules에서 각 헤더를 정규화할지, 원래 값 그대로 비교할지, 해당 응답을 캐시하지 않을지 선택하도록 했습니다.
Vary가 필요한 이유
예를 들어 같은 /catalog 주소에 브라우저가 Accept: text/html을 보내면 원본은 HTML을 반환하고, API 클라이언트가 Accept: application/json을 보내면 JSON을 반환할 수 있습니다. 응답에 Vary: Accept가 없으면 캐시는 먼저 저장된 응답을 다른 요청에도 돌려줄 수 있습니다. 그러면 브라우저에 JSON을 보내거나 API 클라이언트에 HTML을 보내는 문제가 생깁니다.
Vary는 이런 오응답을 막지만, 어떤 요청 값들이 같은 응답으로 이어지는지까지 설명하지는 않습니다. 원본이 영어·프랑스어·독일어만 제공한다고 해도 Accept-Language 값은 순서와 지역 태그에 따라 매우 다양합니다. en-US, fr;q=0.8과 fr;q=0.8, en-GB는 원본이 같은 영어 응답으로 처리할 수 있지만, 단순히 원래 문자열을 비교하는 캐시는 다른 항목으로 저장할 수 있습니다.
요청 헤더가 여러 개면 조합 수도 빠르게 늘어납니다. 필드 하나에 값이 열 가지씩 있고 그런 필드가 세 개라면 조합은 1,000개입니다. User-Agent처럼 값이 많은 헤더나 사용자별 Cookie도 캐시를 잘게 나눌 수 있습니다. 각 항목의 응답이 정확하더라도 재사용률은 낮아지고, 항목끼리 캐시 용량을 차지하며 원본 요청이 늘어납니다. Cloudflare가 인기 사이트 약 5만 곳에서 응답 1억 2천만 건 이상을 분석한 결과, 약 3,000개 사이트가 네 개 이상의 필드에 따라 응답을 달리했습니다. 일부는 10개, 23개, 많게는 47개 필드에 따라 응답을 나눴습니다.
Cache Rules에서 선택하는 세 가지 처리
원본 서버는 Vary 헤더로 응답에 영향을 줄 수 있는 요청 헤더를 선언합니다. Cloudflare 고객은 Cache Rules에서 선언된 각 헤더 값을 어떻게 캐시할지 정합니다. 헤더별 설정이 없으면 규칙의 기본 동작을 따릅니다.
- normalize: 값의 표현이 달라도 같은 응답으로 이어질 수 있는 협상 헤더를 정규화합니다. Accept, Accept-Language, Accept-Encoding에는 헤더별 처리를 적용합니다. 다른 헤더는 앞뒤의 선택적 공백을 정리하고, 반복된 헤더 줄을 순서대로 합칩니다. Cloudflare는 이 방식을 기본값으로 권장합니다.
- passthrough: 대소문자, 공백, 순서, 중복 값을 포함해 원래 값 그대로 캐시를 비교합니다. 원본이 실제로 구분하는 값이거나 값의 종류가 제한된 헤더에 적합합니다. 원본에서 헤더 줄이 여러 개면 캐시 비교 시 쉼표로 연결합니다.
- bypass: Vary가 해당 헤더를 지정하면 응답을 저장하지 않습니다. 사용자별 값이나 종류가 무제한인 Cookie, User-Agent 등에 쓸 수 있습니다. 기존 캐시 항목은 자동으로 삭제하지 않으므로 필요하면 별도로 purge해야 합니다.
Vary: *는 어떤 요청 정보든 응답에 영향을 줄 수 있다는 뜻이므로 설정과 관계없이 캐시를 우회합니다. 클라이언트 IP처럼 HTTP 메시지 밖의 정보가 응답을 결정할 수도 있어, 다음 요청에 응답을 재사용할 수 없기 때문입니다.
정규화와 원본 응답의 일치
Cloudflare는 Accept, Accept-Language, Accept-Encoding 값을 소문자로 바꾸고 품질 값이 높은 순서로 정렬합니다. 품질 값이 같으면 알파벳순으로 정렬합니다. 그 뒤 품질 값이 0이 아닌 항목에서 매개변수를 제거합니다. 설정에서 허용할 미디어 형식과 언어를 지정하면 목록을 제한할 수도 있습니다. 예를 들어 en-US는 전체 지역 태그를 허용 목록에 넣지 않으면 en으로 줄어듭니다.
정규화 결과만 캐시 비교에 쓰고 원본에는 정규화 전 값을 보내면 문제가 생길 수 있습니다. 캐시는 두 요청을 같은 것으로 판단했는데 원본은 서로 다른 응답을 만들 수 있기 때문입니다. 따라서 Cloudflare는 정규화된 Accept와 Accept-Language를 원본에도 전달합니다. Respect Strong ETags가 켜져 있으면 Accept-Encoding도 정규화한 값으로 전달합니다. Accept나 Accept-Language의 q=0 제외 조건을 원본이 구별해야 한다면 passthrough를 써야 합니다. 정규화 과정에서 q=0 정보가 사라질 수 있기 때문입니다.
요청 처리와 운영 시 주의점
첫 요청에서 캐시가 Vary 정보를 아직 모르면 캐시 미스가 발생합니다. Cache Rule은 원본 응답에 Vary가 있는지 확인하기 전부터 설정된 헤더를 정규화할 수 있습니다. 원본이 Vary: Accept, Accept-Language를 반환하면 Cloudflare는 응답을 변형 캐시 항목으로 저장하고, 뒤이은 요청은 해당 헤더의 처리 결과를 이용해 같은 항목을 찾습니다. 일치하는 신선한 항목이 있으면 캐시 적중이며, 없으면 원본에 요청하고 새 항목을 저장할 수 있습니다.
캐시 가능한 응답마다 원본이 일관된 Vary 헤더를 반환해야 합니다. 오류 응답이나 대체 응답에서 헤더를 빠뜨리면 Cloudflare가 해당 응답을 변형 구분 없이 저장할 수 있습니다. 원본이 Vary를 반환하지 않는 응답은 일반 응답처럼 캐시합니다. Cache Rule을 바꿔도 기존 항목은 자동 삭제되지 않습니다. 새 정책으로 캐시 키가 달라지면 새 항목을 채우는 동안 이전 항목이 남을 수 있습니다. purge 요청은 해당 리소스의 Vary 변형 전체를 대상으로 합니다.
설정은 대시보드의 Caching > Cache Rules에서 추가하거나 Rulesets API의 http_request_cache_settings 단계, Terraform으로 적용합니다. 배포 뒤에는 같은 클라이언트에서 같은 URL에 서로 다른 헤더 값을 보내고, 기대한 형식과 언어가 반환되는지 확인해야 합니다. 캐시가 채워진 뒤 CF-Cache-Status를 살펴 적중 여부와 예상치 못한 우회를 점검하라고 안내합니다.
사용자 지정 캐시 키와 차이
Accept나 Accept-Language를 사용자 지정 캐시 키에 넣는 방법도 있지만, 그 규칙에 포함된 모든 응답에 해당 차원을 적용합니다. 원본이 실제로 그 헤더를 사용하지 않아도 캐시가 나뉩니다. Vary는 응답마다 원본이 선언하는 방식이지만, 같은 기본 캐시 키를 공유하는 캐시 가능 응답은 일관된 Vary 필드 집합을 사용해야 합니다. 요청 속성이 항상 리소스 정체성을 정한다면 사용자 지정 캐시 키를 쓰고, 원본이 같은 요청 필드 집합을 일관되게 선언한다면 Vary를 쓰라는 설명입니다. 두 방식에 같은 헤더를 중복 설정하는 것은 의도와 테스트가 분명한 경우에만 권합니다.
Cloudflare는 Free, Pro, Business, Enterprise 요금제에서 이 기능을 제공합니다. 원본이 제공하는 형식과 언어를 직접 설정하는 방식이 모든 애플리케이션에 적합하지 않을 수 있어, 원본이 지원 표현을 직접 알리는 Availability Hints 초안의 아이디어도 검토 중이라고 밝혔습니다.
Hacker News 반응
- @simonw — Cloudflare에서 이 기능을 몇 년째 기다렸습니다. User-Agent가 Accept: text/html을 보내면 HTML을 받고, 보내지 않으면 JSON 같은 다른 형식을 받는 방식이 대표적인 문제입니다. 예전에는 Cloudflare 캐시가 이미지 외에는 Vary를 무시했으므로 JSON 응답이 캐시된 뒤 HTML을 기대하는 사용자에게 전달될 위험이 있었습니다. 다만 저는 이 패턴 자체를 쓰지 않기로 했습니다. URL이 HTML인지 JSON인지 예측 가능하도록 만들고, 앱에서는 JSON에 .json 접미사를 붙이는 쪽을 선호합니다.
- @Joker_vD — Accept 헤더로 응답을 바꾸는 방식은 예전부터 썩 마음에 들지 않았습니다. 저는 명시적으로 버전을 붙인 엔드포인트를 선호합니다. Accept를 하드코딩해야 하는 일은 특히 답답합니다. 그 값을 빼면 406 Not Acceptable을 반환하는데, 3년 동안 계속 작동하다 서비스가 폐기될 때까지 API가 받아들이는 Accept 값이 그것 하나뿐인 경우도 있습니다. 더 황당한 건 v5가 별도 URI로 나오고, 그쪽도 정확히 v5 Accept 값만 받는 경우입니다.
- @yoavm — 저는 Accept와 Accept-Language가 꽤 괜찮다고 생각했습니다. article.es.md를 먼저 요청하고 404를 받은 뒤 article.en.md, article.es.txt, article.en.html을 차례로 시도할 수도 있겠지만, Accept를 쓰면 여러 형식 중 하나를 원하는 순서대로 요청할 수 있습니다. 에이전트가 웹을 읽고 HTML 대신 텍스트만 필요로 하는 지금 특히 유용합니다. 다만 API 전체의 버전을 나누는 경우에는 /v2/ 같은 경로를 쓰는 편이 낫다는 데 동의합니다.
- @bhouston — 반갑습니다. 예전에 CloudFront에서 Vary를 꽤 성공적으로 썼습니다. Cloudflare를 쓰기 시작할 때도 Vary를 지원한다고 생각했는데, 그 때문에 당시 제 Sass 앱에 심각한 버그가 생겼습니다.
- @rob-olmos — “원본 응답에 Vary 헤더가 없으면 Cloudflare는 응답을 정상적으로 캐시한다”는 설명과 “응답 하나라도 Vary를 빠뜨리면 변형을 격리하는 데 필요한 정보 없이 캐시할 수 있다”는 설명이 있습니다. Vary가 없는 캐시 항목이 Vary별로 나뉜 캐시 항목보다 먼저 응답하는 상황이 생기나요? 그렇다면 모든 응답에 Vary 헤더가 있도록 규칙을 추가하는 게 좋을 것 같습니다.
- @jrochkind1 — Vary가 이렇게 엉망인지 몰랐습니다. 웹이 실제로 작동한다는 사실이 놀랍다고 생각할 때가 있는데, 또 그런 기분이 듭니다.
- @saltcured — 세션 쿠키와 Authorization 헤더처럼 중요한 값에 따라 응답을 나누는 일을 생각하면 걱정됩니다. 미들웨어가 이를 잘못 처리하면 큰 혼란이 생깁니다. 저희는 인증된 사용자별 콘텐츠를 만듭니다. 사용자 에이전트 캐시는 원하지만 인증된 사용자 기준으로 캐시 키를 나누고 싶습니다. 그렇지 않으면 로그아웃한 뒤 다른 계정으로 로그인했을 때 SPA가 캐시 응답과 새 응답을 섞어 심각하게 잘못된 결과를 보여줄 수 있습니다.
- @rmunn — 캐시 무효화는 컴퓨터 과학에서 어려운 두 가지 문제 중 하나입니다. 이 농담을 아는 사람이 많겠지만, 나머지 1만 명을 위해 전체 문장을 적습니다. 컴퓨터 과학에서 어려운 문제는 이름 짓기, 캐시 무효화, 그리고 1씩 차이 나는 오류, 이렇게 두 가지입니다.
- @sandeepkd — 프로토콜 중개자가 되기로 했다면 기능을 빼앗지 말고 가치를 더해야 합니다. 기사만 봐서는 확실하지 않지만, 큰 고객이 계약 갱신 전에 지원을 요구했을 것 같다는 느낌이 듭니다.
- @Joker_vD — 프록시 서버에서 HTTP/1.1 캐싱을 구현하는 건 그냥 무시하면 놀랄 만큼 간단합니다. 하지만 성능을 높이려고 직접 캐싱하기로 했다면 클라이언트와 서버가 기대하는 의미를 구현해야 합니다. 그건 전혀 간단하지 않습니다.
- @yellow_lead — 제가 일하는 사이트는 Accept-Language 헤더에 따라 일부 동적 페이지의 언어를 정합니다. 다른 페이지는 /en/page 같은 주소로 리디렉션합니다. 언어 헤더에 따라 바뀌는 동적 페이지는 캐시할 수 없었는데, 이제 가능할지도 모르겠습니다.
- @colmmacc — 20년도 더 전에 Apache 2.0의 여러 mod_cache 하위 모듈에 Vary 지원을 추가했던 기억이 납니다. 들인 수고에 비해 얻는 게 너무 적었습니다. 사용자 에이전트 버그와 별난 백엔드가 너무 많이 드러났습니다. 지역 헤더 같은 실제 HTTP 헤더가 아닌 값에 따라 캐시할 수 있느냐는 논쟁도 있었고, Date 헤더에 따라 Vary를 해달라는 말도 들었습니다. 말이 안 되는 요청이었죠. 너무 영리하게 만들려다 문제가 생겼습니다. 언어 선택이 Accept-Language 같은 기능 대신 URL의 /en-US/..로 자리 잡은 것도 놀랍지 않습니다.
- @Gigachad — 가능한 한 많은 정보를 URL에 넣는 게 좋다고 생각합니다. 링크를 보내면 상대방 브라우저에도 제가 본 것과 같은 문서가 표시된다고 알 수 있습니다.
원문: Cloudflare Blog / 번역·요약: Trawling