Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

レコメンドへの大規模アクセスを支えるGo製サーバーの裏側

Hiroaki Egashira
March 09, 2023
3.6k

 レコメンドへの大規模アクセスを支えるGo製サーバーの裏側

Hiroaki Egashira

March 09, 2023
Tweet

Transcript

  1. AbemaTV, Inc. All Rights Reserved
 AbemaTV, Inc. All Rights Reserved


    1 ABEMAのレコメンドへの 大規模アクセスを支える Go製サーバーの裏側 2023 March 10th 株式会社サイバーエージェント 江頭 宏亮
  2. AbemaTV, Inc. All Rights Reserved
 自己紹介 2 江頭 宏亮(えがしら ひろあき)

    バックエンドエンジニア 2018年4月 株式会社サイバーエージェント入社 WINTICKET / 公営競技事業 2020年6月 エンタメDX事業 2021年11月 ABEMA
  3. AbemaTV, Inc. All Rights Reserved
 話さないこと 話すこと 本日の内容 3 •

    レコメンドシステムの概要 • 開発をすることになった背景 • 具体的な実装 • レコメンドエンジンの実装 • 機械学習のアルゴリズムや手法
  4. AbemaTV, Inc. All Rights Reserved
 レコメンド 5 • ユーザーの属性や過去の行動に応じてコンテンツを推薦 •

    モジュールごとに異なる特徴量を使用 • レコメンドのフロー 1. 候補生成 2. 並び替え モジュール1 モジュール2 モジュール3 動画1 動画2 動画3 動画4 動画5 動画6
  5. AbemaTV, Inc. All Rights Reserved
 Yatagarasu / ヤタガラス Dragon /

    ドラゴン レコメンドシステム 6 • 広告配信のような仕組み • 手動でコンテンツ(候補)を配信設定 • きめ細かなターゲティングが可能 • 機械学習(並び替え) • 計算量が少ない • Go • 機械学習(候補生成・並び替え) • 計算量が多い • Python, Go
  6. AbemaTV, Inc. All Rights Reserved
 Yatagarasu / ヤタガラス Dragon /

    ドラゴン レコメンドシステム 7 • 広告配信のような仕組み • 手動でコンテンツ(候補)を配信設定 • きめ細かなターゲティングが可能 • 機械学習(並び替え) • 計算量が少ない • Go • 機械学習(候補生成・並び替え) • 計算量が多い • Python, Go
  7. AbemaTV, Inc. All Rights Reserved
 開発することになった背景 8 リリース前の負荷試験により • 計算量が多く、非常に多くの計算リソースが必要なことが判明

    → リソース不足時の障害発生リスク、インフラコストの増大 • 並行処理をより最適化したい Goでプロキシサーバーを開発
  8. AbemaTV, Inc. All Rights Reserved
 全体像 9 Service A Service

    B Service C ・・・ Gateway Proxy Python Go Yatagarasu Microservices リクエストをまとめたり、キャッシュを導入することによりオリジンへのアクセスを削減
  9. AbemaTV, Inc. All Rights Reserved
 処理の流れ 10 1. キャッシュキーの生成 2.

    キャッシュへリクエスト a. インメモリ b. Redis 3. オリジン(Yatagarasu)へリクエスト 4. キャッシュへ保存
  10. AbemaTV, Inc. All Rights Reserved
 キャッシュキーの生成 11 • モジュールが使用する特徴量からハッシュ値を生成 •

    xxHash アルゴリズムを採用 Hash Name Width Bandwidth (GB/s) xxHash 64 19.4 GB/s Murmur3 32 3.9 GB/s FNV64 63 1.2 GB/s SHA1 160 0.8 GB/s MD5 128 0.6 GB/s 引用: “xxHash” http://cyan4973.github.io/xxHash/
  11. AbemaTV, Inc. All Rights Reserved
 キャッシュキーの生成 12 https://github.com/cespare/xxhash func (a

    *app) GenerateCacheKey(moduleName string, attributes map[string]string) string { // features var features []string switch moduleName { case "moduleA": features = []string{attributes["age"], attributes["gender"]} case "moduleB": features = []string{attributes["lastWatchedEpisodeId"]} case "moduleC": features = []string{attributes["paymentStatus"]} } // hash hash := xxhash.New() for i := range features { _, _ = hash.WriteString(features[i]) } return hex.EncodeToString(hash.Sum(nil)) }
  12. AbemaTV, Inc. All Rights Reserved
 インメモリキャッシュ 14 https://github.com/dgraph-io/ristretto • メモリ使用量を制限するためLRU

    (Least Recently Used) を利用 • TTLもサポートする必要があったので dgraph-io/ristretto を採用 • 初期は https://github.com/patrickmn/go-cache を利用していた
  13. AbemaTV, Inc. All Rights Reserved
 Gob 16 https://pkg.go.dev/encoding/gob Go標準パッケージ func

    (a *app) Encode(module *Module) ([]byte, error) { buf := &bytes.Buffer{} err := gob.NewEncoder(buf).Encode(module) if err != nil { return nil, err } return buf.Bytes(), nil } func (a *app) Decode(data []byte) (*Module, error) { module := &Module{} err := gob.NewDecoder(bytes.NewReader(data)).Decode(module) if err != nil { return nil, err } return module, nil }
  14. AbemaTV, Inc. All Rights Reserved
 キャッシュ 17 func (a *app)

    LookupCaches(ctx context.Context, key string) (*Module, bool) { // In-Memory module, ok := a.inmemory.Get(ctx, key) if ok { return module, ok } // Redis module, ok = a.redis.Get(ctx, key) if ok { a.inmemory.Set(ctx, module) return module, ok } return nil, false }
  15. AbemaTV, Inc. All Rights Reserved
 オリジン(Yatagarasu)へリクエスト 18 func (a *app)

    FetchModules(ctx context.Context, ids []string) ([]*Module, error) { // Timeout timeoutCtx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() // Fetch modules := make([]*Module, 0, len(ids)) wg, mutex := &sync.WaitGroup{}, &sync.Mutex{} wg.Add(len(ids)) for i := range ids { id := ids[i] go func() { defer wg.Done() module, err := a.service.FetchModule(timeoutCtx, id) if err != nil { log.Println(err) return } mutex.Lock() modules = append(modules, module) mutex.Unlock() }() } wg.Wait() // Cache a.inmemory.BatchSet(ctx, modules) a.redis.BatchSet(ctx, modules) return modules, nil }
  16. AbemaTV, Inc. All Rights Reserved
 Singleflight 19 https://pkg.go.dev/golang.org/x/sync/singleflight 同時に関数を呼び出すことを抑制する仕組み func

    (a *app) Fetch(ctx context.Context, id, key string) (*Module, error) { module, err, shared := a.singleflight.Do(key, func() (interface{}, error) { return a.service.FetchModule(ctx, id) }) if shared { fmt.Println("Function call was shared") } if err != nil { return nil, err } return module.(*Module), nil }
  17. AbemaTV, Inc. All Rights Reserved
 まとめ 20 • レコメンドシステムへのリクエストを削減するためにプロキシを開発 •

    インメモリLRUとRedisの2層キャッシュ構成 • singleflightパッケージでオリジンへの同一リクエストを抑制 • 結果 ◦ オリジンへのリクエストを90~95%削減 ◦ インフラリソースの最適化を実現