Quantcast
Channel: クックパッド開発者ブログ
Viewing all 726 articles
Browse latest View live

クックパッドアプリはみんなが寝ている間にサブミットされる

$
0
0

こんにちは、技術部モバイル基盤グループの茂呂(@slightair)です。 先日のiOSDCは大盛況でしたね。とても楽しく、実りあるカンファレンスでした。この記事で僕は ididblog! ということにしようと思っています 😋

クックパッドからは @giginetと僕の二人が登壇しました。発表を聞きに来ていただいた方はありがとうございました。 @giginet詳解Fastfileという発表中でさらっと話された、”毎週自動的にリリースされる”という言葉が気になった方はいるのではないでしょうか。実はこのリリースフローについての話もプロポーザルに出していたのです(もっともっと細かくリリースをしてユーザーに最速で価値を届けるためのリリースフロー)。

この記事ではこのリリースフローについての話をしたいと思います。

クックパッドアプリの開発体制

クックパッドアプリの開発体制は人数の変動はあるものの、ここ数年で大きく変わっているものはありません。 この記事にあるように、モバイルアプリを専門で開発するようなチームはなく、有料会員獲得やレシピ投稿、検索結果の向上などを目的とした部署に所属するエンジニアがそれぞれの目的に合わせた機能追加・修正のPRを出しマージして定期的にリリースする、というスタイルを続けています。

開発環境のメトリクスとして記録している数字を見ると、最近では各リリースごとにだいたい10数人のコミットが含まれているようでした。この数字にはアプリのコードを書くエンジニアだけでなく、テストエンジニアや画像リソースなどを変更するデザイナー、更新文言やアプリの説明文などを編集するディレクターの数も含まれます。

以前のリリースフロー

基本的には前述の記事で説明しているように開発期間やテスト期間などの日を細かく決め、その期間で各チームが開発を行い、コードフリーズ後に動作確認を行ってサブミット、審査が通ればリリース、というフローを回していました。1イテレーションはだいたい2週間のペースです。ひとつのイテレーションが終わると開発関係者で集まって振り返りを行い、課題や改善点が見つかれば次のイテレーションで解決するというのを繰り返すことで、不具合を抑え安定したペースでリリースし続ける状況を維持していました。このフローは僕たちの組織構造にも合っていて長年うまくいっていたリリースフローなのですが、問題もありました。

ひとつめはリリースマネージャーの負担が大きいことです。リリースマネージャーというリリースに責任を負う人物を立てて、問題なくリリースが行えるように部署間の施策の調整をしたり、スケジュールの調整を行う役目を担ってもらっていました。次のリリースに入るはずの機能がマージできていないときに関係者をつっつくような役目も持っていたので、とにかく細かい作業や人間の間の調整で忙殺されてしまうのです。リリースマネージャー役を関係者間で交代するようにしたり、リリース作業とリリース進行役を別の人間で分担するようにもしてみましたが、手順がまとめられていても人によってケアの仕方に差がでたり関係者が増えることによる負担は増加する一方でした。

ふたつめはだいたい2週間に一度のリリースのペースを守ろうとしていてもばらつきが起きていたこと、場合によってリリース間隔が開いてしまっていたことです。ある部署の大事な施策の開発が遅れているのでリリーススケジュールを遅らせたい、広範囲にわたる変更があるので他の部署の開発は次のリリースでは我慢してもらいたい、というようなリリースごとの特別対応が求められるケースがたびたび発生していました。そのような調整作業をまとめているリリースマネージャーがさらに疲弊してしまったり、想定のリリーススケジュールが守られず期待していたタイミングで機能を提供できない、価値検証がうまく行えない、タイミング次第では検証に基づく変更を反映するのに時間がかかりすぎてしまうというチームが出てしまう状況でした。

つまり、リリースマネージャーを立てて様々なケースに融通が利くようなフローにしていた結果、当事者間の調整コストが高まって色々な問題を引き起こしていたのです。

新しいリリースフローは「機械に人間が合わせる」

当事者間の調整でみんなが苦しんでいるのがわかったので、この「調整」を無くすことはできないだろうかと考え始めました。もう調整はしない!

そこで僕たちは可能な限りサブミット・リリース作業を自動化する仕組みを構築し、人間が動かなくても自動でアプリがサブミットされる状況を作ることにしました。 タイミングが来ると自動でサブミットを行うジョブが実行されるのでそれに合わせて開発するスタイルです。人間たちの準備が整ってからサブミットを実行するのではなく、機械のペースに人間が合わせて行動するということです。開発が間に合わなかったら次のリリースには入れられるように頑張ってね、という感じになります。

具体的にはこのようなルールで運用しています。

  • リリース予定日は毎週月曜日と仮定する
  • バージョン番号には 年.リリース予定日の週番号.パッチ番号.0を採用する
    • 基本的に再サブミットを行わないのでパッチ番号は 0 になる
  • 毎週金曜AM2:00にサブミットジョブを実行し、その時点のmasterブランチの内容をビルド、サブミットする
  • サブミット後に開発関係者各自で動作確認を行いリリース判定を行う、あわせてAppleの審査を通過したらリリースを行う
    • もし致命的な不具合が見つかったり、バイナリの変更が必要な理由で審査に落ちたらそのバージョンのリリースをあきらめ、次のバージョンで問題を解決する
    • ある程度の不具合を許容する。リリースサイクルが早いので次の機会で確実に直せればよいと考える。
  • master ブランチには確実にリリース可能なコードのみをマージする。もしマージ後に問題が発覚すれば即リバートする。

「機械に人間が合わせる」というコンセプトでリリースフローを設計したので、ある意味ドライな意思決定がされるようになった部分もありますが、思い切った変更により自動化できる箇所が増え、開発者の負担を減らしたり毎週というリリースサイクルでも回せるようになりました。

このリリースフローに切り替えてから、毎週金曜日に出社したり仕事を始めようとするとサブミットが終わっている状況になっています。早いときは審査が終わっている場合もあります。ジョブの実行を金曜の早朝に設定している理由は、なにかサブミット作業で問題が起きたときに金曜日中に対応できるようにするためです。

f:id:Slightair:20180913183311p:plain

自動化しているもの、機械がやってくれること

以下のタスクを自動化しています。実際のサブミット作業だけでなく、このリリースフローを安定して回したり当事者間のコミュニケーションを促進する施策を実行します。

  • 更新文言(fastlane/metadata/ja/release_notes.txt)の変更があるか確認
    • AppStoreReviewガイドラインの最近の変更(2.3.12)のため、毎回同じ更新文言を出さないようにしている
    • 各チームから文言を決めるissueに書き込んだり、PRを出してもらう形にしている
    • サブミットジョブの終わりでテンプレートファイルを用いてリセットする。diffがなかったらサブミット失敗として扱う。
  • リリースに含まれるコミットをしたメンバーをリストにしたリリース前確認issueをGHEに作成する
  • git タグを作成する
  • アプリをビルド、サブミットする (fastlane deliver)
  • アプリのバージョンを更新しリポジトリにpushする
  • GHEへ次の次のマイルストーンを作成する
  • groupad(社内Wiki)に次のバージョンのリリース内容を記述するページを作成する
  • サブミットしたアプリと同等のバイナリをRC版としてビルドし、haneda(社内アプリ配信サービス)にアップロードする

一連の作業は fastlane と Jenkins で実現しています。人間がやる作業はアプリを開発したりメタデータを更新すること、動作確認をして全員の確認がそろったらリリースボタンを押すだけです。

アプリの品質管理について

新しいリリースフローに移行する前は、開発後にコードフリーズを行いテストをする期間を設けていました。この期間で開発者が動作確認するのはもちろんですが、テストエンジニアが自動シナリオテストを実行したり重要な機能の動作確認を集中的に行い、不具合を含んだアプリがリリースされてしまうのを防ごうとしていました。この方法ではテストエンジニアがアプリの品質に対して集中的に責任を持って守れる一方、アプリが持つ機能の詳細や新規機能、修正内容を把握した上で動作確認を行う必要があり、情報のキャッチアップにも動作確認にも時間がかかってしまっていました。スケールもしません。

そのため新しいリリースフローに移行すると同時に、開発するチーム単位でも品質をコントロールできるようにする取り組みも始めています。これはリリースサイクルを早めるためにも必要な試みで、機能を開発するチームが自分たちの担当する部分の品質をコントロールしてリリース可否や不具合対応方針を決められる体制のほうが健全であり、結果的にスピードも出るだろうという考えです。

ある程度の不具合を許容すると前節で書きましたが、正確には不具合を見つけたとしてもすべてを解消してからリリースすることを目指すのではなく、不具合とどう向き合うのかも含めて機能を開発しているチームがアプリの品質をコントロールし動けるようにする、ということです。

リリースフローを運用してみて

新しいリリースフローに移行してからだいたい1ヶ月くらい、リリース回数でいうと4,5回行ったところですが、今の所大きな事故はなく、うまくいっています。以前と違いAppleの審査時間が短くなったのもこのリリースフローが成立する理由のひとつです。うれしいですね。

移行してからリジェクトもいくつかありましたが、メタデータリジェクトが多く、この場合は修正してリリースを行っています。 一度だけサブミット後にバイナリに変更が必要な状況になり、どうしてもということでリリース延期を行わずイテレーション内の再サブミットを実行しています。可能な限りこのような特別対応は無いほうがよいと思っていますが今後はどうなるでしょう、様子をみていくつもりです。リリース前確認もすりぬけて致命的な不具合を含んだ状態でリリースしてしまい、1週間は待てないのでそれを修正するための緊急リリースをしたいということもいつかは起こってしまうはず。どういう対応が必要になってくるかはこれから見えてくると思います。

単純にリリース回数が多くなっただけではないということを繰り返し社内でも伝えていますが、調整をしたがる声が時々出てしまっています。難しいですね。気持ちもわかるし、このやり方が定着するにはもう少し時間がかかりそうです。

まとめ

iOSDC では発表できなかった、クックパッドアプリの新しいリリースフローの取り組みについて紹介しました。機械に人間が合わせるというコンセプトをもとに、自動化できるところはとことん自動化し機械にまかせることで、リリースサイクルを早めたり開発関係者の負担を下げられるような努力をしています。このリリースフローが今後もうまく続けられるかどうかはもう少し経ってみないとわかりませんが、なかなかおもしろい取り組みなのではないかと考えています。


Cookpad Summer Internship 2018 10 day 技術インターンシップ を開催しました

$
0
0

技術広報を担当している外村(@hokaccha)です。

クックパッドでは毎年恒例となっているサマーインターンシップのうち「10 day 技術インターンシップ」を開催しました。今年は8月6日〜8月17日、8月27日〜9月7日という日程で二度開催し、たくさんの学生の方に参加していただきました。

f:id:hokaccha:20180828130243j:plain

今回の 10 day 技術インターンシップは、前半5日が講義パート、後半5日はOJTコースとPBLコースに分かれるという構成でした。OJTコースではクックパッドの現場に配属され、メンターの指導のもとサービス開発を実践してもらい、PBLコースではチーム開発でプロジェクトを運営していく手法について学びながら、サービス開発の実習に取り組んでもらいました。

前半パートでの講義について資料を公開いたします。

1日目: 基礎技術

初日はインターンで必要になる基礎的な知識の足並みを揃えるために、GitやRuby、JavaScriptなどについての講義を行いました。

2日目: サービス開発

2日目は、毎年恒例となっているサービス開発の講義で、コードは一行も書かずクックパッドで実践されているサービス開発の手法を実践してもらいました。プロトタイピングやユーザーインタビューを通じてサービスの設計をしました。

3日目: API(サーバーサイド)

3日目はRailsを使ったWebアプリケーションの講義です。今回のお題はチャットアプリケーションで、途中まで実装したRailsアプリケーションこちらで準備し、そのアプリケーションを完成させたり、思い思いの機能を実装してもらいました。

4日目: ReactNative

4日目は、3日目に作ったチャットアプリケーションのクライアントアプリをReactNativeとTypeScriptで作るという内容でした。Expoを使い、簡単にネイティブアプリの開発を始められるという体験をしてもらいました。

cookpad/cookpad-internship-2018-summer react-native - GitHub

5日目: インフラ

最終日のインフラの講義では、AWSやDocker、パフォーマンスチューニングなどの基礎的な知識について解説し、3日目に作ったRailsのアプリケーションを高速化するという課題に取り組みました。サーバーのパフォーマンスをコンテスト形式でスコアを競い、大変盛り上がりました。

cookpad/cookpad-internship-2018-summer infra - GitHub


全員が真剣に講義に取り組み、後半のOJTコースとPBLコースでの実践に活かしてくれました。参加していただいた皆様、本当にありがとうござまいました。

Cookpad Product Internship 2018 の振り返り

$
0
0

新規サービス開発部の出口 (@dex1t) です。普段はデザインからアプリ開発まで、新規サービス立ち上げに必要なことを浅く広くやっています。

さて、R&Dインターン技術インターンに続きまして、9月10日~14日にかけてデザイナーとサービス開発エンジニア向けのプロダクトインターンシップを開催しました。私は本インターンの全体設計と講師を担当しました。この記事ではその内容を簡単にご紹介します。

f:id:dex1t:20180926223117j:plain

このインターンはざっくりいうと、デザイナー・エンジニアでペアを組み、ゼロから"使える"サービスを作るという内容です。なかなかハードですね 😉

今年は「一人暮らししている人の料理が楽しみになるサービス」というテーマで、5日間のサービス開発を実践していただきました!

Day 1-2. 基礎編 ✍🏻

1日目から2日目午前は、「サービス開発を実践するための道具を提供する」という建て付けで、講義やミニワークを行いました。

ざっくり以下のような内容で、体験やコンテキストといったサービスデザインに関する抽象的な話から、サービス開発における具体的な手法・ツールまで駆け足で網羅しました。

  • サービスとは?体験とは?
  • サービス開発におけるマインドセットとプロセス
  • ユーザー理解
    • ワーク: ユーザーインタビューの実践
  • アイデア発想と言語化
    • ワーク: 価値仮説とストーリー作成
  • 試作とテスト
    • ワーク: ペーパープロトタイピング、アクティングアウト

サービス開発に初挑戦の方も多くいたこともあり、調査・発想・試作・試行の各ステップをなるべく丁寧に説明しました。実際の講義資料 (公開可能分のみ) はこちらです。

Day 2-5. 実践編 💪

2日目の午後からはいよいよテーマに沿って実践編のスタートです。

参加者の方にはテーマだけをお渡しし、具体的にどんなサービスを作るのか、言い換えるとどんな問いを立てるのか、その解き方も含めて、全てチーム毎に自ら考えて実行してもらいました。各チームには現場のデザイナーが専属メンターとして付き、全力でチームをサポートします。

序盤

肌感覚をつかむための最初の一歩として、インタビューを各チーム自発的に行っていました。参加者同士でのインタビューはもちろん、その場にいる社員を捕まえたり、電話インタビューしたりと、限られた時間のなかでインプットを増やすために各チーム工夫して動いていました。

f:id:dex1t:20180911180345j:plain

中盤

インタビューを繰り返し、課題が見えはじめたところで、次はコンセプトの設計です。メンターに企画案を壁当てしながら、各チーム頭を悩ませていました。コンセプト設計には、価値仮説シートやストーリーシートなど、初日に講義の中で紹介した道具を活用してもらいました。

f:id:dex1t:20180911180304j:plainf:id:dex1t:20180911175934j:plain

三日目の午後は中間発表です。ここでは企画案とその動作モックを使って、アクティングアウト形式での発表を必須としました。アクティングアウトは体験のプロトタイピングとも呼ばれる手法で、寸劇形式でサービスの利用体験を表現することで、そのリアリティの有無を確かめることができます。

この中間発表の様子は、動画撮影をして発表者自身にも確認してもらいます。こうすることで、この時点での企画案を客観視することができ、自らの判断で軌道修正するチームも見られました。

また講評者の観点では、テキストベースの企画書を読み込むよりも寸劇形式のほうが理解しやすく、より本質的なフィードバックに集中できるという利点もあります。

f:id:dex1t:20180914171016j:plain
即席の名札で登場人物を演じ分けているチームも

終盤

終盤はいよいよ実装です。残り少ない時間の中で、実装すべきところはどこなのか、どこを捨てるのか判断するのはサービス開発エンジニアの腕の見せ所です。弊社オフィスのキッチンで、自分たちのプロトタイプを試しに使いつつ料理するチームも出てきました。

f:id:dex1t:20180911180620j:plain

最終発表

5日目の夕方には最終発表として、成果物のデモを中心に発表してもらいました!タイトなスケジュールのなかで、全チーム何らかの形で試すことができるサービスが出来上がっていて素晴らしかったです。

f:id:dex1t:20180914174147j:plainf:id:dex1t:20180914173128j:plain

最終発表では、弊社CTOやデザイナー統括マネージャーを含む5名が審査員となり、優秀賞1チームを選ばせていただきました。

優勝チームは「その場限りのコミュニティでの料理通話体験」というコンセプトで、通話をしながら料理が楽しめるアプリの提案でした。デザインの観点では「料理経験が浅く失敗が多い」「1人で作って1人で食べるのが孤独」というマイナスをただ埋めるだけでなく、「失敗やハプニングも含めてみんなで楽しもう」という考え方でプラスに転換している点を評価しました。また、赤の他人との料理通話をどうアイスブレイクするかといった細かい配慮が、UIとしても表現できており素晴らしかったです。エンジニアリングの観点では、時間が無い中で「通話しながら料理する」という多くの人にとって未知な体験を、実際にその場で試せるクオリティで仕上げた点を高く評価しました!

f:id:dex1t:20180914191259j:plainf:id:dex1t:20180914210033j:plain

最後は懇親会で終了しました!チェキコーナーも人気 ✌️

ということで、5日間でゼロからサービス作りをするというハードな内容でしたが、皆さんやり切っていただけました👏👏👏

プロダクトインターンシップで伝えたかったこと

ここからは裏話として、インターンの設計面についてです。今回5日間に渡って講義や実践を行いましたが、伝えたいことは大きく3つありました。

リアリティのある仮説をもつ

サービス開発は正解がなく、「やってみないと分からない」が大前提なのですが、無闇にやればいいという訳でもなく、仮説の質を上げるのが大事なポイントです。

良い仮説 (企画) とは何なのか。非常に難しい問題で私も分かりませんが、そのひとつにリアリティがあること、企画を聞いただけでサービスを使う情景がありありと目に浮かぶことは必要条件であると私は思います。(十分条件ではない😑)

講師側としては、問いの立て方をインターンシップで教えることは難しく、1day形式など特に時間が限られるワークショップでは、問いが立てやすいように仕立てた材料を、ペルソナのような形で渡しています。

ただし現場のサービス開発では、ペルソナが上から降ってくることはあり得ません。自分たちで情報を取りに行き、肌感を掴むことが求められます。

そのやり方も現場では人それぞれ様々ですが、今回は最も汎用的なツールとして、ユーザーインタビューを実践してもらいました。各チーム自ら工夫して情報を取りに行き、自分たちで見聞きした一次情報を元にしているからこそ、リアリティのある仮説が立てられることを体感してもらえたかな、と思います。

とりあえずやってみる

今回講義の冒頭では、次のようなインターン中に求める姿勢を明文化しました。過去に開催したこの手のワークショップでの反省も踏まえてなのですが、コンセプトワークだけでなく実際のモノに落とし込む部分を強く求めました。

f:id:dex1t:20180926224726p:plain

この3つは、デザインファーム IDEOのValuesから表現を一部借りています。余談ですが、このValuesは新規サービス開発をやってる人間としてすごく共感できます。

やってみないと分からない状況下では、長々と議論やブレストをしても非効率になり得ます。荒削りでも良いので一度形にしてみると、正解でなくても、その形が間違っていることは最低限分かります。そうして徐々にボケた輪郭をシャープに描いていく姿勢は大切です。

試作と試行を繰り返す

また形にするのは一度限りではありません。それを壊してまた作ってを繰り返すのがサービス開発の基本姿勢です。今回最終発表で評価が高かったチームはいずれも、プロトタイプを何らかの形で自分たちで試してみたチームでした。今回優秀賞となったチームは、初対面の社員を捕まえて実際に電話しながら料理したり、帰宅後もお互いに通話しながら料理したりと、試行錯誤を繰り返しつつサービスを形づくるプロセスも素晴らしかったです。

f:id:dex1t:20180926224914p:plain
インターン生 (奥) が初対面の社員 (手前) と通話しながら料理する様子

5日間という現場以上にハードなスケジュールなこともあり、繰り返しができても1, 2回だったことは講師としての反省点でした。またインターンの制約上、テストする対象が自分たちや社員止まりでしたが、本来は実際の想定ユーザーにテストできるとベストです。これらの点は来年のインターンシップでは改善できればと思っています。

まとめ

今回のサマーインターンシップでは、講師により仕立てられたサービス開発ではなく、より実践的なリアリティのあるサービス開発を体感していただけたかな、と思います。参加していただいた皆さま、本当にありがとうございました!!

また、このようなサービス開発は現場でももちろん実践しています。新卒・中途問わず、ご興味あるかたは各ポジションにご応募いただくか、@dex1tまでお声がけください 🙌

【開催レポ】Cookpad Tech Kitchen #18 生鮮食品EC クックパッドマートの開発秘話

$
0
0

こんにちは。広報部のとくなり餃子大好き( id:tokunarigyozadaisuki)です。

2018年9月26日に、Cookpad Tech Kitchen #18 生鮮食品EC クックパッドマートの開発秘話を開催いたしました。クックパッドでは、Cookpad Tech Kitchenを通して、技術やサービス開発に関する知見を定期的に発信しています。

f:id:tokunarigyozadaisuki:20181010105834j:plain
本イベントの登壇者4名
第18回は2018年9月20日にリリースいたしました、「毎日が楽しみになる、食材店。『クックパッドマート』」の開発秘話をテーマとし、事業開始の背景から初期の価値検証プロセス、アプリのデザイン、iOS開発の進め方についてなどたっぷりとお話をさせていただきたました。 本ブログを通して当日の様子をご来場いただけなかったみなさまにもお届けしたいと思います。

生鮮食品ネットスーパー「クックパッドマート」 

f:id:tokunarigyozadaisuki:20181010105007j:plain

クックパッドマートは、精肉店や鮮魚店、ベーカリーなど地域で有名な店や農家の「こだわり食材」をアプリから購入できる生鮮食品ネットスーパーです。 「焼きたてパン」や「朝採れ野菜」などの新鮮な食材を、販売店から集荷した当日に受け取ることができます。1品からでも送料無料で注文が可能、毎回必要な分だけ手軽に購入することができます。 商品は、提供地域の様々な店舗・施設等に設置された「受け取り場所」の中から、利用者が選んだ場所・時間に受け取ることが可能。そのため、日中忙しくて買い物をする時間がない方でも、新鮮なこだわり食材を手軽に入手することができます。

