Catchpointを使ったWebページのパフォーマンス計測

技術部開発基盤グループの外村です。最近はクックパッドのレシピサービスのWebフロントエンドの改善に取り組んでいます。その一環でWebサイトのページロードのパフォーマンス計測をおこなっているので、今回はその取り組みについて紹介します。

Webページのパフォーマンスといっても、文脈によってそれが指すものは様々です。サーバーのレスポンスタイムのみを指すこともあれば、ブラウザがページをレンダリングするまでの時間を指すこともあります。また、レンダリング後にUIの操作やアニメーションがどのぐらいの速度で動くかというのもWebページのパフォーマンスの1つです。今回はブラウザでWebサイトを開いてからページが表示されるまでのパフォーマンス(ページロードのパフォーマンス)にフォーカスします。

継続的なパフォーマンスの計測

ページロードのパフォーマンスを計測する手法はいくつかあります。まず、簡単なのは Google Chrome の DevTools に付属している Lighthouse を使う方法です。DevTools の Audits タブを選択して計測をおこなうと、次のように詳細なパフォーマンスレポートを見ることができます。

f:id:hokaccha:20181129161257p:plain

DevTools を使った計測は便利なのですが、単発の計測では意図せずパフォーマンスが劣化したときに気づけませんし、マシンのスペックによって数値がばらけてしまうこともあります。パフォーマンス改善のための計測は、同一の環境から自動で定期的に計測することが重要です。

ページロードのパフォーマンスを自動で測定するツールはWebPagetestSpeedCurveNew Relic Syntheticsなど様々なものがあります。クックパッドでは、以前以下の記事でも紹介した、Catchpointというサービスを利用しています。

Synthetic Monitoring を活用したグローバルサービスのネットワークレイテンシの測定と改善 - クックパッド開発者ブログ

指標を決める

パフォーマンスの改善をおこなうために改善すべき目標の指標を決める必要がありますが、ページロードにおける指標は様々なものがあり、どの指標を改善すべきかはコンテンツの特性や目的によっても変わってきます。

例えば DOM のロード完了(DOMContentLoaded)や、サブリソースの取得まで含めたロードの完了(load)のイベントが発火するまでの時間はよく知られた指標の1つです。これらは簡単に取得できるというメリットがありますが、実際にページにコンテンツが表示されるまでの速度とは異なるため、改善の目安の1つとして使うのはよいですが、目標とする指標にするには不十分です。

DOM やリソースのロード時間と比べ、コンテンツが描画されたり、ユーザーが操作できるようなタイミングといった指標は、より実際のユーザー体験に近い指標を得ることができます。例えば以下のようなものがあります。

  • First Paint
    • 画面に何かしら(背景色などでもよい)描画されたタイミング
  • First Contentful Paint
    • テキストや画像などのコンテンツが最初に描画されたタイミング
  • First Meaningful Paint
    • ユーザーにとって意味のあるコンテンツが描画されたタイミング
  • Time To Interactive
    • ユーザーの入力に反応できるようになったタイミング

f:id:hokaccha:20181129161429p:plain
User-centric Performance Metrics | Web Fundamentals | Google Developers
Licensed under a Creative Commons Attribution 3.0 License.

First Paint と First Contentful Paint は簡単に機械的に判定できるものなので、ブラウザの API から取得できますし、標準化も進んでいます。一方、First Meaningful Paint や Time To Interactive は何を持って意味のあるコンテンツとするのか、ユーザーの入力に反応できる状態と判断するのか、という基準を決めるのが難しいので、曖昧さが残る指標ですが、実際のユーザーの体験に近い数値を取ることができます。

これらの指標が有効なケースも多いのですが、実際のブラウザによるページのロードはもう少し複雑で、ある特定のタイミングを測るだけでは不十分なケースも多くあります。そこで考えられたのが Speed Index という指標です。Speed Index は単純なある地点でのタイミングでなく、ファーストビューが表示されるまでの進捗を含んだ数値です。

