NHN 메인서비스개발팀 강흠근

웹 서비스에서는 사회적 이슈나 각종 이벤트, DDoS(Distributed Denial of Service) 공격 등으로 인해 트래픽이 갑자기 폭주하는 경우가 있습니다. 이러한 트래픽 폭주로 발생하는 장애를 예방하기 위해 일반적으로는 불필요한 자원을 미리 할당해 놓습니다. 그러나 자원을 미리 할당하는 것은 낭비일 수 있습니다. 자원을 낭비하지 않으면서 트래픽 폭주로 발생하는 장애를 예방하기 위해 Varnish를 사용할 수 있습니다.

이 글에서는 Vanish의 특징과 기능을 설명하고 서비스에 적용한 사례를 소개하겠습니다.

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)도 등장했다.

a5001b1e30165ed310e06af9f4a354c6.jpg

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 조건을 검사하여, 조건을 만족시키면 저장된 데이터를 사용하지 않는다.

아래는 ban 명령의 한 예이다. www.naver.com에서 가져온 모든 .jpg 파일이 검색되지 않도록 하는 명령이다. 이 명령은 현재 캐시에 저장된 데이터에만 적용되며, 이 명령의 실행 후에 저장된 데이터는 영향을 받지 않는다.

ban req.http.host == "www.naver.com" && req.url ~ "\.jpg$"

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가 웹 요청을 처리하는 과정을 간략하게 나타낸 것이다.

de8596d755949d6085f9c03d64652fb0.png

그림 1 Varnish의 웹 요청 처리 과정(이미지 출처: https://www.varnish-software.com/static/book/VCL_Basics.html)

캐시에 저장된 데이터가 사용되는 경우에는 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에 제출했다. 다음 그림은 하나의 페이지를 캐싱하기 위해 여러 부분으로 나눈 예이다.

3e985714279d6f52c1180e1d94835d56.png

그림 2 캐싱 관점의 웹 페이지 구성(이미지 출처: Per Andreas Buer. "Extreme web performance with Varnish." Ez conference 2009, Paris.)

위 그림의 페이지에서 어떤 부분은 개인 정보가 포함되어 있어서 캐싱할 수 없고, 어떤 부분은 자주 변경되며 어떤 부분은 잘 변경되지 않는다. 개인 정보가 포함되어 있기 때문에 이런 페이지를 통째로 캐싱할 수는 없다. 따라서 페이지를 여러 부분으로 나누어 캐싱하고, 캐싱할 수 없는 부분은 매번 백엔드 서버에 요청하여 가져온 다음 각 부분을 조합하여 전체 페이지를 생산한다. 백엔드 서버는 매번 페이지 전체를 생성하는 대신에 캐싱할 수 없는 부분만 생성한다.

프로세스 구조

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 스레드가 담당한다.

적용 사례: 2012년 대통령 선거

대통령 선거는 전국적인 이벤트이다. 개표 현황을 보기 위해 트래픽이 폭주할 가능성이 있다. 2012년 대통령 선거일 뉴스 서비스에 Varnish를 적용하였다. 다음 그림은 기존 서비스 구조에 Varnish를 적용한 형태이다.

891f8e7c540a468e8a3c9daaf36f1e3a.png

그림 3 뉴스 서비스에 Varnish를 적용한 형태

Apache 웹 서버와 Tomcat 사이에 Varnish를 적용했다. 일반적으로 Apache 웹 서버와 Tomcat은 Apache JServ Protocol로 연결하지만, Varnish는 HTTP만을 지원하므로 세 소프트웨어는 모두 HTTP로 통신한다.

다음 그림은 nGrinder를 사용하여 Apache 웹 서버와 Tomcat만 사용한 경우(AT)와 Varnish까지 사용한 경우(AVT)의 초당 처리한 요청의 수를 비교한 결과로, Varnish를 사용한 경우에 기존에 비해 TPS(Transaction Per Second)가 10배 정도 높은 것을 볼 수 있다.

5d7a249349206b3025b246bff4fdb59e.png

그림 4 뉴스 서비스 성능 비교(단위: TPS)

이는 캐시에 저장될 수 있는 URL에 최대한의 부하를 주어 캐시 적중률이 100%에 가까운 상황에서 비교한 결과이다. 실제 서비스에서는 위 그림과 다를 수 있는데, 그 첫 번째 이유는 캐시에 모든 데이터를 저장할 수는 없다는 것이다. 사용자마다 다른 콘텐츠가 생성되는 스크랩 URL(예: http://news.naver.com/main/scrap/index.nhn)이나 매번 key 생성이 필요한 동영상 관련 URL 등이 여기에 속한다.

다음 표는 성능 테스트에 사용한 URL의 비율이다. 이 표의 URL은 모두 캐시에 저장될 수 있다.

표 1 성능 테스트에 사용된 URL의 비율

구분

URL

비율

기사 본문

/main/read.nhn?~~~ (2만개)

60%

뉴스 홈

/main/home.nhn

3%

대선 홈

/predisent2012/index.nhn

8%

대선 뉴스

/president2012/news/index.nhn

8%

대선 트렌드

/president2012/trend/index.nhn

8%

대선 후보자

/president2012/candidate/index.nhn

8%

정치 섹션 홈

/main.nhn?mode=LSD&sid=100

2%

연예 섹션 홈

/main.nhn?mode=LSD&sid=106

3%

위 테스트와 실제 서비스의 결과가 다른 두 번째 이유는 성능 테스트에서 발생시키는 트래픽보다 훨씬 적은 트래픽이 유입된다는 것이다. 트래픽이 적으면 캐시 적중률이 낮아져서 캐시의 효과가 감소한다.

마치며

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

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

  • 임시 저장 공간: Varnish에는 주 저장 공간과 임시 저장 공간이 있다. 주 저장 공간은 malloc, file, persistent 중의 하나로 지정된다. 주 저장 공간은 그 크기를 제한할 수 있지만, 임시 저장 공간은 크기 제한이 없다. TTL의 값이 shortlived 파라미터(기본값은 10초)보다 작거나 같으면 임시 저장 공간에 저장된다. 모든 데이터의 TTL을 일률적으로 10초 미만으로 설정하면 모든 데이터가 임시 저장 공간에 저장된다. 뉴스 서비스에서는 shortlived 파라미터를 변경하여 데이터가 임시 저장 공간에 저장되지 않게 했다.
  • keepalive: 특별한 경우를 제외하면 NHN의 웹 서버들은 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의 크기를 증가시킨다.

참고 자료

7d3aad68ee1585ed170221b3ed0faee3.jpg
NHN 메인서비스개발팀 강흠근
If your application is doing unneeded or unappreciated work—repainting the screen multiple times, computing statistics too frequently, etc.—then eliminating such waste is a lucrative area for performance work.
- Bart Smaalders

네이버지도 상위등록

네이버 지도순위로
매장 및 가게를 홍보하세요.

통합홈페이지

네이버, 구글
사이트 상위노출