Slide 1

Slide 1 text

AbemaTV, Inc. All Rights Reserved
 AbemaTV, Inc. All Rights Reserved
 1 ABEMAのレコメンドへの 大規模アクセスを支える Go製サーバーの裏側 2023 March 10th 株式会社サイバーエージェント 江頭 宏亮

Slide 2

Slide 2 text

AbemaTV, Inc. All Rights Reserved
 自己紹介 2 江頭 宏亮(えがしら ひろあき) バックエンドエンジニア 2018年4月 株式会社サイバーエージェント入社 WINTICKET / 公営競技事業 2020年6月 エンタメDX事業 2021年11月 ABEMA

Slide 3

Slide 3 text

AbemaTV, Inc. All Rights Reserved
 話さないこと 話すこと 本日の内容 3 ● レコメンドシステムの概要 ● 開発をすることになった背景 ● 具体的な実装 ● レコメンドエンジンの実装 ● 機械学習のアルゴリズムや手法

Slide 4

Slide 4 text

AbemaTV, Inc. All Rights Reserved
 ABEMA 新しい未来のテレビとして展開する動画配信事業 4

Slide 5

Slide 5 text

AbemaTV, Inc. All Rights Reserved
 レコメンド 5 ● ユーザーの属性や過去の行動に応じてコンテンツを推薦 ● モジュールごとに異なる特徴量を使用 ● レコメンドのフロー 1. 候補生成 2. 並び替え モジュール1 モジュール2 モジュール3 動画1 動画2 動画3 動画4 動画5 動画6

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

AbemaTV, Inc. All Rights Reserved
 開発することになった背景 8 リリース前の負荷試験により ● 計算量が多く、非常に多くの計算リソースが必要なことが判明 → リソース不足時の障害発生リスク、インフラコストの増大 ● 並行処理をより最適化したい Goでプロキシサーバーを開発

Slide 9

Slide 9 text

AbemaTV, Inc. All Rights Reserved
 全体像 9 Service A Service B Service C ・・・ Gateway Proxy Python Go Yatagarasu Microservices リクエストをまとめたり、キャッシュを導入することによりオリジンへのアクセスを削減

Slide 10

Slide 10 text

AbemaTV, Inc. All Rights Reserved
 処理の流れ 10 1. キャッシュキーの生成 2. キャッシュへリクエスト a. インメモリ b. Redis 3. オリジン(Yatagarasu)へリクエスト 4. キャッシュへ保存

Slide 11

Slide 11 text

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/

Slide 12

Slide 12 text

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)) }

Slide 13

Slide 13 text

AbemaTV, Inc. All Rights Reserved
 キャッシュ 13 オリジンへのリクエストをより減らすため2層構成 ● インメモリキャッシュ ● Redisキャッシュ

Slide 14

Slide 14 text

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 を利用していた

Slide 15

Slide 15 text

AbemaTV, Inc. All Rights Reserved
 Redis 15 https://github.com/redis/go-redis ● Ringクライアントを活用 ● Consistent Hash Ring構成 ● Memorystore for Redis / Basic Tier

Slide 16

Slide 16 text

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 }

Slide 17

Slide 17 text

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 }

Slide 18

Slide 18 text

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 }

Slide 19

Slide 19 text

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 }

Slide 20

Slide 20 text

AbemaTV, Inc. All Rights Reserved
 まとめ 20 ● レコメンドシステムへのリクエストを削減するためにプロキシを開発 ● インメモリLRUとRedisの2層キャッシュ構成 ● singleflightパッケージでオリジンへの同一リクエストを抑制 ● 結果 ○ オリジンへのリクエストを90~95%削減 ○ インフラリソースの最適化を実現

Slide 21

Slide 21 text

AbemaTV, Inc. All Rights Reserved
 ありがとうございました 21