LODY/정리

Nginx / 캐시와 성능

캐시와 성능

🟣 매번 다시 만드는 낭비

리버스 프록시 뒤의 앱 서버는 요청이 올 때마다 응답을 만든다. 그런데 그 응답 중 상당수는 바로 직전과 똑같다. 첫 화면의 인기 상품 목록, 바뀌지 않는 공지, 정적 이미지. 1초에 수백 명이 같은 목록을 요청하면 앱 서버는 같은 계산과 같은 DB 조회를 수백 번 반복한다.

캐시의 발상은 단순하다. 한 번 만든 응답을 저장해 두고, 같은 요청이 또 오면 앱 서버까지 가지 않고 저장된 것을 바로 돌려준다. Nginx가 앞단에 있으니 이 저장을 앞단에서 하면, 반복 요청의 대부분이 뒷단에 닿기도 전에 처리된다. 앞 장에서 본 리눅스 시스템 프로그래밍의 공유 메모리 캐시가 "한 기계 안 프로세스들의 반복 읽기"를 줄였다면, 여기서는 같은 원리가 "네트워크 너머 반복 요청"을 줄인다.


🟣 proxy_cache: 프록시가 응답을 기억한다

Nginx는 리버스 프록시로 받아 온 뒷단 응답을 디스크에 저장하고 재사용할 수 있다.

proxy_cache_path /var/cache/nginx keys_zone=api_cache:10m max_size=1g inactive=60m;
 
location /api/products {
    proxy_cache api_cache;
    proxy_cache_valid 200 5m;
    proxy_pass http://backend;
}

proxy_cache_valid 200 5m은 정상 응답(200)을 5분간 캐시한다는 뜻이다. 5분 안에 같은 요청이 오면 뒷단으로 가지 않고 저장된 응답을 돌려준다. keys_zone은 어떤 응답을 어떤 키로 저장할지 관리하는 메모리 영역이다. 기본 키는 요청 URL이라, 같은 URL이면 같은 캐시로 취급한다.

핵심 판단은 무엇을 얼마나 캐시할 것인가다. 자주 조회되고 자주 바뀌지 않는 것일수록 캐시 효과가 크다. 반대로 사용자마다 다른 개인화 응답이나 실시간으로 바뀌는 데이터를 캐시하면, 남의 정보가 보이거나 오래된 값이 나가는 사고로 이어진다. 그래서 로그인 사용자별 응답에는 보통 캐시를 걸지 않거나, 쿠키를 캐시 키에 포함시켜 사람별로 분리한다.


🟣 캐시의 어려운 부분: 무효화

캐시에서 가장 까다로운 것은 저장이 아니라 언제 버릴 것인가다. 상품 가격이 바뀌었는데 캐시가 5분간 옛 가격을 계속 내보내면, 사용자는 잘못된 값을 본다. 유효 기간을 짧게 잡으면 이 위험이 줄지만 캐시 효과도 줄고, 길게 잡으면 효과는 크지만 낡은 데이터 위험이 커진다. 이 사이의 균형이 캐시 설계의 본질이다.

요청캐시에있나?저장된 응답바로 반환뒷단에서 만들고저장 후 반환HITMISS

HIT는 앞단에서 즉시 끝나고, MISS만 뒷단까지 간다. 캐시가 잘 맞을수록 초록 경로의 비율이 높아지고 앱 서버의 부하가 그만큼 줄어든다. 응답 헤더의 X-Cache-Status로 각 요청이 HIT인지 MISS인지 확인할 수 있어서, 튜닝할 때 이 비율을 보고 유효 기간을 조정한다.


🟣 캐시 말고도 있는 성능 손잡이

캐시가 반복 계산을 줄인다면, 나머지 성능 개선은 주로 네트워크에 실리는 양과 연결 비용을 줄이는 쪽이다.

전송량을 줄이는 대표적인 방법이 압축이다. gzip을 켜면 HTML, CSS, JS, JSON 같은 텍스트 응답을 압축해 보낸다. 텍스트는 대개 절반 이하로 줄어서, 모바일 네트워크에서 체감 속도 차이가 크다. 다만 이미 압축된 이미지나 동영상에 다시 gzip을 걸면 CPU만 쓰고 효과가 없으니 텍스트 계열에만 적용한다.

연결 비용을 줄이는 것은 keepalive다. 요청마다 TCP 연결을 새로 맺고 끊으면, 특히 TLS 핸드셰이크까지 매번 하면 비용이 크다. keepalive는 한 번 맺은 연결을 여러 요청에 재사용한다. Nginx와 클라이언트 사이뿐 아니라, Nginx와 뒷단 앱 서버 사이에도 연결을 재사용하도록 설정하면 프록시 구간의 지연이 줄어든다.

정적 파일은 앱을 거칠 이유가 아예 없다. 이미지나 빌드된 프런트엔드 자산은 Nginx가 디스크에서 직접 내보내는 것이 가장 빠르다. sendfile을 켜면 파일을 사용자 공간으로 복사하지 않고 커널에서 바로 소켓으로 보낸다. 리눅스 시스템 프로그래밍에서 본 불필요한 복사를 없앤다는 발상, 그 커널 최적화가 여기서도 그대로 쓰인다.


🟣 정리

  • 앱 서버가 매번 같은 응답을 다시 만드는 것은 낭비다. 앞단 캐시는 반복 요청을 뒷단에 닿기 전에 처리한다.
  • proxy_cache로 뒷단 응답을 저장하되, 개인화·실시간 데이터를 캐시하면 정보 노출과 낡은 값 사고로 이어진다.
  • 캐시의 본질적 어려움은 무효화다. 유효 기간의 길이가 캐시 효과와 데이터 신선도 사이의 균형을 정한다.
  • gzip은 전송량을, keepalive는 연결 비용을, sendfile은 커널 복사를 줄인다. 각각 다른 층위의 낭비를 없앤다.
  • 정적 자산은 앱을 거치지 않고 Nginx가 직접 내보내는 것이 가장 빠르다.

여기까지가 Nginx를 복사한 설정이 아니라 이해한 도구로 다루기 위한 정리였다. 이벤트 기반 구조에서 시작해 설정의 계층, 리버스 프록시와 로드 밸런싱, 그리고 캐시와 성능까지, 요청 하나가 들어와 응답으로 나가기까지 앞단에서 벌어지는 일을 순서대로 밟았다. 남이 써 둔 nginx.conf를 열었을 때 이제는 각 줄이 어떤 동작으로 이어지는지 읽힌다면, 이 책의 목적은 이룬 셈이다.