f:id:hokaccha:20181129161758p:plain

上記の図において、A と B はファーストビューの表示速度は同じですが、A のほうは徐々にコンテンツが表示されており、B はファーストビューがでるまでほぼ真っ白な状態です。Speed Index は最終フレームと途中経過のフレームの差分を計算し、それを足し合わせることでスコアを算出します。そうすることで実際のユーザー体験により近い指標を得ることができます。上記のケースでは当然体験がよいのは A で、Speed Index のスコアも A のほうが低く(Speed Index は低い方がよい)なります。

もっと具体的な算出法方法については公式のドキュメントを参照してください。

Speed Index - WebPagetest Documentation

今回はこれらの指標を検討し、Speed Index を目標指標として採用することにしました。

Catchpointでの計測

Catchpoint では Speed Index の計測はデフォルトで有効になっていません。Advanced Settings から Filmstrip Capture を有効にすることで Speed Index を計測できるようになります。

f:id:hokaccha:20181129161843p:plain

Filmstrip Capture は特定の間隔で画面のキャプチャを取る機能で、このキャプチャを元に Speed Index が算出されます。Catchpoint ではこのキャプチャを取る間隔を 200ms 〜 2000ms で選ぶことができます。有効にして測定すると次のように、画面がレンダリングされていく様子がキャプチャで記録され、Speed Index のスコアが取得できます。

f:id:hokaccha:20181129161934p:plain

また、Speed Index 以外にも改善の助けとなる指標はたくさんあるので、それらをまとめてダッシュボードを作り継続して観測できるようにしています。

f:id:hokaccha:20181129162010p:plain

ページ遷移のパフォーマンス計測

ページロードのパフォーマンス改善は、初回アクセス時と別のページから遷移時、2度目のアクセス時などによって大きく性質がことなります。初回アクセス時には何もキャッシュを持っていない状態なのでクライアント側のキャッシュを使う方法は役に立ちません。一方、クックパッドのサイトで検索ページからレシピページに遷移するようなケースでは、検索ページにアクセスした時点で共通のアセットをキャッシュしたり、検索結果のレシピのページを先読みしてキャッシュしておくなどの対策が可能になります。

Catchpoint ではこのようなページ遷移時のパフォーマンスも測定することができます。

Transaction Test Type – Catchpoint Help

ページ遷移するためのスクリプトを設定に書くことができます。例えば検索画面にアクセスし、一番上のレシピをクリックしてレシピ画面に遷移するためには、以下のようなスクリプトを設定画面に書きます。

// Step - 1
open("https://cookpad.com/search/%E3%83%91%E3%82%B9%E3%82%BF")
setStepName("Step 1: Open Search Page")

// Step - 2
clickAndWait("//*[@id='recipe_0']/div[@class='recipe-text']/span[1]/a")
setStepName("Step 2: Transition To Recipe Page")

計測結果は各ステップごとに保存され、以下のように結果を見ることができます。

f:id:hokaccha:20181129162052p:plain

様々なキャッシュが効くので初期ロードのときよりも各種スコアがよくなっており、遷移時のほうが高速にページがロードされていることがわかります。

まとめ

Catchpoint を使ったWebページのパフォーマンス計測について紹介しました。パフォーマンスの改善についてはまだ着手し始めたばかりで、具体的な施策をおこなうのはこれからです。具体的な改善について成果がでたらまた別の機会に報告したいと思います。

クックパッド機械学習チームのメンバが働く環境と役割

研究開発部の takahi_i です。本稿ではクックパッド研究開発部の機械学習チームに所属するメンバがタスクに取り組む体制および、働く環境について紹介します。

準備

機械学習はそれら単体が学ぶのにコストが掛かる分野で、高い専門性を獲得するためには多くの時間をかける必要があります。そのため機械学習の専門家はソフトウェア開発を十分に経験する 機会を得られにくい状況にあります。

