![](https://images.logmi.jp/media/article/331351/images/main_image_6b591c1ac9b2fd8d4da82a0cccfed54e8b9e39b6.jpg?w=600)
2025.02.12
職員一人あたり52時間の残業削減に成功 kintone導入がもたらした富士吉田市の自治体DX“変革”ハウツー
リンクをコピー
記事をブックマーク
江頭宏亮氏:それでは、株式会社サイバーエージェント・江頭から「ABEMAのレコメンドへの大規模アクセスを支えるGo製サーバーの裏側」というタイトルで発表いたします。よろしくお願いします。
「レコメンド」と聞くと、パーソナライズされているという印象を持つ方もけっこういると思います。そういったものと、この大規模配信など多くのユーザーが一度に訪れるユースケースの掛け合わせだと、非常に負荷対策などの面で苦労するんじゃないかな? と思う方もいると思います。
そういったところで具体的にどういった工夫をしたのか、どういった実装を入れることによって、パーソナライズを最大限維持したまま大規模負荷や大規模ユーザーアクセス(への対策)を実現したのかを具体的にお話できればと思います。
まず軽く自己紹介をさせてください。2018年にバックエンドエンジニアとして株式会社サイバーエージェントに入社して、最初は競輪とオートレースの投票券の販売を行う「WINTICKET」というサービスの立ち上げに関わっていました。
2020年からは、コロナ禍もありオフラインでのイベントがけっこう難しい状況になってきたので、この期間に芸能人とファンがオンラインでコミュニケーションを取れるサービスを提供するエンタメDX事業に携わっていました。
そして、2021年11月にABEMAに異動して、今はレコメンドシステムの本体を作ることもあるのですが、主にバックエンド寄りの部分と、サービスをグロースさせるために新機能の実装や既存機能の拡張を担うチームを担当しています。よろしくお願いします。
本日は、まず始めにABEMAのレコメンドシステムの概要についてお話できればと思います。そして、今回Go製サーバーを作ることになった背景、また、その具体的な実装についてお話できればと思います。
一方で、いろいろなレコメンドのロジックやエンジンを持っているのですが、その具体的な実装、機械学習のアルゴリズムや手法みたいなところは、Goの範疇を少し超えてしまう都合で割愛させていただければと思います。
まず「ABEMA」についてですが、新しい未来のテレビとして展開する動画配信事業で、主に2つの形態の動画を扱っています。1つは24時間編成で編成されている専門チャンネル。具体的には、釣りチャンネル、麻雀チャンネル、ニュースチャンネルみたいな、いわゆる僕らが「リニア配信」と呼んでいるような配信形態です。もう1つは、NetflixやU-NEXTなどがやっているような、ビデオ・オン・デマンドのサービス。主にこの2つを提供しています。
その中で、レコメンド機能を提供しています。(スライドを示して)具体的にはこの右側のスクショみたいな感じで、いろいろなコンテンツの推薦を行っています。私たちは、この右側の図の横の並びのことを「モジュール」と呼んでいて、さまざまな切り口でユーザーにとって最適と思われるコンテンツのレコメンドを提供しています。
このレコメンドが具体的にどのようになっているかというと、ユーザーの属性や過去の行動に応じてコンテンツを推薦しています。ここで言う「ユーザーの属性」というのは、例えば年齢だったり性別だったり、過去に視聴したコンテンツだったりみたいなもの……あ、そっか。「過去に視聴した」じゃないですね。何日に初めて訪れたとか、あとは課金ステータスみたいな、そういったものをユーザーの属性として扱っています。
そしてもう1つ、過去の行動に応じても推薦をしています。この過去の行動というのが、いつどのページを訪れたとか、PPV(ペイ・パー・ビュー)の販売もやっているのですが、いつどのPPVを買ったか、直近でどの動画を視聴したかとか、そういった情報を基にコンテンツの推薦を行っています。
そしてこのモジュールごとに異なる特徴量。ここで言う特徴量とは、ユーザーの属性や過去の行動量を使用して実現しています。例えば、(スライドを示して)この右側のスクショの上から2番目の「あなたへの関連度が高い作品」に関しては、そのユーザーが直近で視聴したコンテンツに似ているコンテンツを推薦しています。
レコメンドの具体的な処理のフローですが、2つのフロー、ステップに分かれています。1つが「候補生成」と言われる処理で、(スライドを示して)この上から2番目の「あなたへの関連度が高い作品」であれば、まずユーザーが直近で視聴したコンテンツのIDなどを基に、似ているコンテンツを洗い出す作業を私たちは「候補生成作業」と呼んでいます。
(スライドを示して)この上から2番目のモジュール2はユーザーに視聴を促す、ユーザーの視聴につなげたいモジュールなので、候補生成が完了したあと、候補生成したものからそのユーザーが一番視聴する確率が高いものを機械学習などを用いてスコアを出しして、そのスコアの高い順に左から並べる並び替え作業をしています。この2つの処理を行うことでレコメンドを実現しています。
ABEMAのレコメンドの仕組みは、大きく2つのシステムがあります。1つが左手に表示されている「Dragon」と呼ばれているものです。これは広告配信のような仕組みになっていて、手動でコンテンツの配信設定を行います。それによって、けっこうきめ細かなターゲティングが可能になっています。
例えば、「直近でこのPPVを買った人にこのPPVを推薦したい」とか、「直近でこのコンテンツを観た人にこのコンテンツを出したい」とか。逆に「これを観た人には、もうこれは推薦しない」とか、けっこうきめ細かいターゲティング設定ができるようになっています。
また、Frequency Controlで「1日2回まで表示されるように」など、制限もできるようになっています。このDragonに関しては、並び替えだけで機械学習を用いているため、計算量が比較的少ない範囲で済んでいます。そしてGoで実装しています。
次に(スライドを示して)この右側の「Yatagarasu」と呼ばれるところですが、これは、候補生成から並び替えまで基本的に機械学習をすべて利用しているため、計算量が非常に多くなっています。また、基本的にML・DSエンジニア、マシンラーニング、データサイエンティストチームが中心となって作っているため、Pythonで書かれています。今回このYatagarasuのシステムについて説明しますが、自分たちが作ったGo製サーバー以外の部分は、すべてPythonで書かれています。
今回、Go製サーバーを開発することになった背景です。もともとGo製サーバーを使う予定はなかったのですが、リリース前の負荷試験により、想像以上に計算量が多く、非常に多くの計算リソースが必要だということが判明しました。
また、それによってリソースが不足したケースに障害が発生するリスクが高かったり、たくさんサーバーを置くことで障害の発生のリスクは軽減できるのですが、インフラコストが増大してしまうという問題が出てきました。ほかにも、Pythonで作っているので、並行処理をより最適化したいというニーズも出てきたため、今回Goでプロキシサーバーを開発することになりました。
具体的にYatagarasuシステムの全体像は(スライドを示して)このようになっています。この右側に見えるところがYatagarasu、マイクロサービスとなっていて、Pythonで書かれています。このマイクロサービスは、基本的にはモジュール、先ほどのレコメンドの目的や種類ごとにマイクロサービスが切られているマイクロサービスアーキテクチャになっています。
今回は、このYatagarasuマイクロサービスへの負荷やリクエストをより削減するために、フロント側にGoでプロキシサーバーを作り、そこでリクエストをまとめたり、キャッシュを導入することにより、オリジンへのアクセスを削減しました。
ここからは具体的に処理の流れについて説明したいと思います。今回は、キャッシュ処理が入っているので、キャッシュキーの生成を最初にやります。
今回キャッシュは、GoのインメモリキャッシュとRedisを使ったインメモリキャッシュの2層構造にしているので、まずインメモリにリクエストを行って、インメモリになかった場合は、Redisにリクエストを行う。そして、Redisにもなかった場合、初めてオリジンのYatagarasuのシステムへリクエストに行く。こういった構造になっています。そして、Yatagarasuからレスポンスが来たら、それをキャッシュへ保存するという処理になっています。
ここからは、より一つひとつの処理について具体的に説明したいと思います。
まず、キャッシュキーの生成ですが、モジュールが使用する特徴量からハッシュ値を生成しています。具体的にこの特徴量は、ユーザーの視聴履歴だったり属性情報だったりするので、簡単にキャッシュのキーを生成することが難しく、ハッシュ値から生成するアプローチを取っています。
そして、キャッシュキーの生成にはxxHashというアルゴリズムを採用しています。これを採用した理由は、下に図を載せたのですが、MD5やFNVと比較しても20〜30倍ほどパフォーマンスが良いので、このxxHashというアルゴリズムを採用しました。
具体的にGoでは、cespare/xxhashというライブラリが存在しており、今回このライブラリを活用して実際に実装に落とし込んでいます。
(スライドの)下にちょっとサンプルコードを書いてみました。実際に利用しているコードとは少し違うのですが、だいたいこのような処理になっています。まず引数にモジュールの名前、attribute、ユーザー属性を受け取り、最初のswitch文でモジュールごとに使用する特徴量を取り出すという作業をしています。
例えばモジュールAであれば、年齢と性別を、モジュールBであれば最後に視聴したエピソードIDを、モジュールCであれば支払いステータスを使用するという感じで、モジュールごとに使用する特徴量が違うので、まずはその特徴量を取り出す作業をします。
これを下のほうで、for文を回してxxHashに入れる。そして最後にhexでエンコードして、文字列として出てきたものをキャッシュのキーとして利用しています。
次に具体的なキャッシュの部分ですが、オリジンへのリクエストをより減らすために2層構造になっています。もともとはGoのインメモリキャッシュだけを使っていたのですが、どうしてもデプロイ時、ロールアウト時に揮発してしまうため、一時的にオリジンへの負荷が上がってしまうという課題がありました。そこを改善するためにRedisも導入して2層構造になっています。
Goのインメモリキャッシュは、DgraphのRistrettoというライブラリを活用しています。もともとはgo-cacheという、LRUがなく基本的にはTTLだけを指定できるシンプルなキャッシュを利用していました。
しかし、パーソナライズされたレコメンドのものをキャッシュする都合上、かなりバリエーションや種類があり、単純にTTLだけで載せてしまうとメモリが何ギガあっても足りないみたいな、OOMになるリスクもあったので、途中からLRUのDgraphのRistrettoのライブラリを使うように変更しました。Go製のLRUは調べるとけっこういろいろあったのですが、TTLも使う必要があったので、今回はこのライブラリを選定するに至りました。
次に、Redisへのリクエストにはライブラリとしてgo-redisを利用しています。RedisはRedis ClusterとかRedis Sentinelとか、いろいろなクラスタリングや構成があると思いますが、今回はConsistent Hash Ring構成を採用したかったのでこのgo-redisを採用しました。
go-redisはRingクライアントというクライアントが実装されており、このRingクライアントはRedisのノードへ定期的にヘルスチェックを行い、アンヘルシーな場合はRingから勝手に抜くこともきちんとやってくれる実装になっています。具体的に、リリース前に障害試験をやったところ、万が一Redisが1台ダウンしてもユーザーに影響がなくレスポンスをきちんと返せることが確認できています。
また、インフラの話になってしまうのですが、ABEMAの本体のプログラムはほとんどGoogle Cloudで動いているので、RedisはMemorystore for Redisを利用しています。今回は単純なキャッシュを載せるだけだったので、冗長構成ではなくBasic Tierを採用しています。これによりコスト的なメリットも生まれています。
そして、Redisへ書き込む時のエンコーディングのフォーマットですが、Goの標準パッケージのgobを利用しています。Redisとかに載せるデータのエンコーディングの手法としては、JSONやProtocol Bufferなどさまざまありますが、今回はシンプルなものを採用したかったので、標準パッケージのgobを利用しています。
この下の部分に、具体的なgobの実装のサンプルのコードを書いてみたのですが、エンコードの部分は、bytes.bufferを定義・作成して、それをgobのNewEncoderに入れて、Encodeメソッドにmoduleのstructをそのまま渡すだけでエンコードが完了します。このようにむちゃくちゃシンプルな実装ができるので、今回はgobを採用しました。
下にデコードの例も書いているのですが、byte配列、byteのスライスがあった時に、それをReaderにしてデコーダーに入れて、Decodeにdestinationとなるstructを入れればデコードもできます。こんな感じでめちゃくちゃシンプルなので、これを採用しました。
そして2層のキャッシュの具体的な実装例がこのようになっています。まずinmemoryからGetして、あればそれを利用して、なければRedisを見に行って、RedisにあればそれをinmemoryにSetして、それを返す。Redisにもなければキャッシュミスというかたちで処理をするという、実装になっています。
(次回へつづく)
関連タグ:
2025.02.13
“最近の新人は報連相をしない”という、管理職の他責思考 部下に対する「NG指示」から見る、認識のズレを防ぐコツ
2025.02.06
すかいらーく創業者が、社長を辞めて75歳で再起業したわけ “あえて長居させるコーヒー店”の経営に込めるこだわり
2025.02.13
AIを使いこなせない人が直面する本当の課題 元マッキンゼー・赤羽雄二氏が“英語の情報”を追い続ける理由
2025.02.12
マネージャーは「プレイング3割」が適切 チームの業績を上げるためのマネジメントと業務の比率
2025.02.12
何度言っても変わらない人への指示のポイント 相手が主体的に動き出す“お願い”の仕方
2025.02.13
「みんなで決めたから」を言い訳にして仲良しクラブで終わる組織 インパクトも多様性も両立させるソース原理
2025.01.07
1月から始めたい「日記」を書く習慣 ビジネスパーソンにおすすめな3つの理由
2025.02.10
32歳で「すかいらーく」を創業、75歳で「高倉町珈琲」で再起業 「失敗したからすかいらーくができた」横川竟氏流の経営哲学
2025.02.14
報連相ができない部下に対するコミュニケーションの取り方 「部下が悪い」で終わらせない、管理職のスキル向上のポイント
2025.02.10
A4用紙を持ち歩いて殴り書きでアウトプット コクヨのワークスタイルコンサルタントが語る、2種類のメモ術
2025.02.13
“最近の新人は報連相をしない”という、管理職の他責思考 部下に対する「NG指示」から見る、認識のズレを防ぐコツ
2025.02.06
すかいらーく創業者が、社長を辞めて75歳で再起業したわけ “あえて長居させるコーヒー店”の経営に込めるこだわり
2025.02.13
AIを使いこなせない人が直面する本当の課題 元マッキンゼー・赤羽雄二氏が“英語の情報”を追い続ける理由
2025.02.12
マネージャーは「プレイング3割」が適切 チームの業績を上げるためのマネジメントと業務の比率
2025.02.12
何度言っても変わらない人への指示のポイント 相手が主体的に動き出す“お願い”の仕方
2025.02.13
「みんなで決めたから」を言い訳にして仲良しクラブで終わる組織 インパクトも多様性も両立させるソース原理
2025.01.07
1月から始めたい「日記」を書く習慣 ビジネスパーソンにおすすめな3つの理由
2025.02.10
32歳で「すかいらーく」を創業、75歳で「高倉町珈琲」で再起業 「失敗したからすかいらーくができた」横川竟氏流の経営哲学
2025.02.14
報連相ができない部下に対するコミュニケーションの取り方 「部下が悪い」で終わらせない、管理職のスキル向上のポイント
2025.02.10
A4用紙を持ち歩いて殴り書きでアウトプット コクヨのワークスタイルコンサルタントが語る、2種類のメモ術
着想から2か月でローンチ!爆速で新規事業を立ち上げる方法
2025.01.21 - 2025.01.21
新人の報連相スキルはマネージメントで引きあげろ!~管理職の「他責思考」を排除~
2025.01.29 - 2025.01.29
【手放すTALK LIVE#45】人と組織のポテンシャルが継承されるソース原理 ~人と組織のポテンシャルが花開く「ソース原理」とは~
2024.12.09 - 2024.12.09
『これで採用はうまくいく』著者が語る、今こそ採用担当に届けたい「口説く」力のすべて
2024.11.29 - 2024.11.29
【著者来館】『成果を上げるプレイングマネジャーは「これ」をやらない』出版記念イベント!
2025.01.10 - 2025.01.10