クックパッド動画事業開発のチャレンジ

渡辺慎也氏(以下、渡辺):では、発表を始めさせていただきます。

まず自己紹介です。

私は渡辺と申します。クックパッドではメディアプロダクト開発部の部長をやらせていただいています。そして、CookpadTV株式会社の取締役のCTOもやらせていただいたりもしています。

まず、CookpadTV株式会社について簡単にご紹介させてください。CookpadTVは、料理動画事業部を事業ごとクックパッドから切り出した子会社です。

子会社化した理由は、意思決定速度の加速や採用を独自に行うためです。ただ、エンジニアやデザイナー、ディレクターの採用に関してはクックパッド本社で行い、兼務出向というかたちで働いています。

CookpadTVも、「毎日の料理を楽しみにする」というミッションでやっているのですが、こちらに書いてあるとおり、良いきっかけがなくて料理が楽しめていない人たちに向けてサービスを展開していたりもします。

普段料理をしない、する必要がない人たちにも料理をするきっかけが作れて、料理の作り手が増やせたらいいなと考えています。

そんなCookpadTVが展開している主な事業はLIVE事業、Store事業、Studio事業です。

動画事業開発の歴史

2018年は、サービスとして何が当たるかわからない状態でしたので、可能なかぎり事業になるサービスを広げることを目的にしていた年でした。

どんな感じだったか、動画事業開発の年表を簡単にまとめました。

緑の線の下にあるものが、リリースしたサービスです。上にあるのは、そのサービスに対してリリースした主な機能を表しています。ご覧のとおり、いくつものサービスを短期間にリリースし続けています。

CookpadTVは1つのサービスをやるというより、動画領域という広いくくりでサービスを展開しているので、サービスがいくつも立ち上がっています。

リリースした機能という観点ではなくて、技術的なチャレンジというところでも、裏では実はいろいろやっています。

そして、今年2019年はですね、これまで立ち上げてきたサービスのなかから、収益化できるものを見つけて、収益化にチャレンジをしていこうと思っています。

2018年を振り返ってみると、クックパッドの新規事業開発のなかも数多くのサービスを開発してきたと思っています。

それを実現するために意識してきたことは、開発速度を出すために、クックパッドが整備してくれている開発基盤があるんですが、それを最大限活用して、自立した開発を行うところを意識してきました。

cookpad storeTVとはなにか

今日は、2018年の開発事例を紹介させていただきたいと思います。ただ、時間の都合上端折る部分や詳細までは説明できない部分がありますが、興味があればこの後の懇親会で質問していただければ、可能なかぎりお答えしたいと思います。

まず1つ目は、cookpad storeTVです。

storeTVは先ほど紹介したStore事業の1つでして、写真のとおりスーパーの売り場にタブレットを設置して、売り場に関連する料理動画を繰り返し再生して、買い物客の「今日の献立は何にしようかな?」というレシピ決定を補助するサービスです。もちろん置いてる売り場側としては、PI値をアップさせることにつながっています。

このstoreTVは、日本全国約4,500店舗以上、タブレットの台数としては1万3,000台ぐらい配布しています。storeTVのビジネスモデルとしては、広告収益のモデルでやっています。

広告動画は料理動画の合間に流しています。現在では、料理動画4回に1回広告動画を挿入していて、この図にあるとおり、1回のサイクルで90秒で、1時間で換算すると40サイクル回るというかたちでループしています。この販促効果を高めるための料理動画と広告動画の再生割合は、それに関連する特許も取得していたりします。

cookpad storeTVにおける課題

ただ、広告商品を開発するうえで少し課題がありました。それは広告動画の再生数の制御についてです。商品として「再生回数○回でいくら」という売り方をするので、想定再生数よりも多すぎても少なすぎても問題になります。多すぎるとこちらが損するという話なんですけど、少ない場合は当然クレームになってしまいます。