2018年10月10日現在、商品の受け取り場所は学芸大学駅周辺の「なんでも酒やカクヤス 学芸大学前店」「カラオケの鉄人 学芸大学店」の2店舗となります。都市圏を中心に順次拡大を予定しています。

▶︎アプリダウンロードはこちらから https://itunes.apple.com/jp/app/id1434632076

発表プログラム

「クックパッドマートが目指すもの」

はじめにお話いたしましたのは、クックパッドマートの事業責任者である福崎です。クックパッドマートがどのような課題を解決するサービスであるか、またその課題に向き合うチームの紹介をさせていただきました。 f:id:tokunarigyozadaisuki:20181009193239j:plain

「リリースまでに捨てた10のこと」

デザイナー兼エンジニアで、現在は主にアプリのサーバーサイド実装を担当している長野より、クックパッドマートの事業立ち上げの際に行ったプロトタイピングとその結果をふまえた学びについてお話いたしました。 f:id:tokunarigyozadaisuki:20181010105824j:plain

本登壇内容についてはクックパッド開発者ブログでも記事として公開しております! こちらもぜひご覧ください。 

「クックパッドマートでアプリをデザインした話」

アプリデザインとマーケティングを担当している米田からは、 クックパッドマートをデザインという切り口から振り返り、ラフデザインから実装デザインまでの経緯を発表させていただきました。

f:id:tokunarigyozadaisuki:20181010105827j:plain

発表終盤におまけとしてご紹介しました、クックパッドマート公式キャラクターの「トマート」のTwitterアカウントも、ぜひフォローしてくださいね!

「クックパッドマートのアプリ開発について」

最後は、クックパッドマートのiOSアプリ開発を担う中山の登壇です。 f:id:tokunarigyozadaisuki:20181010105830j:plainなぜアプリを開発することに決めたのか、実際の開発方針や開発速度を上げるために行ったこと、毎日使ってもらえるアプリをつくるために心がけていることなどについて、お話しさせていただきました。

付箋形式でお答えするQ&Aディスカッション

Cookpad Tech Kitchenでは参加者のみなさまからの質問を付箋で集めております。開発体制やスケジュール、iOSアプリのアーキテクチャについてなど具体的な内容から、クックパッドマートの今後の展開などについてなどたくさんのご質問をいただきました。ありがとうございました!

f:id:tokunarigyozadaisuki:20181009193249j:plain
クックパッドマート開発責任者の勝間が司会を担当いたしました

シェフの手作り料理

Cookpad Tech Kitchen ではイベントに参加してくださったみなさまにおもてなしの気持ちを込めて、シェフ手作りのごはんをご用意し、食べながら飲みながらカジュアルに発表を聞いていただけるように工夫しています。

今回は、クックパッドマートで実際にご購入いただける食材でお料理を用意させていただきました!

f:id:tokunarigyozadaisuki:20181010105323j:plainf:id:tokunarigyozadaisuki:20181009195319j:plain
f:id:tokunarigyozadaisuki:20181009193311j:plainf:id:tokunarigyozadaisuki:20181009193307j:plain

おわりに

今回のイベントでは、「毎日が楽しみになる、食材店。『クックパッドマート』」の開発秘話についてご紹介いたしました。 クックパッドマートでは、サービス展開に向けて日々取り組んでいるところですが、実現したいことに対して、まだまだ力が足りていません。そこで、サービス立ち上げにコミットしていただける方を募集しております。

サーバーサイドエンジニア
オペレーションマネジャー
コンテンツディレクター
オープンポジション

https://info.cookpad.com/careers

次回のCookpad Tech Kitchenは、11月1日(木)を予定しております。今後のイベント情報についてはConnpassページについて随時更新予定です。イベント更新情報にご興味がある方は、ぜひメンバー登録をポチっとお願いします!

cookpad.connpass.com

Hackarade #04: Create Your Own Interpreter

$
0
0

技術部の遠藤(@mametter)です。Rubyの開発やってます。

クックパッドでは、Hackaradeという社内ハッカソンを定期的に開催しています。第1回はRubyインタプリタのハック(MRI Internal Challenge)、第2回は機械学習の体験(Machine Learning Challenge)、第3回はISUCON風の社内コンテストを行いました。 4回目となる今回は、遠藤が講師となり、「言語処理系を自作する」というテーマで開催しました。その概要と成果の一部をご紹介します。

f:id:ku-ma-me:20181009143312j:plain

概要

言語処理系の作り方の基本を一日で習得することを目標として、「RubyインタプリタをRubyで書くこと」を具体的な課題としました。

言語処理系は通常、プログラムテキストを抽象構文木に変換する「パーサ」と、抽象構文木を元に指示を実行する「評価器」からなります。今回は、評価器の実装にフォーカスしました(パーサは講師が公開しているminruby gemを利用しました)。

フルセットのRubyを一日で作るのは不可能に近いので、"MinRuby"というサブセット言語をターゲット言語としました。MinRubyは、Rubyの言語機能のうち、次のものだけを持つ言語です。

  • 四則演算、比較演算
  • 文、変数
  • 分岐とループ
  • 関数呼び出し
  • 関数定義
  • 配列、ハッシュ

講師からは、インタプリタの骨格となるスクリプト(interp.rb)とテストケースを提供しました。このスクリプトには多数のraise(NotImplementedError)が含まれています。この穴を上から順に埋めていくと、上記の機能が順に実装されていって、テストケースも徐々に通っていきます。このあたりはゲーム感覚で進められるように配慮しました。

すべてのテストが通ったら、最後の課題は「セルフホスト」です。セルフホストとは、ホスト言語(インタプリタを実装している言語)をターゲット言語にすることです。つまり、interp.rbをRubyではなくMinRubyで書き直します。これにより、「interp.rbをinterp.rbの上で動かす」とか、「interp.rbを動かすinterp.rbをinterp.rbの上で動かす」というようなことが可能になります。

発展課題と成果

セルフホストに成功した後は、自由にインタプリタを拡張してもらいました。正確に数えていませんが、少なくとも10人以上の参加者がセルフホストに成功し、さらにいろいろ拡張してくれました。

f:id:ku-ma-me:20181009142303j:plain

Hackarade当日の夜はパーティで、有志に成果を発表していただきました。その一部を以下に紹介します。

  • 高速化1:case文を直接実装したり頻出ケースを優先したりして3倍以上速くした
  • 高速化2:定数畳み込みや部分評価を実装して実行時の無駄な計算を省いた
  • 型チェッカ:定数参照の構文を型注釈として使い、num :: Integer = "str"などとするとエラーが出るようにした
  • オブジェクト指向:クラスやメソッドを実装した
  • splatの実装:可変長引数の関数呼び出しを実装した
  • ブロックの実装: yieldでdo...endが呼べるようにした
  • 正規表現:マッチングのエンジンから自力で実装した
  • gotoの実装:gotoを実装した
  • MinSwift:Swiftで同じことを再現しようとした(未完)
  • コンパイラ:内部的にバイトコードを生成してからVM実行するようにした
  • 完全セルフホスト:MinRubyでパーサを書き、minruby gemに依存せずにinterp.rb単体でセルフホストするようにした

MinRubyは規模が小さいので、手軽に新機能を実験する土台になります。とはいえ、一日でコンパイラや完全セルフホストに到達する人まで出るとは、正直予想を上回りました。

まとめ

「言語処理系を自作する」というテーマで開催した第4回のHackaradeを紹介しました。もともと言語処理系に詳しいエンジニアから、そもそもRubyをあまり触ったことのないエンジニアまで、幅広い人に楽しんでいただけたと思っています。

Hackaradeはクックパッドのエンジニアの技術力向上を目的としているイベントです。社員エンジニアは原則全員参加(もちろん業務として)、今回はさらにエンジニアアルバイトも業務として参加可能でした。そんなクックパッドに興味を持った方は、募集要項ページをご覧ください。

なお、今回の資料はSlideShareGitHubに公開しています。

https://github.com/mame/cookpad-hackarade-minruby

言語処理系のセルフホストは概念的にはむずかしくないですが、実際にやってみると意外とデバッグがむずかしい(多段になると、どのレベルで例外が起きているのか把握するのに頭を使う)です。読者の方もぜひ一度やってみてください。今回の講義は講師(遠藤)の著書である『RubyでつくるRuby - ゼロから学びなおすプログラミング言語入門』(Amazon)をベースとしています。自力で解くのが難しかった方、答え合わせがしたい方、復習したい方などはご参照ください。

簡潔ビットベクトルでRubyをlog N倍速くした

$
0
0

技術部のフルタイムRubyコミッタの遠藤(@mametter)です。昨日の Hackarade #04 の開催報告に続き、2日連続で記事を投稿します。

今回は、ある条件下でのRubyの実行速度を高速化した話を紹介します。この改善はすでにMRIの先端にコミットされていて*1、年末リリース予定のRuby 2.6に含まれる予定です。

ひとことで言うと、「簡潔ビットベクトルを索引に使うことで、プログラムカウンタから行番号を計算するアルゴリズムをO(log N)からO(1)に改善した。これにより、TracePoint有効時やコードカバレッジ測定下で、長さ N のメソッドの実行が O(N log N) から O(N) に高速化される」ということです。順に説明します。

背景:Rubyのバイトコードの構造

この最適化を理解するにはまず、Rubyのバイトコードのある特徴を知る必要があります。

たとえば

x = :foo
y = :bar
z = :baz

というRubyプログラムは、概念的には次のようなバイトコードになります。

PC 命令 行番号
0000 putobject :foo 1
0002 setlocal x
0004 putobject :bar 2
0006 setlocal y
0008 putobject :baz 3
0010 setlocal z

PCはプログラムカウンタ、putobjectはオブジェクトをスタックにpushする命令、setlocalはスタックのトップを指定変数に代入する命令です。ソースコードの行番号を保持しているのがポイントです。行番号の情報は、例外発生時のバックトレースや、コードカバレッジの測定などで必要になります。

ここで注目してほしいのは、行番号の情報は一部の命令だけが持っているということです(他に、TracePointのイベントタイプも、一部の命令だけが保持します)。そのため、このバイトコードをそのままメモリに保持すると、無駄が生じることになります*2。そこで、RubyのVMはこのテーブルを「命令列」と「命令情報テーブル」という2つのテーブルに分解して保持しています。

PC 命令
0000 putobject :foo
0002 setlocal x
0004 putobject :bar
0006 setlocal y
0008 putobject :baz
0010 setlocal z

図↑:命令列

図↓:命令情報テーブル

PC 行番号
0000 1
0004 2
0008 3

命令情報テーブルは、行番号のある命令の分しか情報を保持しないので、余計なメモリを消費しません。

性能の問題

メモリ消費量を減らすことはできましたが、実際に行番号を調べる必要が生じたとき、命令情報テーブルを表引きする必要があります。この表引きは、Ruby 2.4は線形探索でO(N)、Ruby 2.5は二分探索に改善してO(log N)でした。

この表引きが定数時間でないために、行番号やTracePointイベントフラグを頻繁に参照するような状況下(たとえばTracePoint有効下)で、ちょっとびっくりする性能特性が生まれます。次の図は、ローカル変数への代入をN回やるだけのN行のメソッドを、TracePoint有効下で実行するのにかかる時間を測定した結果です。

N行のメソッドの実行にかかる時間が非線形になっていることがわかります。10000行もあるようなメソッドを書くことは(自動生成を除けば)稀なので、あまり問題になっていませんでしたが、直観的な挙動とは言えません。このようになるのは、N行のメソッドはO(N)個の命令からなり、線形探索(Ruby 2.4)だと命令ごとにO(N)の表引きが行われるので、メソッドの実行全体ではO(N ^ 2)の時間がかかるためです。二分探索(Ruby 2.5)は圧倒的に改善していますが、まだO(N log N)で、線形にはなっていません。

今回は、さらなる最適化として、命令情報テーブルに「簡潔ビットベクトル」を索引としてもたせることで、表引きを定数時間に改善しました。これにより、N行のメソッドの実行にかかる時間がO(N)という、当たり前の期待を達成することができます。

簡潔ビットベクトルとは

簡潔ビットベクトルは、ビットの列を表すデータ構造です。

インデックス0123456789...
ビット列  1010010010...

図:ビットベクトルの例

インデックスが0..nの間にある1の数を数える操作をrank(n)と言います。上のビット列で考えると、たとえばrank(2)=2、rank(5)=3、rank(9)=4です。

ただのビット列では、rank(n)を計算するためにO(n)の時間がかかります。しかし簡潔ビットベクトルはとてもかしこいので、rank(n)の問い合わせに定数時間で応えてしまいます。そのために、少し補助データを持つ必要がありますが、そのサイズはビット列自体の20%から25%程度です(ただのビット列に比べると、1.25倍のメモリ消費)。*3

このように、補助データをほとんど使わずに(漸近的にo(n)の補助データで)高速に問い合わせに応えることができるデータ構造を、簡潔データ構造といいます。簡潔ビットベクトルは、簡潔データ構造の最も基本的なものです。簡潔データ構造をうまく使うと、高速全文検索や、圧縮データを展開せずに検索するアルゴリズムなどの応用があります。具体的な実装方法や応用を詳しく知りたい方は、今年発売された『簡潔データ構造』(定兼邦彦 著)などをご参照ください。

簡潔ビットベクトルによる命令情報テーブルの表引き

簡潔ビットベクトルを用いることで、命令情報テーブルの表引きをO(1)にする方法を説明します。命令情報テーブルを再掲します。

PC 行番号
0000 1
0004 2
0008 3

図:命令情報テーブル

PCのインデックスだけ1になった簡潔ビットベクトルを作ります。この例だと、PCは0000、0004、0008なので、"10001000100.."というビット列を簡潔ビットベクトルで表現します。これが索引になります。

インデックス012345678910...
ビット列  1000100010 0...

図:簡潔ビットベクトルによる命令情報テーブルの索引

あるPCに対応する行番号を得たいときは、rank(PC)を計算します。たとえば0004だったら、rank(4) = 2になります。この値は、命令情報テーブルの上から2番目の行(行番号は2)であることを表しています。PC=0008だったらrank(8) = 3なので、上から3番目の行(行番号は3)になります。簡潔ビットベクトルのrank操作はO(1)なので、命令情報テーブルの表引きが定数時間でできることになります。

簡潔ビットベクトルの分だけメモリ使用が増えることが心配になるかもしれません。しかし、この最適化によって命令情報テーブルからPCの列を消すことができる(二分探索ではPCの情報が必要だった)ので、結果的にはだいたい相殺されます。

実験

N行のメソッドをTracePoint有効下で実行するのにかかる時間を測定した結果が次の図です。

図:線形探索→二分探索→簡潔ビットベクトル

線形探索→二分探索ほどの大きな改善ではありませんが、行数が大きくなったときに二分探索を凌駕していることがわかります。

メモリ使用量に変化がないことも確かめるため、cookpadのWebアプリのソースコードをすべてバイトコードにコンパイルしたときのインタプリタのメモリ使用量を測定しました。実行のたびにブレが発生するので、それぞれ1000回実行したときの頻度を取ってみました。

  • RubyVM::InstructionSequence.compile_fileで測定。
  • /usr/bin/time -f %M kbで測定。

とくにメモリ使用量の傾向が変わっていないことがわかると思います。

まとめ

簡潔ビットベクトルを使って、Rubyの命令情報テーブルの表引き速度を改善しました。これにより、頻繁に命令情報テーブルの表引きを行う条件下での実行(具体的にはTracePoint有効時やコードカバレッジ測定下での実行)が高速化されます。

簡潔データ構造は理論的にも実用的にも非常に興味深い技術で、全文検索やゲノム解析など一部の分野では有名ですが、言語処理系の実装にも活用の可能性があるというのは意外でした*4。みなさんも応用を考えてみるとおもしろいのではないでしょうか。

追記:この話は、クックパッドのフルタイムRubyコミッタである笹田が現状の問題を整理し、遠藤が簡潔ビットベクトルを用いるアイデアを思いつき、笹田が初期の実装を行い、遠藤がそれを改良して実現したものです。フルタイムRubyコミッタが机を並べることでRubyが改良された良い例です。

*1:実は1月にコミットした古いもので、会津大学でのLTイベントで紹介済みだったりもするのですが、記録の意味もこめて記事にしてます。

*2:「ケチな話」と思うかもしれませんが、Rubyではバイトコードのサイズはわりと無視できない関心事のひとつになっています。単なるメモリの無駄遣いというだけでなく、キャッシュを無駄遣いことになるのでキャッシュミスにつながり、速度低下の原因にもなります。

*3:簡潔ビットベクトルは、n番目の1のインデックスを求めるselect(n)という操作も高速に扱えます。しかし、今回の最適化にselect操作は登場しないので、説明を省略します。

*4:関連研究をご存知の方はぜひ教えてください。

cookpad storeTV の広告配信を支えるリアルタイムログ集計基盤

$
0
0

こんにちは。メディアプロダクト開発部の我妻謙樹(id:kenju)です。 サーバーサイドエンジニアとして、広告配信システムの開発・運用を担当しています。

今回は、cookpad storeTV (以下略:storeTV )の広告商品における、リアルタイムログ集計基盤の紹介をします。

storeTV における広告開発

storeTV とは?

storeTV は、スーパーで料理動画を流すサービスで、店頭に独自の Android 端末を設置し、その売り場に適したレシピ動画を再生するサービスです。

より詳しいサービス概要にについては、弊社メンバーの Cookpad TechConf 2018 における以下の発表スライドを御覧ください。

storeTV における広告商品の概要

storeTV では、imp 保証型の広告商品を提供予定です。imp 保証型の広告商品とは、例えば「週に N 回広告を表示することを保証する」商品です。storeTV では、レシピ動画を複数回表示させるたびに広告動画を再生する仕様を想定しています。

f:id:itiskj:20181017154217j:plain

基本的には、実際の再生稼働数から在庫(広告動画の再生できる総回数)をおおまかに予測し、その在庫を元に比率を計算、配信比率を含む配信計画を全国に設置されている端末に配布する流れになります。

具体的な計算ロジックについて説明します。

配信比率は1時間を単位として算出しています。ある 1 時間の「配信比率」は、「 1 時間あたりの割当量 / 1 時間あたりの在庫量」と定義しています。

f:id:itiskj:20181017154304j:plain

  • Ratio ... 「配信比率」
  • HourlyAllocated ... 「1 時間あたりの割当量」
  • HourlyInventory ... 「1 時間あたりの在庫量」

ここで、「1 時間あたりの割当量」とは、1 時間あたりに再生したい回数です。広告商品によって達成したい目標 imp が異なりますので、「目標 imp」と「実績 imp」の差を、「残り単位時間」で割ることで算出できます。

また、「1 時間あたりの在庫量」とは、1時間あたりに再生され得る最大の広告再生回数と定義しています。storeTV の imp 保証型の広告商品の場合、先述したとおり「90 秒に 1 回配信される」という特性から、「1 時間あたりの最大広告再生回数」は 40 回と置くことができます。

したがって、「1 時間あたりの在庫量」は、「現在稼働中の端末数 / 1時間あたりの最大広告再生回数」で算出できます。

f:id:itiskj:20181017154316j:plain

  • Goal ... 「目標 imp」
  • TotalImpression ... 「実績 imp」
  • RemainingHours ... 「残り単位時間」
  • = MaxCommercialMoviePlayCount ... 「1 時間あたりの最大広告再生回数」

最後に、「現在稼働中の端末数」ですが、正確な値をリアルタイムで取ることはほぼ不可能であるため、実際に発生した imp ログから大体の稼働数を予測する必要があります。

具体的には、まだ imp ログの発生が見られていない時(例:広告配信開始直後で、まだどの端末からもログが到達していない状態)は、「配布済みの全端末数 / カテゴリ数1」で算出したものをデフォルト値として利用します。

imp ログが到達し始めたら、「直近 2 時間あたりの imp / 1 時間あたりの最大広告再生回数 / 直近 2 時間あたりの配信比率」で大体の値を算出しています。

f:id:itiskj:20181017154328j:plain

  • TotalDeviceCount ... 「配布済みの全端末数」
  • CategoryCount ... 「カテゴリ数」
  • EstimatedRunningDeviceCount ... 「現在稼働中の端末数」
  • HourlyImpression(currenthour - 2hours) ... 「直近 2 時間あたりの imp」
  • MaxCommercialMovierPlaycount ... 「1 時間あたりの最大広告再生回数」
  • Ratio(currenthour - 2hours) ... 「直近 2 時間あたりの配信比率」

storeTV ならではの課題

これだけでみると、一見簡単そうな要件に見えますが、cookpad 本体で配信している純広告とは異なる課題が多々存在します。

例えば、実際のハードウェアの配布数と実際の稼働数が大きく乖離するという点です。具体的には、「システム起因」(ネットワーク遅延、アップデート不具合)や「オペレーション起因」(バッテリー切れ、動画を流す手順操作をしない)などの様々な理由によって、広告動画が再生できない状態にある可能性があります。そういった広告が再生できない端末数を加味して配信比率を計算しないと、商品としての保証再生数を達成できないリスクが生じてしまいます。

そこで、稼働状態を把握するために、動画再生のログをリアルタイムで集計する必要がありました。リアルタイムで imp ログを参照できることで、より実際の稼働状況に即した配信比率が計算できます。より正確な配信比率に沿って広告を再生できることで、imp がショートしてしまったり、逆に出すぎてしまったりするようなリスクを可能な限り避けることができます。

なお、本件については、以下の資料もご参考ください。

storeTV 広告におけるリアルタイムログ集計基盤

そこで、リアルタイムログ集計基盤を用意する必要がありました。次節より、具体的なアーキテクチャや、実装上の課題、それに対する工夫についてお話しておきます。adtech研究会#1にて発表した以下のスライドがベースとなっています。

当プロジェクトをまさに実装中に発表した資料で、ストリーミング処理における理論や資料についてまとめてあります。そのため、今回紹介する最終形とは異なり、一部古い記述(具体的には、Analysis レイヤーのアーキテクチャ)があります。

Architecture

まずは、全体のアーキテクチャを紹介します。

f:id:itiskj:20181017154341j:plain

主に、以下の 3 レイヤーから構成されています。

  • Aggregation ... imp ログを集計するレイヤー
  • Monitoring ... ストリームの詰まりやログの遅延を検出するためのモニタリングレイヤー
  • Analysis ... リアルタイムログを、分析用に Redshift に入れるための分析レイヤー

先ほど説明したように、元々は配信比率の計算のためのリアルタイムでの imp ログの集約が最優先タスクでした。それに伴い、Aggregation/Monitoring レイヤーを作成しました。

