이런 저런 것을 캐시해야 하는 상황에 자주 직면하게 됩니다. 종종 이러한 값은 일정 기간 동안 캐시됩니다. 당신은 아마도 패턴에 익숙할 것입니다. 캐시에서 값을 얻으려고 시도하고, 성공하면 이를 호출자에게 반환하고 하루를 호출합니다. 값이 없으면 (아마도 데이터베이스에서) 가져오거나 계산하여 캐시에 넣습니다. 대부분의 경우 이는 훌륭하게 작동합니다. 그러나 캐시 항목에 사용하는 키에 자주 액세스하고 데이터를 계산하는 작업에 시간이 걸리면 여러 병렬 요청이 동시에 캐시 누락되는 상황이 발생하게 됩니다. 이러한 모든 요청은 소스에서 독립적으로 로드되고 값을 캐시에 저장합니다. 이로 인해 리소스가 낭비되고 서비스 거부가 발생할 수도 있습니다.
예를 들어 설명하겠습니다. 캐시에는 redis를 사용하고 그 위에는 간단한 Go http 서버를 사용하겠습니다. 전체 코드는 다음과 같습니다.
package main import ( "errors" "log" "net/http" "time" "github.com/redis/go-redis/v9" ) type handler struct { rdb *redis.Client cacheTTL time.Duration } func (ch *handler) simple(w http.ResponseWriter, r *http.Request) { cacheKey := "my_cache_key" // we'll use 200 to signify a cache hit & 201 to signify a miss responseCode := http.StatusOK cachedData, err := ch.rdb.Get(r.Context(), cacheKey).Result() if err != nil { if !errors.Is(err, redis.Nil) { log.Println("could not reach redis", err.Error()) http.Error(w, "could not reach redis", http.StatusInternalServerError) return } // cache miss - fetch & store res := longRunningOperation() responseCode = http.StatusCreated err = ch.rdb.Set(r.Context(), cacheKey, res, ch.cacheTTL).Err() if err != nil { log.Println("failed to set cache value", err.Error()) http.Error(w, "failed to set cache value", http.StatusInternalServerError) return } cachedData = res } w.WriteHeader(responseCode) _, _ = w.Write([]byte(cachedData)) } func longRunningOperation() string { time.Sleep(time.Millisecond * 500) return "hello" } func main() { ttl := time.Second * 3 rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", }) handler := &handler{ rdb: rdb, cacheTTL: ttl, } http.HandleFunc("/simple", handler.simple) if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatalf("Could not start server: %s\n", err.Error()) } }
/simple 엔드포인트에 약간의 로드를 가하고 어떤 일이 일어나는지 살펴보겠습니다. 이를 위해 베지터를 사용하겠습니다.
vegeta 공격 -duration=30s -rate=500 -targets=./targets_simple.txt > res_simple.bin을 실행합니다. Vegeta는 결국 30초 동안 초당 500개의 요청을 보냅니다. 나는 이를 각각 100ms에 걸쳐 있는 버킷이 포함된 HTTP 결과 코드의 히스토그램으로 그래프로 표시합니다. 그 결과는 다음과 같은 그래프입니다.
실험을 시작하면 캐시가 비어 있습니다. 캐시에 저장된 값이 없습니다. 많은 요청이 서버에 도달하면 초기에 엄청난 속도로 몰려들게 됩니다. 그들 모두는 캐시를 확인하여 거기에 아무것도 없다는 것을 발견하고 longRunningOperation을 호출하여 캐시에 저장합니다. longRunningOperation은 처음 500ms 동안 이루어진 요청을 완료하는 데 최대 500ms가 걸리므로 결국 longRunningOperation을 호출하게 됩니다. 요청 중 하나가 캐시에 값을 저장하면 다음 요청은 모두 캐시에서 값을 가져오고 상태 코드 200으로 응답을 보기 시작합니다. 그러면 Redis의 만료 메커니즘이 시작되면서 패턴이 3초마다 반복됩니다. &&&]
이 장난감 예에서는 이로 인해 아무런 문제가 발생하지 않지만 프로덕션 환경에서는 시스템에 불필요한 로드가 발생하고 사용자 경험이 저하되거나 자체적으로 서비스 거부가 발생할 수도 있습니다. 그렇다면 어떻게 이를 방지할 수 있을까요? 음, 몇 가지 방법이 있습니다. 잠금을 도입할 수 있습니다. 캐시가 누락되면 코드가 잠금을 달성하려고 시도하게 됩니다. 분산 잠금은 사소한 일이 아니며 종종 섬세한 처리가 필요한 미묘한 경우가 있습니다. 백그라운드 작업을 사용하여 주기적으로 값을 다시 계산할 수도 있지만 이를 위해서는 코드에서 유지 관리하고 모니터링해야 하는 또 다른 톱니바퀴를 도입하는 추가 프로세스가 필요합니다. 동적 캐시 키가 있는 경우에는 이 접근 방식을 수행하지 못할 수도 있습니다. 확률적 조기 만료라는 또 다른 접근 방식이 있는데, 이에 대해 더 자세히 살펴보고 싶습니다.확률적 조기 만료
저는 A. Vattani, F.Chierichetti, K. Lowenstein의 Optimal Probabilistic Cache Stampede Prevention에서 XFetch를 기반으로 특정 구현을 기반으로 하고 있습니다.
비용이 많이 드는 계산을 수행하지만 이번에는 캐싱 시 XFetch를 사용하는 HTTP 서버에 새 엔드포인트를 도입하겠습니다. XFetch가 작동하려면 비용이 많이 드는 작업에 걸린 시간(델타)과 캐시 키가 만료되는 시기를 저장해야 합니다. 이를 달성하기 위해 이러한 값과 메시지 자체를 담는 구조체를 소개하겠습니다.
type probabilisticValue struct { Message string Expiry time.Time Delta time.Duration }원본 메시지를 다음 속성으로 래핑하고 redis에 저장하기 위해 직렬화하는 함수를 추가합니다.
type probabilisticValue struct { Message string Expiry time.Time Delta time.Duration }redis에서 값을 다시 계산하고 저장하는 메서드도 작성해 보겠습니다.
type probabilisticValue struct { Message string Expiry time.Time Delta time.Duration }확률에 따라 값을 업데이트해야 하는지 결정하기 위해 probabilisticValue:
에 메소드를 추가할 수 있습니다.
type probabilisticValue struct { Message string Expiry time.Time Delta time.Duration }모두 연결하면 다음 핸들러로 끝납니다.
type probabilisticValue struct { Message string Expiry time.Time Delta time.Duration }핸들러는 첫 번째 핸들러와 매우 유사하게 작동하지만 캐시 히트가 발생하면 주사위를 굴립니다. 결과에 따라 방금 가져온 값을 반환하거나 값을 일찍 업데이트합니다.
HTTP 상태 코드를 사용하여 3가지 경우를 결정합니다.
캐싱을 보다 적극적으로 사용하고 값을 더 자주 새로 고치려면 베타 매개변수를 사용해 볼 수 있습니다. 베타 매개변수를 2로 설정한 동일한 실험은 다음과 같습니다.
이 모든 것은 캐시 압류를 피하는 데 도움이 될 수 있는 깔끔하고 작은 기술입니다. 하지만 이는 캐시에서 동일한 키를 주기적으로 가져오는 경우에만 작동한다는 점을 명심하세요. 그렇지 않으면 큰 이점을 볼 수 없습니다.
캐시 폭주를 처리하는 다른 방법이 있나요? 실수를 발견하셨나요? 아래 댓글로 알려주세요!
부인 성명: 제공된 모든 리소스는 부분적으로 인터넷에서 가져온 것입니다. 귀하의 저작권이나 기타 권리 및 이익이 침해된 경우 자세한 이유를 설명하고 저작권 또는 권리 및 이익에 대한 증거를 제공한 후 이메일([email protected])로 보내주십시오. 최대한 빨리 처리해 드리겠습니다.
Copyright© 2022 湘ICP备2022001581号-3