효율적인 Byte 범위 캐싱 솔루션 NGINX 및 NGINX Plus

캐싱 을 올바르게 배포하면 웹 콘텐츠를 가속화하는 가장 빠른 방법 중 하나입니다. 캐싱은 콘텐츠를 최종 사용자에게 더 가깝게 배치하여 지연 시간을 단축할 뿐만 아니라 Upstream Origin 서버에 대한 요청 횟수를 줄여 용량을 늘리고 대역폭 비용을 절감할 수 있습니다.

AWS와 같이 전 세계적으로 분산된 클라우드 플랫폼과 Route 53과 같은 DNS 기반 글로벌 Load Balancing 시스템을 이용하면 자체 글로벌 CDN(Content Delivery Network)을 구축할 수 있습니다.

이 포스트에서는 NGINX Open Source와 NGINX Plus가 Byte 범위 요청을 사용하여 액세스하는 트래픽을 캐시하고 전송하는 방법을 살펴보겠습니다. 일반적인 사용 사례는 HTML5 MP4 비디오로, 요청이 Byte 범위를 사용하여 Trick‑Play(Skip and Seek) 비디오 재생을 구현합니다. 우리의 목표는 Byte 범위를 지원하고 사용자 지연 시간 및 Upstream 네트워크 트래픽을 최소화하는 비디오 전송용 캐싱 솔루션을 구현하는 것입니다.

목차

1. 테스트 프레임워크
2. NGINX의 기본 Byte 범위 캐싱 동작
3. 단일 캐시 채우기 작업에 Cache Lock 사용
4. Cache Slice 별로 채우기
5. 최적의 Slice 크기 선택
6. 요약

1. 테스트 프레임워크

NGINX를 사용한 캐싱의 다양한 전략을 조사하기 위해서는 간단하고 재현 가능한 테스트 프레임워크가 필요합니다.

A simple, reproducible test bed used to investigate strategies for caching in NGINX

NGINX를 사용한 캐싱 전략을 조사하는 데 사용되는 테스트 프레임워크

Byte 범위 요청이 올바르게 작동하는지 확인할 수 있도록 10Byte마다 Byte Offset이 포함된 10-MB 테스트 파일로 시작합니다.

origin$ perl -e 'foreach $i ( 0 ... 1024*1024-1 ) { printf "%09d\n", 
            $i*10 }' > 10Mb.txt

파일의 첫 번째 줄은 다음과 같습니다.

origin$ head 10Mb.txt
000000000
000000010
000000020
000000030
000000040
000000050
000000060
000000070
000000080
000000090

파일의 중간 Byte 범위(500,000 ~ 500,009)에 대한 curl 요청은 예상 Byte 범위를 반환합니다.

client$ curl -r 500000-500009 http://origin/10Mb.txt
000500000

이제 Origin 서버와 NGINX Proxy 캐시 간의 개별 연결에 1MB/s 대역폭 제한을 추가해 보겠습니다.

origin# tc qdisc add dev eth1 handle 1: root htb default 11
origin# tc class add dev eth1 parent 1: classid 1:1 htb rate 1000Mbps
origin# tc class add dev eth1 parent 1:1 classid 1:11 htb rate 1Mbps

지연이 예상대로 작동하는지 확인하기 위해 Origin 서버에서 직접 전체 파일을 검색합니다.

cache$ time curl -o /tmp/foo http://origin/10Mb.txt
% Total    % Received    % Xferd  Average  Speed   Time      ...
                                  Dload    Upload  Total     ...
100 10.0M  100 10.0M     0     0  933k     0       0:00:10   ...

    ... Time     Time      Current
    ... Spent    Left      Speed
    ... 0:00:10  --:--:--  933k
 
real    0m10.993s
user    0m0.460s
sys     0m0.127s

파일이 전송되는 데 거의 11초가 걸리는데, 이는 대역폭이 제한된 WAN 네트워크를 통해 Origin 서버에서 대용량 파일을 가져오는 Edge 캐시의 성능을 합리적으로 시뮬레이션한 결과입니다.

2. NGINX의 기본 Byte 범위 캐싱 동작

NGINX가 전체 리소스를 캐시한 후에는 디스크에 캐시된 복사본에서 직접 Byte 범위 요청을 서비스합니다.