その後、せっかく Kinesis Streams ですべての広告動画再生ログを受け取っているので、ニアリアルタイム(大体 10 ~ 15 分程度)で分析するための Analysis レイヤーを作成しました。弊社では、DWH チームが提供するデータ活用基盤に分析するデータを寄せることで、全社的に品質の高い分析フローを低コストで用意することができます。今回も、S3 Bucket に Kinesis Firehose 経由で投げたら、後は DWH の仕組みに乗って Redshift にデータが入ってきます。

Aggregation

f:id:itiskj:20181017154354j:plain

Core Design

Aggregation レイヤーでは、Android 端末から、以下のような JSON ログが送信されてきます。

{"delivery_id": 123,
  "delivery_plan_commercial_movie_hourly_goal_id": 100,
  "identifier": "foobar1234",
  "sending_time": "2018-07-12T10:22:58+09:00"
}

端末が送信してきたタイムスタンプに従って、1 分間ごとのログに集約させ、DynamoDB に格納します。

name type schema example
delivery_id String Hash Key 123
period_id Int Sort Key 201807121022
impressions Int - 1

この時、DynamodDB - Update Expressions の ADD 演算を利用し、impressionsの項目は、インクリメントさせます。これによって、JSON ログが流れてくるたびに、imp が徐々に増えていくようになります。

次に、このレコードを集約させた DynamoDB の Streams を有効にし、別の Lambda で吸い取ります。その Lambda は、minute 単位で格納されたレコードを、今度は hourly で集約し、別の DynamoDB に格納します。さらにその DynamoDB Streams から、daily 別に格納します。

このように徐々に集約させていくことで、ストリームデータを適切な粒度でカウントさせます。配信比率を計算するクライアントは、適宜必要なテーブルの imp レコードを参照します。

Late Logs & Watermarks

1 つめの Lambda が書き込んでいる DynamoDB が、2 つあることに気づいた方もいらっしゃるかもしれません。

f:id:itiskj:20181017154412j:plain

ここでは、ある一定時間以上遅延したログは、それ以降の集約ロジックに含めず、別の遅延専用テーブルに書き込んでいます。

一般に、ストリーミング処理において「遅延したログをどう扱うか」というのは、古くからある命題の一つです。ログは、様々な理由で遅延します。クライアントがオフライン状態にあったり、message broker が障害で落ちていたり、message broker からデータを吸い取り処理する consumer 側にバグが有ってデータが処理されなかったり、バグはないが想定以上のデータが来ることで consumer が一定時間以内に処理しきれず、かつスケールアウトも間に合わなかったり。

ストリーミング処理における理論や一般的プラクティスについては、『Designing Data-Intensive Applications』の十一章と、Oreilly における二部長編エッセイに詳しいので、興味がある方はそちらをおすすめします。

今回、遅延してきたログの対処法として、具体的には、Watermarks2で処理することにしました。Watermarks とは、「どこまでログを処理したか」という情報を「透かし」としてストリームデータに仕込み、各 consumer がデータを処理する時、遅延したかどうかを判別できるようにするものです。

特に、今回は Apache Spark における Watermarks[^2] 実装 を参考に、オンメモリで計算できる設計にしました。外部データソースに、consumer ごとに「最後に処理したタイムスタンプ」を保存しておく実装も考えられましたが、別にテーブルを用意するコストや、毎回問い合わせが発生することによるコストを考慮し、避けました。

具体的には、冒頭で紹介したスライド p31に擬似コードを載せています。「consumer が処理する一連のログのタイムスタンプの内、中央値に対してプラスマイナス 5 分以上乖離しているかどうか」で判別しています。

Apache Spark の例では最大値を利用していますが、storeTV ではクライアント側のシステム時間を操作できてしまうという特性上、かなり未来の時間が誤って送られてくることも考えられます。最大値を用いる手法だと、そのような外れ値に引っ張られて、本来集約ロジックに含めたい大量の正しいログを捨ててしまうことになります。ですので、中央値を利用しています。

Monitoring

Streaming Delay

次は、モニタリングです。ストリーミング処理においては、先程も説明したとおり、以下のような様々な理由によって詰まる可能性があります。

  • クライアントがオフライン状態
  • message broker が障害で落ちている
  • message broker からデータを吸い取り処理する consumer 側にバグが有ってデータが処理されない
  • バグはないが想定以上のデータが来ることで consumer が一定時間以内に処理しきれず、かつスケールアウトも間に合わない

f:id:itiskj:20181017154430j:plain

そこで、2 通りの方法を使ってモニタリングしています。

  1. Kinesis Streams の GetRecords.IteratorAgeMilliseconds3
  2. DynamoDB に実際に書き込まれた imp ログのタイムスタンプが一定の閾値以内かどうか

まず、Kinesis Streams の IteratorAgeMillisecondsは、最後に処理されたレコードの時間を示します。もし、IteratorAge と現在時刻が乖離すればするほど、この値は大きくなっていきます。この値をモニタリングすることによって、Lambda 側でバグや障害が発生するなどしてレコードが処理されない事象を検知します。

また、DynamoDB に実際に書き込まれた imp ログが、現在時刻から一定の閾値以内かどうかを検知するための Lambda も存在しています。これは、例えば Kinesis に障害が発生するなどして、一定時間ストリーム自体が流れて来ず、復旧後に溜まっていた一連のストリームが流れてくるような場合を検知します。

CloudWatch Alarms の通知先として、SNS Topics を指定します。この Topics を、Slack に通知する責務をもたせた Lambda の event source として指定しておきます。こうすることで、通知ロジックの責務自体は分散させずに一連のモニタリングフローを整備できました。

CloudWatch Dashboard

また、Kinesis Streams や Lambda, DynamoDB など各種コンポーネントの稼働率も別途モニタリングする必要があります。今回は、全て AWS のサービス群で構成しているので、モニタリングツールには CloudWatch Dashboard を利用しました。既存の Metrics から必要な指標を手軽に作ることができるのでおすすめです。

Analysis

最後に、Analysis レイヤーです。Kinesis Streams の consumer として、Kinesis Firehose を指定し、S3 に吐くだけです。指定のフォーマットで S3 に出力した後は、DWH の仕組みに乗って Redshift に書き込まれていきます。

f:id:itiskj:20181017154511j:plain

ポイントは、Kinesis Firehose の Transformer として Lambda を指定している箇所です。Firehose では、Streams から来たログをそのまま S3 に流すのではなく、Lambda で前処理を加えることができます。これは、一般的な ETL4処理における、Transform 機構をシンプルに実現できるということです。

Firehose はバッファリングとしての機構を持つので、例えば「5分間 or ログの総量が 1 MB を越えたら S3 に出力する」といった設定をすることができます。No Configuration が Firehose のウリでもあるので、例えばこのバッファリングの頻度を変更したいときは、コードを変更することなく、AWS Console から設定一つで変更することができます。

Misc

その他、一連の Lambada の実装には Golang を用いました。デプロイツールとしては Serverless Framework を使いました。ここらへんの技術選定の背景は、冒頭でも紹介したスライドの後半に記載してありますので、興味がある方はそちらを御覧ください。

Conclusion

普段は cookpad 本体の広告配信サーバーの開発を担当していますが、動画広告領域も、技術的にチャレンジングな課題が数多く、非常に面白い領域です。ぜひ、興味を持っていただけたら、Twitterからご連絡ください。

また、メディアプロダクト開発部では、一緒に働いてくれるメンバーを募集しています。少しでも興味を持っていただけたら、以下をご覧ください。



  1. 「カテゴリ数」とは、農産・畜産・水産の部門ごとの数で、カテゴリごとに配信する広告動画が違うことからカテゴリ数で割ります

  2. https://cdn.oreillystatic.com/en/assets/1/event/155/Watermarks_%20Time%20and%20progress%20in%20streaming%20dataflow%20and%20beyond%20Presentation.pdf

  3. https://docs.aws.amazon.com/streams/latest/dev/monitoring-with-cloudwatch.html

  4. https://en.wikipedia.org/wiki/Extract,transform,load

R&D ができて 2 年が経ちました

$
0
0

R&D(研究開発部)部長の原島です。普段は部のマネージメントと自然言語処理関連の研究開発に従事しています。

タイトルの通り、クックパッドに R&D ができて 2 年(正確には 2 年 3 ヶ月)が経ちました。2 年の間に様々な取り組みがありました。また、ありがたいことに、それらについて聞かせてほしいと言っていただく機会も増えてきました。

そこで、このエントリでは R&D のこの 2 年間の主な取り組みを紹介したいと思います。

R&D の役割と体制

これまでの取り組みを紹介する前に、クックパッドにおける R&D の役割と体制を簡単に紹介しておきます。クックパッドの R&D の役割は「社内外の最新の研究成果にもとづくサービスの企画と開発」です。「研究成果」は、より具体的には、「食や料理、レシピに関する研究成果」です。これらのシーズとユーザーのニーズを紐付け、他部署と一緒にサービスを開発するのが R&D の役割です。また、シーズからサービスを企画し、他部署に提案するのも役割の一つです。

現在、R&D には 22 人(兼任やアルバイトを除くと 17 人)が所属しています。9 人は機械学習関連の研究開発に、8 人はスマートキッチン関連の研究開発に従事しています。また、それぞれの業務をサポートするメンバーが 5 人いて、マネージメントやアノテーションに従事しています。8 人(兼任やアルバイトを除くと 4 人)だった 2 年前と比べると、大分しっかりした体制になりました。最初の 1 年間は採用活動ばかりしていた気がします。

これまでの取り組み

さて、上記の役割と体制で様々な取り組みに挑戦してきました。これらの取り組みは下記のように大別できます。

  • 機械学習
    • 自然言語処理
    • 画像認識
    • MLOps
    • オープンサイエンス
  • スマートキッチン
    • VUI(Voice User Interface)
    • IoT

以下で、それぞれの取り組みを紹介します。

機械学習

自然言語処理

クックパッドにおいて自然言語処理は必要不可欠です。というのも、レシピが自然言語で記述されているからです。クックパッドには約 300 万品もの日本語のレシピが投稿されており、これらが自然言語処理の対象となります。また、クックパッドは 2014 年からサービスの海外展開を加速させており、この 4 年で 22 言語約 160 万品のレシピが追加されました。自然言語処理はますます重要な技術となっています。

この 2 年間の主な取り組みとしては、Encoder-Decoder による材料の正規化やロジスティック回帰による手順の分類等があります。前者の成果は今年 4 月にリリースした食材購入サービス(Amazon フレッシュ連携)で、後者の成果は 7 月にリリースした Amazon Echo Spot 向けサービス(後述)で利用されています。また、他の取り組みとして、SVM によるユーザーのご意見の分類等もあります。

画像認識

画像認識もクックパッドにおいて必要不可欠です。クックパッドのほとんどのレシピに画像が付与されています。レシピを検索する時も、投稿する時も、画像は大きな影響力を持ちます。また、近年の画像認識の進歩は目覚ましいものがあります。画像認識が実用段階になったことで、クックパッドに限らず、画像認識にもとづく様々なサービスが開発されています。

この 2 年間の主な取り組みとしては、Inception v3 による料理写真の検出や分類、類似写真の検索、SRGAN による料理写真の超解像等があります。特に、料理写真の検出は、2016 年 12 月にリリースした料理きろくというサービスの根幹をなす技術になりました。料理きろくは 26 万人以上に利用されるサービスになり、この 2 年間の最大の成果の一つになりました。

MLOps

機械学習の運用は、この分野における最近のホットトピックの一つです。GPU 環境をどのように用意するか、実験結果をどのように共有するか、モデルをどのようにデプロイするか、バッチをどのように実行するか、データをどのように管理するか。まだベストプラクティスはありません。クックパッドでも、日々、より良い方法を模索しています。

クックパッドの R&D では、GPU 環境を簡便に用意するため、Slack Chat Bot で Amazon EC2 インスタンスを管理できるようにしています。また、実験結果を関係者間で共有するため、Dockercookiecutterで実験環境を構築しています。さらに、クックパッドがこれまでに開発してきたツールや環境もフル活用しています。具体的には、hakoでモデルをデプロイし、Kuroko2でバッチを実行し、DWH でデータを管理しています。

オープンサイエンス

機械学習に関する取り組みとして、最後に、オープンサイエンスに関するものを紹介します。ここ数年、機械学習の研究が注目されているのは周知の通りです。そして、R&D の取り組みの多くはそれらの成果にもとづいています。機械学習の研究に少しでも貢献するため、R&D はオープンサイエンスにも注力しています。

この 2 年間の主な取り組みとしては去年の第 1 回 AI チャレンジコンテストがあります。これは人工知能技術戦略会議や内閣府、文部科学省と共催したものです。今年はデータ解析コンペティションを人工知能学会と共催しました。また、R&D ができる前からの取り組みとして、クックパッドは国立情報学研究所からレシピデータを研究者に公開しています。このデータは今や国内約 150 の研究室に利用されるものになりました。さらに、我々自身が論文を投稿することもあります。この 2 年間で 9 本の査読付き論文(ほとんどはワークショップの論文ですが)がアクセプトされました。

スマートキッチン

VUI

クックパッドでは VUI の可能性に注目しています。調理中にスマートフォンでレシピを閲覧するのは珍しいことではありません。しかし、汚れた手でスマートフォンに触れるのは抵抗があります。このような状況で VUI はその真価を発揮します。スマートスピーカーの半分はキッチンに設置されているという調査もあり、クックパッドとしては無視できない存在です。

R&D は昨年 11 月に Amazon Echo 向けサービスを、今年 3 月に Google Home 向けサービスをリリースしました。これらは VUI でレシピを検索できるものです。また、7 月には Amazon Echo Spot 向けサービスをリリースしました。さらに、12 月には Amazon Echo Show 向けサービスをリリースします。これらは VUI で料理動画を再生できたり、レシピの内容を確認できるものです。ありがたいことに、クックパッドの Alexa スキルは Amazon ランキング大賞 2018 上半期 Alexa スキルで 7 位にランキングされました。

IoT

IoT も無視できない存在です。上述のスマートスピーカーはもちろん、近年では多くの調理器具がインターネットと繋がっています。海外では、この 1 〜 2 年、調理器具メーカーとレシピサービスのアライアンスが加速しています。クックパッドがこの流れを看過するわけにはいきません。

そこで、R&D ではスマートキッチンサービス OiCyを開発しています。OiCy は、インターネット上のレシピと現実世界の調理器具を繋ぐサービスです。機械可読なレシピを定義し、これを調理器具メーカーに提供する予定です。今年 5 月には OiCy のコンセプトを体現した調味料サーバー OiCy Tasteを、8 月にはあるべきスマートキッチンの姿を表現した Smart Kitchen Level を発表しました。既に国内メーカー数社と連携しており、今後は海外メーカーとも連携していく予定です。

おわりに

このエントリではクックパッドの R&D のこの 2 年間の主な取り組みを紹介しました。R&D ができた当初は、「どんな取り組みがありえるだろうか」「ちゃんとユーザーや会社の役に立てるだろうか」という一抹の不安がありました。今はそのような不安はありません。逆に、以下の点でクックパッドと研究開発は相性が良いと感じています。

  • 自社サービスがあり、研究開発の成果を活かすチャンスが沢山ある
  • データが沢山あるだけでなく、それらを簡単に利用できる環境(DWH)がある
  • 部内外に優秀なエンジニアが多く、開発やデプロイで困ることが少ない
  • 研究開発の対象(食や料理、レシピ)が普遍的で、また、幅広い

クックパッドの R&D は今後も様々な取り組みに挑戦していきます。メンバーもまだまだ募集しているので、ご興味がある方は採用ページを是非ご覧ください。ご応募をお待ちしています。


インフラストラクチャー部SREグループが『WEB+DB PRESS 』で連載中!

$
0
0

こんにちは! 広報部のとくなり餃子大好き( id:tokunarigyozadaisuki)です。

昨日 2018年10月24日、『WEB+DB PRESS Vol.107』が発売されましたね!  実は、Vol.105から弊社インフラストラクチャー部SREグループによる連載が始まっており、部長の星(@kani_b)と吉川 ( @rrreeeyyy ) が交代で執筆しています。

Vol.105では吉川が Prometheus を使ったモニタリングについて、Vol.106では星がオペレーションのセルフサービス化について、クックパッドで実際に行われているデータベーススキーマの管理や自由に使える AWS 開発環境、アプリケーションのデプロイ環境などに関することを書いております。

f:id:tokunarigyozadaisuki:20181025170528j:plain

連載第3回目となるVol.107の発売に際し、吉川には今回の内容について、星には次回への意気込みを聞いてみました! 

gihyo.jp

吉川よりコメント

Vol.105では実際にクックパッドでも導入され始めている Prometheus のモニタリングについて、インストール方法や実際の活用方法などについて書きました。 今回の Vol.107 ではこのブログでも既に紹介しているサービスメッシュと、それを支える Envoy というミドルウェアについて書きました。

どちらの記事も、比較的入門的な観点から書いているつもりなので、Prometheus や Envoy を聞いたことはあるけどどうやって使い始めていいか分からない、という方にぜひ手に取ってもらえればな、と思います。 記事の感想や疑問点などの反響はいつでもお待ちしてますので、Twitter などでぜひ呟いてもらえると嬉しいです。よろしくおねがいします。

星よりコメント

Vol.106では生産性向上のための「セルフサービス化」ということで、開発者が「自分でできる」ようにするために行っている SRE の取り組みについてご紹介しました。今は Vol.108 の内容を全力で執筆しているところです。

連載を通して、クックパッド SRE の取り組みを紹介するだけではなく、お読みいただいた方がすぐ実践に移せるような内容となるよう心がけて書くようにしています。巻末かつ8ページほどという小さめの記事ではありますが、お読みいただけると大変嬉しいです。また @rrreeeyyy同様ですが、感想や疑問点などありましたらいつでもお声がけください。反響があることが何より嬉しいので。

f:id:tokunarigyozadaisuki:20181025170538j:plain

次号以降もSREに絡んだ様々なお話をしていくとのことですので、ご興味のある方はお手にとっていただけますと幸いです! 

インタプリタ開発者によるRubyの挙動解析への道

$
0
0

Ruby インタプリタを開発している笹田です。今年のクリスマスにリリース予定の Ruby 2.6、楽しみですね(無事、出るといいな)。

この記事では、私がRubyの挙動を調べるために頑張った記録を書いておきます。 基本的に、単純作業の積み重ねなので、難しい内容はありません。お気楽にお読みいただければ幸いです。

大雑把にまとめると、こんな内容が書いてあります。

  • デバッグカウンタの導入によるRubyの詳細な挙動調査の紹介
    • (私には)簡単な話で、Rubyをいろいろいじって、Rubyの細かい挙動、しかもほとんどの人が気にしない挙動を調べられるようにした話です。
    • 多くの人が興味ないだろう、Rubyに仕込まれている統計情報をとる仕組みを紹介します。
  • クックパッドアプリを手元で調査できるようにした話
    • (私には)難しい話で、Ruby 開発版で弊社アプリを手元で動かすために四苦八苦した記録です。
    • Ruby の機能を用いて、よく知らない Ruby アプリの挙動を調べるための方法を紹介します。

ならべてみると、どう考えても需要がない気がしますが、気にせず進めます。

デバッグカウンタの導入によるRubyの詳細な挙動調査の紹介

Ruby インタプリタ(のソースコード)にこっそり入っているデバッグカウンタを使って、Ruby プログラムの詳細な挙動を追う方法をご紹介します。

詳細といっても、「あるメソッドがどのメソッドを呼んで...」という情報を事細かくとるわけではなく(取ろうと思えば別の方法で取れます)、Ruby インタプリタに内蔵されている機能を、何を何回使っているか、例えばオブジェクトの生成は何回行ったか、等を調べます。これは、(多分)アプリケーションを改善するためではなく、インタプリタを改善するために取得します。人生長いですから、インタプリタを改善したくなる日も来るかもしれませんので、その際にご活用下さい。

よくある調査の仕方

Ruby プログラムのベンチマークをとろうとすると、多くの場合、プログラムの実行時間をとることが多いと思います。また、ほかにもメモリプロファイラなどを用いることが多いと思います。私が知っているプロファイリング手法を書き出してみます。おそらく、もっといろいろあると思います(あんまりちゃんと Ruby プログラミングしたことないので...)。

  • あるプログラムの実行時間を測る
    • time ruby ...としてプログラムの実行時間を測る
    • 標準添付の benchmark ライブラリを用いて、ブロックの実行時間を測る
    • benchmark/ipsbenchmark-driverといったライブラリ使って、コードブロックの実行時間をはかる
    • stackprofなどを用いて、Rubyのどのメソッドが処理に時間がかかっているか調べる
    • gprof や perf などのシステムプロファイルをとる
  • メモリの利用法についてのプロファイルをとる
    • memory_profilerによって、メモリ使用量を調べる
    • allocation_tracerによって、どこでどのようなオブジェクトが生成されているかを調べる
    • gc_tracerによって GC ごとのヒープの状況を調べる(だいたい GC.statGC.latest_gc_infoの情報が取れます)
    • gperftoolsを使って malloc の詳細な統計を取る
    • valgrind massif によって、メモリ確保の詳細を調べる(いくつかの時点のスナップショットを取得できます)

これらは、単にツールを使うだけですので、状況を手軽に知るにはよい手段です。ただ、これらの機能はインタプリタのプロファイリング支援機能を用いているので、さらにそれ以上の情報が欲しい、と思っても難しいです。いくつか手法はあり、例えば valgrind というツールでは、malloc()(C言語によるメモリ確保関数)を置き換えることで、メモリ確保の詳細な記録をとっています(valgrind では、もっとすごいことをいろいろしていますが、その一例です)。Ruby の TracePointを使うと、Ruby がサポートしているフックを用いて、例えば「何回メソッド呼び出しが起こったか?」という数は計測可能です。が、導入にはオーバヘッドがとても大きいです。

ただ、例えば「何回、どのようなメソッドやブロックを呼び出したか?」、さらにインタプリタに内蔵されている最適化の仕組み、たとえば「メソッドキャッシュのヒット率は?」といった情報は、既存のツールや言語機能で取ろうとすると、そこそこ難しいです。インタプリタのバイナリを変更するバイナリパッチをあてる、というのが一つの解でしょうか。ただ、それを実現するのは面倒そうです。

デバッグカウンタの準備

そこで、Rubyのソースコードにはデバッグカウンタという仕組みが内蔵されています(入れたの私なんですけど)。 カウンタの宣言と、イベントが発生したときにインクリメントをする処理を書けば終わりです。

// debug_counter.h
// カウンタの宣言部
...
RB_DEBUG_COUNTER(mc_inline_hit)
RB_DEBUG_COUNTER(mc_inline_miss)
...
// カウンタの宣言。同じようにテキトーに追加すればカウンタが増える。
// vm_insnhelper.c
// カウンタのインクリメントの例
...
    /* cache hit! */
    VM_ASSERT(cc->call != NULL);
    // mc_inline_hit は、メソッドキャッシュがヒットした回数をカウント
    // このパスを通っているということは、キャッシュがヒットした、ということなのでカウントアップ
    RB_DEBUG_COUNTER_INC(mc_inline_hit);
    return;

