본문 바로가기

프로그래밍/golang

[Golang] signalflight.Group

 

 

 

1.  singleflight란?

캐시 스탬피드(Cache Stampede) 현상
특정 데이터의 캐시가 만료되는 순간, 수 많은 고루틴이 동시에 DB나 API로 몰려들어 시스템 전체에 과부하를 주는 상황

이 캐시 스탬피드를 해결하기 위해서 나온 패키지가 signalflight 이며, 
"동일한 키"로 들어온 여러 요청에 대한 작업을 단 한 번만 실행하고,
그 결과 값을 기다리는 모든 호출자들에게 공유하는 기능을 제공합니다.

예를 들면 인기 게시글을 100명의 유저가 동시에 조회 요청을 보낸 상황에서 하필 캐시가 만료가 되었다고 가정해봅시다.
그러면 100명의 사용자가 SELECT 요청을 보내게 될 것이고 쿼리도 100번 요청이 될 것입니다.
singleflight를 사용하면 가장 먼저 요청을 보낸 사용자가 DB에 접근해서 쿼리 요청을 보내고,
나머지 99명은 결과를 공유를 받게 됩니다. 

 

  • 첫 번째 호출 : 실제로 비용이 큰 작업을 실행
  • 두 번째 이후 호출 : 이미 실행 중인 작업이 있다면 새로 실행하지 않고, 첫 번째 작업이 끝날 때 까지 대기 후 결과를 받습니다.

 

2.   예제 코드

var g singleflight.Group
var calls atomic.Int64

// 예 : 120ms가 걸리는 비싼 작업
work := func(ctx context.Context) (string, error) {
    calls.Add(1)
    time.Sleep(120 * time.Millisecond)
    return "value-from-expensive-work", nil
}

// 5개의 고루틴이 동시에 호출
for i := 0; i < 5; i++ {
    go func() {
        v, err, shared := g.Do("key:users:123", func() (any, error) {
            return work(ctx)
        })
        fmt.Printf("value=%v shared=%v\n", v, shared)
    }()
}

 

3.  핵심 정리

  • g.Do(key, fn) : 여기에서 key가 동일한 요청들은 하나의 그룹으로 묶여 결과를 공유 받습니다.

  • shared 의 결과 값은 boolean 타입의 값으로 true 일 경우, 자신이 실행 한게 아니라 다른 고루틴이 실행한 결과 값을 공유 받았다는 의미 입니다.

 

4. 주의사항

  1. 사용할 때 정말 조심해야할 사항이 있는데, "g.Do"를 사용해 같은 그룹으로 묶일 경우,
    각 고루틴이 모두 완료 될 때까지 블로킹이 된다는 점입니다.
    이는 오히려 무한 루프 상태에 빠지게 될 수 있어서 "g.DoChan" 을 사용해서 context 를 활용하는 것이 좋습니다.
  2. 만약 첫 작업이 에러가 나면 , 나머지 모든 고루틴도 동일하게 에러를 받게 됩니다.
    에러 상황에 따른 재로직 구현도 검토해야 합니다.

 

5. 전체 코드

https://github.com/reochoi109/go-handbook/blob/main/singleflight/basic/main.go

 

go-handbook/singleflight/basic/main.go at main · reochoi109/go-handbook

A personal handbook of Go patterns and best practices. Lightweight, practical code snippets for real-world backend development. - reochoi109/go-handbook

github.com