request collapsing
같은 것을 달라는 요청이 동시에 여러 개 들어왔을 때 그중 하나만 뒤로 내보내는 방식입니다. 나머지는 그 하나가 끝날 때까지 대기열에서 기다립니다. 응답이 돌아오면 기다리던 요청 전부에게 같은 응답을 나눠 줍니다.
상세
Fastly 공식 문서는 request collapsing 을 같은 객체에 대한 여러 요청을 오리진으로 가는 하나의 요청으로 합치는 관행이라고 적습니다. 그렇게 얻은 응답으로 대기 중인 요청을 전부 만족시킬 수 있다고 덧붙입니다. 같은 문서는 이것이 매우 인기 있는 객체가 캐시에서 만료될 때 오리진 서버로 즉시 요청 홍수가 몰리는 것을 막는다고 적습니다. 그런 홍수는 오리진을 압도하거나 값비싼 자원을 소모할 수 있습니다.
이 결정이 성립하는 조건을 같은 문서가 숫자로 듭니다. 실제로 같은 객체에 대한 요청 간격은 그 객체를 오리진에서 가져오는 데 걸리는 시간보다 짧은 경우가 잦습니다. 예로 든 것은 인기 있는 웹사이트의 홈페이지입니다. 초당 50번 요청됩니다. 오리진에서 가져오는 데는 네트워크 지연과 오리진 처리 시간을 합쳐 500ms 가 걸립니다. request collapsing 이 없으면 오리진은 같은 페이지를 동시에 여러 번 만들어야 합니다. 문서는 이런 효과를 흔히 캐시 스탬피드 문제라고 부른다고 적습니다.
Fastly 가 답으로 든 것은 큐입니다. 이 회사는 그것을 waiting list 라고 부릅니다. 두 번째 요청이 도착하면 그 자원을 이미 가져오는 중이라는 것을 압니다. 두 번째 가져오기를 시작하는 대신 사용자 2 의 요청을 사용자 1 의 요청에 붙입니다. 가져오기가 끝나면 응답을 캐시에 저장합니다. 동시에 기다리던 두 사용자에게 같이 내보냅니다. 그 사이에 합류한 다른 요청들에도 같이 나갑니다.
sequenceDiagram
participant 요청1
participant 요청2
participant 앞단
participant 오리진
요청1->>앞단: 같은 키로 요청
앞단->>오리진: 첫 요청 하나만 내보냄
요청2->>앞단: 같은 키로 요청
Note over 앞단: 대기열에 붙임
오리진-->>앞단: 응답
앞단-->>요청1: 응답
앞단-->>요청2: 같은 응답
무엇을 하나로 볼지는 키가 정합니다. Squid 는 같은 것을 collapsed forwarding 이라고 부르고, 공식
문서는 이것을 기본이 꺼진 옵션으로 적습니다.
이 옵션은 같은 URI(Uniform Resource Identifier, 통합 자원 식별자)에 대한 잠정적으로 캐시 가능한
여러 요청을 Squid 가 합치도록 허용할지를 정합니다. 합치는 시점은 그 응답이 캐시 가능할지 Squid
가 알기 전입니다. 켜면 같은 URI 에 대한 동시 요청 중 첫 번째만 전달합니다. 여기서 동시 요청은
첫 요청 헤더가 파싱된 뒤부터 그에 해당하는 응답 헤더가 파싱되기 전까지 받은 요청을 뜻한다고
같은 문서가 못 박습니다. nginx 는 proxy_cache_key 지시자로 식별되는 캐시 요소를 단위로
봅니다. proxy_cache_lock 을 켜면 새 캐시 요소 하나를 채우는 요청이 한 번에 하나만 프록시된
서버로 전달됩니다. 기본값은 꺼짐입니다.
Squid 는 두 종류를 묶습니다. 하나는 리스닝 포트로 받은 일반 클라이언트 요청입니다. 다른 하나는
그 요청들이 낡은 캐시 객체에 부딪혀 유발한 내부 캐시 재검증 요청입니다. 묶인 요청이 재사용한
응답은 그 요청의 처리 맥락에서 신선한 것으로 간주됩니다. Squid 는 refresh_pattern 과 내부
신선도 검사를 묶인 트랜잭션에 적용하지 않습니다.
Varnish 는 같은 것을 request coalescing 이라고 부릅니다. 공식 사용자 안내서는 캐시 가능한 객체가 캐시에 없으면 기본적으로 백엔드에서 가져온다고 적습니다. 그 결과 같은 객체에 대한 동시 백엔드 요청이 합쳐집니다. 한 번에 하나의 가져오기만 실행됩니다. 나머지 대기 중인 가져오기는 그 결과를 기다립니다. 다만 같은 문서가 캐시 불가 콘텐츠로 적어 둔 상태 중 하나를 만들어 놓은 경우는 예외입니다. 문서는 이것이 캐시된 응답이 만료됐을 때, 또는 애초에 캐시된 적이 없을 때 백엔드가 thundering herd 를 맞는 것을 막기 위한 것이라고 적습니다.
캐시 앞단만의 이야기가 아닙니다. 애플리케이션 프로세스 안에서도 같은 결정을 내립니다. Go 의 groupcache 는 캐시 채우기를 조율합니다. 공식 README 는 복제된 프로세스 집합 전체에서 한 프로세스의 한 번의 로드만 캐시를 채운다고 적습니다. 그렇게 로드된 값을 모든 호출자에게 다중화합니다. 다른 호출자가 같은 프로세스 안에서 들어오든 피어의 RPC(Remote Procedure Call, 원격 프로시저 호출) 요청으로 들어오든, 그 호출자들은 로드가 끝나기를 블록해 기다렸다가 같은 답을 받습니다. Go 쪽 이름은 duplicate suppression 입니다. 같은 README 는 그것을 dup suppression 으로 줄여 부릅니다.
대가
Fastly 문서는 request collapsing 이 요청을 큐에 넣기 때문에 어떤 상황에서는 최종 사용자의 응답 시간을 늦출 수 있다고 적습니다. 뒤에 온 요청은 자기 순서를 따로 갖지 않습니다. 앞선 가져오기가 끝나야 응답을 받습니다.
내주는 것이 더 커지는 자리가 있습니다. 같은 문서는 많은 클라이언트 요청이 하나의 백엔드 요청을 기다리는 경우를 듭니다. 그 백엔드 응답이 캐시 불가로 밝혀질 수 있습니다. 그러면 큐에 있던 요청 각각이 백엔드로 따로 보내져야 합니다. 문서는 hit-for-pass 나 pass on request 같은 장치가 이것이 문제가 되는 것을 피하는 데 도움이 될 수 있다고 적습니다.
Squid 는 이 대가를 기본값으로 표현했습니다. collapsed_forwarding 은 기본이 꺼짐입니다. 공식
문서가 그 이유를 적어 뒀습니다. 이 기능을 켜면 캐시 가능해 보이는 요청의 전달이 불필요하게
늦어집니다. 그런데 결국 캐시 불가 콘텐츠라서 어차피 개별적으로 전달해야 하는 경우가 있습니다.
다만 같은 문서는 어떤 경우에는 그런 지연에서 오는 손실보다 묶어서 얻는 이득이 크다고 적습니다.
주기적이거나 그룹으로 만료되는 시각을 가진, 캐시 가능성 높은 콘텐츠를 가속하는 경우입니다.
Squid 위키도 같은 자리를 짚습니다. 이 기능은 동적 콘텐츠에서 지연이 늘어나는 것을 피하려고 보통 비활성화되어 있습니다. 다만 accelerator 셋업에서는 켜서 이득을 볼 수 있다고 적습니다. 웹 서버가 병목이지만 신뢰할 만한 셋업입니다. 그 서버가 대체로 캐시 가능한 정보를 반환합니다.
예시
nginx
proxy_cache_lock on;
proxy_cache_lock 의 문법은 proxy_cache_lock on | off; 입니다. 기본값은
proxy_cache_lock off; 입니다. 컨텍스트는 http, server, location 입니다. 이 지시자는 1.1.12
판에서 등장했습니다. 켜면 proxy_cache_key 지시자에 따라 식별되는 새 캐시 요소 하나를 채우는
요청이 한 번에 하나만 프록시된 서버로 전달됩니다. 같은 캐시 요소에 대한 다른 요청들은 응답이
캐시에 나타나기를 기다리거나 그 요소의 캐시 락이 풀리기를 기다립니다. proxy_cache_lock_timeout
지시자가 정한 시간까지입니다.
Squid
collapsed_forwarding on
기본값은 collapsed_forwarding off 입니다. 켜면 같은 URL(Uniform Resource Locator, 통합 자원
위치 지정자)에 대한 동시 요청을 각각 전달하는 대신 그중 첫 번째만 보냅니다. 나머지는 공식
문서의 표현으로 collapsed 된 요청입니다. 이들은 첫 요청의 응답을 기다립니다. 그 응답이 캐시
가능한 것으로 밝혀지면 그것을 씁니다.
Go singleflight
func (g *Group) Do(key string, fn func() (any, error)) (v any, err error, shared bool)
Do 는 주어진 함수를 실행합니다. 그 결과를 돌려줍니다. 공식 문서는 주어진 키 하나에 대해 한 번에
하나의 실행만 진행 중이도록 보장한다고 적습니다. 중복 호출이 들어오면 그 호출자는 원래 호출이
끝나기를 기다렸다가 같은 결과를 받습니다. 반환값 shared 는 v 가 여러 호출자에게 주어졌는지를
알려줍니다.
실패
묶기가 깨지는 자리는 응답이 캐시 불가일 때입니다.
Fastly 문서는 이것을 경고로 적어 뒀습니다. 어떤 요청이 request collapsing 대상이고 오리진 응답이 캐시 불가이면, 그 응답으로 큐에 있는 요청들을 만족시킬 수 없습니다. hit-for-pass 표시도 만들 수 없습니다. 이 상황에서는 큐의 다음 요청이 오리진으로 보내집니다. 남은 요청들은 새 큐를 만듭니다. 결과적으로 요청들이 동시가 아니라 연속으로 보내집니다. 문서는 어떤 경우에는 이것이 몇 분에 이르는 극단적인 응답 시간을 만들 수 있다고 적습니다.
한 번 대기 목록에 붙은 요청이 얼마나 기다리는지는 진행 중인 가져오기에 달려 있습니다. 그
가져오기의 시간 제한은 between_bytes_timeout · connect_timeout · first_byte_timeout 같은
백엔드 속성으로 제어합니다. 가져오기가 타임아웃되거나 오류가 나거나 캐시 불가 응답을 내면, 큐에
있던 요청 일부가 큐에서 빠져나와 처리됩니다. 큐가 생기는 것을 막을 hit-for-pass 객체가 캐시에
없으므로 새 가져오기와 새 큐가 생길 가능성이 높습니다. 그것들도 오래 걸리면 객체가 큐에 잡혀
있는 시간이 계속 길어집니다. 문서는 평균적인 사용자가 응답을 받기까지 여러 번의 순차 백엔드
가져오기 시도가 끝나기를 기다려야 할 수 있다고 적습니다. 그 응답이 오류일 수도 있습니다.
Varnish 는 이 직렬화를 피하려고 아예 묶지 않는 상태를 둡니다. 공식 문서는 pass 에는 request
coalescing 이 없다고 적습니다. pass 는 응답이 캐시 불가일 것임을 가리키므로 캐시될지도 모를
응답을 기다릴 이유가 없습니다. 그 객체에 대한 대기 중인 가져오기는 전부 동시에 나갑니다.
그렇지 않으면 결국 캐시 불가로 밝혀진 객체를 기다리던 가져오기들이 직렬화될 수 있습니다. 캐시
불가 콘텐츠의 기본 처리는 hit-for-miss 입니다. builtin.vcl 의 어느 부분도 hit-for-pass 를
부르지 않습니다. 필요하면 직접 VCL(Varnish Configuration Language)에 return 문을 넣어야 합니다.
nginx 는 대기 자체에 상한을 둡니다. 두 지시자가 만료 시 동작을 각각 적어 뒀습니다.
| 조건 | 그때 벌어지는 일 |
|---|---|
proxy_cache_lock_timeout 이 정한 시간이 지난다 |
요청이 프록시된 서버로 전달됩니다. 다만 그 응답은 캐시되지 않습니다. 1.7.8 이전에는 응답이 캐시될 수 있었습니다 |
새 캐시 요소를 채우려고 마지막으로 전달된 요청이 proxy_cache_lock_age 가 정한 시간 안에 완료되지 않는다 |
요청 하나가 더 프록시된 서버로 전달될 수 있습니다 |
묶인 쪽은 원래 호출의 결과를 그대로 받습니다. singleflight 의 Do 는 값과 에러를 함께
돌려줍니다. 문서는 중복 호출자가 원래 호출이 끝나기를 기다렸다가 같은 결과를 받는다고 적습니다.
이 묶음에서 빠져나오는 손잡이가 Forget 입니다. 문서는 Forget(key) 이 singleflight 에게 그
키를 잊게 한다고 적습니다. 이후 그 키에 대한 Do 호출은 앞선 호출이 끝나기를 기다리는 대신
함수를 부릅니다.
운영
켜고 끄는 손잡이의 기본값이 제품마다 다릅니다.
Squid 는 기본이 꺼짐입니다. collapsed_forwarding 지시자 하나로 켜고 끕니다.
nginx 는 세 지시자를 씁니다.
| 지시자 | 기본값 | 컨텍스트 | 등장 판 |
|---|---|---|---|
proxy_cache_lock |
off |
http, server, location | 1.1.12 |
proxy_cache_lock_timeout |
5s |
http, server, location | 1.1.12 |
proxy_cache_lock_age |
5s |
http, server, location | 1.7.8 |
Fastly 는 반대로 기본이 켜짐입니다. 공식 문서는 readthrough 또는 simple 캐시 인터페이스를 쓸 때 VCL 과 Compute 서비스 양쪽에서 캐시 미스가 request collapsing 대상이 된다고 적습니다. core 캐시 인터페이스는 이것을 지원하되 캐시 트랜잭션 안에서 명시적으로 설정했을 때만입니다.
끄는 자리는 vcl_recv 입니다. req.hash_ignore_busy 를 true 로 두면 그 요청은 묶임 대상에서
빠집니다. 같은 문서는 그렇게 하면 낡은 객체를 쓸 수 없게 된다고 적습니다.
stale-while-revalidate 와 stale-if-error 지시자가 비활성화됩니다.
대기 상한은 vcl_miss 서브루틴에 조건을 넣어 잡습니다. 문서가 든 예제입니다.
if (time.elapsed > 1s) {
error 601 "Waiting too long";
}
요청이 대기 목록에 얼마나 있었는지를 검사하는 조건입니다.
Varnish 쪽 손잡이는 grace time 입니다. 공식 문서는 캐시된 객체에 grace time 을 설정하면 합쳐진 가져오기를 기다리는 동안 낡은 콘텐츠를 낼 수 있다고 적습니다. 기본값은 10초입니다. 그 가져오기는 낡은 응답이 클라이언트로 나가는 동안 비동기로 실행됩니다.
관련 항목
이것을 실제로 구현·채택한 제품
Fastly · Squid · nginx · Varnish · groupcache · singleflight
이것이 비롯된 배경
캐시 미스 · 캐시 스탬피드 · thundering herd
이것이 하나로 볼 단위를 정하는 키
proxy_cache_key · URI(Uniform Resource Identifier, 통합 자원 식별자) · URL(Uniform Resource Locator, 통합 자원 위치 지정자)
이 실패를 피하려고 두는 캐시 상태
hit-for-pass · hit-for-miss · pass
이것을 켜고 끄는 설정 지시자
proxy_cache_lock · proxy_cache_lock_timeout · proxy_cache_lock_age · collapsed_forwarding · req.hash_ignore_busy · grace time · stale-while-revalidate · stale-if-error · VCL(Varnish Configuration Language)
대기 시간을 제한하는 백엔드 속성
between_bytes_timeout · connect_timeout · first_byte_timeout
이 결정이 코드로 나타나는 VCL 지점
vcl_recv · vcl_miss · builtin.vcl
네 이름을 각각 세운 사람
Henrik Nordstrom · Alex Rousskov · Brad Fitzpatrick
이것이 아끼는 통신 자원
이것이 딛고 선 캐시 개념
다른 이름: request coalescing · collapsed forwarding · duplicate suppression · 요청 병합