Troubleshooting 2026.08.12 10 min read

Ghost 블로그가 느려진 원인 분리하기: 테마 중복 조회부터 Cloudflare LAX 라우팅까지

블로그가 느려져 Ghost 렌더링 시간과 외부 접속 시간을 따로 재봤다. 테마의 중복 조회와 점검·백업 부하를 줄였고, Cloudflare의 미국 경유는 캐시로 영향을 줄였다.

이슈 발생: 2026-08-11 · 검증: 2026-08-12
환경: Ghost 6.56, RKE2 Kubernetes, Cloudflare Tunnel, Cloudflare Cache

블로그를 열 때 응답이 눈에 띄게 느려졌다. 처음에는 Ghost나 Kubernetes의 자원이 부족한 줄 알았다. 그런데 시간을 재보니 원인이 하나가 아니었다. 홈 테마가 같은 데이터를 여러 번 조회했고, Kubernetes 상태 점검도 주기적으로 홈 전체를 렌더링했다. 백업은 같은 저장소를 사용하고 있었다. 외부 접속 경로를 보니 한국에서 보낸 요청이 ICN 대신 LAX와 SJC로 들어가는 문제도 있었다.

서버 안에서 걸리는 시간과 외부 네트워크에서 걸리는 시간부터 따로 확인했다. 중복 조회를 줄이고 공개 HTML에 짧은 캐시를 적용할 수는 있었다. 다만 Cloudflare Anycast의 접속 POP를 운영자가 ICN으로 고정할 수는 없었다. 이 경로를 해결했다고 볼 수는 없고, 캐시로 원점 서버까지 왕복하는 횟수를 줄이는 데 그쳤다.

느리다는 현상을 구간별로 나눴다

화면이 늦게 열린다고 바로 Ghost 설정부터 바꿀 수는 없었다. 요청이 어느 구간에서 오래 걸리는지 다음 순서로 확인했다.

사용자
  → DNS
  → Cloudflare 접속 POP
  → Cloudflare Tunnel
  → Kubernetes Service
  → Ghost 테마 렌더링
  → MySQL

같은 URL을 외부와 원점에 가깝게 각각 측정하고, Cloudflare 응답에서는 CF-RAY, CF-Cache-Status, Age를 함께 확인했다.

curl.exe -sSI https://sw-data.com/ |
  Select-String 'cf-cache-status|age|cf-ray|cache-control'

curl.exe -s https://sw-data.com/cdn-cgi/trace
curl.exe -s https://1.1.1.1/cdn-cgi/trace

원점 서버 가까이에서 측정한 응답은 외부 접속보다 훨씬 빨랐다. Ghost 렌더링을 줄이는 작업과 Cloudflare 접속 경로를 확인하는 작업을 나눠 진행했다.

홈 테마의 get 9개를 먼저 분해했다

