마젠토 2 로 구축된 쇼핑몰의 경우 서버 최적화를 다양한 방법으로 하고 서버의 스팩을 최대한 올려도 트래픽이 몰릴 경우 유난히 무겁다는 느낌을 받습니다. 물론 마젠토 2 로 제작되지 않은 사이트도 트래픽이 폭주할 경우 장애가 발생할 수 있고 사이트가 느려질 수 있습니다.

마젠토 2와 다양한 커스터마이징 된 기능들을 갖춘 쇼핑몰 웹사이트의 속도를 더 쾌적하게 만들기 위해 다양한 노력을 요즘 해보고 있는데, 그 중 대표적인 방법 중 한가지인 바니쉬 캐시(Varnish Cache)에 대해 특징과 기능을 정리해보고자 합니다.

Varnish 소개

Varnish는 BSD 라이선스를 따르는 오픈 소스 웹 캐시 소프트웨어입니다. Varnish는 2006년 노르웨이의 최대 신문사인 Verdens Gang(VG)에서 사용하기 위해 개발되기 시작했으며, 유명한 FreeBSD의 커널 개발자인 Poul-Henning Kamp(PHK)가 개발을 주도했습니다. PHK는 OS 커널에 대한 해박한 지식을 바탕으로, Varnish가 OS 커널과 조화롭게 동작하여 최적의 성능을 발휘할 수 있도록 설계했습니다. Varnish가 개발된 후, VG는 기존에 사용하던 12대의 Squid 서버를 3대의 Varnish 서버로 교체할 수 있게 되었습니다. 현재는 Varnish Software(www.varnish-software.com)가 Varnish를 개발하고 있으며, Varnish에 대한 자문 및 교육 서비스도 제공하고 있습니다.

최근에는 Facebook과 Twitter를 비롯한 많은 사이트에서 Varnish를 사용하고 있습니다. Facebook은 사진 서비스에 Varnish를 사용하며, Twitter는 검색 결과의 캐싱에 Varnish를 사용하고 있습니다. 또한 Varnish를 기반으로 북미와 유럽에서 CDN 서비스를 제공하는 회사 Fastly(www.fastly.com)도 등장했습니다.

Varnish의 특징

Varnish Configuration Language(VCL)

대부분의 소프트웨어는 설정을 위해 많은 Directive를 제공합니다. 그리고 사용자는 제공되는 지시자에 원하는 값을 부여하여 소프트웨어의 동작을 제어하게 됩니다.

Varnish는 설정을 위해 VCL이라는 별도의 DSL(Domain-Specific Language)을 제공합니다. 사용자는 VCL을 사용하여 설정 파일을 작성합니다. VCL로 작성된 설정 파일의 내용은 C 프로그램으로 변환되었다가 공유 라이브러리로 컴파일됩니다. Varnish는 시작할 때나 실행 중일 때 이 공유 라이브러리 파일을 로드하여 사용합니다. 동시에 여러 개의 설정을 로드한 후에 Varnish 실행 중에 설정을 변경할 수 있습니다.

Edge-Side Include(ESI) 지원

CSS 파일이나 JS 파일은 대부분 캐싱되는 데 비해, HTML 파일은 캐싱되지 않습니다. 이는 HTML이 동적이기 때문입니다. ESI는 동적 콘텐츠를 여러 조각으로 분리하여 따로 캐싱하는 기술입니다. 예를 들면 사용자 프로파일 같은 부분은 캐싱하지 않고 다른 부분만 캐싱할 수 있습니다. Varnish는 기본적인 ESI 기능을 제공합니다.

purge 기능

purge는 캐싱되어 있는 데이터를 Time-To-Live(TTL)가 지나기 전에 강제로 삭제하는 기능입니다. Varnish는 두 가지 purge 기능을 제공합니다. 첫 번째는 특정 URL을 지정하여 해당하는 데이터를 삭제하는 것입니다. 두 번째는 정규 표현식을 사용하여 원하는 형태의 데이터가 사용되지 않도록 하는 것입니다.