最後に、debug_counter.h にある USE_DEBUG_COUNTERを 1 にして Ruby インタプリタをビルドすれば完成です。

おや、Ruby のビルド方法や miniruby の存在をご存じないですか? Ruby Hack Challengeの資料をごらん下さい。また、次回の Ruby Hack Challenge イベントへご参加もオススメします。クックパッド株式会社 - connpassをチェックしておいて下さい。

なお、なぜプロファイルカウンタではなく、デバッグカウンタという名前にしているかというと、プロファイルカウンタにしておくと、通常のプロダクション環境でも値が取りたい、などと言われそうだからです。デバッグ用なんで、ビルド時にしか使えません、と言い張るためです。実際、カウントアップするために、いくつか冗長な処理が入っており、少し遅くなってしまいます。

実は、似たような処理は、特定の高速化のための仕組みを入れるごとに、効果を測定するために、毎回カウンタ用変数の用意、カウントアップ、カウンタの表示、のようなことを書いていました(そして、コミット時には余計なコードとして消す)。さすがに毎回こんなことやっているのは、いい加減鬱陶しくなってきたので、一つの仕組みに統一しようと思い、デバッグカウンタとして導入したのでした。

なお、DTrace や SystemTap といった、性能評価フレームワークの上で動作させる、というのも当然考えられますが(実際、それが格好良く、順当だと思いますが)、私が使い方がよくわからなかったのと、性能測定しないとき(つまり、ほとんどの場合)に、オーバヘッドを残さないようにする自信がなかったため、現在のような素朴な実装になっています。

デバッグカウンタの利用

では、ちょっと miniruby を使って次のサンプルプログラムを実行してみましょう。

# sample code10_000.times{
  s = [Object.new, "foo", {bar: 1}, :baz]
}

実行終了後、とても沢山のカウンタの情報が出てきます。手元の環境では、下記のような出力になりました。 長いですが、どーんと載せます。どーんと読み飛ばして下さい。

[RUBY_DEBUG_COUNTER]    71695 normal exit.
[RUBY_DEBUG_COUNTER]    mc_inline_hit                           19,998
[RUBY_DEBUG_COUNTER]    mc_inline_miss                               3
[RUBY_DEBUG_COUNTER]    mc_global_hit                           10,014
[RUBY_DEBUG_COUNTER]    mc_global_miss                             277
[RUBY_DEBUG_COUNTER]    mc_global_state_miss                         3
[RUBY_DEBUG_COUNTER]    mc_class_serial_miss                         0
[RUBY_DEBUG_COUNTER]    mc_cme_complement                            0
[RUBY_DEBUG_COUNTER]    mc_cme_complement_hit                        0
[RUBY_DEBUG_COUNTER]    mc_search_super                          1,394
[RUBY_DEBUG_COUNTER]    frame_push                              40,244
[RUBY_DEBUG_COUNTER]    frame_push_method                            0
[RUBY_DEBUG_COUNTER]    frame_push_block                        10,000
[RUBY_DEBUG_COUNTER]    frame_push_class                             0
[RUBY_DEBUG_COUNTER]    frame_push_top                               0
[RUBY_DEBUG_COUNTER]    frame_push_cfunc                        30,242
[RUBY_DEBUG_COUNTER]    frame_push_ifunc                             0
[RUBY_DEBUG_COUNTER]    frame_push_eval                              1
[RUBY_DEBUG_COUNTER]    frame_push_rescue                            0
[RUBY_DEBUG_COUNTER]    frame_push_dummy                             1
[RUBY_DEBUG_COUNTER]    frame_R2R                                    1
[RUBY_DEBUG_COUNTER]    frame_R2C                               20,026
[RUBY_DEBUG_COUNTER]    frame_C2C                               10,216
[RUBY_DEBUG_COUNTER]    frame_C2R                               10,000
[RUBY_DEBUG_COUNTER]    ivar_get_ic_hit                              0
[RUBY_DEBUG_COUNTER]    ivar_get_ic_miss                             0
[RUBY_DEBUG_COUNTER]    ivar_get_ic_miss_serial                      0
[RUBY_DEBUG_COUNTER]    ivar_get_ic_miss_unset                       0
[RUBY_DEBUG_COUNTER]    ivar_get_ic_miss_noobject                    0
[RUBY_DEBUG_COUNTER]    ivar_set_ic_hit                              0
[RUBY_DEBUG_COUNTER]    ivar_set_ic_miss                             0
[RUBY_DEBUG_COUNTER]    ivar_set_ic_miss_serial                      0
[RUBY_DEBUG_COUNTER]    ivar_set_ic_miss_unset                       0
[RUBY_DEBUG_COUNTER]    ivar_set_ic_miss_oorange                     0
[RUBY_DEBUG_COUNTER]    ivar_set_ic_miss_noobject                    0
[RUBY_DEBUG_COUNTER]    ivar_get_base                              447
[RUBY_DEBUG_COUNTER]    ivar_set_base                              479
[RUBY_DEBUG_COUNTER]    lvar_get                                     0
[RUBY_DEBUG_COUNTER]    lvar_get_dynamic                             0
[RUBY_DEBUG_COUNTER]    lvar_set                                10,000
[RUBY_DEBUG_COUNTER]    lvar_set_dynamic                             0
[RUBY_DEBUG_COUNTER]    lvar_set_slowpath                            0
[RUBY_DEBUG_COUNTER]    obj_newobj                              46,638
[RUBY_DEBUG_COUNTER]    obj_newobj_slowpath                        212
[RUBY_DEBUG_COUNTER]    obj_newobj_wb_unprotected                   25
[RUBY_DEBUG_COUNTER]    obj_free                                38,775
[RUBY_DEBUG_COUNTER]    obj_promote                              4,872
[RUBY_DEBUG_COUNTER]    obj_wb_unprotect                            35
[RUBY_DEBUG_COUNTER]    obj_obj_ptr                                  0
[RUBY_DEBUG_COUNTER]    obj_obj_embed                            9,266
[RUBY_DEBUG_COUNTER]    obj_str_ptr                                  4
[RUBY_DEBUG_COUNTER]    obj_str_embed                           10,897
[RUBY_DEBUG_COUNTER]    obj_str_shared                              39
[RUBY_DEBUG_COUNTER]    obj_str_nofree                               0
[RUBY_DEBUG_COUNTER]    obj_str_fstr                                 0
[RUBY_DEBUG_COUNTER]    obj_ary_ptr                              9,270
[RUBY_DEBUG_COUNTER]    obj_ary_embed                               17
[RUBY_DEBUG_COUNTER]    obj_hash_empty                               0
[RUBY_DEBUG_COUNTER]    obj_hash_under4                          9,269
[RUBY_DEBUG_COUNTER]    obj_hash_ge4                                 1
[RUBY_DEBUG_COUNTER]    obj_hash_ge8                                 0
[RUBY_DEBUG_COUNTER]    obj_struct_ptr                               0
[RUBY_DEBUG_COUNTER]    obj_struct_embed                             0
[RUBY_DEBUG_COUNTER]    obj_regexp_ptr                               0
[RUBY_DEBUG_COUNTER]    obj_data_empty                               0
[RUBY_DEBUG_COUNTER]    obj_data_xfree                               0
[RUBY_DEBUG_COUNTER]    obj_data_imm_free                            1
[RUBY_DEBUG_COUNTER]    obj_data_zombie                              0
[RUBY_DEBUG_COUNTER]    obj_match_ptr                                0
[RUBY_DEBUG_COUNTER]    obj_file_ptr                                 0
[RUBY_DEBUG_COUNTER]    obj_bignum_ptr                               0
[RUBY_DEBUG_COUNTER]    obj_symbol                                   0
[RUBY_DEBUG_COUNTER]    obj_imemo_ment                               1
[RUBY_DEBUG_COUNTER]    obj_imemo_iseq                               0
[RUBY_DEBUG_COUNTER]    obj_imemo_env                                0
[RUBY_DEBUG_COUNTER]    obj_imemo_tmpbuf                             2
[RUBY_DEBUG_COUNTER]    obj_imemo_ast                                1
[RUBY_DEBUG_COUNTER]    obj_imemo_cref                               0
[RUBY_DEBUG_COUNTER]    obj_imemo_svar                               0
[RUBY_DEBUG_COUNTER]    obj_imemo_throw_data                         0
[RUBY_DEBUG_COUNTER]    obj_imemo_ifunc                              4
[RUBY_DEBUG_COUNTER]    obj_imemo_memo                               0
[RUBY_DEBUG_COUNTER]    obj_imemo_parser_strterm                     1
[RUBY_DEBUG_COUNTER]    obj_iclass_ptr                               0
[RUBY_DEBUG_COUNTER]    obj_class_ptr                                0
[RUBY_DEBUG_COUNTER]    obj_module_ptr                               0
[RUBY_DEBUG_COUNTER]    heap_xmalloc                            32,722
[RUBY_DEBUG_COUNTER]    heap_xrealloc                               20
[RUBY_DEBUG_COUNTER]    heap_xfree                              28,272

沢山出力しちゃうので、使うときは grep などを使ってフィルタリングして下さい。

さて、暗号みたいな名前がイッパイ出てきています。「やっぱり Ruby インタプリタって難しいんだな」と思うかもしれません。安心して下さい。全部わかるのは私しか居ません、多分。そりゃ、私が都合の良いように作ったので、わかるわけがないのです(私も昔、別の言語や OS のよくわからない用語を見て、わからなくて悲しい思いをしたものですが、大抵は作ってる人がその場のノリで決めてるだけから、わからなくて当然だ、と思うと、少し楽になりました。ノリを掴むまでが大変ですね)。ですので、これを機会にちょっと解説してみます。

PID

71695 normal exit

ここは、PID を示しています。プログラムの実行終了時にこの表示を行うのですが、最近の Ruby システムは bundler とかツールを色々使って実行されるため、複数のプロセスから出力されるため、よくわからなくなっちゃうんですよね。そのため、PID を出力するようにしています。

メソッドキャッシュ

[RUBY_DEBUG_COUNTER]    mc_inline_hit                           19,998
[RUBY_DEBUG_COUNTER]    mc_inline_miss                               3
[RUBY_DEBUG_COUNTER]    mc_global_hit                           10,014
[RUBY_DEBUG_COUNTER]    mc_global_miss                             277

この辺は、メソッドキャッシュの利用状況です。メソッドキャッシュというのは、メソッド探索結果をキャッシュしておくことで、メソッド呼び出しを速く実行するという、古典的な手法ですが、それがどれくらい効いているか、を示しています。インラインとかグローバルとかは、まぁいくつかキャッシュの種類があると思って下さい(詳細は「YARV Maniacs 【第 6 回】 YARV 命令セット (3) メソッドディスパッチ」をご覧下さい)。

mc_inline_hit、つまりインラインメソッドキャッシュのヒットした回数を見てみると、19,998 回、mc_inline_miss は 3 回なので、インラインメソッドキャッシュを使っているところでは、驚異の 99.985% のキャッシュヒット率。 キャッシュがあって良かったね、ということになります。

ちなみに、キャッシュミスした3回は、一体どこにあるでしょうか。

まず、インラインメソッドキャッシュが失敗する理由は次の2つです。

  1. まだキャッシュされていなかった(一度もメソッドを呼んでいなかった)
  2. 以前キャッシュしたものにヒットしなかった(例えば、キャッシュしたときと違うクラスのオブジェクトをレシーバとしたメソッド呼び出しとなった)

このサンプルコードは単純なので、2 が起こるようなことはありません。つまり、1 の初期参照ミスだけ探せば良いことになります。

# sample code10_000.times{
  s = [Object.new, "foo", {bar: 1}, :baz]
}

さて、このサンプルコード中にあるメソッド呼び出しは、次の2つに見えます。

  • 10_000.times
  • Object.new

もう一つはどこにあるでしょうか? 実は、書きながら私もわからなかったので調べてみたのですが、{bar: 1}にありました。なんで Hash リテラルなのにメソッド呼び出しを行っているのでしょうか。実は、Hash リテラルの中身がすべて Symbol や Integer などの特別なリテラルの場合、コンパイル時に配列を作っておき、その配列から hash オブジェクトを生成する、ということをしています。配列から Hash オブジェクトを生成するために、秘密のメソッド(core_hash_from_ary)を呼んでいる、というわけです。

ちょっと考えた結果、少し弄って、メソッドの代わりに新しい命令を作って、それを使うように変えました(r65343。もうメソッドは使わない)。typical な例では 30% 高速化しています。そもそもなぜメソッドにしていたかというと、命令数が多くなると、いくつかの理由で VM の挙動が遅くなるからなのですが、まぁ、これはいいかなぁ、という気分でやり直しました。数要素のハッシュリテラル(要素はすべてシンボルや数値などである場合)のとき、ちょっと速くなります。

フレーム

[RUBY_DEBUG_COUNTER]    frame_push                              40,244

ちょっと飛んで frame です。frame というのは、メソッドやブロックを実行するときに必要になるものです。

ちょっと数えてみましょう。この例だと1万回のブロックの起動+Object.newメソッドの起動、core_hash_from_aryメソッドの起動がそれぞれ1万回行われるので、これで合計 3 万回。あれ、約1万回足りませんね? 実は、まだ隠れているのが initializeです。どこにも現れていませんが、Object.newを実行すると、デフォルトの initializeメソッドが呼ばれるわけです。こいつは C で実装されています。

その後に出てくる下記の情報ですが、R2R が Ruby で実装された何かのフレーム(以降 Ruby フレーム)から Ruby フレームを作った、R2C が Ruby フレームから C で実装されたなにかのフレーム(以降 C フレーム)を作った、以下同じ、という感じです。

[RUBY_DEBUG_COUNTER]    frame_R2R                                    1
[RUBY_DEBUG_COUNTER]    frame_R2C                               20,026
[RUBY_DEBUG_COUNTER]    frame_C2C                               10,216
[RUBY_DEBUG_COUNTER]    frame_C2R                               10,000

R2C 2万回、というのが、Object.newの呼び出し(ブロックから C で実装した .newを呼んでいる)と、同じく core_hash_from_aryの呼び出しです。C2C 1万回が、Object.new ->initalizeの呼び出し。ちなみに、C2R の1万回は、C で実装された timesが Ruby で書かれたブロックを呼び出している回数ですね。ぴったり1万回。

オブジェクトの生成

次は、obj_という prefix のついたカウンタです。ご想像の通り、オブジェクトの数に関係するカウンタです。一部抜粋します。

[RUBY_DEBUG_COUNTER]    obj_newobj                              46,638
[RUBY_DEBUG_COUNTER]    obj_free                                38,775
[RUBY_DEBUG_COUNTER]    obj_promote                              4,872
[RUBY_DEBUG_COUNTER]    obj_obj_ptr                                  0
[RUBY_DEBUG_COUNTER]    obj_obj_embed                            9,266
[RUBY_DEBUG_COUNTER]    obj_str_ptr                                  4
[RUBY_DEBUG_COUNTER]    obj_str_embed                           10,897
[RUBY_DEBUG_COUNTER]    obj_ary_ptr                              9,270
[RUBY_DEBUG_COUNTER]    obj_ary_embed                               17
[RUBY_DEBUG_COUNTER]    obj_hash_empty                               0
[RUBY_DEBUG_COUNTER]    obj_hash_under4                          9,269
[RUBY_DEBUG_COUNTER]    obj_hash_ge4                                 1
[RUBY_DEBUG_COUNTER]    obj_hash_ge8                                 0

newobj は、新しく生成したオブジェクトの数、free は解放した数です。まぁ、わかりやすいですよね。[Object.new, "foo", {bar: 1}, :baz]のコードでは、(1) Object.new (2) "foo" (3) {bar: 1}、それから (4) これらをまとめる配列、の計 4 個のオブジェクトを生成し、それを1万回繰り返すので4万個。obj_newobj の値は 46,638 なので、まぁだいたいあってますね。余分な6000個くらいは、Ruby インタプリタ起動時に生成される内部のオブジェクトです。free は、4万個弱ですが、これは終了時に残っていたものは、Ruby は必要がなければ解放せず、そのままプロセスを終了してしまうので、こうなっています。

obj_obj_...は、Object.newや、ユーザ定義クラスのオブジェクトが「解放」された数になります。そのため、1万個あるはずが、obj_obj_embedの値が1万個切っているのは、プロセスの最後まで残っていたオブジェクトがいるからですね。

さて、suffix に _ptr_embedがついています。Ruby のデータ構造の話になるのですが、例えば Object の場合、インスタンス変数の数が4以上あると、さらに外部にメモリ領域を確保(malloc)し、そうでなければしない、という構造になっており、それぞれ _ptr_embedと数えています。オブジェクト生成自は必ず _embedなので、free するときに数を数えているわけです。

straryも同じなのでわかりますよね。文字列と配列です。

hashの場合は、ちょっと違って、emptyunder4ge4ge8とあります。empty と under4 は文字通り空、4要素未満のハッシュオブジェクトの数(解放時)、geは greater than or equal to の略で、ge4ge8はそれぞれ4以上、8以上のハッシュオブジェクトの数を示しています。なぜ、こんなに細かく見ているかというと、私はハッシュの要素数って、もっと沢山あるものだと思ってたんですが、後で見ますが、少ない要素数のハッシュオブジェクトが支配的だったんですよね。その辺を確認するために入れました。

その他

面倒になってきたので、その他の詳細は https://github.com/ruby/ruby/blob/trunk/debug_counter.hをご参照下さい。頑張ってコメント書きました。

実際のアプリケーションでのデバッグカウンタ

いくつかの実例を見てみます。以前、私が ruby-core メーリングリストに投稿した [ruby-core:89203] FYI: debug countersをベースにご紹介します。デバッグカウンタのデータ詳細は、長いので引用しません。元のメールをご参照下さい。

optcarrot

optcarrot はベンチマークのために作られた、数値演算が支配的なベンチマークです。インスタンス変数アクセスが多いことも特徴的です。

[ruby-core:89203] FYI: debug countersからデータを引用します。

[RUBY_DEBUG_COUNTER]    mc_inline_hit                       89,376,363
[RUBY_DEBUG_COUNTER]    mc_inline_miss                         103,503

これを見る限り、インラインメソッドキャッシュのヒット率は 99% を超えています。ほぼキャッシュにあたっていると思って良いですね。しかし、約 100M 回、つまり 1 億回もメソッドを呼び出してるんですね。

[RUBY_DEBUG_COUNTER]    obj_newobj                             390,384
[RUBY_DEBUG_COUNTER]    obj_free                                14,408
[RUBY_DEBUG_COUNTER]    obj_promote                            270,020

newobj の数は390K個と、たいしたことがありません。特筆すべきは、(上で解説していなかった)promote の数ですね。これは、世代別 GC において、古い世代であり、「おそらく解放されないだろう」とインタプリタが思っているオブジェクトの数であり、普通のアプリでは、この数は(生成されたオブジェクトの数に比べて)ずっと少ないことが期待されます。optcarrot は、「オブジェクトを生成したら(速度的に)負けである」という設計なので、最初にオブジェクトを作っているため、このようになるのではないかと思います。GC の回数が多くなると、現存する promote されたオブジェクトの数が重要になるのですが、optcarrot の場合、GC の発生回数自体が少ないため、問題にならないです。そういえば、GC の回数をこれに入れておくのを忘れていたなあ(→入れました r65359)。

rdoc

Ruby をインストールするときには、Ruby に入っているソースコードを、rdoc コマンドを通してすべて読み込み、ri ドキュメントを生成しています。その挙動を対象アプリにした結果を見てみます。

[ruby-core:89203] FYI: debug countersからデータを引用します。

[RUBY_DEBUG_COUNTER]    mc_inline_hit                     77,428,972
[RUBY_DEBUG_COUNTER]    mc_inline_miss                       398,444

これも、インラインメソッドキャッシュヒット率高いですね。

[RUBY_DEBUG_COUNTER]    obj_newobj                       162,910,687
[RUBY_DEBUG_COUNTER]    obj_promote                        7,289,299

オブジェクトは、95% promote していない、つまり若いまま死んでいくので、しっかり世代別 GC が効いています。しかし、やはり沢山オブジェクト使いますね。

[RUBY_DEBUG_COUNTER]    obj_obj_ptr                        8,505,888
[RUBY_DEBUG_COUNTER]    obj_obj_embed                      4,395,142
[RUBY_DEBUG_COUNTER]    obj_str_ptr                        6,345,111
[RUBY_DEBUG_COUNTER]    obj_str_embed                     63,065,862
[RUBY_DEBUG_COUNTER]    obj_str_shared                     7,865,747
[RUBY_DEBUG_COUNTER]    obj_ary_ptr                        5,016,497
[RUBY_DEBUG_COUNTER]    obj_ary_embed                     35,748,852
[RUBY_DEBUG_COUNTER]    obj_hash_empty                     3,632,018
[RUBY_DEBUG_COUNTER]    obj_hash_under4                    4,204,927
[RUBY_DEBUG_COUNTER]    obj_hash_ge4                       2,453,149
[RUBY_DEBUG_COUNTER]    obj_hash_ge8                         841,866
[RUBY_DEBUG_COUNTER]    obj_struct_ptr                            99
[RUBY_DEBUG_COUNTER]    obj_struct_embed                   1,640,606

オブジェクトの種類別に見ると、文字列、配列が多いことがわかります。その次は Object かな。Struct も 1.6M 個作っていますね。embed が多いので、あまり大きくないオブジェクトが多いみたいですね。

discourse

discourseは Rails アプリケーションの一つで、ベンチマークプログラムを整備している(bench.rb)ので、そのベンチマークを用いた結果です。いくつかのページ(6ページ)に対して、ab を用いて 500 回アクセスするので、計 3,000 アクセスをエミュレーションしています。

同じく [ruby-core:89203] FYI: debug countersからデータを引用します。

[RUBY_DEBUG_COUNTER]    frame_push                     1,011,877,345
[RUBY_DEBUG_COUNTER]    frame_push_method                456,075,698
[RUBY_DEBUG_COUNTER]    frame_push_block                  79,600,045
[RUBY_DEBUG_COUNTER]    frame_push_class                       8,230
[RUBY_DEBUG_COUNTER]    frame_push_top                         3,360
[RUBY_DEBUG_COUNTER]    frame_push_cfunc                 460,186,970
[RUBY_DEBUG_COUNTER]    frame_push_ifunc                  15,987,591
[RUBY_DEBUG_COUNTER]    frame_push_eval                        8,956
[RUBY_DEBUG_COUNTER]    frame_push_rescue                      6,452
[RUBY_DEBUG_COUNTER]    frame_push_dummy                          43