このような前提において民間企業で機械学習を導入する場合、二つの可能性が考えられます。一つは完全に分業する方向で、機械学習のエキスパートはモデルだけを作り、ソフトウェアエンジニアが導入 を引き受けます。もう一つは機械学習の実験、モデル作成から導入までを一人がおこないます。

分業による利点と問題点

機械学習は日進月歩の分野です。機械学習エンジニアやデータサイエンティストといった職が単体で存在します。特に大きな組織では、機械学習のモデル作成を担当する機械学習 エキスパート(リサーチャー)とモデルの導入および運用を担当するソフトウェアエンジニアは区別されていることが多いです。

分業体制の利点

分析を担当する機械学習エキスパートと導入を担当するソフトウェアエンジニアが担当箇所を分けることで、以下のような利点があります。

  1. 各自が自分の専門性を追求できる
  2. 分業による効率化

各自が自身の専門性を磨けるので、新規性のある構成を追求したいメンバは満足できる環境です。また、うまくリソース配分ができれば、各自は自分の専門性に適合するタスクだけが与えられるために 高い生産性が期待できます。

分業体制の問題点

次に問題点を考えてみると、以下のようなものがあります。

コスト

機械学習エキスパートが機械学習のモデルだけをつくればよいという環境を作るにはコストがかかります。シンプルには社内に Jupyter サーバを立てて、そこで作業をしてもらうのが考えられますが、実際にサービスで利用するにはもう少し大掛かりな機構が必要になります。Jupyter Notebook で作ったモデルを自動でプロダクション環境にデプロイする機構や、自動デプロイされたモデルのバージョン管理、自動テストをシステムとして作り込む必要があります。

また一つのプロジェクトを遂行するにはマネージメントのコストも発生します。よくあるのがモデルは完成したが、それを組み込んでくれるソフトウェアエンジニアのリソースが足りないため数ヶ月待たされるというものです。

さらに将来システムに問題が起こった時にも、複数名をアサインする必要があるため十分な人員を確保しつづけるのは頭のいたい問題です。

担当箇所が曖昧になりやすい

機械学習タスクの実装を細かく分業すればするだけ、だれが個々の箇所を担当するのかが曖昧になっていきます。たとえば多くの機械学習タスクは単純に学習器自体を適用するだけではなく、前処理 を駆使して精度を上げてゆきます。この前処理部分は実験をしている時に作られ Jupyter Notebook にべた書きされています。

この前処理部分がプロダクション環境に移植されないと、入力データをモデルに入れても動作しません。ではこの前処理部分をプロダクション環境に持ってゆくのは誰でしょうか。タスクやアルゴリズムを理解しているという意味では、機械学習エキスパートですし、システムへの組み込みに慣れていることを優先するのであればソフトウェアエンジニアが適任と言えます。

通常この前処理部分の組み込みはソフトウェアエンジニアがモデルを作った機械学習エキスパート指示を受けつつ作成します。残念ながら、この共同作業は機械学習エキスパートもソフトウェアエンジニアも理解が中途半端な部分がありつつ仕上げるので、バグが混入しやすいです。さらに、精度向上が必要になった場合、前処理の書き換えが必要になる場合があります。前処理の書き換えが発生するすると、共同作業が必要になりコストは更に膨らんでゆきます。

クックパッド研究開発部の体制〜メンバがモデルの配備まで責任を持つ

現在クックパッドの研究開発部では、機械学習のモデル生成のみを担当するメンバはいません。メンバ全員が機械学習エンジニアです。ここで言う機械学習エンジニアはモデルの作成からモデルの結果を配備するところまでの責任を持ちます。

このような一人で一気通貫するシステムにも利点と問題点があります。

利点

タスクを一気通貫して受け持つ体制には以下のメリットがあります。

責任の所在が分かりやすい(責任の空白地帯が発生しにくい)