두 번째는 실제로 데이터를 삭제하지는 않기 때문에 ban이라고 합니다. 캐시에서 데이터를 검색할 때 ban으로 지정된 정규 표현식에 해당하는지 검사를 합니다. 정규 표현식을 추가할 때에는 데이터가 정규 표현식에 일치하는지 검사하지 않게 됩니다. 데이터를 검색할 때 새로이 추가된 ban 조건을 검사하여, 조건을 만족시키면 저장된 데이터를 사용하지 않는 방식입니다.

grace mode 지원

Varnish에 저장되는 데이터에는 TTL 정보가 붙는데, 이는 해당 데이터가 유효한 시간을 의미합니다. 캐시에 저장된 데이터의 TTL이 지나면, 그 데이터를 사용하지 않고 원본 서버에서 해당 데이터를 다시 가져옵니다. 그런데 어떤 경우에는 TTL이 지난 데이터를 사용하는 것이 유리할 수 있습니다. 예를 들면, 원본 서버나 네트워크에 장애가 있는 경우, 또는 특정 데이터에 대한 요청을 이미 원본 서버에 보낸 경우가 있습니다. 이와 같은 경우에 grace mode가 사용됩니다.

grace mode에서는 동일 데이터에 대해 한 번에 하나의 요청만을 원본 서버에 보내게 됩니다. grace mode에서는 TTL이 지났지만 캐시에 저장되어 있는 데이터를 사용하며, 캐시에 저장된 데이터는 TTL + grace time이 지난 후에 캐시에서 삭제됩니다.

load balancing 기능

원본에 대한 요청을 여러 개의 원본 서버에 나누어 보낼 수 있습니다. 부하를 여러 원본 서버로 분산하는 방법에는 random, client, hash, round-robin, DNS, fallback 등이 있습니다.

원본 서버 health check 기능

원본 서버로 지정된 daemon에 대해 지정된 주기로 health check를 수행합니다. 원본 서버에 문제가 있으면 그 원본 서버에는 요청을 보내지 않습니다. 사용할 원본 서버가 없는 경우에는 grace mode로 동작합니다.

saint mode 지원

grace mode와 마찬가지로 saint mode에서는 원본 서버에 요청을 보내지 않고, TTL이 지났지만 캐시에 저장되어 있는 데이터를 사용합니다. 원본 서버가 보낸 답장에 문제가 있으면 saint mode로 동작할 수 있습니다. 원본 서버가 status code 500을 보냈거나 특정 헤더를 추가하여 보내면, 그 답장을 사용하는 대신 캐시에 있는 데이터를 사용합니다.

압축 지원

일반적으로 캐시는 원본 서버가 보낸 데이터를 그대로 저장하는데, Varnish는 원본 서버가 보낸 데이터가 압축되지 않은 경우에 데이터를 압축하여 캐시에 저장할 수 있습니다. 사용자 요청이 압축되지 않은 데이터를 요구하는 경우에는, 압축되어 저장된 데이터의 압축을 푼 다음 사용자에게 전송합니다. 이러한 방식은 데이터 저장에 필요한 공간을 획기적으로 줄일 수 있으며, 같은 크기의 메모리에 훨씬 많은 데이터를 저장하여 캐시 적중률(cache-hit ratio)을 높일 수 있습니다.

저장 공간

Varnish를 실행할 때 저장 공간의 종류를 지정해야 합니다. 저장 공간의 종류에는 malloc, file, persistent가 있습니다. malloc은 malloc() 함수를 사용하여 메모리를 할당받게 됩니다. file은 지정된 파일을 mmap() 함수로 메모리에 매핑하여 사용하며, 이때 여러 개의 파일을 지정할 수 있습니다. 데이터가 전부 메모리에 저장될 수 있으면, malloc을 사용하고 그렇지 않으면 file을 사용합니다.