再生数の制御とは、必要再生数を再生したら広告の再生を止める、もしくは配信期間、クライアント側からしても「この1週間はキャンペーンをやっているので、広告動画を流しててほしい」ということがありますので、前半に再生数を終わらせてしまうと、キャンペーン期間として意味がなくなってしまうので、可能なかぎり平準化して配信します。これが、広告動画の配信数制御です。

そのために、端末には配信計画というかたちで、広告配信情報を伝えます。配信計画について簡単に説明すると、例えば8時間で広告Aと広告Bという出稿があったとして、Aを100回、Bを200回再生させたいとします。

本当は端末が複数台ありますが、1台だけ稼動してるとすると、配信できる回数、在庫としては「8時間×3,600秒÷1サイクル90秒」なので、約320回在庫があることになります。

そこから1サイクルごとに、Aの広告なのかBの広告なのか、どれだけの比率でどちらを再生するかを計算すると、「それぞれ100回のほうが31.25パーセント、200回のほうが62.5パーセントの確率で広告を再生してください」という配信計画になります。

繰り返しになりますが、台数の変動や広告動画の配信期間も絡んでくるので、実際の配信計画ではもっと複雑な計算が必要なんですが、配信計画とはこのようになっています。

配信計画を出すためには、正確な在庫がわかれば簡単です。ですが、端末がついているかどうかや、選ばれている料理動画によっては広告が再生できないこともあるので、在庫がわかりづらいという問題があります。そこで、詳しく予測することをあきらめて、実測から求める形にしました。

つまり、いったん広告動画の配信計画をデフォルト値で配信してみて、しばらく再生させると、「何台稼働してるか?」とか「どういう在庫があるか?」みたいなのがわからなくとも、ログとしては「何回再生できました」という情報が出てきます。それを逆算すると、「これって何台ぐらい稼動してるんだろうな」ということがわかりますので、在庫がわかります。

常にこうして実測して、配信比率をそれに合わせて変動させていくと、在庫はすばやく変動することがありますが、それにも素早く対応できるメリットがあります。

各数字を出すための仕組み

細かい話になりますが、一番出したい配信比率、Ratioを算出する方法は、「再生したい回数(HourlyAllocated)÷在庫数(HourlyInventory)」で求められますが、この2つがわかれば逆算でこきます。

再生したい回数は、目標再生数、先ほどでいうと100回再生とか、そこから何回再生したのか、あとどれだけ残っているか、残り時間が算出ができます。

在庫数は「稼働台数×最大広告動画再生数(先ほどでいうと1時間につき40回)」ですね。ただ、ここで稼働台数は先ほど言ったとおりわからない部分がありますので、次のように予測しています。

この稼働台数、EstimatedRunningDeviceCountは「再生数(実際に再生された再生数、広告が再生された回数)÷最大広告動画再生数」、つまり40で割ります。さらにそれを前回の配信比率を用いて計算することで、実際の推定稼働台数が計算できます。

ここでまた最初に出したい比率が2回出てきていますが、これは前回設定した比率なので、問題なく計算できます。これは細かい話なので、参考程度にしていただければと思います。

リアルタイムログ集計のしくみ

これをやるためには。リアルタイムログ集計が必要です。

ログの集計に関して、左側のタブレットからKinesis Data StreamsにPutRecordされたログを、Lambdaを使ってDynamoDBに分単位のキーでインクリメントしていきます。

その後、DynamoDB Streamという機能を使って、1時間単位、1日単位にどんどん集約していきます。この1時間単位に集約された再生数を見て、現実の稼働台数や在庫数などを導き出して、その後に計算した配信比率を配信計画として使っています。

この広告配信はstoreTVの事業の要なので、監視用にKinesis Data StreamsのCloudWatchメトリクスの値やDynamoDBの集約の遅延などを監視して、SNSに通知して、Slackに通知し、エンジニアが検知できるようにしています。

また、リアルタイムログがちゃんと集計できているのかといったことを分析する用に、Kinesis Data Firehoseにも流していて、クックパッドのログ分析基盤を活用して、必要な検証等を行えるようにしています。

cookpadTV

次に、cookpadTVです。

