全国のスーパーに置かれる storeTV 端末の情報の取得にかかる時間を15分から10秒にした話

こんにちは。
メディアプロダクト開発部の柴原です。
普段は CookpadTV のサービスである storeTV や storeLive の Android アプリを開発を担当しています。

storeTV では現在、サービスを高品質に継続開発・運用するための仕組みづくりをしており、この記事ではその中の1つである「端末が再生した動画をリアルタイムで把握する機能」について紹介します。
このリアルタイムログの導入でもともと実際の端末で動画再生がされているのかを確認するのに15分かかっていたものが10秒程度に短縮できました。

storeTV とは

storeTV は、スーパーで料理動画を流すサービスで、店頭に独自の Android 端末を設置し、その売り場に適したレシピ動画を再生するサービスです。 より詳しいサービス概要にについては、弊社メンバーの Cookpad TechConf 2018 における以下の発表スライドを御覧ください。

動画の再生順序の概要

storeTV では、「手順動画15秒 * 4回 + 広告動画30秒 * 1回」の90秒を1ロールとし動画をループ再生しています。

f:id:Nshiba:20210607140255j:plain
1ロールの動画再生順序

背景

storeTV では通常のアプリよりも端末の状態把握が重要です。 なぜなら「storeTV」は、買い物客の「今日何作ろう?」と、店舗の「この食材がおすすめ!」をマッチングさせるサービスなので、サービスとして動き続けることが重要だからです。 万が一停止していると問い合わせが発生した場合こちらで端末の状態を把握し回答をする必要があります。
そのため、いくつか端末の状態を把握するための仕組みがあり、主に端末から送信しているログと端末管理システムから取得できる情報があります。

端末から送信しているログについては、ログが送信されてから実際に見れるようになるまで約15分程度の時間がかかります。 また、端末管理システムから取得できる情報はアプリバージョンやOSバージョンといった基本的なものと、1台ずつですがその時のスクリーンショットを取得することが可能です。

よって、端末の状態を把握するにはログが来るまでの15分程度待つか、端末管理システムで1台ずつ能動的にスクリーンショットを確認していく必要がありました。

このような状況を改善するためにリアルタイムで各端末の動画再生ログを収集するリアルタイムログを導入しました。

構成


リアルタイムログを実現するために以下のような構成を採用しました。

f:id:Nshiba:20210607140435j:plain
構成図

IoT

まずログの受け取りは AWS IoT の Rule Action を用いています。
なぜ AWS IoT を用いているかというと、端末管理システムは内製したもので AWS IoT を用いて開発しており、今回のリアルタイムログも端末管理システムの一部機能として実装しました。
端末管理システムのより詳しい話は、以下のAWS導入事例の記事を御覧ください。

aws.amazon.com

Rule Action とは、特定の Topic に対してデータが送られてきた際に特定の動作を行わせることができる機能です。 例として、受け取ったデータを DynamoDB や S3 への書き込みや Lambda Function の起動といったことが可能になってます。

しかし今回のリアルタイムログでは、受け取ったデータを Kinesis に流す構成を採用しています。

Kinesis & Lambda

IoT Rule Action で受け取ったデータは一度 Kinesis に入れ Lambda から DynamoDB に書き込みます。 これは、大量のデータを DynamoDB に直接書き込んでしまうと大量の WCU が必要になってしまうため、書き込み量を制御するためです。
 構成図を見ると分かる通り、最終的に DynamoDB にログを書き込みます。
IoT の Rule Action ではそのまま DynamoDB に書き込むことも可能ですが、今回は一度 Kinesis を挟んでから DynamoDB に書き込むようにしています。
これは、そのまま DynamoDB に書き込んでしまうと秒間の書き込みの最大が端末数に依存してしまうためです。
現在でも約5000台程度の端末から約15秒に1件のログが送られてきます。さらに端末数は今後増えていく見込みであるためシステム側で DynamoDB への書き込み量を制御するために Kinesis を挟み、一度ハンドリングしてから DynamoDB に書き込むようにしています。

DynamoDB

データは storeTV 端末から以下のような JSON が送られてきます。
thing_name とは端末を識別するためにそれぞれ割り当てられている固有の値です。

{
    "thing_name": #{thingName}, 
    “creative_id”: 1, 
    “creative_type”: 1,
    "published_at": "2021-06-01 10:00:00"
    “project”: “store_tv”   
}