分離型ではタスクのなかの自分が興味のある部分だけ貢献することになります。結果、矢継ぎ早に別プロジェクトに移りながらタスクをつまみ食いするモラルハザード(タスクホッピング)が起こりがちです。

このような状況だと問題が起こった時に、貢献した人はすでに別プロジェクトをしているから問題解決は別の人がやってくれという話になります。しかし実装から数ヶ月、数年経った機械学習プロジェクトの問題解決は実装した当事者でも難しい問題です。別の人が解決にあたる場合には、さらに大きなコストが掛かってしまいます。

これに対して個人が責任を負うシステムでは、基本モデルを作ったエンジニアが責任を持つので、責任がはっきりし前処理やつなぎ込み部分において責任の空白地帯が発生しません。将来問題が起こっても、作った(もしくは正式に引き継いだ)メンバが問題の解決にあたってくれることが期待できます。

省コスト

大規模なシステムの作り込みを必要としません。機械学習エンジニアは Jupyter Notebook で実験し、自分でコードを整形、ライブラリ化し、それらをプロダクション環境にデプロイします。すくなくとも通常のサービスへのデプロイではモデル配備のために特別に自動化されたインフラは必要ありません。

問題点

とはいえ、このやり方にも問題点があります。具体的には以下の問題点があります。

知見が共有しにくい

各自が一気通貫して作業するので、各タスクの知識を一人だけが保持するという事態が起こりやすくなります。そのため、タスクの実装が適当になってしまいやすいという問題があります。

クックパッドの研究開発部ではプロジェクトの規約を提案してくれたメンバがいて、プロダクションで利用されるプロジェクトはその規約にしたがって作られています。規約には、実サービスに導入するレポジトリにはテストをつけ CI を導入する。ほかに関連するリソース(S3)の置き場、利用する Role などがあります。

専門性を磨く時間が削られる

機械学習の結果をサービスに繋ぎこむ部分にもコストがかかります。そのため各自が専門性を磨く時間は分業体制に比べて少なくなります。この状況に対応するため、研究開発部として学習をサポートする仕組みを導入しています。「5%ルール」呼ばれる仕組みで、二週間に一回(半日の間)、新しい技術をキャッチアップする時間を自由に取得できるようになっています。

さらに、この問題についてはクックパッド社の社員に提供されている作り込まれたインフラでかなり改善できていると感じています。以下の節で、機械学習エンジニアがインフラからどのような恩恵を受けているのかについて解説します。

インフラによるサポート

クックパッドで機械学習プロジェクトの作業をしていて、助かっていると感じる部分が二つあります。一つは DWH(データウェアハウス)、もう一つは各自が構築できるインフラです。

データウェアハウス(DWH)

クックパッドの社員は DWH を使って必要なデータをほぼ全て取得できます。社員がデータ取得するには分析用 SQL を入力するだけです。データ取得 SQL は機械学習用の前処理スクリプトからでも埋め込んで実行できます(詳しくはこちらを参照してください)。これによって、日々変化してゆくデータを取り込んだ状態の機械学習モデル及び出力結果をすくないコストで提供し続けられます。

作り込まれたインフラ

現在、クックパッドのインフラは ECS 上に構築されています(くわしくはこちらを参照してください)。提供される仕組みのおかげで機械学習エンジニアはプロダクション環境にインスタンスを自由に構築できます(もちろんレビューを受ける必要はあります)。

我々が機械学習に関するコンポーネントをプロダクション環境に構築するには、まずバッチや API サーバを Docker コンテナで動作するようにまとめたレポジトリを作ります。次に、Jsonnet で記述する設定ファイルに Docker イメージ、Role、環境変数などの設定を記述します。このような環境だとサーバ構築にコストがかからないですし、必要であればサーバの構成(CPU、メモリ)も設定ファイルの書き換えにより簡単に修正できます。チーム間の複雑なやり取りが必要ないので、機械学習エンジニアはすくないコストでプロダクション環境に機械学習周りの計算機リソースを構築できます。