とりあえず、frame_pushのカウントが 1G、つまり 10 億回です。HDD のサイズだと、たいしたことの無い数ですが、何かを 10 億回繰り返すというのは凄いですね。数分で終わるベンチマーク。まぁ、1Ghz のプロセッサなら、1秒あれば、1G回の計算はできるのですが。

1G 回として、ちょっと frame_push にかかる時間を見積もってみます。1Ghz の CPU を前提とします。

  • 10 cycle、つまり 10ns かかるとすると、1G 回で 10 秒。
  • 100 cycle、つまり 100ns かかるとすると、1G 回で 100 秒。

結構な時間です。実際は、空のメソッド呼び出しおよびメソッドからのリターンで 50 cycle 程度なので、もうちょっと速くしておきたいですね。フレームを積む作業を軽量化する、フレームを積む回数を減らす、の2通りの高速化手法が考えられます。

後続する frame_push_xxxというのは、積むフレームの種類なのですが、methodcfuncが多いですね。それぞれ、Ruby で書かれたメソッド、C で書かれたメソッドを呼んだ、ということです。若干、cfuncのほうが多いのは、実は結構驚きの結果でした。というのも、Ruby で記述されたメソッドをいかに速くするか、という高速化に重点を置いていたんですが、C で書かれたメソッドの呼び出しはあまり取り組んでいなかったためです。cfuncがこれだけ多いなら、このようなメソッドの高速化もちゃんと取り組まないといけないですね。ちゃんと計測しましょう、という話でした。

[RUBY_DEBUG_COUNTER]    obj_newobj                       162,910,687
[RUBY_DEBUG_COUNTER]    obj_free                         161,117,628
[RUBY_DEBUG_COUNTER]    obj_promote                        7,289,299
[RUBY_DEBUG_COUNTER]    obj_wb_unprotect                      83,599
[RUBY_DEBUG_COUNTER]    obj_obj_ptr                        8,505,888
[RUBY_DEBUG_COUNTER]    obj_obj_embed                      4,395,142
[RUBY_DEBUG_COUNTER]    obj_str_ptr                        6,345,111
[RUBY_DEBUG_COUNTER]    obj_str_embed                     63,065,862
[RUBY_DEBUG_COUNTER]    obj_ary_ptr                        5,016,497
[RUBY_DEBUG_COUNTER]    obj_ary_embed                     35,748,852
[RUBY_DEBUG_COUNTER]    obj_hash_empty                     3,632,018
[RUBY_DEBUG_COUNTER]    obj_hash_under4                    4,204,927
[RUBY_DEBUG_COUNTER]    obj_hash_ge4                       2,453,149
[RUBY_DEBUG_COUNTER]    obj_hash_ge8                         841,866

オブジェクトの数は、162M 個と、やはり多いですね。ハッシュオブジェクトの要素爽雨を見ると、ほぼ 8 要素未満で、8 要素以上は 7.5% しかありません。小さな Hash 値向けの高速化は導入の価値がありそうです(ハッシュオブジェクト自体、162M 個中の 11M 個なので、どの程度効くかは微妙ですが)。

クックパッドアプリを手元で調査できるようにした話

さて、では弊社のクックパッドアプリはどうなのよ、と気になるところです。ただ、そもそも手元でベンチマークを行う環境を作成するのがいくつかの理由で困難でした。

一番大きな問題は、私が Ruby on Rails アプリの読み解き方を知らない、というものなのですが、まぁそれは置いといて、手元で動かすためには、開発用に用意している環境(DB やサービス)を使うのですが、それらはベンチマークで負荷をかけるようなことは考慮していないためです。

なお、クックパッドの開発環境についての詳細はこちらをご覧下さい:開発環境のデータをできるだけ本番に近づける

mysql2_recorder

一番単純な解決策は、開発用DB(等)をベンチマーク実行環境にコピーすることですが、そこそこデータ量も大きく、必要なデータを選別する、というのも面倒です。そこで、一度アクセスして結果を取得し、どこかに結果をキャッシュすることにしました。ただ、私は Rails のソースコードがよくわからないので、どこにどうやってキャッシュしておけばいいかよくわかりません。一番良さそうなのは、ActiveRecord (AR) のキャッシュ機構をうまいこと利用することでしょうか。でも、AR って大きそうでよくわからない。

一度、Rails アプリから DB(弊社の場合、MySQL)のつながりがどうなっているか考えてみました。

(ruby の世界) rails アプリ  -> ActiveRecord (sql を組み立ててリクエスト) -> Mysql2
->
(C の世界) Mysql2 の C extension -> libmysqlclient-dev
->
(network の外側) MySQL server

こんな感じでリクエストが飛んで、結果を取ってきます。

そこで、Rails 層がわからないのなら、AR がアクセスしている Mysql2 を見れば、なんとなくわかるんじゃないかと思いました。そこそこ C で書いてあるし、小さいし。多分、データのやりとりしている部分って少ないんじゃ無いかとふんだんですよね(低レイヤ脳)。

Mysql2 の機能を見ると、query メソッドが色々やってくれそうですが、そこから先は、libmysqlclient-dev のほうに処理を丸投げしているようで手が出せない(さすがにこれ以降は手をだすのが面倒そうだ)、というわけで、Mysql2 あたりで結果を貯めておき、既知のクエリには貯めておいた結果を返すようにすることにしました。

Rails がどのように Mysql2 を使っているかわからないので、まずは小さな Rails アプリを作り、挙動を観察してみることにしました。 この場合、Mysql2 側のメソッドがどんなふうに呼ばれているか知りたいため、TracePointを利用します。

# この行をどこかに挟んでおく。私は mysql2.gem の mysql2.rb に追加してみました。TracePoint.new(:call, :c_call){|tp| 
  if/mysql2/ =~ tp.path # 見たいところだけ情報を出す
    p tp # 呼び出し元の情報を知るために、caller を出してもいいですね。end
}.enable

この1行を、どこか適当なところに挟んでおくと、ファイル名が mysql2を含む行のメソッドが呼ばれたりすると、情報が表示されます。tp.defined_classなどを使って、クラスで絞ったりするのもいいかもしれませんね。そんな感じで、なるほどこうやって AR から呼ばれるのか、ということを調べていくと、どうやら Mysql2::Client#query(sql)Mysql2::Resultを返し、しかも実際のデータの取得は Mysql2::Result#to_aの結果しか使っていないことがわかりました。シメシメ。

つまり、Mysql2::Client#query(sql)をフックし、Mysql2::Result#to_aの結果をすぐに取り出してしまい、その値をキャッシュしてやれば、以降は to_aの結果を返す FakeResultでも返してあげれば良さそうです。というわけで、作った結果がこちら: mysql2_recorder/mysql2_recorder.rb

ベンチマークをとりたい、という必要性から、あまりワークロードを変えたくないのですが、まぁ少しくらいの変更ならいいだろう、と思ってよしとしました。

これで、開発用DBから一度取得したデータは、二度とアクセスしないでよくなりました。

あ、select文しか対応しないようにしています。ベンチマークが目的なので。それ以外は通しますが、継続的に発行している select 以外のクエリはないように、ベンチマークのシナリオを構成しています。こういうログをじっと見るのも楽しいですね。

クエリは変るよ

「これで、開発用DBから一度取得したデータは、二度とアクセスしないでよくなりました」、と思って走らせてみると、同じページへの2度目のリクエストでも、SQL クエリは変るんですね。

いくつかの典型的なパターンと、その今回の対処法についてご紹介します。

  • 現在時刻を含めたクエリを発行している → キャッシュするとき、日付っぽいデータはマスクしました L110
  • AR のクエリはクエリ発行元の情報が入るので、同じクエリでも同一クエリにならない → キャッシュするとき、コメントっぽいデータはマスクしました L111
  • アプリケーションコード中に Array#shuffleとか使っている → srand(0)することで乱数を潰しました L5

他にも、どうしてもわからないところはコードを削ったりしています。 いや本当に役に立たない情報ですね...。

そういえば、エラーが出ても、バックトレース情報が潰されてしまい、どこで何が起こっていたかわからないことがありました。今回は、具体的には ActiveRecord::StatementInvalidが、その前に出た例外を上書きしてしまう、という問題でした。こういう時にはバックトレースを知りたいだけなので、慌てず騒がず、

TracePoint.new(:raise){|tp| pp [tp.raised_exception, caller]}.enable

という一行を、どこかアプリケーションの起動処理の中に挟み込んでおくことで、例外が本当に起こった場所を探しました。例外が発生するたびに、情報を表示してくれます。実は、Rails アプリでは結構な頻度で例外が飛び交っているため、例外クラスを見てフィルタリングするなどをしてもいいでしょう。

TracePointの解説みたいな記事になってしまった。

もう安心?

弊社のサービスは、他のサービスと連携しているので、その連携先にも迷惑をかけないようにしなければなりません。これについてはいくつか案を考えたんですが、最終的に VCR で十分、という結論になりました。めでたしめでたし。

が、評価で使いたかった Ruby 2.6 (開発版)ではうまく動かすことができなかったので、結局これはまだ調整中です...。

肝心のデバッグカウンタは?

では、その上で、とりあえずデバッグカウンタの中身を見たらどうなるでしょうか。

ベンチマークアプリのシナリオは次の通りです。

  • ある日、ある時間のアクセスログから、この環境で動作させることができるページを 1,000 個取ってくる
  • その1セットへのアクセスを行う、というのを 5 回繰り返す

ということにしました。都合5,000ページのアクセスです。デバッグカウンタの値はどんなもんでしょうか。

mc_inline_hit                    6,008,617,014
mc_inline_miss                     482,996,246
frame_push                       7,670,138,991
obj_newobj                       3,564,050,452

さすがに凄い数ですね。7.6G回のフレームを積んで、下ろす作業を想像しただけでも気が遠くなります。 オブジェクトも 3G 個も作っているんですね。最低 40 Byte 必要なので、少なくとも 120 GB のメモリ使用(のべ)が起こったということになります。他にも mallocによってメモリを使っているので、もっともっとメモリを使っています。これらは自動的に GC によってメモリが解放されています。

こういうのを参考にしながら、次はどういうことをしようかなぁ、と考えます。

おわりに

本稿では、誰も幸せにならなそうな2つのトピックをご紹介しました。

  • デバッグカウンタの導入によるRubyの詳細な挙動調査の紹介
    • 内部の細かい挙動を追うためのカウンタの紹介。
    • 見てきたとおり、Ruby の再ビルドが必要です。カウンタの追加は簡単そうでしょう?
  • クックパッドアプリを手元で調査できるようにした話
    • ベンチマーク用に mysql2 のクエリをキャッシュする仕組みを用意しました。
    • TracePointを用いた、よく知らないソフトウェアの調査方法を紹介しました。

一つ一つは単純なテクニックだったと思いますが、ちゃんと解説しようとすると長いですね。 こんな感じで、中身を確認しながら Ruby の開発を進めています。

たまにはこんな、インタプリタの泥臭い話、でした。

SketchからFigmaに移行してチーム間でのコミュニケーションがしやすくなりました

$
0
0

f:id:puzzeljp:20181031081524p:plain

こんにちは、メディアプロダクト開発部のデザイナ若月 ( id:puzzeljp ) です。

現在関わっている、一緒につくれるクックパッド | cookpadTVのアプリ開発上のチーム間のやりとりについて今回は書きたいと思います。

cookpadTV は料理上手な有名人や料理家がクッキング LIVE を生配信しているサービスです。クッキング LIVE を見られるのは、iOS アプリ・Android アプリ・FireTV アプリとなっています。

cookpadTV のデザインデータを Figma に乗り換えました

f:id:puzzeljp:20181031081540p:plain

cookpadTV に私がジョインしたのは、今年の8月です。 cookpadTV アプリがリリースされたのは3月なので、デザインを引き継ぐ形でジョインをしました。 私がジョインしたタイミングで、社内で Figma が使われる事例が増えてきたので、 Figma に移行を行ってみました。 ただし、デザイナは私一人なので、同時編集は行っていないので今回の記事では触れません。

移行についてはスムーズに行えました

  1. cookpadTV のデザインデータ (Sketchファイル) は、iOS の物しかなく、比較的移行がしやすかったこと
  2. デザインに関心があるチームメンバーが多かったこと

チームメンバーには Figma の プロジェクトへの View 権限のみ渡し、 Figma の使い方をレクチャーすることで、問題無く運用することができました。
結果としては、Figma に移行して良かったです。 その理由について紹介していきます。是非 Figma を導入しようと思っている方に参考にしてもらえればと思います。

移行して期待していたこと: チーム間でデザインの共有が楽になること

f:id:puzzeljp:20181031081556p:plain

今までは、Sketch で、 Marvel と Zeplin 、画像の書き出しを行いプロトタイピングから実装までをやりとりしていました。
Figma にしてからは、Figma のプロジェクトをURLを共有するだけで、デザイン・プロトタイピング・実装に必要な要素が揃うようになりました。
チームではブラウザで確認できることやリアルタイムで変更が確認できることが良かったと意見を貰っています。 ※完全なプロトタイピングはまだできてないので、詳細なインタラクションやアニメーションについては口頭でコミュニケーションを取っていました。

不要なコミュニケーションが減った

f:id:puzzeljp:20181031081752p:plain

一番良かったと感じたこと、パーツ画像の書き出しについてでした。
今まではデザイナが画像の書き出しを行っていましたが、現在はエンジニアが画像を書き出して開発を行っています。
デザイナが書き出しをするとどうしてもパーツの書き出し漏れがでてきてしまい、足らない場合エンジニアから都度書き出してくださいという依頼が来ます。
エンジニア側も「依頼・画像を待つ」、デザイナは「書き出し・共有」を行っておりこの時間が無くなります。
エンジニアが画像を書き出すことで、やや時間がかかりますが、文章を書いて依頼するよりは素早く作業を続行することができ、エンジニアからデザイナに頼まなくても自分でデータを書き出せることは便利だったり、このデザインデータ新しい?、どこにデザインあるんだっけ?っていうのが、「Figma みて」というだけでそういった確認なしに Figma を開いてすぐ会話が始められることも良かったと言ってもらえました。

最新版のデザインの共有が楽になった。

Sketch の場合、プロトタイピングツールや指示書などを共有しようとすると、別のツールに対して同期をしなくてはいけないですが、ミスして同期をしていなかった場合、最新のデザインデータを見ることができないことがたまにありましたが(汗)
同じように、デザインデータの確認で個別に PNG に書き出したり、遷移図をPNGに書き出すこともありますが、そのとき同様のことがありました。 Figma でプロトタイピングやデザイン・遷移図を確認できるので、最新版ではない!のミスをなくすことができました。

コメント機能は今のところ活用し切れてない

f:id:puzzeljp:20181031081613p:plain

画面に対してコメントが残せる機能がありますが、実際あまり使う機会がありませんでした。
メモとして残すことはありますが、ディスカッションをすることはありませんでした。
理由としては、席が近いのでそこでコミュニケーションを取ってディスカッションすること方がまだスムーズに開発ができていました。

まとめ

今回は、Sketch から Figma に移行してチーム間でのコミュニケーションがしやすくなった話を書きました。 結果的に、デザイナはもちろん、チーム間での開発中の確認の手間が減り、開発がスムーズになった感覚がチーム間でありました。 Sketch データからの移行も簡単なので、Figma を使ったことがない方はすぐ始められる・操作方法もさほどかわらないので、使って見ると良いと思います。 チームメンバーに話をしてみると興味を持ってもらえるかもしれません。

cookpadTV は、料理上手な有名人や料理家がクッキングLIVEを生配信しています。 是非ダウンロードしてクッキングLIVEを楽しんでみて下さい!

【開催レポ】Cookpad.apk #1 〜筋肉はすべてを解決する〜

$
0
0

こんにちは。 人事部の浅間( id:ayaasama)です。

2018年8月21日に、Cookpad.apk #1を開催いたしました!

クックパッドでは、Cookpad.apkを通して、Android技術やサービス開発に関する知見を定期的に発信していこうということで、今回はその第1回目でした。

f:id:ayaasama:20181031190925p:plain
(当日のイベント開催時の様子)

第1回は弊社内のAndroidエンジニアからそれぞれ、業務を通じて得られた知見の共有や開発のための工夫についてお話をさせていただきました。 対談の中で、「筋肉はすべてを解決する」という言葉がたくさん飛び交いましたが(笑)、実際エンジニアの方々がどのような工夫をしたのか、当日のプログラムや本ブログを通して当日の様子をご来場いただけなかったみなさまにもお届けしたいと思います。

cookpad.connpass.com

 当日の発表プログラム

「 ReactNative for Androidの現状報告」

はじめに、2014年新卒入社の吉田より、ReactNative製のアプリをリリースして実際に起った問題や普段の開発フローについてお話いたしました。

speakerdeck.com

「クックパッドアプリのマルチモジュール化への取り組み」

技術部モバイル基盤グループ所属で、主にAndroidアプリの開発効率改善に取り組んでいる児山からは、巨大なシングルモジュールアプリケーションだったクックパッドアプリを、マルチモジュール化するまでの経緯と実際の進め方、今後の改善についてお話いたしました。

「ML Kitでカスタムモデルを使うまで」

2018年1月入社で、会員事業部所属の山下からは、ML Kitでカスタムモデルを動かすためのノウハウをお話いたしました。

「AndroidXとAOSP」

2018年7月入社でメディアプロダクト開発部所属で主にcookpadTVのAndroidアプリ開発担当している安部からは、AndroidXがAOSPに公開されたのにちなみコードの取得やパッチなどのお話をさせていただきました。

「Google Playアルファリリースを自動化した話」

技術部モバイル基盤グループ所属で、主にAndroidアプリの開発効率改善に取り組み、最近はKotlin大臣やリリース自動化などをやっている門田からは、 Androidアプリのリリースを自動化への取り組みについてお話をしました。

「FireTVことはじめ」

2018年新卒入社で、メディアプロダクト開発部所属し、主に cookpadTVのFireTV向けアプリの開発を担当している柴原からは、FireTV向けアプリの作り方について、cookpadTVで実際に用いた公式のフレームワークFire App Builderを使ったときの良かった点、悪かった点やTV向けアプリで気をつけなけらばならない事をお話させていただきました。

「Gradle Kotlin DSLのあれこれ」

2017年4月入社し、技術部ユーザ決済基盤グループ所属で、現在はモバイルアプリのアカウント管理と決済機能の開発担当をしている宇津からは、Gradle Kotlin DSLに移行するにあたってのノウハウのお話をさせていただきました。

付箋形式でお答えするQ&Aディスカッション

Cookpad.apkでは参加者のみなさまからの質問を付箋で集めています。 ほんの一部ではありますが、当日は下記のような質問に回答いたしました。

f:id:ayaasama:20181031190739p:plain

Q: Firebaseが使えなかったとのことですが、結局Analyticsってどうされましたか?(別のサービス使ったとか?) A: Crashlytics は Fabric のものが使えたので Crash Log は Crashlytics を使っていて、 Analytics は社内のログ基盤があり、そちらを使っています。 Q: マルチモジュール化って初期開発(最初)から行うのは現実的ではないですよね? A: プロジェクトに求められる規模感やスピード感によって差があると思いますが、最初からモジュール分割をしつつ開発を進めることはできると思います。早い段階で共通のテーマやスタイルリソースを切り出すことができればデザイナが見る範囲を狭められるのでおすすめです。 Q: マルチモジュール化は、1人で進めたのでしょうか?新規開発機能とタイミングかぶって大変な目にはあわなかったですか? A: legacy モジュールの作成は1人で進めました。最初に試したときはテストがこけたりして長期化してしまってタイミングがあわなくなり失敗したのですが、二度目に試したときは慣れたのか短時間でマージまで持っていけました。タイミングが被りそうになったら諦めて新機能開発の合間に新しくやりなおすと理解も深まって良い気がします。 Q: ビルド時間のグラフのツールって何を使ってましたか? A: Build Time Tracker の結果を各開発者の端末から集めて Grafana というツールでグラフにしています。 Q: 発表の内容とはずれますが、AndroidX対応できましたか?(無事に) A: まだ対応してませんが、対応予定です。 Q: 直接関係ないのですが、今Andriod開発を始めるとしたら、どんな実機がおすすめですか? A: (イベント当日の回答です)国内でAndroid Pが唯一使えるEssential Phoneがおすすめだと思います。個人的にはAQUOS Phoneが標準に近くておすすめです。(Pixelが日本発売決定したので、今ならPixelがダントツでおすすめです。)

シェフの手作り料理

Cookpad.apkでは、今回はAndroidの最新OSである「P」を頭文字としたシェフ手作りのごはんをご用意させていただきました!食べながら飲みながらカジュアルに発表を聞いていただけるように工夫しています。今回お越しいただけなかった方も、ぜひ次のイベントはご参加くださいね。

f:id:ayaasama:20181031191022p:plain

当日のメニュー(シェフ特製のPにちなんだ料理)

・ポテトサラダ・プルコギ・パイ(ミートパイ)・パスタ トマトソースペンネーム・ポーク(ローストポーク)・パイナップル(野菜とパイナップルのマリネ)

おわりに

筋肉だけですべてを解決しているわけではないこと、お分かりいただけましたでしょうか(笑) クックパッドではAndroidエンジニアはもちろん、その他新規事業、レシピサービス事業などに携わる新しい仲間を募集しています。 ご興味がある方はぜひご応募ください!お待ちしています。

採用情報 | クックパッド株式会社

また、今後のイベント情報についてはConnpassページについて随時更新予定です。イベント更新情報にご興味がある方は、ぜひメンバー登録をポチっとお願いします!

クックパッド株式会社 - イベント一覧 - connpass

KomercoアプリでFirebaseからの画像取得を速くした話

$
0
0

こんにちは。Komerco事業部エンジニアの高橋(id:yosuke403)です。

Komercoは、「料理が楽しくなるマルシェアプリ」をコンセプトに、料理が楽しくなる器やカトラリー、リネン雑貨等を出品/購入できるサービスです。現在はiOS版のアプリケーションを提供しています。

komer.co

Komerco - コメルコ - by クックパッド

Komerco - コメルコ - by クックパッド

  • Cookpad Inc.
  • ショッピング
  • 無料

先日、Komercoアプリの画像表示の速度を改善したので、それについて書こうと思います。

背景と成果

Komercoで商品を選ぶユーザにとって、商品画像は当然重要なものです。 しかし以前は、アプリを起動してみると画像の表示が遅く、商品一覧をスクロールするとしばらく経ってから画像が表示される状況でした。

こちらは改善前のバージョンで、会社のWiFiに接続し、初回起動(キャッシュなし)から新着商品一覧を表示したときの様子です。

f:id:yosuke403:20181101175645g:plain

セルが表示されても画像が表示されるまで少し時間がかかり、特に素早くスクロールされると画面上のセルが全てプレースホルダーになってしまいます。

次に改善後のバージョンで同環境で操作した様子です。