これを thing_name を Hash Key とし、受け取ったものから上書き保存という形で DynamoDB に書き込んで行きます。

name type schema example
thing_name String Hash Key thing_001
creative_type Int Sort Ket 1
creative_id Int 1
published_at String 2021-06-01 10:00:00
project String store_tv

大量のログをさばくためにしたこと

この機能を実現しようとしたときに、まずはじめに困ったことは大量のログが常に送信され続ける、という点です。
そのため、この機能を実装する際に大量のログをさばくためにいくつか対策をしました。

実際どの程度のログが送信されてくるかというと、現在 storeTV が稼働している端末で、端末管理システムが導入できている端末は当時5000台程度になります。
また storeTV は主に15秒の動画をループ再生しているため、1分間に送信されうるログの量は 5000 x 4 で20000件のログが送信されてくることになります。

IoT Rule Action では受け取ったデータをそのまま DynamoDB に書き込むことも可能ですが、この量のログをそのまま書き込んでしまうと大量の WCU が必要になってしまい運用にかかるコストが莫大になります。
そのため、まずは直接 DynamoDB に書き込むのではなく、 Kinesis でバッファし Lambda で DynamoDB に書き込むログの量を制御できるようにしています。

さらに DynamoDB のテーブル構成を見ると thing_name を Hash Key としています。
そのため、このテーブル仕様では1つの端末が sort key 毎に1行のログしか持てないことになります。
これは今回のリアルタイムログでは、端末が「最後に動画を再生したのはいつか」が把握できれば良いという方針で設計を行ったからです。
こうすることで Kinesis に溜まったデータを Lambda で DynamoDB に書き込む際に、同じ thing_name のログは最新のログだけ DynamoDB に書き込むようにすることで書き込む量を削減し大量のログをさばけるようにしています。

また、今回のログは端末側がオフラインのときなどにログが送信できなかった場合、再送などの処理は行っていません。
そのため、もともと正確性はそれほど高くならないような設計になっており、広告動画の再生回数の集計などには使えないものになっていますが、集計のためのログはすでに別の方法で取得し集計しているため今回は必要ありませんでした。

DynamoDB のコスト最適化

今回のリアルタイムログでは、 Scheduled Action を用いて 8:00~22:00 の間だけ DynamoDB の WCU を必要な分確保しています。 これは storeTV は 8:00~22:00 の営業時間というのが設定されており、営業時間外の 22:01~翌日7:59まではアプリは動作しておらず、ログが来るのは 8:00~22:00 の間のみに限定されるからです。

ちなみに、当初は Auto Scaling のみで DynamoDB の WCU を調整するようにしていましたが、これだと 8:00 の段階で急に書き込みが増えるため Auto Scaling が間に合わず数時間にわたりログの遅延が発生する状態に陥っており、これを解消するためにも Scheduled Action を用いるようにしました。
また、オンデマンドでも試しましたがサービスの稼働時間が固定されている状況もあり、 Auto Scaling のほうがコスト的にメリットが大きかったため Auto Scaling を採用しています。

まとめ

今回リアルタイムログを実装し端末ごとに今なんの動画が再生されているのかが迅速に把握できるようになりました。
ちなみにログの遅延具合は約5~10秒程度、遅くても1分以内に再生した動画は確認できるようになりました。
ただし、まだ端末ごとに再生した動画が確認できるだけなのでこのリアルタイムログを活用した新たな機能や、障害対応時を想定した端末内のより詳しいログを欲しいときだけ取得する機能など、端末の状態をより詳しく把握するための仕組みづくりを検討しています。

また、私はアプリエンジニアでしたが、サービスに何が必要なのかを自分で考えそれが自分の得意分野でなかったですが興味のある分野であったため、今回設計から実装までを任せてもらい、実際に Go でプログラム書いたり Lambda のデプロイ環境の準備や DynamoDB のテーブル設計を行いました。
そんなメディアプロダクト開発部では、一緒に働いてくれるメンバーを募集しています。少しでも興味を持っていただけたら、ぜひ採用情報を御覧ください。

info.cookpad.com

料理動画サービスに強く興味がある方は以下のリンクから「料理動画サービス」のついた項目を御覧ください。

info.cookpad.com

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/