Varnish Cache #2 : Configuration

Varnish 를 커스터마이징 또는 유지보수 하실때 도움이 될수 있도록
항공권 서비스에 세팅한 Varnish 설정에 대해 설명드리려 합니다

실행 명령어

아래 varnishd 명령으로 항공권 통검 Varnish 를 실행시키는데요
사용한 옵션들에 대해 항목별로 설명해 드리도록 하겠습니다

/home/apps/varnish/sbin/varnishd -F 
-f /home/apps/varnish/vcl/hit.vcl
-s malloc,1G
-n /home/apps/varnish/var/run
-a :80
-p vsl_mask=+Hash

-F

Foreground mode 로 실행한다는 옵션입니다
Docker container 실행시 container run 상태를 유지하기 위해 설정했습니다

-f /home/apps/varnish/vcl/hit.vcl

Varnish 가 참조할 VCL 설정 파일의 경로를 지정해주는 옵션입니다
Cache On / Off 기능을 구현하기 위해
hit.vcl 과 pass.vcl 파일을 각각 생성했다고 말씀드렸었는데요
-f 옵션으로 어떤 VCL 파일을 참조하느냐에 따라 Cache On / Off 설정이 바뀌게 됩니다

-s malloc,1G

html 페이지를 캐싱하려면 메모리 공간이 필요하겠죠?
Varnish가 사용할 storage 사이즈를 설정해주는 옵션입니다.

처음에는 사용량이 가늠이 안되서 4G 정도로 넉넉하게 잡았었는데
모니터링을 해보니 100M 정도밖에 사용하지 않아서
1G 로 잡아도 충분할것 같아서 위와 같이 설정했습니다

-n /home/apps/varnish/var/run

Varnish working directory를 지정해주는 옵션입니다
varnish 실행에 관련된 정보들을 저장하고 있습니다 (ex. varnish process pid)

working directory 는 추후 varnish 모니터링/로깅 툴을 실행할때도 사용됩니다
예를들어 ./varnishstat -n /home/apps/varnish/var/run 과 같이 사용됩니다

-a :80

varnish listen port 를 지정해줍니다
참고로 irteam 계정으로 80 포트를 사용하기 위해서
Varnish Dockerfile 에서 아래와 같은 명령을 수행했습니다

USER root
RUN /usr/sbin/setcap 'cap_net_bind_service=+ep'
/home/apps/varnish/sbin/varnishd

-p vsl_mask=+Hash

-p 는 기타 파라미터 옵션들을 설정해줄때 사용합니다
vsl_mask=+Hash 옵션은 cache key 가 의도한 대로 설정되었는지 확인하기 위해서 세팅했습니다
만약 저 옵션이 없으면 cache key 가 hash 된 값으로 보여서 의도한대로 설정되었는지 확인이 어렵습니다

varnishlog 기능으로 cache key 가 어떻게 적용되었는지 확인할수 있습니다. varnishlog 에 대해서는 모니터링에 대해 설명할때 다시 말씀드리겠습니다

설정파일

Varnish 는 VCL(Varnish Configuration Language) 에 있는 설정을 참조해 실행됩니다
요청이 들어오면 Varnish 는 각 단계별로 짜여진 로직에 따라 처리방법을 결정하는데요
이때 처리방법을 어떻게 할지 설정하는게 VCL 파일입니다

먼저 Cache Off 모드인 pass.vcl 파일을 예시로 설명드리도록 하겠습니다

pass.vcl

vcl_recv -> vcl_hash -> vcl_pass -> Backend fetch -> vcl_deliver 흐름으로 진행됩니다

vcl_recv 는 request handling 을 하는 로직인데요
여기서 cache 할 request 대상을 선별할 수 있습니다

pass.vcl 는 cache 를 하지 않는 VCL 설정이므로
모든 요청에 대해 일괄적으로 return(pass); 를 했습니다

sub vcl_recv {
return(pass);
}

이후 캐시에 관한 별다른 처리를 하지 않고 vcl_hash -> vcl_pass -> Backend fetch 단계까지 넘어가게 됩니다

Backend fetch 단계는 원본서버인 어플리케이션 서버에서
html 응답을 받아오는 단계를 말합니다

여기서 정상적으로 응답이 전달되면(deliver)
client 에게 응답을 해주기 전 vcl_deliver 단계로 오게 되는데요
여기에서는 response header 에 필요한 값을 추가하거나 정리해주는 로직을 넣습니다

sub vcl_deliver {
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
} else {
set resp.http.X-Cache = "MISS";
}
}

Client 에서도 Cache Hit / Miss 에 대한 정보를 쉽게 파악할수 있게
response header X-Cache 값에 Cache Hit / Miss 여부를 넣었습니다

hit.vcl

hit.vcl 에서는 크게 두가지 흐름으로 진행이 되는데요
Cache Hit 과 Cache Miss 의 경우로 진행이 됩니다

Cache Hit

요청이 왔을때 Varnish 에 기존에 저장해둔 Cache 가 있으면
Backend Fetch 없이 Cache 를 바로 반환하고 이걸 Cache Hit 이라고 합니다

요청을 처리하는 첫 단계인 vcl_recv 설정에서
request url 들에 대해 기본적으로 cache 처리하도록 했는데요
단, health check 를 하는 /monitor/l7check 요청은 cache 처리하지 않고 pass 하도록 설정했습니다