研究開発部における省力化の取り組み

これまで述べてきたように我々は社内環境に助けてもらっていますが、研究開発部でも実験や導入作業が効率化できるようにいろいろな取り組みをしています。

一つには研究開発部にはインフラに強いメンバがいます。彼らが高速な研究開発を支える GPU 計算機環境を作ってくれ、メンバが必要とする計算機リソースを常に確保できる状態になっています。

機械学習エンジニア自身も各自がツールをつくって自分の業務を効率化するのに役立てています。たとえば Kelner という爆速で Keras のモデルから API サーバを構築するツールを作っているメンバもいますし、私も各プロジェクトごとに異なるポートフォーワードの設定を管理するツール、pfmを自作して自身の業務を効率化してます。

また、機械学習プロジェクトはタスクは異なっても、デプロイ方法は似ているものが多いです。たとえば、機械学習が出力する判別結果の多くはいくつかの方法で利用されます。DB や Redshift のテーブルに入れる。API サーバを立てる。検索エンジンのインデクスに登録するなどです。このあたりはタスクが変わってもやり方は変わらないため、過去の Issue やそれらを抽象化したドキュメントが役に立ちます。チームメンバが各自経験したタスクをもとにしたドキュメントを書いてくれているので、自分自身が 初めてやるタスクでも比較的低コストに実装できる環境になっています。

まとめ

このエントリではクックパッドの研究開発部における機械学習エンジニアの役割について解説しました。クックパッド研究開発部は今後も様々な取り組みに挑戦していきます。メンバを募集しているので、ご興味がある方は是非ご応募ください!

最新のログもすぐクエリできる速くて容量無限の最強ログ基盤をRedshift Spectrumで作る

こんにちは。去年の今頃は Rust を書いていました。 インフラストラクチャー部データ基盤グループの id:koba789 です。

背景

クックパッドではデータ基盤の DBMS として Amazon Redshift を利用しています。 既存のデータ基盤について詳しいことは クックパッドのデータ活用基盤 - クックパッド開発者ブログ を参照してください。

今まで、ログは数時間に1度、定期実行ジョブで Redshift 内のテーブルにロードしていました。 ロードジョブの実行間隔が "数時間" と長めなのは、Redshift のトランザクションのコミットが遅いためです。 クックパッドでは数百ものログテーブルがあるため、仮に1分おきにすべてを取り込もうとすると秒間数回以上のコミットを行わなければなりません。 このような頻繁なコミットは Redshift 全体のパフォーマンスを悪化させてしまいます。

サービスの開発者はリリースした新機能の様子をすぐに確認したいものです。 にもかかわらず、ログがクエリできるようになるまで、最大で数時間も待たなければなりませんでした。 最悪の場合、その日のうちに確認することは叶わず、翌朝まで待たされることもありました。 これではサービスの改善を鈍化させてしまいます。

また、問題はもう一つありました。 当然ですが、ログは日々単調増加し、ストレージの容量を消費し続けます。 一方で、Redshift のストレージサイズはノード数に対して固定です。 ノードを追加すればストレージを増やせますが、ノードには CPU やメモリも付いているため、ストレージが欲しいだけの場合は割高です。 すなわち、ログにとって、Redshift のストレージは高価だったのです。

求められているもの

絶え間なく流れ続けるログを遅延なくロードし続けられるログ基盤が必要でした。 もちろん、ロードされたデータはなるべくすぐにクエリ可能になるべきです。

その上で、クエリの実行速度を犠牲にすることはできません。 クエリが遅くなれば夜間のバッチジョブの実行時間に影響が出ます。 そのため、従来の Redshift 内のテーブルへのクエリと同等かそれ以上のクエリ速度が求められていました。

そして最後に、願わくばストレージが安くて無限にスケールすることを。

