Carpe Diem

備忘録

singleflight でキャッシュのOriginへのリクエストを抑制

概要

golangにはsingleflightという重複したの関数呼び出しを抑制する仕組みがあります。
例えばキャッシュが切れた時にOriginへのアクセスが並行して走ってしまうケースでサーバやDBがスローダウンする問題がありますが、こういった時にsingleflightを挟むと解決できます。

環境

簡単なサンプル

通常だと10回呼び出す処理を、singleflightで抑制してみます。

実装

package main

import (
        "fmt"
        "log"
        "sync"
        "time"

        "golang.org/x/sync/singleflight"
)

var group singleflight.Group

func doSomething(name string) {
        v, err, shared := group.Do(name, func() (interface{}, error) {
                time.Sleep(5 * time.Millisecond)
                return time.Now(), nil
        })
        if err != nil {
                log.Fatal(err)
        }
        fmt.Printf("result: %s, shared: %t\n", v, shared)
}

func main() {
        var wg sync.WaitGroup
        for i := 0; i < 10; i++ {
                wg.Add(1)
                go func() {
                        defer wg.Done()
                        doSomething("hogehoge")
                }()
                time.Sleep(time.Millisecond)
        }
        wg.Wait()
}

結果

$ go run main.go
result: 2019-06-06 07:40:06.977784 +0900 JST m=+0.006349595, shared: true
result: 2019-06-06 07:40:06.977784 +0900 JST m=+0.006349595, shared: true
result: 2019-06-06 07:40:06.977784 +0900 JST m=+0.006349595, shared: true
result: 2019-06-06 07:40:06.977784 +0900 JST m=+0.006349595, shared: true
result: 2019-06-06 07:40:06.977784 +0900 JST m=+0.006349595, shared: true
result: 2019-06-06 07:40:06.984543 +0900 JST m=+0.013108129, shared: true
result: 2019-06-06 07:40:06.984543 +0900 JST m=+0.013108129, shared: true
result: 2019-06-06 07:40:06.984543 +0900 JST m=+0.013108129, shared: true
result: 2019-06-06 07:40:06.984543 +0900 JST m=+0.013108129, shared: true
result: 2019-06-06 07:40:06.990454 +0900 JST m=+0.019019767, shared: false

このように重複したリクエストはshared: trueとなり、結果が他のリクエストと一致(=時刻が同じになる)します。
今回だと

  • 07:40:06.977784
  • 07:40:06.984543
  • 07:40:06.990454
    • 重複なし

という結果で、合計10回呼び出しましたが、中身の処理は3回に収まっています

具体的な実装

システム構成

以下のようなユーザ -> CloudFront -> 画像プロキシ -> S3みたいな構成を想定します。

f:id:quoll00:20190606102003p:plain

基本的にCloudFrontのエッジがキャッシュを返すことで普段画像プロキシの負荷は低いです。
しかしCloudFrontのキャッシュがexpireしたり、invalidationで一時的にキャッシュが無効化された場合画像プロキシにアクセスが集中します。
S3バケットから画像を取得する処理は時間がかかる&メモリを喰うため、場合によっては負荷が高まりスローダウンする可能性があります。

これをsingleflightで解決します。

実装

var group singleflight.Group

func download(path string) ([]byte, error) {
        log.Println("download")
        resp, err := http.Get(path)
        if err != nil {
                return nil, err
        }
        defer resp.Body.Close()
        b, err := ioutil.ReadAll(resp.Body)
        if err != nil {
                return nil, err
        }
        return b, nil
}

func suppressDupCall(path string) ([]byte, error) {
        v, err, _ := group.Do(path, func() (interface{}, error) {
                return download(path)
        })
        if err != nil {
                return nil, err
        }
        return v.([]byte), nil
}

func main() {
        log.SetFlags(log.LstdFlags | log.Lmicroseconds)

        path := "https://www.gstatic.com/webp/gallery3/1.png"
        var wg sync.WaitGroup
        for i := 0; i < 10; i++ {
                wg.Add(1)
                go func() {
                        defer wg.Done()
                        img, err := download(path)
                        //img, err := suppressDupCall(path)
                        fmt.Printf("result: %d, err: %v\n", len(img), err)
                }()
        }
        wg.Wait()
}

結果

singleflightを挟まないケース

以下のように毎回ダウンロードが走ってしまいます。

$ go run main.go 
2019/06/06 10:09:29.784576 download
2019/06/06 10:09:29.784576 download
2019/06/06 10:09:29.784580 download
2019/06/06 10:09:29.784581 download
2019/06/06 10:09:29.784576 download
2019/06/06 10:09:29.784576 download
2019/06/06 10:09:29.784589 download
2019/06/06 10:09:29.784591 download
2019/06/06 10:09:29.784593 download
2019/06/06 10:09:29.784673 download
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>

singleflightを挟むケース

ダウンロードは最初の1回で済んでいます。

$ go run main.go 
2019/06/06 10:11:18.562771 download
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>
result: 121363, err: <nil>

まとめ

singleflightによる重複した関数呼び出しの抑制を紹介しました。

  • キャッシュのOriginへのアクセスを抑制したい
  • 重い関数だけど結果は一緒

というケースではsingleflightは非常に有効です。

ソース