콘텐츠가 캐시되지 않으면 어떻게 되나요? 캐시되지 않은 콘텐츠에 대한 Byte 범위 요청을 받으면 NGINX는 Origin 서버에서 Byte 범위가 아닌 전체 파일을 요청하고 응답을 임시 저장소로 스트리밍하기 시작합니다.

클라이언트의 원래 Byte 범위 요청을 충족하는 데 필요한 데이터를 수신하는 즉시 NGINX는 해당 데이터를 클라이언트로 전송합니다. 백그라운드에서 NGINX는 임시 저장소에 있는 파일에 대한 전체 응답을 계속 스트리밍합니다. 전송이 완료되면 NGINX는 파일을 캐시로 이동합니다.

다음과 같은 간단한 NGINX 구성을 통해 기본 동작을 아주 쉽게 시연할 수 있습니다.

proxy_cache_path /tmp/mycache keys_zone=mycache:10m;
 
server {
    listen 80;
 
    proxy_cache mycache;
 
    location / {
        proxy_pass http://origin:80;
    }
}

먼저 캐시를 비우는 것으로 시작합니다.

cache # rm –rf /tmp/mycache/*

그런 다음 10Mb.txt의 중간 10Byte를 요청합니다.

client$ time curl -r 5000000-5000009 http://cache/10Mb.txt
005000000
 
real    0m5.352s
user    0m0.007s
sys     0m0.002s

NGINX는 Origin 서버에 전체 10Mb.txt 파일에 대한 요청을 전송하고 캐시에 로딩을 시작합니다. 요청된 Byte 범위가 캐시되는 즉시 NGINX는 이를 클라이언트에 전달합니다. time 명령에 의해 보고된 바와 같이, 이 과정은 5초가 조금 넘게 걸립니다.

이전 테스트에서 전체 파일을 전송하는 데 10초가 조금 넘게 걸렸는데, 이는 중간 Byte 범위가 클라이언트에 전송된 후 10Mb.txt의 전체 내용을 검색하고 캐싱 하는 데 약 5초가 더 걸린다는 것을 의미합니다. 가상 서버의 액세스 로그에는 10,486,039바이트(10MB)의 전체 파일 전송이 상태 코드 200과 함께 기록되어 있습니다.

192.168.56.10 - - [08/Dec/2015:12:04:02 -0800] "GET /10Mb.txt HTTP/1.0" 200 10486039 "-" "-" "curl/7.35.0"

전체 파일이 캐시된 후 curl 요청을 반복하면 NGINX가 캐시에서 요청된 Byte 범위를 제공하기 때문에 응답이 즉시 이루어집니다.

그러나 이 기본 구성(및 그에 따른 기본 동작)에는 문제가 있습니다. 동일한 Byte 범위를 캐시된 후 전체 파일이 캐시에 추가되기 전에 두 번째로 요청하면 NGINX는 전체 파일에 대한 새 요청을 Origin 서버로 전송하고 새 캐시 채우기 작업을 시작합니다. 이 명령으로 이 동작을 시연할 수 있습니다.

client$ while true ; do time curl -r 5000000-5000009 http://dev/10Mb.txt ; done

Origin 서버에 대한 모든 새로운 요청은 새로운 캐시 채우기 작업을 트리거하며 다른 작업이 진행 중이지 않은 상태에서 캐시 채우기 작업이 완료될 때까지 캐시는 “안정화”되지 않습니다.

사용자가 동영상 파일을 게시한 직후 바로보기 시작하는 시나리오를 상상해 보세요. 예를 들어 캐시 채우기 작업에 30초가 걸리지만 추가 요청 사이의 지연 시간이 이보다 짧으면 캐시가 채워지지 않을 수 있으며 NGINX는 전체 파일에 대한 요청을 Origin 서버로 계속 더 많이 전송할 것입니다.

NGINX는 이 문제에 대한 효과적인 해결책이 될 수 있는 두 가지 캐싱 구성을 제공합니다.

  • Cache Lock – 이 구성을 사용하면 첫 번째 Byte 범위 요청에 의해 트리거되는 캐시 채우기 작업 중에 NGINX는 이후의 모든 Byte 범위 요청을 Origin 서버로 직접 전달합니다. 캐시 채우기 작업이 완료되면 NGINX는 Byte 범위와 전체 파일에 대한 모든 요청을 캐시에서 제공합니다.
  • Cache Slicing – NGINX Plus R8 및 NGINX Open Source 1.9.8에 도입된 이 전략을 통해 NGINX는 파일을 빠르게 검색할 수 있는 더 작은 하위 범위로 분할하고 필요에 따라 Origin 서버에 각 하위 범위를 요청합니다.