f:id:yosuke403:20181101175724g:plain

プレースホルダーはほとんど表示されなくなりました!

原因

商品画像に関するシステム構成

KomercoはFirebaseを利用したサービスで、データベースとしてCloud Firestoreを、画像データの置き場としてCloud Storageを使用しています。

f:id:yosuke403:20181101175944p:plain

Firestoreはデータをドキュメントという単位で扱い、ドキュメントには数値や文字列といった値を格納することができます。また、ドキュメントはコレクションというグループでまとめられています。

Komercoの場合、「Product」コレクションには商品名や画像のIDを持つドキュメントが格納され、「Image」コレクションにはCloud Storageのリファレンスを持つドキュメントが格納されています。画像のIDはImageドキュメントのIDに対応しており、これによりアプリはProductドキュメントから、対応するImageドキュメントを取得できます。

画像取得のフローに原因

商品一覧画面が表示されてから商品画像を取得するまでのフローは次のようになっています。

  • ① ProductドキュメントのリストをFirestoreから取得
  • ② UI上にセルが表示される直前に、そのセルに対応したProductドキュメントのimageIDからImageドキュメントをFirestoreから取得
  • ③ ImageドキュメントのstorageRefから画像のURLをCloud Storageから取得
  • ④ Cloud Storageから画像データの取得

f:id:yosuke403:20181101180034p:plain

ここから分かるように画像を取得するまでにFirebaseと何度か通信する必要があります。 しかも②〜④についてはセルの表示ごとに発生するため、素早く商品一覧をスクロールした際などは、リクエストが大量に発生して処理に時間がかかっていました。 加えて、個々のリクエストについてもレスポンスが遅く、こちらもリクエストが詰まる要因になっていました。

既存の対応策

この課題は以前から認識しており、対策としてキャッシュを利用したリクエスト数の低減をすでに行っています。 一度取得したImageドキュメントや画像データをローカルにキャッシュして、何度も通信が発生しないようにしています。

speakerdeck.com

しかし初回起動時や久しぶりに起動したときなど、キャッシュがない状態のときはやはり画像取得は遅い状態でした。

そこで今回は、キャッシュに無い画像データを取得する際に必要な通信回数を減らすことと、各通信自体を速くすることを考えました。

改善内容

改善後の画像取得フロー

ProductドキュメントにimageURLという画像のURLを格納するフィールドを追加しました。 これによりフローは次のようになりました。

  • ① ProductドキュメントのリストをFirestoreから取得
  • ② UI上にセルが表示される直前に、そのセルに対応したProductドキュメントのimageURLから画像データを取得

画像取得に必要な通信は②のみになりました。また、②の通信速度もかなり改善しています。

f:id:yosuke403:20181101180132p:plain

以下では行ったことについて詳しく説明します。

Productドキュメントに画像のURLが付与されるようにする

元々画像データを取得する際は、Imageドキュメントを介することで、Cloud Storage上の画像データの在り処を他ドキュメントと共有したり、Imageドキュメントから得られる画像リサイズの完了イベントを取得したりすることができました。しかし今回はパフォーマンスを優先し、Imageドキュメントを介さず、Productドキュメントに付与された画像のURLを使うようにしました。これにより、改善前の画像取得フローの②と③の通信を省略することができました。

ここで言う画像のURLは、以前のフローの③で得られるダウンロードURLとは違うのですが、理由は次の節で説明します。

画像のURLの付与はCloud Functionsを利用しました。 Cloud FunctionsではFirestoreのドキュメントの作成・更新をトリガーに処理を実行できます。 これを利用して、Productドキュメントに更新がかかったときにimageIDをチェックし、変化があれば画像のURLを取得してProductドキュメントに付与します。

f:id:yosuke403:20181101180154p:plain

Cloud Functions側の実装は概ね次のようになっています。 これはProductドキュメントの新規追加時の例ですが、更新の際もimageIDが変更されたかをチェックしている以外は同じような実装になります。

exportconst productCreated = functions.firestore.document('/Product/{productID}').onCreate(async(snapshot, context)=>{const db = firebase.firestore()const imageDoc =await db.collection('Image').doc(product.data.imageID).get()return product.ref.update({
    imageURL: makeImageURL(imageDoc.ref.id, imageDoc.data))})})

Cloud Functionsを利用するメリットは、Productドキュメントを更新するクライアントのアップデートが不要であることです。 Komercoでは購入用のKomercoアプリとは別に出品用のアプリがあるのですが、出品用アプリはimageURLの存在を気にする必要はありません。 そのため、このアプリの強制アップデートはせずに今回の対応が可能でした。

画像データを取得する時間を短縮する

全体的にレスポンスが遅い原因はリージョンの問題でした。 画像データについては、デフォルトの「Multi-Regional 米国」ではなく「Regional アジア太平洋(東京)」に画像データを移すだけで、かなり改善しました。

さらにFirebaseのCloud Storageではなく、その背後にあるGCPのCloud Storageを直接見に行くようにすると更に速くなりました。 これは、Firebaseではリクエストの検証をし、アクセス権の有無を確認しているためと思われます。 今回の商品画像は公開しても問題なかったので、全ユーザに閲覧権限をつけて公開状態にしています。 GCPのCloud StorageのURLはFirebaseから取得できないので、Imageドキュメントの内容を基にCloud Functions自身がURLを生成してProductドキュメントに付与します。

ちなみに、Firestoreも「Multi-Regional 米国」のためにレスポンスが遅いのですが、現状では東京リージョンは利用できません。 リリースは予定されている模様なので、もう少し待ちたいと思います。

改善結果の数値

改善前のユーザのデータは無いのですが、手元のビルドで計測してみたところ、セルの表示が始まってから画像が表示されるまでに平均1.5秒ほどかかっていました。 これは会社のWiFi環境で計測したので、実際ユーザの手元ではもっとかかっていると思います。

改善後については、画像データ取得自体の時間は中央値で66ミリ秒程度、95パーセンタイル値で841ミリ秒でした。 画面上での表示も概ねこれに近い時間で行われていると思います。

f:id:yosuke403:20181101180220p:plain

最後に

Komercoは6月にリリースしたばかりのサービスで、改善の余地はたくさんある状態です。 作りたい機能もまだまだありますが、こういったサービスのパフォーマンスの改善も怠らないようにしたいと思います。

また、先日Firebase Summit 2018が催され、様々なニュースが発表されましたね。

firebase.google.com

個人的にはFirestoreのlocal emulatorと、Management APIが気になっています。 進化するFirebaseの機能を積極的に導入して、よりスピーディーにサービスを成長させていきたいと思います!

Cloud Firestoreのrulesのテストを全てローカルエミュレータを使うように書き換えた話

$
0
0

Komerco事業部エンジニアの岸本(id:sgrksmt)です。
先日Firebase Summit2018が催され、その中でCloud Firestore(以下Firestore)とRealtime Databaseにローカルエミュレータがβ版として追加されたという発表がありました。
Komercoでは、前回投稿した記事の通り、テスト用のfirebaseプロジェクトを立てて、そこにrulesをデプロイし、オンラインテストといった形でrulesをテストしていましたが、
全てローカルエミュレータを用いたrulesのテストに書き換えました。

今回はローカルエミュレータを用いたFirestoreのrulesのテストの話をします。

使うと何が変わるか

ローカルエミュレータを使ったrulesのテストに切り替えることによって、良い点がいくつかでてきます。

  • テストを実行するためのfirebaseプロジェクトを別途用意する必要がなくなる
  • firebaseプロジェクトにrulesをデプロイしてからテストする必要がなくなる
  • オンラインテストと比べてテストの所要時間が短くなる
  • 開発環境のバッティングによってテストが壊れることがなくなる

特にテストの時間が短くなることと、開発環境のバッティングによってテストが壊れることがなくなるのは大きいです。
KomercoではPull Requestが出たり、masterにマージされたタイミングでにCIでテストを実行しているのですが、rulesの変更が伴う機能開発や改善があると、 CIで用いるfirebaseプロジェクトは1つなのでCIの走るタイミングによってはrulesが違うものに変わってしまいテストが通らない...なんてこともありました。
ローカルエミュレータを使うことでそれらの問題から解放され、デプロイ前に手元でテストができるようになります。
変更前と後を図で示すとこのようになります。

  • Before

f:id:sgrksmt:20181105134322j:plain

  • After

f:id:sgrksmt:20181105134349j:plain

また、テストの所要時間ですが変更前後でこのように変わりました。(各5回ずつ実行の平均値を取っています。)
テストケースはおよそ60ケースほどです。

Before After
約50秒 約13秒

テストケースの大きさ、複雑度によって変わりますが、1/3ほどに減らせたのはとても大きい改善となりました。

実際に使ってみる

実際にFirestoreのローカルエミュレータを使ったテストを書くところまで説明していきます。
公式のドキュメントはこちらにもあるのですが、細かく解説していきます。

※なお、この記事の投稿時点ではβの機能となっています。

事前準備

toolsの更新とemulatorのインストール

まず最初に、firebase-toolsのバージョンは6.0.0以上である必要があります。

npm i -g firebase-tools

あるいは、package.json等でバージョン管理している場合は

"devDependencies": {"firebase-tools": "^6.0.0",}

のように記述し、firebase-toolsをインストールあるいはアップデートします。
その後、emulatorの準備をします。

firebase --open-sesame emulators
firebase setup:emulators:firestore

firebase --open-sesame emulatorsをすると、 firebase loginで再ログインを求められることがあるのでその場合は再ログインします.
CIの場合は基本的に FIREBASE_TOKENを使って各種コマンドを実行していると思うので、firebase loginをし直すのは不要かと思います。

ここまで成功したら、firebase serve --only firestoreを実行し、プロセスが起動してlocalhost:8000が立ち上がるのを確認します。

$ firebase serve --only firestore
✔  firestore: started on http://localhost:8080

Dev App Server is now running.

API endpoint: http://localhost:8080

.gitignoreに追加

firebase serve --only firestoreを実行すると、firestore-debug.logが吐き出されるので、不要ならignoreします。

# .gitignore

firestore-debug.log

firebase/testingの追加

package.jsonにfirebase/testingを追加し、インストールをします。

"devDependencies": {"@firebase/testing": "0.3.0"
  }

ここまで問題なければ事前準備は終了です。

firebase/testing

firebase/testingモジュールを使うことで、firestoreの読み書きのテストを書くことができるようになります。 テストを書くのに使用するインターフェースを紐解いていきます。

projectIDについて

firebase/testingを使ってテスト用のFirebaseのAppを作成します。
その時にprojectIDを指定して作成するのですが、このIDはテストケース毎に一意である必要があります。
ドキュメントには

The Cloud Firestore emulator persists data. This might impact your results. To run tests independently, assign a different project ID for each, independent test. When you call firebase.initializeAdminApp or firebase.initializeTestApp, append a unique ID, timestamp, or random integer to the projectID. You can also resolve race conditions by waiting for promises to resolve through async or await keywords in JavaScript.

と書かれており、エミュレータ起動中は作成したデータを保持しているので、異なるテストケースで同じIDを使ってFirebase Appを立ててアクセスしてしまうと、他のテストケースで作成したデータが混じっていてテスト結果に影響を及ぼす可能性があります。
なので、projectIDを作成するときはタイムスタンプや乱数を含めて一意にすると良さそうです。
公式のサンプルでは次のようにprojectIDを作っています。

// 一部だけ抜粋import * as firebase from'@firebase/testing';const projectIdBase ='firestore-emulator-example-' + Date.now();let testNumber =0;function getProjectId(){return`${projectIdBase}-${testNumber}`;}class TestingBase {async before(){// Create new project ID for each test.
    testNumber++;await firebase.loadFirestoreRules({
      projectId: getProjectId(),
      rules: rules,});}}

テストケース毎に、

firestore-emulator-example-[テスト開始時のタイムスタンプ]-[テスト番号]

という文字列でprojectIDを生成しています。testNumberはbefore(Each)が実行されるときにインクリメントしています。
このように一意となるprojectIDを生成すれば、テスト時に問題になることはないでしょう。

rulesファイルをロードする

エミュレータにprojectIDを指定してrulesを読み込ませます。 先のコードの例にもありましたが、このようにしてrulesの内容を文字列として渡してあげます。

import * as firebase from'@firebase/testing'const rules = fs.readFileSync('firestore.rules','utf8')await firebase.loadFirestoreRules({
  projectId: getProjectId(),
  rules: rules
})

今後はこのprojectIDと一致するFirebaseのAppを作成することで、そのAppはここでロードしたrulesを見るようになります。(後述)

ProjectID、authを指定してFirebaseのAppを作成する

テストで用いるFirebase Appを作成する場合は、initializeTestAppを使います

const db = firebase.initializeTestApp({
  projectId: getProjectId(),
  auth: { uid: 'user'}}).firestore()

引数にはprojectIDと、auth情報を渡します。
注目すべき点は、認証情報を変えた複数のFirebase App(Firestore)を用意できる点です。

今までのrulesのテストの場合、認証ユーザーを切り替えながらデータを作成するときは次のように一々signInとsignOutを繰り返していました。

// userAとして認証してモデルを作成const userA =await auth.signInAnnonymously()
firestore.collection('post').doc().set({ title: 'test1', body: 'xxxx', author: userA.user.uid })await auth.signOut()// userBとして認証してモデルを作成const userB =await auth.signInAnnonymously()
firestore.collection('post').doc().set({ title: 'test2', body: 'xxxx', author: userB.user.uid })await auth.signOut()

firebase/testingを使う場合はユーザーごとにfirestoreを分けることができ、

const userADB = firebase.initializeTestApp({
  projectId: getProjectId(),
  auth: { uid: 'userA'}}).firestore()const userBDB = firebase.initializeTestApp({
  projectId: getProjectId(),
  auth: { uid: 'userB'}}).firestore()// 認証したuserAで書き込みをする
userADB.collection('post').doc().set({ title: 'test1', body: 'xxxx', auhtor: 'userA'})// 認証したuserBで書き込みをする
userBDB.collection('post').doc().set({ title: 'test2', body: 'xxxx', author: 'userB'})

このように書くことができます。
signInしたりsignOutしたりがなくなるので見通しがよくなりますし、「userAで作成したドキュメントのpathを基に、userBでは書き込みができない」、といったテストを以前より簡単に書くことができるようになります。
また、認証が通っていないユーザーとして振る舞いたい場合は、authパラメータにnullを渡します。

const unAuthedDB = firebase.initializeTestApp({
  projectId: getProjectId(),
  auth: null}).firestore()

これで認証の通っていないユーザーに対するテストもかけるようになります。

admin権限でFirebaseのAppを作成する

テストを書くときに、事前にFirestoreにテストデータを入れたい場合があります。このときに、rulesの影響を受けずにデータを作成したり、作成したデータを読み出したい場合には、admin権限でFirebaseのAppを作って利用します。

const adminDB = firebase.initializeAdminApp({
  projectId: this.getProjectID()}).firestore()

後始末

各テストケース実行後は、そのテストケース内で作成したFirebase Appを削除する必要があります。

await Promise.all(firebase.apps().map(app => app.delete()))

これらの処理をclassにまとめてモジュール化

rulesファイルのロードから、各テスト実行ごとに一意のProjectIDを作成してAppを作ってfirestoreを利用する処理を、classとしてまとめてimportできるようにすると便利になります。
KomercoではFirestoreTestProviderというクラスを作り、各テストファイルでimportしています。

import * as firebase from'@firebase/testing'import * as fs from'fs'exportdefaultclass FirestoreTestProvider {private testNumber: number=0private projectName: stringprivate rules: stringconstructor(projectName: string, rulesFilePath: string='firestore.rules'){this.projectName = projectName + '-' + Date.now()this.rules = fs.readFileSync(rulesFilePath,'utf8')}

  increment(){this.testNumber++}private getProjectID(){return`${this.projectName}-${this.testNumber}`}async loadRules(){return firebase.loadFirestoreRules({
      projectId: this.getProjectID(),
      rules: this.rules
    })}

  getFirestoreWithAuth(auth?: {[key in'uid' | 'email']?: string}){return firebase.initializeTestApp({
      projectId: this.getProjectID(),
      auth: auth
    }).firestore()}

  getAdminFirestore(){return firebase.initializeAdminApp({ projectId: this.getProjectID()}).firestore()}async cleanup(){return Promise.all(firebase.apps().map(app => app.delete()))}}

エミュレートしているfirestoreへの書き込みの成功失敗を評価する

書き込みが成功する/失敗するのを評価する場合は、assertSucceedsassertFailsを使います。

const db = firebase.initializeTestApp({
  projectId: getProjectId(),
  auth: { uid: 'user'}}).firestore()await firebase.assertSucceeds(db.collection('users').doc('user').get())

ちなみに、assertSucceedsassertFailsの実装はこちらで確認ができます。
Succeedsは単純にpromiseを返し、failsの場合はPromiseが成功したらエラーを返し、失敗したらエラーを値として返すようにしています。(成功と失敗を反転させている)
これは使っても使わなくても問題ありません。(jestの場合はexpectで評価することもできますし。)

テストを書く

先程のFirestoreTestProviderと、テストフレームワークであるjestを使ってテストを実際に書いてみます。
rulesとテストケースはCloud Firestore emulator quickstartと同じもので組んでみました。

import * as ftest from'@firebase/testing'import FirestoreTestProvider from'./firestoreTestProvider'const testName ='firestore-emulator-example'const provider =new FirestoreTestProvider(testName)function getUsersRef(db: firebase.firestore.Firestore){return db.collection('users')}

describe(testName,()=>{
  beforeEach(async()=>{
    provider.increment()await provider.loadRules()})

  afterEach(async()=>{await provider.cleanup()})

  describe('users collection test',()=>{
    test('require users to log in before creating a profile',async()=>{const db = provider.getFirestoreWithAuth(null)const profile = getUsersRef(db).doc('alice')await ftest.assertFails(profile.set({ birthday: 'January 1'}))})

    test('should let anyone create their own profile',async()=>{const db = provider.getFirestoreWithAuth({ uid: 'alice'})const profile = getUsersRef(db).doc('alice')await ftest.assertSucceeds(profile.set({ birthday: 'January 1'}))})

    test('should let anyone read any profile',async()=>{const db = provider.getFirestoreWithAuth(null)const profile = getUsersRef(db).doc('alice')await ftest.assertSucceeds(profile.get())})})})

これで、 firebase serve --only firestoreを実行してエミュレータを起動した上で、jestを実行するとテストが動きます。
サンプルはこちらにあります。

最後に

ローカルエミュレータは投稿時点ではβ版ということですが、とても強力かつ手軽に扱えるのでこれからテスト書こうかなとか、以前僕が書いた記事のようにテスト書いてる場合は是非検討してみてはいかがでしょうか。

ローカルエミュレータ以外にも様々な発表があったので、キャッチアップしていきながら導入や実践できそうなものがあればどんどんKomercoで試していこうと思います。

参考

【開催レポ】Security Engineering Casual Talks #1

$
0
0

こんにちは。インフラストラクチャー部セキュリティグループの水谷(@m_mizutani)です。2018年10月31日にクックパッドにて Security Engineering Casual Talks #1 を開催しました。

f:id:mztnex:20181108151253j:plain

sect.connpass.com

セキュリティに関する様々なトピックが議論されている昨今ですが、「実際のところサービス開発や運用にどうセキュリティを組み込んでいるのか」という話はまだなかなか表にでてこない、と感じる方もいらっしゃるのではないでしょうか。各種製品・サービスによってセキュリティを高めるための手段は増えてきましたが、それを自社のサービスやプロダクト、社内のセキュリティ運用にどう役立てているか、あるいはどのような工夫をして運用されているのかの知見は組織に閉じてしまっていることがまだまだ多いかと思います。

この会は「開発や運用に実際携わっているエンジニア」があつまって「現場におけるセキュリティに対し、エンジニアはどのようにアプローチしているのか」にフォーカスし、ビールでも飲みながらざっくばらんに話せるように、ということを目的として開催しました。

第1回は「クラウドサービスでつくるセキュリティ」と題してクックパッドを含む3社からの発表がありました。各クラウドサービスが提供しているセキュリティ機能をどのように実環境で構築・実用化し、どのように運用しているのか?という実情について話をしました。

[発表1] 自由でセキュアな環境のつくりかた by @kani_b

1件目の発表は弊社の星(@kani_b)が「自由でセキュアな環境のつくりかた」と題して、いかにセキュリティを維持しつつ開発者が自由にAWSを使える環境を作るかというテーマについて話しました。

[発表2] 踏み台環境のあれそれ by @ken5scal

2件目の発表はFolioの @ken5scalさんによる「踏み台環境のあれそれ」で、クラウド環境における踏み台を Folio でどのように構築しているかという話をしていただきました。Folioでは Teleportというリモートアクセスをサポートするツールを活用して実際の運用をされているとのことでした。

残念ながらsshから完全に開放されるわけではないとのことでしたが、WebUIから使えるコンソールを利用したりロールなどに基づいたログインの制御ができる点でメリットがあるようでした。まだAWS環境との密な連携ができなかったり、Enterprise版でしか使えない機能もあったり、構築・管理・運用での課題もなくはないので完璧というわけではないようですが、参加者の皆さんも反応をみるとかなり実用的に使える段階に来ているのではないか、ということを実感できているようでした。

[発表3] 開発現場で使えるAWS KMS by okzk

3件目の発表はCyberAgentのokzkさんによる「開発現場で使えるAWS KMS」でAWSのKMS、およびAWS System Manager の Parameter StoreやAWS Secrets Managerについてご紹介いただきました。それぞれで秘匿値を扱う際のポイントやtipsなどを共有してもらいました。

個人的に面白かったのはKMSやSecrets Managerによって秘匿値を取り出す実装をアプリに持たないようにしているということです。ではどうするかというと実際にKMSやSecretsManagerにアクセスする実装 env-injectorが秘匿値を取得し、アプリとなるプロセスを起動させる際に環境変数で渡すようにしているとのことでした。これによってアプリ側では実行環境に依存せず、透過的に環境変数から秘匿値を引き出せるため実装がシンプルになって管理しやすくなるというメリットがあり、実践的で参考になるtipsだなと感じました。

次回開催に向けて

懇親会中に「次回以降で聞きたいネタをホワイトボードに書き込んでください」というお願いをしてみたところ、想像以上にいろいろなトピックが集まりました。どのトピックも「で、実際のところどうなのよ」というのがなかなか見えにくく、気になる方は少なくないのではないかなと想像しています。

  • データ暗号化ってぶっちゃけどうしてるの(対象、方法とか)
  • エンドポイントセキュリティ・FIMどうしてますか
  • DBのクエリログとってますか、監査してますか
  • 予算どう確保&上に説明していますか
  • マイクロサービスとセキュリティ
  • CI、CDの文脈におけるセキュリティ、監査
  • FaaSの権限管理
  • セキュリティ部署の立ち上げ、各サービスのセキュリティを保障する方法
  • docker imageとかLambdaの脆弱性診断
  • 開発環境のアクセス制限
  • 開発者にどれくらい権限を渡しているか@本番環境
  • どれくらいアップデート作業をちゃんとやってますか
  • 複数サービスの権限管理
  • 誰に本番のアクセス権限を渡すのか(ルール・選別・etc)
  • 固定IP以外での接続制限(クライアント証明書以外)
  • 社員のID管理
  • イケてる社内NW

