groupcache
groupcache 는 캐시를 프로그램 안에 넣어 쓰는 Go 라이브러리입니다. 캐시 서버를 따로 띄우지 않습니다. 같은 코드를 도는 프로세스들이 서로의 피어가 되어 캐시를 나눠 갖습니다. 캐시에 없는 값을 채우는 일까지 이 라이브러리가 맡습니다.
상세
프로젝트 README 는 groupcache 를 분산 캐시이자 캐시 채움 라이브러리라고 적습니다. 많은 경우에 memcached 노드 풀을 대신하려고 만들었다고 밝힙니다. 대신한다고 못 박지 않고 "많은 경우에" 라는 한정을 붙여 둔 자리입니다.
이 라이브러리는 클라이언트이면서 동시에 서버입니다. 자기 피어들에 직접 붙어 분산 캐시를 이룹니다. 그래서 캐시 서버를 따로 굴릴 필요가 없습니다. README 는 이것이 배포와 설정의 고통을 크게 줄이는 자리라고 적습니다.
키로 샤딩합니다. 어느 피어가 그 키를 책임지는지를 키에서 정합니다. README 는 이 대목을 memcached 와 같은 자리로 분류합니다.
갈리는 자리는 캐시를 채우는 일까지 맡는다는 것입니다. README 는 memcached 가 캐시 미스일 때 "없다" 고 답하고 끝낸다고 적습니다. 그 결과로 데이터베이스 적재가 수가 정해지지 않은 클라이언트들에서 한꺼번에 몰려드는 일이 벌어지고, 실제로 그 때문에 몇 차례 장애가 났다고도 덧붙입니다. groupcache 는 복제된 프로세스 집합 전체에서 단 한 프로세스의 단 한 번 적재만 캐시를 채우도록 조율하고, 그렇게 적재한 값을 모든 호출자에게 나눠 줍니다.
공개 API(Application Programming Interface) 문서는 같은 것을 이렇게 적습니다. 피어 프로세스 집합에 걸쳐 캐싱과 중복 제거를 하는 데이터 적재 장치라고 합니다. 각 조회는 먼저 자기 로컬 캐시를 보고, 없으면 그 키의 정식 주인에게 넘긴다고 적습니다. 흔한 경우에 여러 피어에서 같은 키로 동시에 난 캐시 미스가 캐시 채움 한 번으로 끝난다고 적습니다.
지금 이 라이브러리는 Go 에서만 쓸 수 있습니다.
포기한 것
값에 판이 없습니다. 키 "foo" 의 값이 "bar" 면 "foo" 는 언제나 "bar" 여야 합니다. 캐시 만료 시간도 없고 명시적인 캐시 축출도 없습니다. 그래서 CAS(Compare-And-Swap)도 증가·감소 연산도 없습니다.
손잡이를 아직 안 붙인 것이 아니라 안 붙이기로 한 것입니다. Getter 인터페이스의 소스 주석이 호출자에게 같은 요구를 되돌려 줍니다. 돌려주는 데이터는 판이 없어야 하고, 키가 적재할 데이터를 유일하게 기술해야 하며, 암묵적인 현재 시각에 기대서도 캐시 만료 장치에 기대서도 안 된다고 적혀 있습니다. 키가 데이터를 유일하게 기술해야 하므로 값이 달라지면 키도 달라져야 합니다.
대신 얻은 것이 있습니다. 값이 변하지 않으니 초인기 항목을 여러 프로세스에 자동으로 미러링해도 됩니다. README 는 이것이 매우 인기 있는 키와 값 때문에 한 머신의 CPU(Central Processing Unit)나 NIC(Network Interface Card)가 과부하되는 memcached 의 hot spotting 을 막는다고 적습니다. 판을 포기한 것과 미러링을 얻은 것은 README 에서 한 항목이 다음 항목으로 이어지는 짝으로 적혀 있습니다.
언어도 하나로 묶었습니다. 원저자는 자기가 이 코드를 다른 언어로 이식할 가능성은 매우 낮다고 README 에 이름을 걸고 적었습니다.
예시
피어 목록 선언
원저자가 2013년 OSCON 발표에서 보인 코드입니다.
me := "http://10.0.0.1"
peers := groupcache.NewHTTPPool(me)
// Whenever peers change:
peers.Set("http://10.0.0.1", "http://10.0.0.2", "http://10.0.0.3")
시그니처는 func NewHTTPPool(self string) *HTTPPool 입니다. self 는 지금 이 서버를 가리키는
기준 URL(Uniform Resource Locator)이어야 합니다. 소스 주석은 "http://example.net:8000" 을
예로 듭니다. 이 함수는 편의를 위해 자기 자신을 http.DefaultServeMux 에 http.Handler 로도
등록합니다. func (p *HTTPPool) Set(peers ...string) 은 풀의 피어 목록을 갱신합니다. 부를
때마다 일관성 해싱 링을 새로 만들고 거기에 피어들을 넣습니다. 발표는 이 피어 인터페이스가
갈아 끼울 수 있는 자리라고 적습니다.
그룹 선언
var thumbNails = groupcache.NewGroup("thumbnail", 64<<20, groupcache.GetterFunc(
func(ctx groupcache.Context, key string, dest groupcache.Sink) error {
fileName := key
dest.SetBytes(generateThumbnail(fileName))
return nil
}))
그룹 이름 "thumbnail" 은 전역에서 유일해야 합니다. 64<<20 은 노드 하나가 쓸 최대 메모리
64 MB 입니다. Sink 는 SetString · SetBytes · SetProto 를 가진 인터페이스입니다.
지금 소스의 시그니처는 func NewGroup(name string, cacheBytes int64, getter Getter) *Group
입니다. 위 코드의 groupcache.Context 자리에는 지금 context.Context 가 옵니다. 소스 주석은
이 함수가 만든 Getter 가 피어 프로세스 집합 전체에서 한 키에 대해 Get 호출을 한 번만 돌리려
시도한다고 적습니다. 보장한다고는 적지 않았습니다.
값 조회
var data []byte
err := thumbNails.Get(ctx, "big-file.jpg",
groupcache.AllocatingByteSliceSink(&data))
지금 소스의 시그니처는 func (g *Group) Get(ctx context.Context, key string, dest Sink) error
입니다. 발표는 이 한 줄의 답이 어디서 오는지를 네 갈래로 적습니다. 로컬 메모리 캐시일 수도,
피어의 메모리 캐시일 수도, 로컬에서 계산한 값일 수도, 원격에서 계산한 값일 수도 있습니다.
그리고 모든 머신의 모든 스레드를 통틀어 썸네일은 하나만 만들어지고, 그 값이 프로세스 안과
네트워크 건너의 모든 대기자에게 퍼진다고 적습니다.
동작
README 는 Get("foo") 한 번이 도는 순서를 번호로 적어 둡니다. 같은 코드를 도는 N 대 중 5번
머신에서 부른다고 놓고 적은 것입니다.
- "foo" 의 값이 초인기 항목이라 로컬 메모리에 있나. 있으면 그걸 씁니다.
- "foo" 의 값이 지금 이 피어가 주인이라서 로컬 메모리에 있나. 있으면 그걸 씁니다.
- N 개 피어 중에 내가 "foo" 의 주인인가. 일관성 해싱으로 5번에 떨어지나를 묻는 것입니다. 주인이면 여기서 적재합니다. 그동안 같은 프로세스에서 오든 피어의 RPC(Remote Procedure Call) 요청으로 오든 다른 호출자들은 적재가 끝날 때까지 블록되었다가 같은 답을 받습니다. 주인이 아니면 주인인 피어에 RPC 를 걸어 답을 받아 옵니다. RPC 가 실패하면 그냥 로컬에서 적재합니다. 그때도 로컬 중복 억제는 그대로 걸립니다.
flowchart TD
A["Get(foo) 호출"] --> B{"로컬 메모리에 있나"}
B -->|있다| Z["그 값을 쓴다"]
B -->|없다| C{"내가 이 키의 주인인가"}
C -->|주인이다| D["여기서 적재한다"]
C -->|아니다| E["주인 피어에 RPC"]
E -->|성공| Z
E -->|실패| D
D --> Z
주인을 정하는 일과 중복을 억제하는 일은 각각 별도 패키지가 맡습니다. consistenthash 패키지는 자기 패키지 주석에 링 해시 구현이라고 적습니다. singleflight 패키지는 함수 호출 중복 억제 장치라고 적습니다.
소스의 load 함수가 이 둘을 이렇게 씁니다. 적재 전체를 loadGroup.Do(key, ...) 로 감싸고,
그 콜백 안에서 캐시를 한 번 더 봅니다. 주석이 이유를 적어 둡니다. singleflight 는 시간이
겹치는 호출만 합칠 수 있어서, 두 요청이 동시에 캐시를 놓치면 load 가 두 번 불릴 수
있습니다. 고루틴 스케줄링이 어긋나면 그 콜백이 연달아 두 번 돌기도 합니다. 캐시를 다시 안
보면 항목은 하나인데 캐시가 센 바이트 수는 두 번 올라갑니다.
캐시를 다시 봐도 없으면 피어를 고릅니다. PickPeer 가 이 키의 주인 피어를 주면 그 피어에서
받아 오고, 성공하면 PeerLoads 통계를 올리고 그 값을 돌려줍니다. 실패하면 PeerErrors 를
올리고 로컬 적재로 내려갑니다. 이 함수에서 populateCache 로 이 프로세스의 캐시에 들어가는
값은 로컬에서 적재한 값입니다.
사용처
dl.google.com 이 원래 사용자라고 README 가 적습니다. 원저자 발표는 이 자리에서 무엇을
캐시했는지를 적어 둡니다. 키는 "<blobref>-<chunk_offset>" 꼴이고 청크 하나는 2 MB 입니다.
청크는 로컬 메모리에서 오거나, 원격 캐시에서 오거나, Google 저장 시스템에서 가져옵니다. 로컬
메모리에서 오는 것은 자기가 주인인 청크와 초인기 청크입니다. 큰 파일 하나를 통째로 캐시
단위로 삼지 않고 2 MB 청크로 잘라 그 청크를 키로 삼은 것입니다.
같은 발표는 groupcache 를 이렇게 요약합니다. memcached 를 대신할 것, 클라이언트이자 서버인 라이브러리, 자기 피어에 붙는 것, 캐시 미스에서 적재가 몰려들지 않도록 조율된 캐시 채움, 그리고 인기 항목의 복제입니다. 앞의 청크 캐시가 이 목록 위에 얹혀 있습니다.
README 는 나머지 사용처를 이름만 적습니다. Blogger 일부, Google Code 일부, Google Fiber 일부, Google 프로덕션 모니터링 시스템 일부입니다. 이 네 곳이 왜 골랐는지는 README 에도 발표 자료에도 적혀 있지 않습니다.
관련 항목
이것을 이루는 구성 요소
Getter · consistenthash · singleflight · LRU(Least Recently Used, 가장 오래 안 쓴 것부터 내보내는 축출 방식)
이것이 memcached 와 겨루는 지점
이것이 memcached 와 갈리는 지점
배포 · 캐시 미스 · 썬더링 허드(thundering herd, 여러 요청이 캐시 미스 때 한꺼번에 원본으로 몰리는 현상) · 데이터베이스 · CAS · 축출 · 캐시 만료 · Go · 미러링
이것이 막는 과부하 현상
이것이 조회 한 번에 거치는 처리 단계
이것이 피어 통신에 쓰는 규격
다른 이름: 그룹캐시