この要件だけ見ると BigQuery を使えばいいのではないかと思われるかもしれません。 確かに BigQuery なら上記の要件は満たせるかもしれません。 しかし、Redshift にある PostgreSQL 互換の接続インターフェイスや AWS IAM との連携などの機能は BigQuery にはないため、Redshift を使うことで既に達成できているその他の要件が満たせなくなってしまいます。 また、大量のデータをクラウドプロバイダ間で日々転送しつづけることによって発生する追加の転送料も問題になります。

以上のような理由から、BigQuery に乗り換えるだけでは我々の理想は達成されないと判断しました。

Redshift Spectrum

まずはじめに、最強のログ基盤の一翼を担っている大切なコンポーネントである Redshift Spectrum を紹介します。

Redshift Spectrum は Redshift の機能のひとつです。 Redshift Spectrum を用いると、Redshift 内から S3 に置かれたデータを直接クエリすることができます。 にもかかわらず、Redshift 内のテーブルと JOIN することもできます。 つまり、S3 に置かれたデータを通常のテーブルと同様に扱うことができるのです。

Redshift Spectrum で読めるテーブル(以下、外部テーブル)の内容は Redshift へではなく、S3 に直接書き込みます。 そのため、トランザクションによる保護はなく、同一トランザクション内であっても、複数回の読み取りはそれぞれ別の結果を返す可能性があります。

しかし、今回はこの特性を逆手にとり、頻繁なロードを可能にしました。 Redshift の内部テーブルへの書き込みと違い、S3 への書き込みならコミットのパフォーマンスに悩まされることはない、という算段です。

そしてみなさんご存知のとおり、S3 は容量が事実上無限にスケールして容量単価も安価なストレージですので、最後の願いが叶えられることは言うまでもありません。

高速なクエリのために

Redshift Spectrum を活用するにあたって、高速なクエリを実現するために実践せねばならないプラクティスがいくつかあります。 これらのプラクティスは AWS の開発者ガイドを参考にしています。

Amazon Redshift Spectrum クエリパフォーマンスの向上 - Amazon Redshift

まず1つ目はパーティション化です。

Redshift Spectrum ではパーティション化をしないとクエリのたびにテーブルの全データをスキャンすることになってしまいます。 今回のケースでは、ログレコードの発生時刻の日付でパーティションを切ることにしました。 パーティション化によって、データオブジェクトの key はスキーマ名・テーブル名のあとに、さらに日付で分割され、以下のようなレイアウトになります。

  • hoge_schema.nanika_table/
    • dt=2018-08-09/
      • 001.parquet
      • 002.parquet
      • ...
    • dt=2018-08-10/
      • 008.parquet
      • 009.parquet
      • ....

2つ目は列指向フォーマットの利用です。

Redshift Spectrum では、CSV や改行区切りの JSON などの一般的な行指向のテキストファイルもクエリすることができますが、クエリをより効率的かつ高速にするには、列指向フォーマットの利用が有効です。 列指向フォーマットにも様々な種類がありますが、今回のケースでは Parquet を採用しました。

3つ目は、各データオブジェクトのサイズを64MB以上の均等なサイズに揃えることです。

あまりに小さなデータオブジェクトは I/O オーバーヘッドの割合を増加させたり、Parquet の圧縮率を低下させたりします。 また、データオブジェクトのサイズの偏りは分散処理の効率性に悪影響を及ぼします。

上記の記事には、以上の3つの他に、効率的なクエリの書き方についてのプラクティスも紹介されていますが、ログのロードではそれらは関係ないため、ファイルの配置に関する内容のみを取り上げています。

Overview

f:id:koba789:20181121111431p:plain

Prism は Redshift Spectrum にログをロードするために私が開発したソフトウェアです。 Prism は3つのコンポーネントからなります。

1つ目は Prism Stream です。 これは S3 に到着した JSON 形式のログオブジェクトを Parquet に変換するコンポーネントです。