file 저장 공간을 사용한다고 해서 Varnish를 재실행했을 때, 이전의 데이터가 남아 있는 것은 아니다. file 저장 공간에서도 어떤 데이터는 디스크에 저장되지 않고 메모리에만 있을 수 있습니다. Varnish가 재실행되어도 이전의 데이터가 남아 있기를 원한다면 persistent 저장 공간을 사용합니다. 그러나 persistent를 사용하면 공간이 부족할 때 LRU(Least Recently Used) 대신 FIFO(First In, First Out)로 victim이 선택됩니다.

VCL flow

Varnish 설정이란 웹 요청을 받아서 처리하는 과정의 중간에 실행할 작업을 VCL로 작성하는 것입니다. 다음 그림은 Varnish가 웹 요청을 처리하는 과정을 간략하게 나타낸 것입니다.

캐시에 저장된 데이터가 사용되는 경우에는 vcl_recv() → vcl_hash() → vcl_hit() → vcl_deliver()의 순서대로 함수가 실행됩니다. 요청한 데이터가 캐시에 없으면 vcl_recv() → vcl_hash() → vcl_miss() → vcl_fetch() → vcl_deliver()의 순서대로 함수가 실행됩니다.

위 그림에서 타원으로 표시된 부분이 VCL로 작성하는 부분입니다. 해당 기능을 전부 작성하는 것은 아니고, 기본으로 제공되는 기능을 변경하는 경우 변경할 부분만 작성하면 됩니다. 사용자가 작성한 부분이 먼저 실행되고 나서 기본으로 제공되는 부분이 실행됩니다. 사용자가 작성한 부분에 return이 포함되어 있으면, 기본으로 제공되는 부분은 실행되지 않습니다.

VCL 함수들은 lookup, pass, pipe 등을 반환할 수 있습니다. lookup은 요청받은 데이터가 캐시에 있는지 확인하라는 의미입니다. pass는 캐시에 저장된 데이터는 무시하고 백엔드(backend) 서버에 요청을 보내라는 의미입니다. 백엔드 서버가 보낸 데이터는 캐시에 저장하지 않습니다. pipe는 pass와 유사한데, 다른 점은 pass를 반환하면 다음의 VCL 함수가 호출되는 반면에 pipe를 반환하면 브라우저와 백엔드 서버가 주고 받는 데이터를 중계만 합니다.

다음은 자주 사용하는 VCL 함수에 관한 간단한 설명입니다.

  • vcl_recv: 웹 요청을 받으면 실행됩니다. 웹 요청을 변경하거나 캐시 사용 여부를 결정합니다.
  • vcl_fetch: 원본 서버가 보낸 답장을 받으면 실행됩니다. 원본 서버가 보낸 답장을 변경하거나 TTL 값을 정합니다.
  • vcl_deliver: 사용자에게 답장을 보내기 전에 실행됩니다. 사용자에게 보낼 답장을 변경할 수 있습니다.
  • vcl_hash: hash 함수의 입력을 결정합니다. 기본은 query string을 포함한 URL과 웹 서비스의 도메인 네임입니다. 쿠키 값이나 User-Agent 값을 추가할 수 있습니다.

Edge Side Includes(ESI)

ESI는 동적 콘텐츠를 여러 부분으로 분리하여 조각 단위로 캐싱하는 방식입니다. Akamai, Oracle, Art Technology Group 등이 ESI Specification을 작성하여 W3C에 제출했습니다.

프로세스 구조

Varnish는 parent 프로세스와 child 프로세스, 두 개의 프로세스로 구성됩니다. child 프로세스는 실제 작업을 수행하며, parent 프로세스는 child 프로세스를 감시하다가 child 프로세스에 문제가 발생하면 새로운 child 프로세스를 생성합니다. child 프로세스는 다수의 스레드로 구성됩니다. 다음은 Varnish의 child 프로세스에 있는 스레드의 종류입니다.

  • acceptor 스레드: 클라이언트가 보내는 연결 요청을 처리합니다.
  • epoll 스레드: 다수의 클라이언트로부터 요청이 오기를 기다립니다.
  • worker 스레드: 클라이언트가 보낸 요청을 처리합니다.
  • expire 스레드: 유효 기간이 지난 데이터를 캐시에서 삭제합니다. 유효 기간은 TTL과 grace time을 더한 값이다. 캐시에 저장된 모든 데이터는 유효 기간을 키로 한 binary heap으로 관리되므로, 유효 기간이 지난 데이터를 쉽게 찾을 수 있습니다.
  • backend health check 스레드: 백엔드 서버마다 하나의 health check 스레드가 생성됩니다. health check 스레드는 일정 주기로 백엔드 서버가 정상적으로 동작하는지 확인합니다.