今後も参加していただいた皆さんにあげていただいたトピックを中心としてSecurity Engineering Casual Talksを開催していきたいと考えています。次回は年明けあたりで開催したいと考えていますので、興味のある方はぜひご参加いただければと思います!!

f:id:mztnex:20181108151736j:plainf:id:mztnex:20181108151853j:plain


Chaos Engineering に向けてレシピサービスの Steady State を追求する

$
0
0

こんにちは、今年ソフトウェアエンジニアとして新卒入社した @itkqです。社会人になってから 1 クールで見るアニメの本数がガクッと減っていることに気づいて最近は無力を感じています。さて、この開発者ブログで「Chaos Engineering やっていく宣言*1」が公開されたことは記憶に新しいと思います。私はインフラストラクチャー部 SRE グループに配属され、最近は Chaos Engineering に関わる取り組みも行っています。その中から今回は「レシピサービスの Steady State を追求する」取り組みについて、背景や現状も含めて紹介します。

Steady State とはなにか、なぜ必要か

一昔前の Web サービスといえば、様々な機能が 1 つのアプリケーション上に実装されたモノリシックアーキテクチャが一般的でした。その後サービスという単位で機能を切り出して別アプリケーションとして実装するサービス指向アーキテクチャを経て、オーナーシップを持って速度をもった開発を行うといった組織論の意味合いも含めたマイクロサービスアーキテクチャに変遷し今に至ることはもはや前提として良いでしょう。クックパッドにおけるアーキテクチャの変遷は、クックパッドとマイクロサービス*2に詳しく書かれています。

アーキテクチャの変化に伴い、Web サービスにおける障害の質も変化しました。モノリシックアーキテクチャでは、データベースの接続に失敗するなどアプリケーションで致命的なエラーが発生した場合、当然ながらアプリケーションのすべての機能は利用できなくなります。一方で、マイクロサービスアーキテクチャでは、あるサービスが何らかの原因で利用できなくなった場合でも、別のサービスは正常に利用できることが起こりうります。複数のサービスが全体としてのサービスを構成しているため、あるサービスが障害に陥った場合に「ユーザから見える (全体としての) サービスは正常に動作しているのか?」という疑問に答えられる必要があります。ここで重要なことは、「あるサービスが障害になっても、サービス全体の機能が停止しない」というフォールトトレランスの考え方です。障害中のサービスを graceful に degradate するなどの手法により、ユーザは依然として目的のサービスを利用できる可能性があります。

Web サービスにおいてそのビジネスのために最も重要なことは、ユーザが快適にサービスを利用できているかどうかです。システムの裏側の内部がどうなっているかということはユーザにとって関心がありません。ゆえにシステムの内的な状態ではなく、ユーザの体験という外的な状態をもってシステムが正常であるかを判断できる必要があります。その判断基準となるものが Steady State です。Chaos Engineering においては、本番環境で意図的に障害を注入するような実験を行い、Steady State に変化が見られないという仮説を検証します。すなわちその障害がユーザの体験を損ねていないことを Steady State によって判断し、システムの回復性に自信を持つ、または未知だったシステムの弱点を修正することが目的です。

クックパッドでは、Envoy proxy (以下 Envoy) を用いたサービスメッシュを本格的に導入しており、マイクロサービスアーキテクチャにおいてクリティカルであるサービス間通信の Observability が確保されつつあります。詳細は Service Mesh and Cookpad*3で述べられています。一方でクックパッドにおける各サービスの依存関係はますます複雑になっており、Chaos Engineering を起点としてシステム全体の Resiliency を高めていく必要がある段階であると意識し始めました。そこでクックパッドのシステムアーキテクチャ上での Chaos Engineering の仕組みや実装を検討すると同時に、Steady State の仮説検証も進めていくことにしました。Steady State を追求する取り組みは、Chaos Engineering の文脈でなくても、マイクロサービスアーキテクチャによるモダンな Web アプリケーションでは重要だと私は考えています。

レシピサービスの Steady State を考える

クックパッドでは現在、komerco*4, クックパッドマート*5など数々の新規事業を進めていますが、事業の柱となっているのはやはりレシピサービス (cookpad.com) です。このレシピサービスは巨大な Rails アプリケーションでありますが、「お台場プロジェクト*6」の一環で、機能の切り出しが進められており、Envoy を通していくつかの別サービスと通信しているのが現状です。先述した Steady State が必要な背景と合わせて考慮し、まずレシピサービスの Steady State を定義することが先決という結論を出しました。

Steady State は Web サービスの歴史からするとまだまだ新しい概念で、Steady State を考えるにあたり、Netflix のソフトウェアエンジニアによって書かれた Chaos Engineering 本*7を大いに参考にしました。しかし、Steady State は事業の性質によって全く異なり、また正解が 1 つに決まっているものではないことは少し考えただけで想像できました。具体的に Netflix のサービスと違う点として、課金していないユーザもレシピサービスを利用できる点が挙げられます。

どのユーザを対象とした Steady State を考えるべきなのか

Netflix のストリーミングサービスとクックパッドのレシピサービスの異なる点として、課金モデルが挙げられます。Netflix では課金しているユーザだけがストリーミングサービスを利用できますが、クックパッドでは課金していないユーザも一部機能を制限されながらレシピサービスを利用できます。

Chaos Engineering 本では、Steady State は “今顧客を失っていないか? という質問に答えられるようなビジネスに関わるメトリクスであること” とされています。クックパッドのレシピサービスは、課金ユーザだけでなく多くの課金していないユーザからも利用されている事実があります。「現時点」のビジネスメトリクスとするならば、Steady State の対象とするユーザは課金ユーザだけに絞るべきかもしれません。しかし、レシピサービスは月間約 5,500 万人が利用する*8、日本の家庭に根付いたものであるということができ、「顧客」は必ずしも課金ユーザに限定すべきではないと私は考えました。その裏側には、課金体系は月額のサブスクリプションのため、試しにプレミアムサービスを利用する障壁は低く、非課金ユーザのレシピサービスの体験が新たな課金ユーザの獲得、ひいてはレシピサービスの存続に関わる重要なものであるのではないかという思いがあります。この考えが適切かどうかに対する回答は未だ持っていません。しかし Steady State を考え始めるにあたって、対象とするユーザは「レシピサービスを利用するすべての人」という前提を置くことにしました。

この前提を元に、以下では 2 つの観点から Steady State となりうるメトリクスを選択し検証した結果を述べます。1 つ目は、レシピサービスのコア機能という観点です。しかし、レシピサービスの性質上そう単純にうまくいかないことが分かりました。この結果を踏まえ、2 つ目の観点としたのがユーザの定常な行動パターンです。こちらは結果的に 1 つ目の観点より妥当であることが分かりました。

(1) レシピサービスにおけるコア機能から考える

クックパッドのレシピサービスは、基本的にクックパッド側でレシピを追加するのではなく、レシピを投稿するユーザがいることによって成り立っており、レシピ投稿者は無くてはならない重要な存在といえます。一方で、レシピが投稿される数とレシピが閲覧される数を比較すると、圧倒的にレシピが閲覧される数のほうが多いです。ゆえに、ユーザにとってサービスが定常かどうかを考える上でユーザの体験に影響が現れやすいと考えられるレシピ閲覧にまず着目しました。レシピ閲覧以外の機能 (例えばレシピ検索やランキング) に障害が発生した場合でも、レシピ閲覧に紐付いていると考えられるものはレシピ閲覧数に影響を与えるはずと考え、記録するメトリクスはひとまずレシピ閲覧数だけで十分としました。

ある一週間における、レシピ閲覧数/sec のグラフが以下の図になります。集計はリバースプロキシ層のアクセスログをもとに行っています。先週の値と今週の値を比較できるように表示しています。最もよくレシピが閲覧されるのは夕方で、晩ごはんの献立を考えるためであることが推測できます。先週と今週の値にあまり変化がなく、定常といえる日が多い一方で、先週と比較して大きく値が変動している期間があります。この期間で障害とみなせる事象は発生していませんでした。実は、先週のこの期間は本州に台風が来ており、その影響で外食を控え家で自炊するケースが増えてこのような結果になったのだと推測しています。レシピサービスという性質上、台風だけでなく天候に大きく影響されることは事実です。アクセス数が通常と比較して増減するという意味で、システム目線では定常ではなくなっているのかもしれませんが、Steady State はユーザ側の視点で定常かどうかを表現するべきものであることを考慮すると、単純なレシピ閲覧数というメトリクスは適切ではないことが分かりました。

f:id:itkq:20181030211329p:plain
今週と先週における レシピ閲覧数/sec の比較

そこで、関連性のある複数の値の関係を相対値として表すことで、ユーザ視点とシステム視点の定常の差を縮めることを考えました。レシピサービスにおいては、天候などの外的要因に影響されて利用者数が増減したとしても、「レシピサービスを使う大半の人の行動パターン」に影響は無いのではないかという仮説を立てました。

余談

社内ブログでこの取り組みの共有をしたところ、予想より多くの人からリアクションがありました。サービス開発チームからはよりユーザに近い立場の目線のフィードバックをもらえたり、機械学習グループからは異常検知についてのコメントをもらうことができ、良い方向に進められている手応えを感じました。Steady State を含めた Chaos Engineering の取り組みは、全社的に導入するためにその必要性を広く周知していく必要があると考えており、以降も定期的な発信を積極的に行う予定です。

(2) ユーザの定常な行動パターンから考える

レシピサービスにとって最も重要な機能の 1 つといえるものが「レシピ検索」です。クックパッドを使って献立を決めたいユーザがとる行動の最も基本的な遷移は、

  1. 手元にある食材を把握する
  2. クックパッドで食材名をもとにレシピ検索
  3. いくつかレシピを見た上で料理するレシピを決める

といえるのではないでしょうか。この例では、レシピ検索とレシピ閲覧は密接に紐付いています*9。これを踏まえて、前回の仮説から次のようにアップデートしました。「ユーザは探しているレシピを閲覧できている」こと、もう一歩踏み込んで、ユーザが上で示した行動パターンを取れていることをもって Steady State とする、という仮説です。前回の失敗からの学びから、今回は相関すると考えられる 2 つのメトリクスの比を Steady State として使えないかを検討します。これは、台風が来てアクセスが増えた場合でもユーザの行動パターンは変化しないのではないかという考えに基づきます。また、このアイディアは同僚の @KOBA789と雑談していたときに生まれたものです。雑談は大事ですね。

レシピ閲覧数と検索数の比を検討するにあたり、まず本番のデータからこの値を抽出・表示するのではなく、今回は過去のデータを使って妥当性を判断することにしました。クックパッドでは、Redshift を中心としたデータ基盤*10が整っており、過去のレシピ閲覧数や検索数を SQL で抽出することができます。また SRE グループでは、障害が発生した後にその障害が与える影響・障害の原因・根本対処などを postmortem として記録する文化が根付いており、過去の障害とその影響も容易に検索することができるため、データ基盤の活用と合わせることでユーザの体験に影響を与えた実際の障害が発生していた区間のメトリクスを検証できます。Chaos Engineering 本において、Steady State を表せるようなメトリクスを検討する際は、そのメトリクスを取るために必要な労力のバランスを取る必要があると書かれていますが、過去のデータを元に検証する労力は、本番環境で新しくメトリクスを採取して時間経過をおいてから検証することに比べてはるかに低いことは明らかです。

以下の図は、ある 2 週間の iOS アプリケーションにおける レシピ検索 / レシピ閲覧 の数の比を示したものです。

f:id:itkq:20181030211355p:plain
レシピ閲覧数と検索数の比 (1)

オレンジ色で値が明らかに跳ねている期間で、実際にレシピサービスを使うユーザに影響があった障害が発生していました。青色で値が突出している部分は、テレビ放映によりアクセス数が増加した時間帯でした。この比の興味深いポイントは、次に示す別の 2 週間の図に表れています。1 つ前の図と同じクエリ・スケールです。このうち、実は大型の台風が来ていた期間があります。レシピ閲覧数は増加していますが、それに伴ってレシピ検索数も同じように増加しているため、値には大きな変化が表れていないと考えられます。

f:id:itkq:20181030211347p:plain
レシピ閲覧数と検索数の比 (2)

他にもレシピサービスに関係する過去の障害に対してこの比を計算してみたところ、1 枚目の図に近いスケールで値が跳ねていました。ところで、年間を通してクックパッドで最もアクセスの多い期間はバレンタイン周辺です。ある年のバレンタイン周辺の 2 週間について同様に描画したグラフが以下の図です。

f:id:itkq:20181030211404p:plain
レシピ閲覧数と検索数の比 (3)

バレンタインの周辺は、相対的に全体の値が下にシフトしていました。しかし、障害の時ほど明らかな外れ値は見られません。

以上のように、ユーザの定常な行動パターンを考え、基本的だと考えられる行動パターンに関係する 2 つのメトリクスの比を検討したところ、1 回目で試した「単純なレシピ閲覧数」よりもユーザから見たシステムの Steady State を表せていることが分かりました。そこで、現在ではレシピ閲覧数とレシピ検索数の比を遅延高々数十秒で記録・表示しています。それ以降ユーザに大きな影響を与えるような事象は (この記事を書いている時点では) 観測できていませんが、過去の事例を使った検証結果より、この値を記録していくことには意味があると考えています。

まとめ

Steady State とは何か、またそれが必要になった背景の説明と、クックパッドのレシピサービスの Steady State を追求する取り組みについて紹介しました。実サービスにおける Steady State に関する情報はあまり見当たらないため、参考になれば幸いです。Chaos Engineering 本を参考にしつつ、レシピサービスの Steady State を考える過程で、以下に挙げるいくつかの学びがありました。

  • Steady State はサービスの性質によって異なり、あらゆるサービスで唯一の正解となるメトリクスは存在しない
  • 仮設を立てながら Steady State となり得るメトリクスを地道に検証していく必要がある
    • そのためにメトリクスの収集・表示・アラーティングなどの環境が整っている必要がある
  • 過去の障害記録と当時のログデータやメトリクスは、妥当性のある Steady State の検証を効率的に行う助けとなる

クックパッドのレシピサービスは、ユーザの生活と密接に関係しており、ユーザの体験を損ねないことが重要です。Steady State の定義だけでなく、その先にある Chaos Engineering も用いるなどしてシステム全体の Resiliency のさらなる向上を目指していきたいと考えています。

デザインとは「問題解決」だけじゃない?

$
0
0

事業開発部のデザイナー平井です。Cookpad Do!というサービスの運営をしているチームに所属しています。

cookpad.do

Cookpad Do!は、前身サービス「Cookpad料理教室」のブランド再開発として2018年8月8日に生まれた新サービスで、食・料理をコンテンツとした体験型イベントを開催するオーナーがイベントを掲載し、参加する人がイベントの予約・決済を行えるプラットフォームサービスです。

今回はグロース期に入ったサービスの開発・運営していく中で、何を考え、どのように企画し、何を気をつけながら価値創出をしようとしているかの話をしようと思います。

料理を「楽しみ」にする

世に存在するサービスデザインは、よく「顧客の問題の解決」という言葉で説明されることがありますが、クックパッドが目指す「毎日の料理を“楽しみ”に」というビジョンを目指す上では、痛みや不満などの問題の解決という文脈だけで語れない側面があると考えています。

例えば「今日の献立を決めきれない」「失敗したくない」「人気のレシピを参考にしたい」…という、面倒な工程を少しでも“楽”にするという価値は、レシピサービスを通して多くの人に提供できており、素晴らしいことです。

しかし、“楽しみ”にというステージまでシフトするには、「今見えている問題の解決」という視点から外れて「料理の楽しさを創出する」というチャレンジを行っていかなければなりません。そんなチャレンジをCookpad Do!で実践しています。

ユーザーさんのPainは目に見えるが、Gainは見えづらい

ユーザーさんの隠れたニーズを発見するための手法として、ユーザーインタビューやプロトタイプを用いたユーザーテストは、今やどのサービス開発会社でも当たり前に行われています。

しかし実際には、「あぁやっぱりね」といった具合に自分たちが考えていたソリューションを正当化してみたり、「問題なかった!よかった!」と、インタビューの時間で新たな発見を生まない結果にしてウヤムヤにしてしまった…なんてケースがあるのではないでしょうか?

私の良くない経験としては、インタビュー中ユーザーさんから発せられた表面的な不満や課題にどうしても目が行ってしまい、いきなりソリューションの説明をして、ユーザーさんをその場で納得したような気持ちにさせてしまう…ということがありました。

なぜそういった事態に陥ってしまうのでしょうか? それは、「痛み」はユーザーさん自身が一番先に意識していて、表現しやすく、話題にしやすいからです。

また、ユーザーさんの「快楽」は、いざ口に出して説明しようと思っても説明できないことが多く、絞り出した結果も「なんとなくかっこいいから」「自己満足」のように曖昧だったり、普遍的な欲求の言葉で語られます。 (あと、「異性にモテる」など、単純に恥ずかしかったりします…)

例えば「料理教室」を商品とした場合「基本を理解できていない」「料理のレパートリーを増やしたい」といった話になりやすいです。

私たちが実現したいのは「楽しみの増幅・創出」であり、本質的にフォーカスすべきなのはPainではなく、Gainです。

楽しみを見つけるアプローチ

当たり前のようですが、私たちは日々、楽しみを何かに「見出して」います。

言い換えれば「これをやったら楽しいのでは?」と自分で動機づけを行い、いろいろ試したりして、発見しています。 (逆に緊急性が高い「問題」に対しては、解決策となる多くのサービスが向こうからやってきます)

「楽しみ」を求める能動的な行動は、各々の「価値観」によって左右されることから、「楽しみ」は「価値観」から洞察されることがわかります。

問題ではなく、人の「価値観」を探りましょう。

価値観を知るためには、その人が何を「問題」として認識しているかという事実も洞察のヒントになり得ます。

例を挙げると、「料理のレパートリーを増やしたい」という声をあげている人が「子どもが最近ご馳走様でしたの挨拶をしなくなった」ことに悲しい顔を浮かべている…という生活背景を持っている場合は、「子どもの喜ぶ顔に価値がある」といった価値観を持っているのではないか?という洞察が得られます。

「子どもの喜ぶ顔を見る」手段は「料理のレパートリーを増やす」に限った話ではないはずです。

同じ「料理のレパートリーを増やしたい」と言っている人でも、「毎日同じご飯だと栄養が偏る」という「健康な生活」に価値を置いているかもしれません。

f:id:yu-hirai:20181112161509p:plain

このように一つの課題感から、生活背景や価値観を抽象化して属性分けできるレベルまでターゲットを言語化すれば、どのようなコンテンツで訴求をして動機付けることができるのか打ち手が考えやすいですよね。

本当に価値のあるアイデアとは?

さて、ここまでどのように「楽しみ」のアイデアの種を生み出すかの話をしてきましたが、プロダクト開発現場でよくある問題が「やりたいことリスト」が溢れてしまって、ただ読むのにすら時間がかかってしまう…というケースです。

そのため、実行優先順位や期待効果を相対的に判断しにくくなり、結果よくわからないまま、数カ月後には誰も話題にしないアイデアがこんなに…なんて経験はありませんか?

そういった事態に陥ってしまう原因の一つに「具体的な要件」を主語としてしまうから、というものがあると考えています。

例えば、「◯◯という訴求文言を設置する」「メッセージ機能をつける」などです。

タスクのラベリングとしては管理しやすいため、実行フェーズに移った際には上記のような共通言語を用いたほうが良いと思いますが、アイデアの価値を測るフェーズでは問題になりやすいです。

そこでCookpad Do!チームで行っている対策として、アイデアには必ずユーザーニーズと価値仮説を、価値仮説には必ず事実と洞察をセットにして起票することにしています。

f:id:yu-hirai:20181112161538p:plain

仮説はほとんどの場合、何らかの事実や背景から洞察されます。

たとえばCookpad Do!の場合「よく知らない人の自宅に上がることに抵抗がある」「イベント参加前に、イベントの内容ページよりもイベントオーナーのプロフィール画面を閲覧している傾向が強い」といった声やデータの事実が背景にあった場合、「イベントのコンテンツ以上に、イベントオーナーの人となりを気にしているユーザーさんが多いのではないか?」といった洞察がなされます。 よって、「人にフォーカスしたコンテンツをより露出させてあげたほうが良いかもしれない」といった具合に仮説が立ち、新たな要件が生まれ、開発の糸口になります。

f:id:yu-hirai:20181112161555p:plain

このような仮説の種となる事実や背景を得るために、普段からユーザーインタビューをする際に意識したり、ユーザーさんの行動ログを見ていると良いですよね。

最後に

「問題を解決すること」は「楽しいこと」ではありません。

よく「ユーザーファースト」と言いますが、ユーザーの希望する機能を聞いたまま開発するのではなく、「なぜこのような事実になっているのか」を深く洞察し、驚きや楽しみを実現するプロダクトを創り出していくことが本当の意味でユーザーファーストな考え方であると思っています。

ユーザーさんが「楽しそう!」と喜んで選んでくれるプロダクトを作るために、常にユーザーファーストの気持ちを忘れずに、時には大胆にユーザーさんにボールを投げかけてみたりしてチャレンジしていきましょう!

【スマートキッチン】まぜまぜ機の検討とプロトタイプ開発

$
0
0

研究開発部 スマートキッチングループ アルバイトの鈴本です.
今回は,最近取り組んだまぜまぜ機の検討とプロトタイプ開発について紹介します.

1.はじめに

↓こんなもの作りました.
まぜまぜ機です.フライパンや鍋に入ったものを勝手に混ぜておいてくれます.

フライパンの上に電子部品丸出しのものが乗っているの,シュールですね....
もちろん,防水なんて考えてません,プロトタイプですから.

今回は,こんなものを作ってしまった経緯とその実装について紹介します.

2.背景 〜まぜまぜ機〜

カレーやシチュー,スープやあめ色玉ねぎを作るとき,一番めんどくさいのが焦げ付かないように混ぜ続けること.
これをなんとかできないかと思い,火にかけたフライパン・鍋をほっておいても混ぜ続けてくれるデバイス,まぜまぜ機を考えてみることにしました.

3.まぜまぜ手法のアイデア整理

まず3つほど,既存製品を例にまぜまぜ機のアイデアを検討してみました.

3-1.三脚型まぜまぜ機