sub vcl_recv {
if (req.url ~ "^/monitor/l7check") {
return (pass);
}
return(hash);
}

/monitor/l7check 같은 경우는 Varnish가 아닌 어플리케이션 서버의 상태를 체크하는것이기 때문에
cache 응답을 주는것보다 어플리케이션 서버의 실시간 응답을 전달하는게 맞다고 생각했기 때문입니다

이후 vcl_hash 단계에서는
hash_data 함수를 통해 cache key 를 설정해주는 작업을 진행합니다

varnish 에 해당 cache가 있으면 vcl_deliver 단계가 실행되어 client에게 캐싱된 결과를 반환하게 됩니다

Cache Miss

요청이 왔을때 Varnish 에 저장해둔 Cache 가 없거나 만료되면
Backend Fetch 를 통해 어플리케이션 서버에서 html을 받아와서
Varnish Cache 에 저장하고 Client 에게 전달합니다

vcl_recv, vcl_hash 설정은 cache hit 인 경우와 동일한데요
cache key 에 해당하는 데이터가 없기 때문에 Backend fetch 로 어플리케이션 서버에서 html 을 가져옵니다

이떄 vcl_backend_response 가 실행되는데요
여기서 cache 정책을 어떻게 설정할지 정할 수 있습니다

sub vcl_backend_response {
set beresp.ttl = 10m;
set beresp.grace = 30m;

if (beresp.status >= 500){
if (bereq.is_bgfetch){
return (abandon);
}
set beresp.uncacheable = true;
}
}

beresp.ttl 을 10분, beresp.grace 를 30분으로 설정해뒀는데요
ttl / grace 에 대해서는 밑에서 따로 설명드리겠지만
간단하게 ttl + grace 로 계산해서 cache 가 40분동안 유지된다고 생각하시면 됩니다

beresp.status >= 500 설정을 통해
backend response status code 가 5xx 인 것들은 에러로 판단해서 캐싱하지 않도록 설정했습니다.

만약 일시적으로 에러가 발생했을때 우연히 캐시가 됐다면
이후 어플리케이션 서버가 정상으로 돌아와서 정상 응답을 반환하더라도
Varnish는 예전에 저장했던 에러 화면을 캐시가 만료될때까지 반환할것이기 때문에
이런 상황을 방지하고자 5xx 응답일때는 캐시하지 않도록 설정했습니다

backend health check

probe 기능으로 html 을 응답하는 어플리케이션 서버의 상태를 주기적으로 체크하도록 설정했습니다

probe healthcheck {
.url = "/monitor/l7check";
.interval = 3s;
.window = 3;
.threshold = 2;
}
backend default {
.host = "{{ .Values.backend.host }}";
.port = "80";
.probe = healthcheck;
}

어플리케이션 서버의 /monitor/l7check url 을 3초에 한번씩 호출해
3번중(window) 2번(threshold) 이상 응답이 성공하면 정상적인것으로 판단하고
그렇지 않으면 어플리케이션 서버가 문제가 있다고 판단해
client 요청에 대해 Varnish가 503 Error 를 바로 반환합니다

grace mode

Varnish 의 유용한 기능중 하나가 grace 인데요
아까 ttl + grace 가 실질적인 cache 시간이라고 했는데 그 이유를 설명드리도록 하겠습니다

예를들어 초당 100건의 요청이 동시에 들어오는 서비스가 있다고 가정했을때
ttl 시간만 10분으로 설정되어 있다면(grace 는 별도 설정이 안되어있음)
10분 후에 cache가 만료되어 backend fetch 를 해야하는 경우
동시다발적으로 100건의 요청이 backend 서버에 보내지게 됩니다

이렇게 되면 cache를 적용했음에도 순간적으로 backend 서버에 부담이 전해지게 됩니다
특히 연산이 오래 걸리는 페이지의 경우 부담은 더 커지겠죠

grace mode 기능은 이러한 문제점을 해결할수 있는데요
아주 쉽게말하면 async cache update 를 해준다고 생각하시면 됩니다

grace를 설정해두게 되면 ttl 시간이 지나서 cache 가 만료된 상태일때
request 가 들어오면 기존에 cache 된 값을 전달해주면서
이와 동시에 1건의 backend fetch 요청을 통해 cache 값을 업데이트합니다

예를들어 ttl 10분 + grace 30분으로 설정된 경우
캐시 생성후 10분이 지났을때 request 가 들어와도
grace 가 설정되어 있기 때문에 cache 값을 varnish 에서 바로 전달합니다
그리고 동시에 backend 서버에 1건의 요청을 보내 cache를 업데이트 해서
이후 요청부터는 새로운 html 이 전달되게 됩니다

backend 입장에서는 100건의 요청을 동시다발적으로 받지 않고
1건의 요청만 받기때문에 순간적인 부하를 줄일 수 있고
client 입장에서도 varnish 를 통해 지연없이 캐싱된 html 을 받아볼수 있습니다

마무리

Varnish 에 적용된 실행 옵션과 VCL 설정값들에 대해 이야기해 보았는데요
더 궁금하신 점이 있으시면 Varnish User Guide 를 참고해주시면 될것같습니다.

이번 글을 참고하셔서 Varnish 를 요구사항에 맞게 세팅하거나 수정하실수 있기를 바라며
다음 글에서는 Varnish 모니터링 방법에 대해 설명드리도록 하겠습니다

--

--