클라이언트의 연결 요청을 받은 acceptor 스레드는 세션을 만들고, 큐를 통해 해당 세션을 worker 스레드에 보냅니다. worker 스레드는 큐에 있는 세션을 가져다 요청을 읽어서 처리하고 그 세션을 epoll 스레드에 보냅니다. epoll 스레드는 다수의 세션을 관리하며, 클라이언트가 보낸 요청이 오면 큐를 통해 해당 세션을 worker 스레드에 보냅니다. 이런 방식으로 active한 세션은 worker 스레드가 담당하고, 요청이 없는 세션은 epoll 스레드가 담당합니다.

마치며

Varnish를 사용하여 동적 콘텐츠를 캐싱하면 두 가지 효과를 기대할 수 있습니다. 첫째는 성능 개선입니다. 동적 콘텐츠를 생산하려면 많은 연산이 필요하므로 동적 콘텐츠를 캐싱하면 성능이 크게 개선됩니다. 둘째는 장애 대응입니다. Varnish는 TTL이 지난 콘텐츠를 바로 삭제하지 않고, 원본 서버에 장애가 발생하면 원본 서버에 요청을 보내는 대신 TTL이 지난 콘텐츠라도 전송하여 장애에 대응할 수 있습니다.

다음은 Varnish를 사용할 때 주의할 사항입니다.

  • 임시 저장 공간: Varnish에는 주 저장 공간과 임시 저장 공간이 있습니다. 주 저장 공간은 malloc, file, persistent 중의 하나로 지정됩니다. 주 저장 공간은 그 크기를 제한할 수 있지만, 임시 저장 공간은 크기 제한이 없습니다. TTL의 값이 shortlived 파라미터(기본값은 10초)보다 작거나 같으면 임시 저장 공간에 저장됩니다. 모든 데이터의 TTL을 일률적으로 10초 미만으로 설정하면 모든 데이터가 임시 저장 공간에 저장됩니다. 뉴스 서비스에서는 shortlived 파라미터를 변경하여 데이터가 임시 저장 공간에 저장되지 않게 했습니다.
  • keepalive: 특별한 경우를 제외하면 웹 서버들은 keepalive-off로 운영합니다. 반면 Varnish는 keepalive-on이 기본입니다. 브라우저와 연결 지속 시간을 제어하는 파라미터로 sess_timeout이 있습니다. 이 파라미터가 지정하는 시간 이내에 요청이 없으면 브라우저와의 연결을 종료시킵니다. 이 파라미터의 기본값은 5초이고 최소값은 1초입니다. Varnish가 keepalive-off로 동작하게 하는 방법은 두 가지가 있습니다. 첫 번째는 Varnish가 보내는 답장의 헤더에 “Connection: close”를 추가하여 보내는 것입니다. 이 헤더를 받은 정상적인 브라우저는 Varnish와의 연결을 종료시킵니다. 그러나 클라이언트가 “Connection: close”를 무시하는 프로그램이라면 연결이 종료되지 않습니다. 두 번째 방법은 연결을 종료시키는 간단한 함수가 포함된 Varnish 모듈을 작성하는 것입니다.
  • overflow: 트래픽이 폭주하는 경우 listen queue overflow가 발생할 수 있습니다. overflow가 발생하면, Linux 커널 파라미터인 net.core.somaxconn의 값과 Varnish의 파라미터인 listen_depth의 값을 조정하여 listen queue의 크기를 증가시켜 줍니다.

0 Comments

Cancel