경로마다 응답 헤더가 다른 원본 서버입니다. CDN을 앞에 붙인 뒤 같은 경로를 두 번 호출해서, 두 번째 요청이 오리진까지 오는지를 보고 캐시 동작을 판정합니다.
판정에 쓰는 근거는 두 가지이고, 항상 둘을 함께 봐야 합니다.
| 근거 | 보는 곳 | 의미 |
|---|---|---|
X-Cache-Lookup |
CDN 응답 헤더 | CDN이 스스로 내린 판정. Hit From MemCache / Hit From Disktank / Miss |
| 오리진 액세스 로그 | 서버의 /var/log/nginx/access.log |
요청이 실제로 오리진까지 왔는지. 캐시 HIT이면 로그에 남지 않습니다. |
CDN을 거치지 않고 오리진에 직접 요청하면 X-Cache-Lookup 은 아예 나타나지 않습니다.
헤더가 안 보인다면 캐시 미스가 아니라 CDN을 경유하지 않은 요청입니다.
Tencent Cloud CDN 콘솔에서 가속 도메인을 만들고 오리진을 다음과 같이 지정합니다.
HTTPS로 회원할 때는 인증서가 자체 서명이라
오리진 인증서 검증을 꺼야 합니다. curl 로 직접 확인할 때도 -k 가 필요합니다.
같은 URL을 연속으로 두 번 호출합니다. 아래에서 <도메인> 은 가속 도메인입니다.
curl -sD - -o /dev/null http://<도메인>/cache/max-age-60/test.txt | grep -i x-cache-lookup
curl -sD - -o /dev/null http://<도메인>/cache/max-age-60/test.txt | grep -i x-cache-lookup
첫 번째는 Miss, 두 번째는 Hit 이면 캐시가 정상 동작합니다. 이때 오리진 로그에는 요청이 한 건만 남아야 합니다.
# 서버에서 실시간으로 확인
sudo tail -f /var/log/nginx/access.log
| 확인할 것 | 사용할 경로 | 기대 결과 |
|---|---|---|
| 기본 캐시 | /cache/max-age-60/test.txt |
2회차 Hit, 오리진 로그 1건 |
| TTL 만료 | 같은 경로를 61초 뒤 재요청 | 다시 회원 발생, 로그에 2번째 요청 기록 |
| 재검증(304) | /cache/validate/test.txt |
ETag 기반 조건부 요청 시 304 |
| 캐시 우회 | /cache/no-store/test.txt |
매 요청 오리진 로그에 기록 |
| CDN 기본 TTL | /cache/no-header/test.txt |
CDN 콘솔의 기본 캐시 정책대로 동작 |
| 대용량 분할 회원 | /static/size/file_100m.bin |
로그의 range= 필드에 부분 요청 다건 |
| 오류 응답 캐싱 | /status/404, /status/500 |
반복 요청이 오리진까지 오는지 확인 |
| 쿼리스트링 캐시 키 | /static/type/image.png?v=1 과 ?v=2 |
CDN 설정에 따라 별도 캐시 / 동일 캐시 |
| 회원 Host 헤더 | /api/echo |
응답 JSON의 Host 값 확인 |
| 회원 타임아웃 | /api/delay/10 |
CDN 타임아웃 설정보다 길면 오류 응답 |
앞의 시나리오가 얼마나 오래 캐시하는지를 봤다면, 여기서는 CDN이 캐시를 어떻게 나누는지와 만료된 사본을 계속 내보내도 되는지를 봅니다.
요청 헤더를 바꿔가며 호출해야 의미가 있습니다. 두 요청이 서로 다른 캐시 항목으로 잡히면 두 번째도 Miss가 됩니다.
URL=http://<도메인>/cache/vary-lang/test.txt
curl -sD - -o /dev/null -H 'Accept-Language: ko' "$URL" | grep -i x-cache-lookup
curl -sD - -o /dev/null -H 'Accept-Language: en' "$URL" | grep -i x-cache-lookup
curl -sD - -o /dev/null -H 'Accept-Language: ko' "$URL" | grep -i x-cache-lookup
Vary 를 존중한다면 1·2번째는 Miss, 3번째는 Hit입니다.
무시한다면 2번째부터 Hit이 됩니다. /cache/vary-ua/ 는
User-Agent 로 같은 확인을 합니다.
이 두 경로는 gzip을 꺼뒀습니다. 켜두면 nginx가 Vary: Accept-Encoding 을
따로 붙여서 Vary 헤더가 두 개가 되고, CDN이 무엇을 기준으로 캐시를
나눴는지 구분할 수 없기 때문입니다.
/cache/set-cookie/test.txt 는 max-age=300 으로 캐시를 허용하면서
동시에 Set-Cookie 를 내보냅니다. 대부분의 CDN은 쿠키가 붙은 응답을
캐시하지 않으므로, 계속 Miss가 나오면 정상입니다. Hit이 난다면 사용자별 쿠키가
다른 사람에게 새어 나갈 수 있다는 뜻이라 반드시 확인해야 합니다.
| 경로 | 헤더 | 확인할 것 |
|---|---|---|
/cache/swr/test.txt |
max-age=10, stale-while-revalidate=60 |
10초가 지난 뒤에도 즉시 응답하면서 뒤에서 갱신하는지 |
/cache/stale-if-error/test.txt |
max-age=10, stale-if-error=300 |
오리진이 오류일 때 만료된 사본으로 버티는지 |
max-age 가 10초라 만료 이후 구간을 금방 관찰할 수 있습니다.
/api/flaky/N 은 N번째 요청마다 502를 내보내고,
성공 응답에는 max-age=10, stale-if-error=300 을 붙입니다.
실패 응답 자체는 no-store 라 CDN이 502를 캐시하지 않습니다.
따라서 CDN이 stale-if-error 를 존중한다면, 오리진이 502를 내는 동안에도
캐시된 200 응답을 계속 내보내야 합니다.
# 반복 테스트 전에는 카운터를 초기화
curl -s http://<도메인>/api/flaky/reset
URL=http://<도메인>/api/flaky/3
for i in $(seq 1 9); do
curl -sD - -o /dev/null "$URL" \
| awk 'NR==1 || tolower($1) ~ /^x-cache-lookup:/ { printf "%s ", $0 }'
echo
done
카운터는 오리진에 도달한 요청만 셉니다. CDN이 캐시로 응답하면 오리진까지 오지 않으므로 N번째마다 정확히 실패하지 않습니다. 그것 자체가 캐시가 동작한다는 증거이기도 합니다. 오리진 카운터를 그대로 보려면 CDN을 거치지 않고 직접 호출하세요.
max-age=60 경로로 확인하는 것이 가장 빠릅니다.
Age 헤더가 60에 가까워질수록 만료가 임박한 것입니다.
URL=http://<도메인>/cache/max-age-60/test.txt
for i in $(seq 1 8); do
curl -sD - -o /dev/null "$URL" \
| awk 'tolower($1) ~ /^(x-cache-lookup|age):/ { printf "%s ", $0 }'
echo " ($(date +%T))"
sleep 10
done
처음에는 Miss, 이후 Hit이 이어지다가, 60초를 넘긴 시점에 다시 Miss 또는 재검증이 일어나면 TTL이 의도대로 적용된 것입니다.
CDN 콘솔의 Purge(캐시 새로 고침) 로 URL 또는 디렉터리를 비운 뒤 다시 요청하면 첫 요청이 Miss로 돌아옵니다. 콘텐츠 자체를 바꿔서 확인하려면 서버에서 파일을 다시 생성하면 됩니다.
# 서버에서: 테스트 콘텐츠를 새 타임스탬프로 재생성
sudo bash ~/cdn-origin/scripts/generate-files.sh /var/www/origin
sudo chown -R nginx:nginx /var/www/origin
각 파일에는 생성 시각이 들어 있어, 받은 내용이 캐시된 예전 사본인지 새로 회원한 사본인지 바로 구분할 수 있습니다. 전체 체크섬은 /MANIFEST.txt 에 있습니다.
| 할 일 | 명령 |
|---|---|
| 전체 동작 검증 | bash ~/cdn-origin/scripts/verify.sh http://localhost |
| 서비스 상태 | systemctl status nginx origin-api |
| 액세스 로그 | sudo tail -f /var/log/nginx/access.log |
| 설정 변경 후 반영 | sudo nginx -t && sudo systemctl reload nginx |
액세스 로그에는 host=(회원 Host), range=(부분 요청),
status= 가 함께 기록되어 CDN 판정과 대조하기 좋습니다.
웹서버에 실제로 도달한 요청과 오리진이 돌려준 응답을 /inspect.html 에서 볼 수 있습니다. 최근 500건을 메모리에 보관하며, 자동으로 새로고침됩니다.
모든 응답에는 X-Origin-Request-Id 가 붙습니다. 이게 이 기능의 핵심입니다 —
CDN을 거쳐 받은 응답을 손에 들고 있을 때, 그 ID로 기록을 찾아보면
오리진까지 온 요청인지 엣지 캐시가 답한 것인지 확실해집니다.
# 가속 도메인으로 두 번 호출하고 요청 ID를 비교한다
curl -sI https://<가속도메인>/cache/max-age-60/test.txt | grep -iE "x-origin-request-id|x-cache-lookup"
curl -sI https://<가속도메인>/cache/max-age-60/test.txt | grep -iE "x-origin-request-id|x-cache-lookup"
# ID가 그대로면 같은 오리진 응답을 캐시해 재사용한 것이고,
# ID가 바뀌었다면 오리진까지 다시 다녀온 것이다.
특정 ID의 기록만 보려면 /inspect.html#<요청ID> 로 열거나,
검색창에 붙여 넣으면 됩니다. API로는
/api/requests?id=<요청ID> 입니다.
| 표시 | 뜻 |
|---|---|
full |
요청 헤더를 그대로 복사해 받은 기록입니다. CDN이 붙인 예상 못 한 헤더까지 모두 남습니다. |
log only |
액세스 로그에 이름을 적어둔 헤더만 남은 기록입니다. /health, /status/200, /redirect/* 처럼 nginx가 본문을 즉시 반환하는 경로가 여기 해당합니다. 응답 쪽은 어느 경우든 완전합니다. |
/inspect.html 이 2초마다 보내는 자기 조회는 기록에서 빼고 건수만 셉니다
(화면 오른쪽 위 “뷰어 폴링 N건 제외”). 남겨두면 탭을 열어둔 것만으로
버퍼가 자기 폴링으로 가득 차서 정작 보려던 트래픽이 밀려나기 때문입니다.
제외 기준은 경로가 아니라 뷰어가 붙이는 표식이므로,
손으로 curl /api/requests 를 부른 것은 그대로 기록됩니다.
같은 내용이 /var/log/nginx/access.json.log 에 한 줄 JSON으로도 쌓입니다.
화면의 버퍼는 서비스가 재시작되면 비지만, 이 파일은 남습니다.
기록은 인증 없이 공개됩니다. 이 오리진에 보낸 요청 헤더는
쿠키를 포함해 누구나 /api/requests 로 볼 수 있습니다. 테스트 오리진의
목적 자체가 그것이지만, 실제 자격증명은 보내지 마세요.
Authorization 헤더만은 값을 가리고 종류와 길이만 기록합니다.
오류 응답은 모두 이 오리진이 직접 만든 페이지로 나갑니다. nginx 기본 페이지는 의도적으로 만든 테스트 응답과 진짜 장애가 글자 하나까지 똑같아서, CDN에 캐시된 오류를 나중에 봤을 때 어느 쪽인지 가릴 수가 없기 때문입니다.
구분은 X-Origin-Intent 헤더 하나로 끝납니다.
| 값 | 뜻 | 예 |
|---|---|---|
test-fixture |
테스트를 위해 일부러 반환한 응답. 오리진은 정상입니다. | /status/503, /api/flaky/3 의 502 |
unexpected |
실제로 잘못된 것. 조치가 필요합니다. | 없는 경로의 404, origin-api 다운으로 인한 502 |
| 헤더 없음 | 오리진이 만든 응답이 아닙니다. CDN 엣지가 자체적으로 만든 오류입니다. | 엣지 타임아웃, CDN 접근 제어 차단 |
마지막 줄이 실무에서 제일 쓸모 있습니다. 502를 받았을 때
X-Origin-Intent 가 아예 없다면 요청이 오리진까지 오지도 못한 것이므로,
오리진 로그를 뒤질 필요 없이 CDN 설정부터 보면 됩니다.
curl -sI https://<가속도메인>/status/404 | grep -i x-origin-intent
# X-Origin-Intent: test-fixture → 의도된 응답
curl -sI https://<가속도메인>/없는경로 | grep -i x-origin-intent
# X-Origin-Intent: unexpected → 진짜 404
오류 응답에는 Cache-Control 을 일부러 붙이지 않았습니다. CDN이 오류를
몇 초나 캐시하는지(네거티브 캐싱)가 검증 대상이라, 오리진에서 못박아버리면
그 동작을 볼 수 없기 때문입니다.
| 증상 | 확인할 것 |
|---|---|
X-Cache-Lookup 이 안 보임 |
CDN을 경유하지 않은 요청입니다. 가속 도메인으로 호출했는지 확인하세요. |
| 계속 Miss만 나옴 | 해당 경로의 Cache-Control 이 캐시를 막는 값인지, CDN 캐시 규칙이 덮어쓰는지 확인하세요. |
| 외부에서 접속 불가 | OCI Security List/NSG의 TCP 80·443 인그레스와 서버의 firewalld를 모두 확인하세요. |
| 요청 기록이 비어 있음 | systemctl status origin-api 를 확인하세요. 기록은 origin-api가 받아 보관하므로, 그 서비스가 죽어 있으면 nginx는 정상 응답하면서 기록만 멈춥니다. |
| 오류 페이지가 nginx 기본 화면으로 보임 | /var/www/origin/_error/ 에 페이지가 있는지 확인하세요. 없다면 generate-files.sh 가 실행되지 않은 것입니다. |
| HTTPS 회원 실패 | 자체 서명 인증서입니다. 오리진 인증서 검증을 끄세요. |
| 오리진 응답이 이상함 | CDN을 건너뛰고 오리진에 직접 요청해 X-Origin-Server, X-Origin-Time 으로 비교하세요. |