2つ目は Prism Merge です。 これは Prism Stream が書き出した細切れのデータオブジェクトを適切なサイズに結合(マージ)するコンポーネントです。

3つ目は Prism Catalog です。 日付変更後に当日分のパーティションを作成したり、階層の切り替え(後述)を行ったりするコンポーネントです。

弊社のコンテナ基盤である Hako を用いてスポットインスタンス上にデプロイされています。 スポットインスタンスの急な停止に耐えるため、上記のコンポーネントのすべての処理は、途中で突然終了してもデータの欠落や不整合が発生しないように設計されています。 Prism は RDB の読み書きのみならず、S3 への書き込みなども行います。 そのため、データの整合性については単に DBMS のトランザクションに委ねるというわけにはいかず、ケースバイケースでのケアが必要になります。*1

このようにすべての処理を冪等ないしはアトミックに実装したことの嬉しい副作用として、ネットワークや S3 の不調などによって偶発的にエラーが起きても、リトライするだけで回復できるという点があります。 もっとも、勝手にリトライするので人間がそれを気にすることは稀ですが。

また、S3 に配置したログオブジェクトや各パーティションのメタデータの DB として PostgreSQL を用いていますが、ログオブジェクトの数に比例して行が増えてしまうテーブルはローテート可能な設計にするなど、スケーラビリティのための工夫をしています。

階層化されたパーティション

各パーティションは prefix を用いて S3 上で論理的に階層化されています。 階層は SMALL と MERGED に分かれており、Glue カタログにはパーティションごとに SMALL では prefix が、MERGED ではマニフェストファイルの key が登録されています。 つまり、Redshift Spectrum が読めるのはパーティションごとに SMALL 階層か MERGED 階層のどちらか一方のみです。 当日(最新)のパーティションは初期状態で SMALL 階層を参照しており、マージ処理後、Prism Catalog によって順次 MERGED 階層を参照するように切り替わっていきます。

SMALL 階層のデータは分単位で細切れになっているため、オブジェクトサイズが最適ではなかったり不揃いだったりしており、高速なクエリには向いていません。 一方 MERGED 階層のデータは適切なサイズに揃えられており、高速なクエリのためのプラクティスに沿っています。

これは、低レイテンシなロードが必要になる場面と、高速なクエリが必要になる場面の違いを踏まえた設計です。

  • 直近(1日程度)のデータについては低レイテンシである必要があるが、クエリは低速でよい
    • "直近のデータ" である時点で対象が小さいため、クエリの速度は問題にならない
  • より古いデータについてはクエリは高速でなければならないが、ロードのレイテンシは高くてよい
    • 「今さっき届いた昨日分のログ」をすぐに見たいということは稀である

SMALL 階層のように細切れのオブジェクトを並べるだけではクエリが低速になってしまいますが、適切なサイズに粒を揃えるために単にバッファリングしてしまってはレイテンシが大きくなってしまいます。 これは低レイテンシなロードと高速なクエリを両立するための設計です。

データの流れと階層切り替え

では当日のパーティションに書き込まれた当日分のデータの流れを追ってみましょう。

まず、到着したログオブジェクトは Prism Stream によって Parquet に変換され SMALL 階層に書き込まれます。 この時点で、Redshift から読むことが可能になります。

その裏で、SMALL 階層に書き込まれたログオブジェクトは定期的に Prism Merge によってマージされ、マージ後のデータオブジェクトは MERGED 階層へ書き込まれます。 ただしまだこの時点では Redshift から MERGED 階層のデータを読むことはできません。