出典 : https://www.amazon.com/Uutensil-Stirr-Unique-Automatic-Stirrer/dp/B008OWT95S

鍋にぽんと入れておくと,まぜまぜ機自体が振動して回転することにより,まぜまぜしてくれるデバイス.

■ メリット

  • お手軽
  • 鍋,フライパンを選ばない(どの鍋・フライパンでも使える)

■ デメリット

  • 構造上,大トルクが出しにくい
  • 実際に使ってみると,全く混ざらない(後述)

3-2.縁取り付け型まぜまぜ機


出典 : https://www.amazon.co.jp/dp/B06XBY9JKM

鍋の縁にはめて使うタイプのまぜまぜ機.

■ メリット

  • 大トルクは出せそう
  • 回転速度などの制御もしやすそう

■ デメリット

  • 機構が複雑で値段が高い
  • 鍋径,深さなどの制約があり,使える鍋・フライパンを選んでしまう

3-3.マグネティックスターラ型まぜまぜ機


出典 : http://www.taiyo-kabu.co.jp/products/detail.php?product_id=1704

鍋やフライパンを選ばず,そして大トルクを出せるまぜまぜ機候補として思いついたのがこれ.
下の台で回転磁場を発生させ,鍋の中に入れた磁石でできた回転子を回転させます.

高校時代,コウジカビなどの菌を使って生物学の研究をしてたので,個人的にはとても馴染みのあるデバイスです.

■ メリット

  • 大トルクは比較的出しやすい
  • 鍋,フライパンを選ばない(どの鍋・フライパンでも使える)

■ デメリット

  • 回転子をどうやってとりだすのかが問題
  • IHクッキングヒーターと連携させた場合,ヒーターの電場と干渉しそう?

4.開発方針

4-1.要求と現状の問題

まぜまぜ機のアイデアが出そろいました.
個人的に,まぜまぜ機に求めるものは,

  • 手軽である
  • 鍋やフライパンに依存しない(どんな鍋・フライパンでも使える)

ことです.

1からシステムを作りたいところですが,今回は開発期間が短かったため,既存のデバイスをもとにプロトタイプを制作しました.

トルクが出しにくそうではありますが,三脚型まぜまぜ機を改良してみることにします.

なぜここで “改良” と言っているかというと,3-1.で上げたまぜまぜ機が,ほぼほぼ使えなかったからです(下図参照).
まあ,1枚目(煮物)は仕方がないとして,2枚目(玉ねぎ炒め)ですら回らず....
ここに水を入れて玉ねぎを浮かせてあげると,多少は回ってくれたのですが,まぜまぜ,とはいきませんでした.

4-2.開発コンセプト

開発コンセプト図はこのような感じ.
まぜまぜ機にマイコンをのせ,今までより大きな電流を流せるようにし,出力をUP.
さらに各種センサからの情報により回転を制御し,またBluetoothで他のデバイスとも繋げられるようにします.

三脚型まぜまぜ機はその足が食材と接するため,温度などを直に測れます.
そのため,今後IHクッキングヒーターなどと連携できれば,面白いのではと思いました.

5.実装(ハードウェア)

5-1.概要

全体図は下図のようになります.
回路面積などの制約から,かなりコンパクトな設計となっています.

5-2.マイコン

マイコンは,社内に転がっていた「ESPr® Developer 32」を使いました.
On-BoardでBLEモジュールがついている優れものです.

しかし,開発を始めてから,

  • すでにディスコンになったらしく,公式HPにすらデータシートがない.
      → ノリで頑張る.
  • Arduino互換マイコンといいつつ,I2CドライバがArduino互換ではない.
      → ネットに上がっている他人のコードを参考に対処.
  • BLEとADCが干渉する.
      → ledcAttachPinという関数を用いることにより,部分的に使用可能.
  • PWM出力に対応していない.
      → タイマー割り込みで自作.

など問題が多発し,なかなか苦労しました.

5-3.電源ライン

  • モーターの出力を上げたい
  • 小型高出力の電池がいい

という要求から7.4V Li-Poバッテリを選定しました.

モーターには7.4V(満充電実測では8.4V)を印加し,マイコン入力用と5V系センサ用に三端子レギュレータ「TA48M05F(SQ)」で5Vを作りました.
3.3Vラインはマイコン内蔵のレギュレータから取り出せます.

また,今回はスペースの都合上,ロジック電源とモーターバス電源が共通です.
モーター側の高ノイズがロジックに悪影響を与えないように,適当にパスコンなどを追加しました.

5-4.9軸センサ

はじめ,ESPr® Developer 32 対応シールド「ESPr® Developer用9軸慣性計測ユニットシールド」を購入しましたが,

  • 1台目はI2C通信のアドレスすら取れない.
  • 2台目はI2Cの通信確立まですすむも,センサ値が読めない.

という不具合に悩まされ続け,結局このセンサを断念.
代わりに「BMX055使用9軸センサーモジュール」を使いました.

5-5.モーター,モータードライバ

モーターは既存のまぜまぜ機についていたモーターを流用しました.

モータードライバはおなじみの「TA7291P」です.
このモータードライバ,定格はロジック電圧入力の最小値が4.5Vで,制御信号のHIGHの最小電圧が3.5Vですが,実はスレッショルドが2.5V近辺にあるので,3.3V系でも使えます.
(回路面積に余裕があれば,本来はロジック変換をかませるべき.)

5-6.温度計

防水のことを考えると,白金抵抗で温度計を自作するのがベストなのですが,定電流回路と増幅回路を載せるスペースがなかったため,今回は適当な温度センサ「LM35DZ」で妥協しました.

5-7.スピーカ

社内に落ちていた,圧電スピーカ「SPT08」を使いました.

6.実装(ソフトウェア)

6-1.概要

ハードウェアが完成したので,次はソフトウェアです.

実装した機能は次の通り.

  • 様々な振動パターン
  • Bluetooth (BLE) を用いたコマンド・テレメトリ
  • 9軸センサを用いた,回転具合の推定
  • 最適モーター出力探索
  • タイマー機能

6-2.様々な振動パターン

まず,モーターをPWM制御することにより,モーターへの電流量を調整できるようにしました.
また,定常振動ではなく,「ブッ ブッ ブッ ブッ」のようなパルス的な振動モードなども実装してみました.

6-3.Bluetooth Low Energy (BLE) を用いたコマンド・テレメトリ

開発段階であるため,セントラルにはスマホではなくMac Bookを用いました.

定期的にまぜまぜ機よりテレメトリ(時刻,モーター出力,振動モード,各種センサ値などを含む)がBLE Notifyされます.

さらに,モーター出力や振動モード変更,後述するタイマー設定などのために,Mac Bookからまぜまぜ機へコマンドがBLE Writeによって送信できます.

6-4.9軸センサを用いた,回転具合の推定

はじめ,愚直に9軸センサの角速度を取得しましたが,まぜまぜ機自体の振動ノイズにより,全く意味のある値が取れませんでした.

そこで,角速度ではなく,磁気センサから方位 \(\theta\) を取得し,その方向角をもった単位ベクトルを足し合わせていくアルゴリズムを組みました.
数式にすると次式です.

$$ \tau = {\frac{1}{N}} \sqrt{ { \left( \sum^{N} \cos\theta \right)}^{2} + { \left( \sum^{N} \sin\theta \right) }^{2} } $$

\(\tau\) は \( 0 \leq \tau \leq 1 \) をとり,値が小さいほどサンプリング時間における方位のばらつきが均質である,つまり高速で回転していることを示します.

ただ,例によって地磁気センサのキャリブレーションは難しく,そこまで高い精度は出てない気がします.

6-5.最適モーター出力探索

8.で詳しく述べますが,実は鍋や水量によって適切なモーター出力が異なることが判明しました.
そのため,モーター出力を変化させながら 6-4.で実装した回転具合の推定値をモニタし,最もよく混ぜれるモーター出力を自動で探索する機能をつけました.

6-6.タイマー機能

「あと5分だけ混ぜておいて欲しい!」などの要求に答えるため,自動停止のためのタイマー機能を設定しました.
BLEでコマンドを打つとセットされ,指定時間が経過するとスピーカが鳴り停止します.

7.結果

結果的に,このくらい混ぜれるようになりました!
(上に乗ってる乾電池は重量調整用の重りです.)

引っかかって回転しなくなったら,自動的に振動モードを変えるなどの機能も実装されています.

8.得られた知見

無事,そこそこ混ざるようになりましたが,ここまでたどり着くまでに,まぜまぜの奥深さを知りました.

8-1.モーター出力パワーを上げればいいわけではない

最初は,「回らないんだったらモーター出力を上げればいいだけではないか.」と考えていましたが,実際はそうではありませんでした.
モーター出力を上げると,モーター回転数が上がるので,振動の振動数が上がります.
しかし,振動数が上がっても,トルクが大きくなる様子は観察されませんでした.
今回はモーターが1つしかなかったので試せませんでしたが,振動振幅を変えてみるとまた新しい発見があるかもしれません.

振動を回転に変えるのは,そう一筋縄ではいかないようです.

さらに面白いことに,鍋の径や入っている水の分量によって,最も速く回転するモーター出力が異なることがわかりました.
7.に載せたGIFアニメですが,スープよりも炒めもののほうが,最適なモーター出力が小さかったのです.

8-2.回転に影響があるパラメタとは?

鍋径や水量などの外部状態も,モーターの回転に影響を与えることがわかりました.
その他にも,まぜまぜ機自体の重さも重要だったようです.
特にモーター出力を上げた場合,まぜまぜ機自体が跳ねるだけで力が伝わっていないことも見受けられました.

今回作ったプロトタイプの実験で,

  • モーター出力(電圧,電流)
  • まぜまぜ機自体の重さ
  • 鍋の径,素材
  • 具材の量や水分量
  • 振動数や振動振幅

など,様々な要因がまぜまぜ機の回転に影響を及ぼしていることがわかってきました.
もしかしたら共振あたりが関係しているのかも...しれません.

4-1.で,鍋やフライパンに依存しない(鍋やフライパンを選ばない)気軽なまぜまぜ機がよいと言いましたが,三脚型でも環境依存性のないデバイスは難しそうです.

9.Future Works

今回のプロトタイプ開発はここまでです.
これを通じて,まぜまぜが意外と奥が深そうだということがわかってきました.

今回のやり残しとしては,

  • 振動から回転への力の変換の特性解明
    • 足の形状の検討
    • 適切な振動数,振幅などの追求
  • 外部連携
    • 温度情報などを共有し,IHクッキングヒーターなどの加熱器と連携
  • 大トルクが出せ,かつできるだけ環境に依存しない手軽なまぜまぜ機の追求
  • 作り込み
    • 防水にするとか,見た目をましにする,とか

などがあげられます.

10.おわりに

ハードウェアからソフトウェアの実装まで,限られた時間で完走するのは大変でしたが,いくつも発見があり面白くもありました.

様々な要因が絡み合って実際の現象として現れ,やってみないとわからない.
これこそが,コンピュータの中で閉じてしまう実装ではなく,実際の物理世界に干渉するデバイスの実装が好きな理由です.

クックパッドは,スマートキッチンというコンセプトの下,物理世界と積極的に関わろうとしているとても興味深い企業なのです.

デザインとエンジニアリングをつなげる取り組み

$
0
0

こんにちは、Komerco事業部デザイナーの藤井(@kenshir0f)です。
主にKomercoのサービスデザイン全般とView周りの開発を担当しております。

今回はKomercoの開発チームで実践している「デザインとエンジニアリングをつなげる取り組み」についてお話します。

なぜデザイナーが実装に入るのか

Komercoではユーザーさんへスピーディーに価値を届けるための取り組みを積極的に採用しています。
素早くリリースすることによって、早い段階でフィードバックをもらうことができるほか、
手戻りなどの失敗リスクを最小限に抑えることもできます。

デザイナーがデザインして、エンジニアが実装する、という流れの中で、
実機でのデザイン確認や細かい調整などで想定以上に工数がかかることがあるため、
見た目に関する部分はデザイナーが実装に入ることで、デザイン確認や調整の時間を巻き取とっています。

実践している取り組み

では実際に行っているデザインとエンジニアリングをつなげる取り組みをいくつかご紹介します。

KomercoFont

Komerco内で使われているアイコンフォントです。
iOSリリース時はアイコン画像をpngファイルで (@1x), @2x, @3xを用意していました。
新しいアイコンが出来上がったらPull Requestで画像を追加するフローでしたが、アイコン画像の管理コストが高いことや、画像容量も大きくなってきたためバージョン管理によるフォント形式に移行しました。

なおアイコンはGithubPagesに静的ページを用意することで、現在どのアイコンが利用可能か見えるようにしています。

f:id:kenshir0f:20181107144230g:plain

アイコンフォントを用意したので次は実際に使えるよう準備します。
Komercoでは管理画面をReactで開発しているため、IconのReactComponentを用意することでエンジニアがサクッと使えるようにしています。

// エビのアイコン<Icon name={'shrimp'} />

同様にiOSではこのような感じに使います。
ここはチームのエンジニアがKomercoFont導入の話をした時にシュッと作ってくれました。すごい。

// UILabel
label.kf.icon = .crab

// UIButton
button.kf.setIcon(.heart)

これでアイコンフォント(ttf)を更新するだけで各アイコンが簡単に使えるようになります。

Komercomponents

ReactのComponent集です。名前は仮なのでかっこいいやつに差し替えたいですね。。
上で述べたとおり管理画面のViewはReactで開発しているため、事前にデザインが当たっているコンポーネントを用意しておくことでエンジニアの見た目の調整に関する負担を減らし、裏側のロジックに時間をさけるようにしています。

import{ Icon, Button } from './../Komercomponents'// ユーザーアイコン(20px)<Icon name={'user'} size={ 20 } />

// プライマリーのボタン<Button color={'primary'} disabled={this.state.isLoading } onClick={this.deleteElement() }>削除</Button>

意図しない挙動を防ぐため、propsはホワイトリスト方式で受け取れるようにしました。
こちらもミニマムで作っていき、必要なステータスを受け渡したくなったら徐々に拡張して行く予定です。
TypeScriptで開発しているため事前に受け取れるpropsを提示したり、許可しない型を警告してくれるため安心してReactComponentを開発することができます。型便利ですね。

f:id:kenshir0f:20181107142320p:plain

またCSSはCSS Moduleを使うことで表示のロジックと見た目の責任を切り分けるだけでなく、新たに入るデザイナーでも簡単に編集できるようにしています。

import * as style from './style.css'<Icon className={ style.primaryColor } name={'komerco'}>

Komercomponentsは当初npmで管理しようと考えていましたが、
開発中にComponent化することが多いためGit Submoduleでインポートするようにしました。
このKomercomponentsは現在非公開ですが、公開しない理由も特に無いため充実してきたらOSSにしようという流れで開発を進めています。

Komercomponents API document

上記KomercomponentsのAPI仕様ページです。
Doczを用い、FirebaseのHostingに静的ページを置くことでwebからComponentsの仕様を把握できるようにしています。

f:id:kenshir0f:20181107145020p:plain

初めはStorybookも検討しましたが、作成・管理するコストがDoczの方が低いと感じたためこちらを採用しました。
propsに仕様のコメントを残すと自動で読み込むため、仕様書の作成コストが低くすばやく開発を進めることができます。

Lottieによるアニメーションの導入

アニメーションにはOSSライブラリのLottieを採用しています。
デザイナーがAfterEffectsからjsonファイルを書き出してそのまま実装まで行えるほか、軽量かつwebでも同じアニメーションを使うことができます。

f:id:kenshir0f:20181107142429g:plain:w400

現在は主に「スキ」のインタラクションで使用していますが、リッチな体験を提供するために今後アニメーションを使うシーンが多くなることを見越して「デザイナーがアニメーションを作成してそのまま本番に載せる仕組み」を用意しました。
KomercoではRxSwiftで書かれた処理が多いのでその部分に関してはエンジニアに相談しつつ、デザイナーが実装してPRでレビューをもらうことでエンジニアの負担を減らしながらデザイン要素を取り入れることができます。

f:id:kenshir0f:20181107142204p:plain

AutoLayoutを使ったデザイン調整

個人的に「デザイナーがやったほうがいいと感じたデザイン調整ランキング1位」はiOSのAutoLayoutでした。

f:id:kenshir0f:20181107143327p:plain:w300

主な理由として、

  • フォント周りにおいて、デザインデータのマージンとXcodeで入力した値は同じでも見た目は微妙に異なる
  • 「数px調整 ->ビルド ->確認 ->調整 ...」でビルドによる待ち時間と確認のコミュニケーション回数が多い

などがあります。調整のたびに毎回ビルドするのは大変ですね。。。 AutoLayoutは最初とっつきにくいと感じましたが、エンジニアとペアプロしつつ

  • leading, trailingなどの用語とそれらの位置関係の把握
  • Constraintsによる要素間のつなげ方

などを理解したうえで、簡単なPRを出すところから少しずつ慣れていきました。
それ以降は軽微なデザイン修正はデザイナーがAssignされて調整するようにしています。

おわりに

Komercoにおけるデザインとエンジニアリングをつなげる取り組みについてご紹介しました。今ある技術を使って少ないコストで開発効率を上げる仕組みを積極的に取り入れています。
体験設計やUIデザインだけでなくどうしたらユーザーさんにすばやく価値を届けられるか、その仕組みをデザインするのもデザイナーの腕の見せどころなので引き続き開発にも取り組んでいきたいと思います。

ではでは👋

【開催レポ】Cookpad Tech Kitchen #19 R&Dにおけるサービス開発者の仕事

$
0
0

こんにちは。広報部のとくなり餃子大好き( id:tokunarigyozadaisuki)です。

2018年11月1日に、Cookpad Tech Kitchen #19 R&Dにおけるサービス開発者の仕事を開催いたしました。クックパッドでは、Cookpad Tech Kitchenを通して、技術やサービス開発に関する知見を定期的に発信しています。

f:id:tokunarigyozadaisuki:20181118162207j:plain
部長の原島が司会を務めました

第19回は弊社の研究開発部から5名が登壇致しました。研究開発部ではサービス開発に積極的に取り組んでおり、今回は機械学習、スマートスピーカーの開発、他社を巻き込んだプラットフォームの開発などについてお話しました。 本ブログを通して当日の様子をご来場いただけなかったみなさまにもお届けしたいと思います。

発表プログラム

「スマートスピーカー向けサービス開発者のお仕事」

はじめにお話いたしましたのは、2016年新卒としてクックパッドに入社し、研究開発部 スマートキッチングループでスマートスピーカー向けのサービス開発を行う山田です。

f:id:tokunarigyozadaisuki:20181118161809j:plain

Alexaスキル「クックパッド」は「アレクサ、クックパッドで大根のレシピを教えて」のように話しかけるだけで、「ぶり大根、おでん、大根ステーキ」のような食べ方からレシピを提案するスキルです。山田からは「アレクサごっこ」と呼んでいる一人がアレクサ役、もうひとりが利用者となり、会話をしながら開発を進めていったプロトタイピングなどをご紹介しました。

「クックパッドのスマートキッチン開発」

同じく、スマートキッチングループに所属する伊尾木からは、クックパッドが考えるスマートキッチンについてや、キッチン家電向けにクックパッドのレシピを提供するスマートキッチンサービス『OiCy』に関してお話をさせていただきました。

f:id:tokunarigyozadaisuki:20181118161633j:plain

OiCyは、クックパッドに投稿されたレシピを機器が読み取り可能な形式(MRR: Machine Readable Recipe)に変換して機器に提供するサービスです。クックパッドはレシピとキッチン家電が連携することで、料理をする人の悩みや負担が軽減され、毎日の料理が楽しくなる、そんなスマートキッチンを目指して日々開発をしております。

MRR化についてはこちらでも紹介しておりますので、ご覧ください。

「クックパッドにおけるCloud AutoML事例」

3番目の発表は、ソフトウェアエンジニアで機械学習グループに所属する林田です。 f:id:tokunarigyozadaisuki:20181118162222j:plain

料理が楽しくなるマルシェアプリ『Komerco-コメルコ-』という新規事業でAutoML使った事例の紹介させていただいた後、「これからのサービス開発における機械学習」と題して、機械学習エンジニアがいなくてもサービス開発の道具として機械学習をうまく使っていこう、という旨のお話をさせていただきました。

「機械学習を用いた見栄えのいい料理画像抽出をサービスに活かすための取り組み」

次に発表させていただきました、機械学習グループに所属する三條は、2018年新卒としてクックパッドに入社した一年目の社員!

f:id:tokunarigyozadaisuki:20181118162226j:plain

機械学習をサービスで活用するためにどう評価したかと、実験時とサービス時のモデル評価のギャップの解消について、実例を用いながらご紹介しました。

「クックパッドにおけるチャットボット開発 / Chatbot Development at Cookpad」

最後は、2017年にクックパッドに中途入社、機械学習グループに所属し、自然言語処理を用いてサービス開発に従事している Huy Van phu quangです。

f:id:tokunarigyozadaisuki:20181118161951j:plain

検索キーワードを考えるのを面倒に感じるユーザーや、新しいレシピに出会うことができないといった課題をどう発見していったか、チャットボットでサポートするためにどのような開発手法を取っているか(こちらでも「botごっこ」は大活躍!)サービスの再設計までのなどについてお話致しました。

付箋形式でお答えするQ&Aディスカッション

Cookpad Tech Kitchenでは参加者のみなさまからの質問を付箋で集めております。第20回では、各発表後の質疑応答の時間も含め、みなさまにたくさんのご質問をいただきました。ありがとうございました! 

シェフの手作り料理

イベントに参加してくださったみなさまにおもてなしの気持ちを込めて、シェフ手作りのごはんをご用意し、食べながら飲みながらカジュアルに発表を聞いていただけるように工夫しています。

f:id:tokunarigyozadaisuki:20181118162138j:plainf:id:tokunarigyozadaisuki:20181118162158j:plain

おわりに

クックパッドに研究開発部ができて2年が経ち、今回ご紹介したことの他にも様々な取り組みがありました。クックパッドにおける研究開発部の役割は「社内外の最新の研究成果にもとづくサービスの企画と開発」で、具体的には「食や料理、レシピに関する研究成果」です。これらのシーズとユーザーのニーズを紐付け、他部署と一緒にサービス開発に日々取り組んでいるところです。今後も様々な取り組みに挑戦すべく、一緒に取り組んでいただける方を募集しておりますので、ご興味がある方は採用ページを是非ご覧ください。ご応募をお待ちしています。

https://info.cookpad.com/careers

次回のCookpad Tech Kitchenは、11月28日(水)、クックパッドのマイクロサービスプラットフォームの現状です! イベント情報についてはConnpassページについて随時更新予定です。イベント更新情報にご興味がある方は、ぜひメンバー登録をポチっとお願いします!

cookpad.connpass.com

Viewing all 726 articles
Browse latest View live