Cache breakdown에서 강건함으로: singleflight in Go
Emily Parker
Product Engineer · Leapcell

서문
고성능 서비스를 구축할 때 캐싱은 데이터베이스 로드를 최적화하고 응답 속도를 향상시키는 핵심 기술입니다. 그러나 캐싱을 사용하면 몇 가지 어려움이 따르는데, 그중 캐시 breakdown이 주요 문제입니다. 캐시 breakdown은 데이터베이스 압력이 급증하여 데이터베이스 성능을 저하시키고 심각한 경우 데이터베이스를 다운시켜 사용할 수 없게 만들 수 있습니다.
Go에서 golang.org/x/sync/singleflight
패키지는 특정 키에 대한 동시 요청이 동시에 한 번만 실행되도록 보장하는 메커니즘을 제공합니다. 이 메커니즘은 캐시 breakdown 문제를 효과적으로 방지합니다.
이 기사에서는 Go에서 singleflight
패키지의 사용법을 자세히 살펴봅니다. 캐시 breakdown 문제의 기본 사항부터 시작하여 singleflight
패키지를 자세히 소개하고 이를 사용하여 캐시 breakdown을 방지하는 방법을 보여줍니다.
캐시 Breakdown
캐시 breakdown은 높은 동시성에서 핫 키가 갑자기 만료되어 많은 요청이 데이터베이스에 직접 액세스하여 데이터베이스에 과부하를 주어 충돌을 일으킬 수 있는 상황을 의미합니다.
일반적인 해결 방법은 다음과 같습니다.
- 핫 데이터를 만료되지 않도록 설정: 잘 정의된 핫 데이터의 경우 만료되지 않도록 설정하여 캐시 만료로 인해 요청이 캐시를 우회하고 데이터베이스에 직접 액세스하지 않도록 할 수 있습니다.
- Mutex 잠금 사용: 캐시가 만료될 때 모든 요청이 동시에 데이터베이스를 쿼리하는 것을 방지하기 위해 잠금 메커니즘을 채택하여 하나의 요청만 데이터베이스를 쿼리하고 캐시를 업데이트하도록 하고 다른 요청은 캐시가 업데이트될 때까지 기다렸다가 액세스하도록 할 수 있습니다.
- 사전 업데이트: 백그라운드에서 캐시 사용량을 모니터링하고 캐시가 만료될 때 비동기적으로 업데이트하여 만료 시간을 연장합니다.
singleflight 패키지
singleflight 패키지는 중복 함수 호출 억제 메커니즘을 제공합니다.
이 문장은 공식 문서에서 가져온 것입니다.
다시 말해, 여러 goroutine이 동시에 동일한 함수(지정된 키 기준)를 호출하려고 시도할 때 singleflight는 함수가 처음 도착한 goroutine에 의해서만 실행되도록 보장합니다. 다른 goroutine은 이 호출 결과를 기다렸다가 결과를 공유하는 대신 동시에 여러 호출을 시작합니다.
요컨대, singleflight는 여러 요청을 하나의 요청으로 병합하여 여러 요청이 동일한 결과를 공유할 수 있도록 합니다.
구성 요소
- Group: 이것은 singleflight 패키지의 핵심 구조입니다. 모든 요청을 관리하고 언제든지 동일한 리소스에 대한 요청이 한 번만 실행되도록 보장합니다. Group 객체를 명시적으로 만들 필요는 없으며 간단히 선언하고 사용할 수 있습니다.
- Do 메서드: Group struct는 요청을 병합하는 주요 메서드인 Do 메서드를 제공합니다. 이 메서드는 두 개의 인수를 사용합니다. 하나는 리소스를 식별하는 문자열 키이고 다른 하나는 실제 작업을 실행하는 함수
fn
입니다. Do를 호출할 때 동일한 키의 요청이 이미 진행 중인 경우 Do는 이 요청이 완료될 때까지 기다렸다가 결과를 공유합니다. 그렇지 않으면fn
을 실행하고 결과를 반환합니다. - Do 메서드에는 세 개의 반환 값이 있습니다. 처음 두 개는
fn
의 반환 값으로, 각각interface{}
및error
유형입니다. 마지막 반환 값은 부울이며 Do의 결과가 여러 호출에서 공유되었는지 여부를 나타냅니다. - DoChan: 이 메서드는 Do와 유사하지만 작업이 완료되면 결과를 수신하는 채널을 반환합니다. 반환 값은 채널이므로 결과를 비차단 방식으로 기다릴 수 있습니다.
- Forget: 이 메서드는 Group에서 키와 관련 요청 레코드를 삭제하는 데 사용되며, 동일한 키로 다음 Do 호출이 이전 결과를 재사용하는 대신 새 요청을 실행하도록 보장합니다.
- Result: 이것은 DoChan 메서드에서 반환되는 struct 유형입니다. 요청 결과를 캡슐화하고 세 개의 필드를 포함합니다.
Val
(interface{}): 요청에서 반환된 결과입니다.Err
(error): 요청 중에 발생한 오류 정보입니다.Shared
(bool): 결과가 현재 요청 이외의 요청과 공유되었는지 여부를 나타냅니다.
설치
다음 명령을 사용하여 Go 애플리케이션에 singleflight 종속성을 설치합니다.
go get golang.org/x/sync/singleflight
예제 사용법
package main import ( "errors" "fmt" "golang.org/x/sync/singleflight" "sync" ) var errRedisKeyNotFound = errors.New("redis: key not found") func fetchDataFromCache() (any, error) { fmt.Println("fetch data from cache") return nil, errRedisKeyNotFound } func fetchDataFromDataBase() (any, error) { fmt.Println("fetch data from database") return "Leapcell", nil } func fetchData() (any, error) { cache, err := fetchDataFromCache() if err != nil && errors.Is(err, errRedisKeyNotFound) { fmt.Println(errRedisKeyNotFound.Error()) return fetchDataFromDataBase() } return cache, err } func main() { var ( st singleflight.Group waitGroup sync.WaitGroup ) for range 5 { waitGroup.Add(1) go func() { defer waitGroup.Done() value, err, shared := st.Do("key", fetchData) if err != nil { panic(err) } fmt.Printf("value: %v, shared: %v\n", value, shared) }() } waitGroup.Wait() }
이 코드는 일반적인 동시 액세스 시나리오인 캐시에서 데이터를 가져오고 캐시가 누락된 경우 데이터베이스에서 검색하는 것을 시뮬레이션합니다. 이 과정에서 singleflight 라이브러리가 중요한 역할을 합니다. 여러 동시 요청이 동시에 동일한 데이터에 액세스하려고 할 때 실제 가져오기 작업(캐시 또는 데이터베이스에서)이 한 번만 수행되도록 보장합니다. 이는 데이터베이스 로드를 줄일 뿐만 아니라 높은 동시성 시나리오에서 캐시 breakdown을 효과적으로 방지합니다.
코드 출력은 다음과 같습니다.
fetch data from cache redis: key not found fetch data from database value: Leapcell, shared: true value: Leapcell, shared: true value: Leapcell, shared: true value: Leapcell, shared: true value: Leapcell, shared: true
보시다시피 5개의 goroutine이 동시에 동일한 데이터를 가져올 때 데이터 가져오기 작업은 실제로 하나의 goroutine에 의해 한 번만 수행됩니다. 또한 반환된 모든 공유 값이 true이므로 결과가 다른 4개의 goroutine과 공유되었음을 의미합니다.
모범 사례
키 설계
키를 생성할 때 고유성과 일관성을 보장해야 합니다.
- 고유성: Do 메서드에 전달된 키가 고유한지 확인하여 Group이 서로 다른 요청을 구별할 수 있도록 합니다.
{type}:{identifier}
와 같은 구조화된 명명 규칙을 사용하는 것이 좋습니다. 예를 들어 사용자 정보를 가져올 때 키는user:1234
가 될 수 있습니다. 여기서user
는 데이터 유형을 나타내고1234
는 특정 사용자 식별자입니다. - 일관성: 동일한 요청의 경우 생성된 키는 언제 호출되든 항상 일관성이 있어야 합니다. 이렇게 하면 Group이 동일한 요청을 적절하게 병합하고 예기치 않은 오류를 방지할 수 있습니다.
제한 시간 제어
Group.Do를 호출할 때 처음 도착한 goroutine은 fn
함수를 성공적으로 실행할 수 있지만 다른 이후 goroutine은 차단됩니다. 차단된 상태가 너무 오래 지속되면 시스템 응답성을 보장하기 위해 다운그레이드 전략이 필요할 수 있습니다. 이러한 경우 select
문과 함께 Group.DoChan을 사용하여 제한 시간 제어를 구현할 수 있습니다.
다음은 제한 시간 제어를 보여주는 간단한 예입니다.
package main import ( "fmt" "golang.org/x/sync/singleflight" "time" ) func main() { var st singleflight.Group DoChan := st.DoChan("key", func() (interface{}, error) { time.Sleep(4 * time.Second) return "Leapcell", nil }) select { case <-DoChan: fmt.Println("done") case <-time.After(2 * time.Second): fmt.Println("timeout") // 여기에 다른 다운그레이드 전략 구현 } }
요약
- 이 기사에서는 먼저 캐시 breakdown의 개념과 일반적인 해결 방법을 소개했습니다.
- 그런 다음 singleflight 패키지의 기본 개념, 구성 요소, 설치 및 사용 예제를 다루면서 심층적으로 살펴보았습니다.
- 다음으로 시뮬레이션된 동시 액세스 예제를 통해 높은 동시성 시나리오에서 캐시 breakdown을 방지하기 위해 singleflight를 사용하는 방법을 보여주었습니다.
- 마지막으로 실제 키 설계 및 요청 제한 시간 제어에 대한 모범 사례를 논의하여 동시 처리 로직을 최적화하기 위해 singleflight를 더 잘 이해하고 적용하는 데 도움이 되는 것을 목표로 합니다.
Leapcell은 Go 프로젝트 호스팅을 위한 최고의 선택입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발합니다.
무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불하십시오. 요청 없음, 요금 없음.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불합니다.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI。
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합。
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅。
간편한 확장성 및 고성능
- 높은 동시성을 쉽게 처리하기 위한 자동 확장。
- 운영 오버헤드가 없으므로 구축에만 집중하면 됩니다.
설명서에서 자세히 알아보세요!
X에서 팔로우하세요: @LeapcellHQ