日付が変わったあと、Prism Catalog は SMALL 階層にある分のデータがすべて MERGED 階層に揃ったことを確認します。 確認が取れると、Prism Catalog は MERGED 階層にあるオブジェクトの一覧をマニフェストファイルに書き出し、Glue カタログにその key を登録することで、階層の切り替えをします。 この処理が走ることによって初めて、パーティションの参照先が MERGED 階層に切り替わります。 もしも MERGED 階層に SMALL 階層と同じだけのデータが揃う前に切り替えてしまうと、それまで読めていたデータが一部減少することになってしまいます。

Out-of-Order Data

当日に到着した、当日分のデータの流れについて説明しましたが、現実にはログは大幅に遅れて届くこともあります。

例として、モバイルアプリの行動ログの場合を説明します。 ユーザーが通信状況の悪い環境でアプリを操作したとします。 すると、ログレコード自体はその場で生成されますが、ログを送出することに失敗します。 クックパッドのモバイルアプリではログの送出に Puree というライブラリを使っており*2、上記のように送出に失敗したログは一旦端末に保管され、次の送出のチャンスを待ちます。 ここでユーザーがアプリを終了させ、翌日になってから通信状況の良好な環境で再度起動したとします。 通信状況が回復したため、アプリは前日の行動ログを再送します。 するとログ基盤には到着時刻に対して発生時刻が1日前になっているレコードが到着します。 ほかにも様々な理由により、ログのレコードはバラバラの順序で到着します。

そのため、日付が変わろうと当該日の SMALL 階層への書き込みが止むことはありません。 つまり、Prism Catalog が確認をしているその最中にも Prism Stream が新たなログオブジェクトを SMALL 階層に書き込むかもしれないということです。 これでは Prism Catalog が "SMALL 階層にある分のデータがすべて MERGED 階層に揃ったことを確認" することができません。

この問題を解決するため、Prism では「締め」という概念を導入し、SMALL 階層を「締め」前に書き込む LIVE 階層と「締め」後に書き込む DELAYED 階層に分割しました。 そして、SMALL 階層のうち、Redshift Spectrum が読み取り可能な部分は LIVE 階層のみとしました。

  • hoge_schema.nanika_table/
    • live/ (LIVE 階層)
      • dt=2018-08-09/
        • 001.parquet
        • 002.parquet
        • ...
    • deleyed/ (DELAYED 階層)
      • dt=2018-08-09/
        • 010.parquet
        • 011.parquet
        • ...
    • merged/ (MERGED 階層)
      • dt=2018-08-09/
        • 001-007.parquet
        • 008-013.parquet
        • ...

「締め」の後では LIVE 階層のデータが増えないことが保証されるため、Prism Catalog が階層切り替えの判断をする際には、LIVE 階層のデータがすべて MERGED 階層に揃っているかどうかを安心して確認することができます。 LIVE 階層のデータが MERGED 階層に揃った後では、どのタイミングで切り替えてもデータの減少は起きませんので、LIVE 階層と MERGED 階層の比較だけもって階層切り替えをすることができます。

まとめ

最強のログ基盤を手に入れるために開発したソフトウェアと、その設計についてご紹介しました。 Redshift Spectrum のようなクラウドサービスはとても大きくて複雑なコンポーネントですが、それ自体の理解はもちろんのこと、自分たちの課題をじっと見つめ、ひとつひとつ丁寧にトレードオフを選択していくことで強力な武器となります。

Prism の各処理をいかにリトライ可能にしたかなど、まだまだ 自慢 解説したい内容は尽きないのですが、それについて書き始めるといつまで経っても本記事を公開できそうになかったため、ここで筆を置かせていただきました。

クックパッドでは絶対ジョブをリトライ可能にしたいエンジニアやデカいデータなんとかしたいエンジニアを募集しています。 Prism の設計や実装について興味があるという方はぜひともご応募ください。

*1:基本方針としては、なんらかアトミックな値の書き換えによってコミットとすることで Atomicity を作り込むというパターンですが、ここに記すには余白が狭すぎる

*2:良い感じにログを収集するライブラリ、Puree-Swiftをリリースしました - クックパッド開発者ブログ