cookpadTVはクッキングLIVEアプリでして、料理に関するコメントや質問が、クッキングLIVEを見ながらリアルタイムに行えるというサービスになっています。最近、iOS、Android、FireTVだけではなく、AndroidTVにも公開しています。

どのようなサービスかは、プロモーションの動画があるので、そちらを見ていただいたほうが早いかなと思っています。

(動画再生)

渡辺:このように、クッキングLIVEを見ながら一緒に料理したりコメントしたりできるサービスになっています。

このcookpadTVでも、視聴者数が増えるにつれて課題が出てきました。それは、クッキングLIVE中に発生する大量のメッセージの扱いです。

cookpadTVでは、クッキングLIVE中にさまざまなメッセージが、AWSのAppSyncというサービスを通して配信されています。ここで言うメッセージとは、この図で言うスタンプやコメント、ハート等が、メッセージにあたります。

メッセージが多いということはLIVEが盛り上がっているということなので、非常に良いことなんですが、その量がすごいことになってくると問題になってきます。ある番組では、ハートだけで150万回以上送信されました。

これは47分の番組でしたが、単純計算すると平均で532リクエスト/秒でした。仮に視聴者が1万人いたとすると、1人がしゃべると1万人がファンアウトするということなので、500万メッセージ/秒以上になります。

このまま人が増えると、AWS AppSync側の書き込みの制限に引っかかってしまい、アプリ側もすごい量の受信をすることになるので、アプリがクラッシュしてしまうといった問題があります。

そこで、メッセージを逐次AppSyncに送信するのではなく、一応バッファリングして送信するということを検討しました。

バッファリング方針

バッファリングするにあたって、スループットや設計のためいくつかの方針を決めました。

コメント反映をなるべく遅らせないというのは、単純にバッファがたまったらFlushするのでは、メッセージが届くのが遅れてしまうので、時限式で1秒前にFlushすることを検討しています。

コメントの順序保証をしないというのは、複数台でバッファリングしているので順序保証がそもそも難しいという話と、順序保証するためには外部ストレージに一度保存する必要があるので、それを避けたかったというのがあります。

あとは、LIVE中のメッセージの特性上、リアルタイムでメッセージが届かないと、1分後にコメントが流れても意味がないものになってしまうので、データのロストは最悪許容します。これはリトライ機構や外部ストレージを不要にすることを目的としています。

バッファリングすると決めた時に、サービスを分けることにしました。これは、バッファリングが複雑になるので、チューニングや改善が必要になると個人的には思っていましたので、その性能試験も含めてやりやすくするために、別サービスとして切り出しました。

「サービスを分ける」って気軽に言いますが(笑)、サービスを分けるとモノリシックなアプリケーション開発よりも問題がたくさんあります。ですがクックパッドの開発基盤では、マイクロサービスやサービスメッシュを活用しているので、とくに苦労もなく実現できています。サービスメッシュについてはこの後の小杉山の発表で出てくると思いますので、ここでは割愛します。

実装言語に関しては、赤丸の前段のところなんですけど、メッセージサービスがGoで実装されていることもあったので、バッファリングもGoで実装して、プロトコルとしてはgRPCで通信することを決めました。これはスループットを高めて、台数を少なく抑えることを目的としています。

オーナー組織への依存を減らしつつ、開発速度を上げる

クックパッドは、かつてレシピサービスだけだった頃は、オーナー組織と密に開発をしていましたが、サービスが増えてきた現在は、オーナー組織がすべてのサービスを全部見ていくのが難しくなってきています。

そのため、うちの部は極力オーナー組織への依存を減らして、整備してくれている開発基盤をうまく活用して、開発速度を落とさないようにしています。なので、自分たちでDesignDocを書いて、設計をして、開発をしていくということをやっています。

それはサービス開発の部分だけではなく、性能試験・監視・運用を含めて、自分たちでやることを意識しています。今回は負荷試験の計画・環境準備・実施も、自分たちで行っています。