Ghost 테마의 {{#get}}은 서버가 페이지를 렌더링할 때 Content API 데이터를 가져오는 헬퍼다. 브라우저의 HTTP GET 요청과는 구분해야 한다. 홈 템플릿에서는 한 번 렌더링할 때 get을 9번 사용했다.

그중 네 곳은 홈 컨텍스트가 이미 가진 데이터를 다시 조회했다.

  • EVIDENCE 영역의 전체 글 수
  • 상단 시각화 영역의 최근 업데이트 날짜
  • Signal Strip의 전체 글 수
  • Signal Strip의 최근 업데이트 날짜

전체 글 수는 홈 컨텍스트의 pagination.total을 그대로 사용하고, 최근 날짜는 이미 전달받은 posts의 첫 항목에서 가져오도록 바꿨다. 화면에 표시하는 숫자와 날짜는 그대로 유지했다.

{{!-- 변경 전: 같은 전체 글 수를 다시 조회 --}}
{{#get "posts" limit="100"}}
  <strong>{{pagination.total}}</strong>
{{/get}}

{{!-- 변경 후: 현재 홈 컨텍스트 재사용 --}}
<strong>{{pagination.total}}</strong>

RCA 글 수, Troubleshooting 글 수, 전체 Topic 수, 상위 Tag 6개, 최신 Troubleshooting 카드는 각각 필터가 필요해 조회를 유지했다. 콘텐츠와 레이아웃은 그대로 두고 get을 9개에서 5개로 줄였다.

변경 전후로 원점 TTFB를 여러 번 측정했다. 평균은 약 314ms에서 184ms로, 중앙값은 약 241ms에서 163ms로 줄었다. 홈을 렌더링하는 시간은 줄었지만, 이 결과가 외부 네트워크 우회까지 해결됐다는 뜻은 아니다.

Kubernetes Probe가 홈 전체를 계속 렌더링하지 않게 했다

Ghost Pod의 startup, liveness, readiness Probe가 모두 /를 호출하도록 설정해뒀다. 홈을 열 때마다 테마 렌더링과 데이터 조회가 실행되므로 생존 여부만 확인하기에는 비용이 컸다. 방문자가 없어도 kubelet이 계속 홈을 열고 있었던 셈이다.

각 점검이 무엇을 확인해야 하는지 다시 정하고 경로를 바꿨다.

  • startupProbe: Ghost가 포트를 열었는지 TCP로 확인
  • livenessProbe: 프로세스가 응답 가능한지 TCP로 확인
  • readinessProbe: /ghost/api/admin/site/ 경량 API로 실제 요청 처리 가능 여부 확인
startupProbe:
  tcpSocket:
    port: http

livenessProbe:
  tcpSocket:
    port: http

readinessProbe:
  httpGet:
    path: /ghost/api/admin/site/
    port: http

경량 API가 HTTP 200을 반환하는지 먼저 확인한 뒤 Deployment를 갱신했다. 새 파드가 Ready가 된 다음 공개 페이지와 관리자 경로도 직접 열어봤다.

같은 저장소를 사용하는 백업 부하도 분리했다

기존에는 30분마다 백업하면서 매번 Restic prune까지 실행했다. prune은 스냅샷을 만드는 것보다 넓은 범위의 저장 데이터를 읽고 정리한다. Google Drive API에서 userRateLimitExceeded가 발생한 상태에서 재시도와 prune을 짧은 주기로 반복하면 블로그 성능과 무관한 백그라운드 부하까지 커진다.

백업 주기는 6시간으로 늘리고 prune은 일요일 새벽의 별도 CronJob으로 분리했다. rclone은 동시 전송 3개, API TPS 4로 제한하고 pacer와 low-level retry를 적용했다. 백업 컨테이너가 Secret의 읽기 전용 설정 파일을 직접 수정하지 않도록 writable emptyDir에 복사해서 사용하게 바꿨다.

설정 오류를 고쳤어도 Google Drive의 사용자 할당량 제한은 바로 풀리지 않았다. 검증 작업에서 같은 403이 계속돼 추가 호출을 중단했다. 블로그 성능 조치와 별도로, 다음 예약 실행에서 백업이 성공하는지 확인할 항목으로 남겼다.

cloudflared를 갱신하고 Tunnel 상태를 다시 확인했다

latest 태그로 서로 다른 구버전이 섞이지 않게 cloudflare/cloudflared:2026.7.3으로 고정했다. 두 Connector를 순차 교체한 뒤 모두 Ready, 재시작 0을 확인했다. 각 Connector는 ICN에 QUIC 연결을 등록했고 요청 오류도 증가하지 않았다.

두 커넥터가 ICN에 연결된 상태에서도 외부 요청은 미국을 경유했다. 이 결과만으로 커넥터가 원인이라고 보기는 어려웠다. 사용자의 접속 POP와 cloudflared의 연결 POP를 따로 봐야 했다.

같은 한국 회선에서 블로그는 LAX, 1.1.1.1은 ICN으로 갔다

한국의 두 회선에서 /cdn-cgi/trace를 비교했다. 개인 회선 IP는 기록에서 마스킹했다.

한국 회선 A
  sw-data.com → colo=LAX
  1.1.1.1    → colo=ICN

한국 회선 B
  sw-data.com → colo=LAX
  1.1.1.1    → colo=ICN

서울 Cloud VM 대조군
  sw-data.com → colo=ICN
  1.1.1.1    → colo=ICN

한국 회선의 traceroute에서 블로그 주소는 약 140ms의 장거리 구간을 거쳐 미국으로 향했고, 1.1.1.1은 약 10ms로 ICN에 도착했다. DNS가 돌려준 두 Cloudflare IPv4 주소를 curl --resolve로 각각 고정해도 한 주소는 계속 LAX로, 다른 주소는 LAX와 SJC 사이로 연결됐다.

LAX는 로스앤젤레스, SJC는 산호세, ICN은 인천을 나타낸다. 일반 Cloudflare Cache Rule이나 DNS 설정으로 접속 POP를 ICN으로 고정할 수 없다. Cloudflare Anycast와 통신사 BGP 경로가 접속 POP를 선택한다.

공개 HTML만 2분 Edge Cache로 완화했다

접속 POP를 직접 바꿀 수 없으니, 공개 HTML이 원점 서버까지 왕복하는 횟수를 줄이기로 했다. 로그인과 개인화 응답이 섞이지 않도록 캐시 대상을 좁혔다.

  • 쿠키가 없는 공개 요청만 캐시
  • 쿼리 문자열이 없는 HTML 경로만 캐시
  • Edge TTL 2분, Browser TTL은 원점 설정 유지
  • /ghost, /.ghost, /members, /p/ 등은 강제 우회
  • 우회 규칙을 마지막에 두어 충돌 시 Bypass가 이기게 구성

아래 결과로 캐시가 적용되는 요청과 우회되는 요청을 확인했다.

공개 홈 1회차  cf-cache-status: MISS  POP: SJC
공개 홈 2회차  cf-cache-status: MISS  POP: LAX
공개 홈 3회차  cf-cache-status: HIT   Age: 3  POP: LAX

/ghost/        Cache-Control: private, no-store
                Age 없음

Cookie 포함 홈 cf-cache-status: DYNAMIC
                Age 없음

처음 두 요청이 연속 MISS였던 이유는 요청이 SJC와 LAX의 서로 다른 Cache로 들어갔기 때문이다. 세 번째 요청이 LAX의 기존 객체를 찾아 HIT가 되면서 규칙 자체는 정상임을 확인했다.

캐시 HIT도 한국에서 약 0.44~0.53초가 걸렸다. Ghost 렌더링은 생략됐지만 TCP와 TLS 연결은 여전히 미국까지 왕복했다. 캐시를 적용한 뒤에도 접속 경로 문제는 남아 있었다.

최종 결과와 남겨둔 제약

  • 홈 테마의 중복 get: 9개 → 5개
  • 원점 TTFB 중앙값: 약 241ms → 163ms
  • Kubernetes 상태 점검: 홈 렌더링 → TCP와 경량 API
  • 백업: 30분 주기 → 6시간 주기
  • Restic prune: 매 백업 → 주 1회 별도 실행
  • cloudflared: 2026.7.3으로 고정하고 두 Connector 정상 확인
  • 공개 HTML: 2분 Edge Cache, Admin·회원·쿠키 요청은 캐시 제외
  • Cloudflare 접속 경로: 운영자가 POP를 지정할 수 없어 LAX/SJC 경로를 그대로 유지

같은 한국 회선에서 sw-data.com=LAX, 1.1.1.1=ICN으로 갈리는 trace와 서울 대조군을 통해 원점 문제가 아님을 확인했다. Cloudflare는 Anycast 경로가 항상 지리적으로 가장 가까운 데이터센터를 선택하는 것은 아니며, 안정성과 가용성을 위해 더 먼 POP를 선택할 수 있다고 안내한다. 일반 Cache Rule이나 DNS 설정에는 특정 POP를 지정하는 기능이 없으므로 현재 경로를 그대로 두기로 했다.

이번 작업에서 정리한 기준

  1. 느린 페이지는 원점 렌더링과 공개 네트워크 시간을 따로 측정한다.
  2. Ghost 테마의 get은 화면 요소가 아니라 서버 측 데이터 조회라는 점을 기준으로 중복을 찾는다.
  3. Probe도 실제 운영 요청이므로 생존 확인에 필요한 최소 비용만 사용한다.
  4. CF-Cache-Status만 보지 않고 CF-RAY와 Age를 함께 확인한다.
  5. Cache HIT가 느리면 원점보다 사용자와 접속 POP 사이의 RTT를 먼저 확인한다.
  6. POP를 바꿀 수 없는 경우에는 대조군과 trace로 원점 문제인지부터 구분하고, 캐시처럼 직접 조정할 수 있는 부분을 확인한다.

테마의 중복 조회와 무거운 상태 점검은 직접 고쳤고, 백업 주기를 바꿔 같은 저장소에 걸리는 부하도 줄였다. 공개 HTML 캐시도 적용했지만 한국에서 LAX와 SJC로 접속하는 경로는 남았다. 다음에 비슷한 지연이 생기면 이때 측정한 원점 TTFB와 외부 접속 시간을 각각 비교할 수 있다. 한쪽 수치가 좋아졌다고 나머지 문제까지 해결됐다고 판단하지 않으려고 한다.

참고 문서

SEARCH JOURNAL

기록 검색

글 제목, 요약, 태그에서 찾습니다.

검색을 준비하고 있어요.

검색어를 입력해 주세요.

    Ctrl/⌘ K 검색 Esc 닫기 ↑ ↓ 결과 이동