3. 단일 캐시 채우기 작업에 Cache Lock 사용

다음 구성은 첫 번째 Byte 범위 요청이 수신되면 즉시 캐시 채우기를 트리거하고 캐시 채우기 작업이 진행되는 동안 다른 모든 요청을 Origin 서버로 전달합니다.

proxy_cache_path /tmp/mycache keys_zone=mycache:10m;
 
server {
    listen 80;
 
    proxy_cache mycache;
    proxy_cache_valid 200 600s;
    proxy_cache_lock on;
       
    # Immediately forward requests to the origin if we are filling the cache
    proxy_cache_lock_timeout 0s;
 
    # Set the 'age' to a value larger than the expected fill time
    proxy_cache_lock_age 200s;
       
    proxy_cache_use_stale updating;
 
    location / {
        proxy_pass http://origin:80;
    }
}
  • proxy_cache_lock on – Cache Lock을 설정합니다. 파일에 대한 첫 번째 Byte 범위 요청을 받으면 NGINX는 Origin 서버에서 전체 파일을 요청하고 캐시 채우기 작업을 시작합니다. NGINX는 후속 Byte 범위 요청을 전체 파일에 대한 요청으로 변환하거나 새 캐시 채우기 작업을 시작하지 않습니다. 대신 첫 번째 캐시 채우기 작업이 완료되거나 잠금 시간이 초과될 때까지 요청을 Queue에 대기시킵니다.
  • proxy_cache_lock_timeout – 캐시가 잠기는 시간을 제어합니다(기본값은 5초). 시간 제한이 만료되면 NGINX는 대기 중인 각 요청을 수정되지 않은 상태로 Origin 서버로 전달하고(전체 파일에 대한 요청이 아니라 Range 헤더가 보존된 Byte 범위 요청으로) Origin 서버에서 반환한 응답을 캐싱 하지 않습니다.
    10Mb.txt를 사용한 테스트와 같은 상황에서는 캐시 채우기 작업에 상당한 시간이 걸릴 수 있으므로 요청을 Queue에 넣을 필요가 없으므로 잠금 시간 제한을 0 초로 설정했습니다. NGINX는 캐시 채우기 작업이 완료될 때까지 파일에 대한 모든 Byte 범위 요청을 Origin 서버로 즉시 전달합니다.
  • proxy_cache_lock_age – 캐시 채우기 작업의 기한을 설정합니다. 지정된 시간 내에 작업이 완료되지 않으면 NGINX는 Origin 서버에 요청을 한 번 더 전달합니다. 항상 예상 캐시 채우기 시간보다 길어야 하므로 기본값인 5초에서 200초로 늘립니다.
  • proxy_cache_use_stale updating – NGINX가 리소스를 업데이트하는 중일 때 현재 캐시된 버전의 리소스를 즉시 사용하도록 NGINX에 지시합니다. 이렇게 하면 캐시 업데이트를 트리거한 첫 번째 요청에는 차이가 없지만 후속 요청에 대한 클라이언트의 응답 속도가 빨라집니다.

테스트를 반복하여 중간 Byte 범위인 10Mb.txt를 요청합니다. 파일은 캐시되지 않았으며, 이전 테스트와 마찬가지로 NGINX가 요청된 Byte 범위를 전송하는 데 5초가 조금 넘는 시간이 걸리는 것으로 나타났습니다(네트워크가 1Mb/s 처리량으로 제한되어 있다는 점을 기억하세요).

client # time curl -r 5000000-5000009 http://cache/10Mb.txt
005000000
 
real    0m5.422s
user    0m0.007s
sys     0m0.003s

캐시 잠금으로 인해 캐시가 채워지는 동안 Byte 범위에 대한 후속 요청이 거의 즉시 충족됩니다. NGINX는 이러한 요청을 캐시에서 처리하지 않고 Origin 서버로 전달합니다.

client # time curl -r 5000000-5000009 http://cache/10Mb.txt
005000000
 
real    0m0.042s
user    0m0.004s
sys     0m0.004s