簡単に説明しますと、本番とは別のECSクラスタにデプロイして、負荷かけて、サーバーでEC2インスタンスを立ち上げて、ghzというgRPCの負荷かけツールを使って、性能試験を行いました。

このへんは、先ほどのグローバルとは違って、新規事業の領域ということで人数も少なかったり、スピード感を持ってやりたいところなので、自分たちでやっていくことを意識してやっています。

バッファリングサービスのアーキテクチャ

次に、バッファリングサービスのアーキテクチャの説明です。まずはコメントの部分だけを説明します。

他のハートとかスタンプも同じ構成だからですね。右上に凡例を載せていますが、丸がGoのChannelで、Gopherくんが付いてるのがGoroutineで動いてる処理と考えてください。

まず左上、gRPCのリクエストが来て、Request Handlerがコメント情報をChannelに書き込みます。このChannelはNon Bufferedです。そして、この時点ですでに書き込みが終わったらクライアントはレスポンスを返せるので、レスポンスを返して終了としています。

このChannelは、別のGoroutineであるWrite Workerが監視していて、GoのsliceのBufferにappendしています。その後、sliceのサイズが閾値を超えているかどうかを判断します。閾値を超えてるようであれば、Flush用のChannelに送信して、超えてなければ何もしません。

これだとBufferがたまりきるまではFlushされなくなってしまうので、先ほど言ったとおり、インターバルでFlushするWorkerを、Goのtime.AfterFuncを使って継続的に動かしています。このインターバルは一定ではなくて、たまっているBufferのサイズの流量によって変動させるようにしています。

少ない流量のコメントの場合は、このInterval WorkerがFlushを担当します。こちらの場合は、件数ではなくて、1件でもBufferにたまっているものがあればFlushする、というふうにしています。

このFlush Channelを監視しているFlush Workerが左のところにあるんですが、これも別のGoroutineで動いていて、それがポスト用のChannelに送信しています。このポスト用のChannelはBuffered Channelになるので、同時に複数、ブロッキングされずに送れるようになっています。

このChannelを監視しているPost Workerは、AWS AppSyncへの書き込み部分を別のGoroutineで実行しています。POST Goroutineは受け取ったコメント一覧をAWS AppSyncに書き込んで終了します。

次は全体像です。

上に4つWorkerがあるんですが、これは1個1個が前のページの詳細部分と同様で、今回注目していただきたいのはその下の部分です。Post Workerが実は4つのWorkerで共有されていて、どんなメッセージでも、結局Buffered Channelで流量が制限される事になっています。

こうすることで、サーバー全体でAWS AppSyncへの最大同時書き込み数を制御できるます。もちろんこれは1台での制限なので、実際はタスクが2つ以上動いたりもすると思うので、それは半分以下の同時送信数に抑える、という設定もしています。

サービスメッシュでは、各サービスの送受信のリクエスト・パー・セカンドも可視化されてますので、今回ちゃんとバッファリングできているのかも簡単に確認ができるようになっています。

技術的なチャレンジをしながら、開発をしていく

おまけなんですが、AWS AppSyncもサービスメッシュで管理するというところで、それに対するエラーやレスポンス、リクエストタイムが、簡単に取得できて便利になっています。

先ほど説明したように、バッファリングサービスはGoroutineをけっこう多用していて、Goのメトリクスをちゃんと取得、可視化できているので、メモリのフットプリントやGoroutineの起動数が想定どおりか、簡単に確認できるようになっています。

このグラフでは発生していないんですが、それぞれのエラーやリトライが発生したかも記録されています。

ちょっと駆け足でしたがまとめです。

2018年はCookpadTVとしてさまざまなサービスを広げてきました。2019年は収益化にチャレンジしていきます。開発としても、課題に対して技術的なチャレンジをしながら、自立した開発を進めています。クックパッドはRubyが有名だと思いますが、実はうちの部はGoもけっこう活用してたりします。

料理動画事業を一緒に成長させてくれる仲間を募集していますので、興味がある人はぜひこの後の懇親会で、私に話しかけていただければと思います。ご清聴ありがとうございました。

(会場拍手)