Origin 서버의 액세스 로그에서 발췌한 다음 항목에서 상태 코드가 206인 항목은 Origin 서버가 캐시 채우기 작업이 완료되는 데 걸리는 시간 동안 Byte 범위 요청을 서비스하고 있음을 확인시켜 줍니다. (어떤 요청이 수정되고 어떤 요청이 수정되지 않았는지 식별하기 위해 log_format 지시문을 사용하여 로그 항목에 범위 요청 헤더를 포함시켰습니다.)

상태 코드 200이 있는 마지막 줄은 첫 번째 Byte 범위 요청의 완료에 해당합니다. NGINX는 이를 전체 파일에 대한 요청으로 수정하고 캐시 채우기 작업을 트리거했습니다.

192.168.56.10 - - [08/Dec/2015:12:18:51 -0800] "GET /10Mb.txt HTTP/1.0" 206 343 "-" "bytes=5000000-5000009" "curl/7.35.0"
192.168.56.10 - - [08/Dec/2015:12:18:52 -0800] "GET /10Mb.txt HTTP/1.0" 206 343 "-" "bytes=5000000-5000009" "curl/7.35.0"
192.168.56.10 - - [08/Dec/2015:12:18:53 -0800] "GET /10Mb.txt HTTP/1.0" 206 343 "-" "bytes=5000000-5000009" "curl/7.35.0"
192.168.56.10 - - [08/Dec/2015:12:18:54 -0800] "GET /10Mb.txt HTTP/1.0" 206 343 "-" "bytes=5000000-5000009" "curl/7.35.0"
192.168.56.10 - - [08/Dec/2015:12:18:55 -0800] "GET /10Mb.txt HTTP/1.0" 206 343 "-" "bytes=5000000-5000009" "curl/7.35.0"
192.168.56.10 - - [08/Dec/2015:12:18:46 -0800] "GET /10Mb.txt HTTP/1.0" 200 10486039 "-" "-" "curl/7.35.0"

전체 파일이 캐시된 후 테스트를 반복하면 NGINX는 캐시에서 추가 Byte 범위 요청을 처리합니다.

client # time curl -r 5000000-5000009 http://cache/10Mb.txt
005000000
 
real    0m0.012s
user    0m0.000s
sys     0m0.002s

Cache Lock을 사용하면 캐시 채우기 작업이 최적화되지만 캐시 채우기 기간 동안 모든 후속 사용자 트래픽을 Origin 서버로 전송해야 하는 비용이 발생합니다.

4. Cache Slice 별로 채우기

NGINX Plus R8 및 NGINX Open Source 1.9.8에 도입된 Cache Slice 모듈은 캐시를 채우는 대체 방법을 제공하며, 대역폭 제약이 심하고 캐시 채우기 작업에 시간이 오래 걸리는 경우 더 효율적입니다.

Cache Slice 방법을 사용하여 NGINX는 파일을 더 작은 세그먼트로 나누고 필요할 때 각 세그먼트를 요청합니다. 세그먼트는 캐시에 누적되며, 리소스에 대한 요청은 하나 이상의 세그먼트 중 적절한 부분을 클라이언트에 전달하여 충족됩니다. 대용량 Byte 범위(또는 실제로는 전체 파일)에 대한 요청은 필요한 각 세그먼트에 대한 하위 요청을 트리거하며, 이 요청은 Origin 서버에서 도착하는 대로 캐시됩니다. 모든 세그먼트가 캐시되는 즉시 NGINX는 해당 세그먼트의 응답을 취합하여 클라이언트로 전송합니다.

Detailed view of NGINX cache slicing

NGINX Cache Slicing이 활성화된 요청 처리

다음 구성 Snippet에서 slice 지시문(NGINX Plus R8 및 NGINX Open Source 1.9.8에 도입됨)은 NGINX가 각 파일을 1MB 조각으로 분할하도록 지시합니다.

slice 지시문을 사용할 때는 파일의 조각을 구분하기 위해 proxy_cache_key 지시문에 $slice_range 변수를 추가해야 하며, NGINX가 Origin 서버에서 적절한 Byte 범위를 요청하도록 요청에서 Range 헤더를 교체해야 합니다. HTTP/1.0은 Byte 범위 요청을 지원하지 않으므로 요청을 HTTP/1.1로 업그레이드합니다.

proxy_cache_path /tmp/mycache keys_zone=mycache:10m;
 
server {
    listen 80;
 
    proxy_cache mycache;
 
    slice              1m;
    proxy_cache_key    $host$uri$is_args$args$slice_range;
    proxy_set_header   Range $slice_range;
    proxy_http_version 1.1;
    proxy_cache_valid  200 206 1h;
 
    location / {
        proxy_pass http://origin:80;
    }
}

이전과 마찬가지로 중간 Byte 범위는 10Mb.txt로 요청합니다.

client$ time curl -r 5000000-5000009 http://cache/10Mb.txt
005000000
 
real    0m0.977s
user    0m0.000s
sys     0m0.007s

NGINX는 요청된 Byte 범위인 5000000-5000009를 포함하는 단일 1MB 파일 세그먼트(바이트 범위 4194304-5242879)를 요청하여 요청을 충족합니다.

KEY: www.example.com/10Mb.txtbytes=4194304-5242879
HTTP/1.1 206 Partial Content
Date: Tue, 08 Dec 2015 19:30:33 GMT
Server: Apache/2.4.7 (Ubuntu)
Last-Modified: Tue, 14 Jul 2015 08:29:12 GMT
ETag: "a00000-51ad1a207accc"
Accept-Ranges: bytes
Content-Length: 1048576
Vary: Accept-Encoding
Content-Range: bytes 4194304-5242879/10485760

Byte 범위 요청이 여러 세그먼트에 걸쳐 있는 경우, NGINX는 필요한 모든 세그먼트(아직 캐시되지 않은 세그먼트)를 요청한 다음 캐시된 세그먼트에서 Byte 범위 응답을 조합합니다.

Cache Slice 모듈은 Byte 범위 요청을 사용하여 브라우저에 콘텐츠를 의사 스트리밍하는 HTML5 비디오 전송을 위해 개발되었습니다. 대역폭이 제한되어 있어 초기 캐시 채우기 작업에 몇 분이 걸릴 수 있고 배포 후 파일이 변경되지 않는 동영상 리소스에 이상적입니다.

5. 최적의 Slice 크기 선택

Slice 크기를 각 세그먼트가 빠르게 전송될 수 있을 만큼 작은 값으로 설정합니다(예: 1~2초 이내). 이렇게 하면 여러 요청으로 인해 위에서 설명한 연속 업데이트 동작이 트리거될 가능성을 줄일 수 있습니다.

반면에 Slice 크기를 너무 작게 설정할 수도 있습니다. 전체 파일에 대한 요청이 동시에 수천 개의 작은 요청을 트리거하는 경우 오버헤드가 높아져 메모리 및 파일 Descriptor 사용량이 과도하게 증가하고 디스크 활동량이 늘어날 수 있습니다.

또한 Cache Slice 모듈은 리소스를 독립적인 세그먼트로 분할하기 때문에 일단 캐시된 리소스는 변경할 수 없습니다. 모듈은 원본에서 세그먼트를 수신할 때마다 리소스의 ETag 헤더를 확인하며, ETag가 변경되면 기본 캐시된 버전이 손상되었으므로 NGINX는 트랜잭션을 중단합니다. Cache Slicing은 동영상 파일과 같이 한 번 배포되면 변경되지 않는 대용량 파일에 대해서만 사용하는 것이 좋습니다.

6. Byte 범위 캐싱 요약

Byte 범위 캐싱 을 사용하여 대용량 리소스를 전송하는 경우 캐시 잠금 및 Cache Slice 기술을 사용하면 네트워크 트래픽을 최소화하고 사용자에게 뛰어난 콘텐츠 전송 성능을 제공할 수 있습니다.

캐시 채우기 작업을 빠르게 수행할 수 있고 채우기가 진행되는 동안 Origin 서버로 트래픽이 급증하는 것을 수용할 수 있는 경우 Cache Lock 기술을 사용합니다.

캐시 채우기 작업이 매우 느리고 콘텐츠가 안정적(변경되지 않음)인 경우 새로운 Cache Slice 기술을 사용합니다.

어떤 방법을 선택하든 NGINX팀은 콘텐츠를 위한 완벽한 캐싱 Edge 네트워크 또는 CDN을 구축할 수 있도록 도와드립니다.

NGINX Plus의 캐싱 기술을 직접 사용해 보거나 테스트해 보려면 지금 30일 무료 평가판을 신청하거나 사용 사례에 대해 최신 소식을 빠르게 전달받고 싶으시면 아래 뉴스레터를 구독하세요.