Genymotion On Demandを使うようになってAndroidのCIがさらに1分短縮した話

こんにちは。技術部モバイル基盤グループの門田(@_litmon_)です。

モバイル基盤グループでは、エンジニアの方々が快適に開発できる環境を整えるため、日々アプリのビルド時間やCIの実行時間などを短くする方法を模索しています。

今回は、Genymotion On Demandを使ってみた結果、CI上でのAndroidアプリのinstrumentation testの実行時間が1分短くなった話をしようと思います。

前回のあらすじ

今回の記事は、OpenSTFでAndroidのCIを2倍早くする の続編のような記事で、AndroidのCI環境を整えている話です。 まだ読んでいない方はぜひ上の記事から読むことをオススメします。

前回は、Jenkins上でAndroidのエミュレータを起動して使用する方法から、OpenSTFというリモートで実機端末を操作することが出来るオープンソースツールを使用する方法に切り替えた結果、エミュレータの起動時間などが削られ実行時間が約9分近く削ることが出来ました。

しかし、OpenSTFをしばらく運用していくといくつかの問題点が見つかりました。

OpenSTFをCIで使う際の問題点

OpenSTFはエミュレータの起動を待つこと無くテストを行うことが出来るため、とても優秀なツールだったのですが、実際に運用を進めていくと以下のような問題点が見つかりました。

  1. OpenSTFサーバー自体が不安定になることが時々あり、サーバーのプロセスが終了してしまうことが度々あった
  2. 実機の状態が不安定で、時々接続に失敗することがあったりしてメンテナンスコストが高い
  3. CI専用の端末を用意する必要があり、slaveごとに端末が必要になるのでスケールしづらい

1.のOpenSTFサーバーが不安定な点に関しては、正しくメンテナンスを行うことで回避することが出来たかもしれませんが、しばらくはプロセスが終了しており失敗している場合は手作業で立ち上げ直すという運用を取っていました。だいたい、2週間に一度は調子が悪くなり、そのたびに再起動を行っている、という状況でした。

また、2.に関しては、実機の状態が不安定で"なぜか"OpenSTFに認識されないときがあり、そのたびに検証端末が置かれた棚を開きにいっており、毎回原因を追求することまではせず運用でカバーしていました。そのため、端末を管理するコストが大きくなっていました。

現状の問題点

ということで、現状の問題点を整理すると、

  • 自社でOpenSTFサーバーを立てると相応のメンテナンスコストがかかる
  • 物理端末に依存することで、端末の管理コストがかかりスケールしづらい

という2点が挙げられます。

これらの問題点を解決するため、様々なサービスを検討した結果、Genymotion On Demandにたどり着きました。

Genymotionとは

Genymotionは、Genymobile Inc.が出している非公式のAndroidエミュレータです。 Android Studioに標準で搭載されている公式のエミュレータよりも多機能なエミュレータで、x86仮想化を使った公式エミュレータが利用できなかった頃はお世話になったAndroidエンジニアのみなさんも多いのではないでしょうか?

Genymotion On Demandとは

Genymotion On Demandは、そんなGenymotionをAmazon EC2(EC2)インスタンス上で起動し、扱うことが出来るものです。 EC2インスタンス上に起動したエミュレータとADB接続でき、ADBコマンドから端末を操作することが出来るすぐれものです。

Genymotion On Demandを導入することで、物理端末の制約から逃れ、快適なCI環境を手に入れることが出来ると考え、クックパッドでは5月頃から導入を始めています。

Genymotion On Demandの使い方

ここで、簡単にGenymotion On Demandの使い方を紹介します。

といっても、手順は公式のチュートリアルにとてもよくまとまっているので、そのとおり進めるだけで簡単に使うことが出来ます。

Genymotion on Demand – Tutorial –

おおまかな流れとしては、以下のような形になります。とても簡単ですね。

  • AWS ConsoleからGenymotion On Demandのインスタンスを購入する
  • sshでエミュレータに接続し、ADBを有効に設定する
    • この設定はインスタンスごとに一度行えばよく、再起動時には不要
  • sshでport:5555に対して接続することで、エミュレータをADBで認識できるようにする

Genymotion On Demandで使用できるAndroid OSバージョンは、現在5.1, 6.0, 7.0の3種類になります。クックパッドでは、導入時の最新である6.0を使用しています(5月時点では5.1, 6.0の2バージョンだった)。

また、Genymotion On Demandでは、EC2のインスタンスタイプも指定できますが、t2.smallではテスト実行時間が若干遅くなるものの、それぞれ大きな差はありませんでした。 クックパッドでは現在m4.largeを使用しています。

CI上でのGenymotion On Demandの利用

現在、クックパッドではJenkinsのSlaveとしてEC2インスタンスを用いています。(参照: OpenSTFでAndroidのCIを2倍早くする)

そして、Slave 用のインスタンスの起動時に localhost:5555 を Genymotion On Demand のインスタンスへと転送する簡易プロキシを起動しています。 こうすることで、Slaveを立ち上げた時点ですでにGenymotionのエミュレータがADBに認識された状態になるため、Job側でなにも設定することなくエミュレータを使うことが出来ます。

そのため、既存のJobの設定を編集する際も、OpenSTFのプラグインの設定を無効にするだけで簡単に移行が完了します。

f:id:litmon:20170822115529p:plain

導入しただけでinstrumentation testの実行時間が1分短く!

導入してしばらく様子を見ていたのですが、明らかにJobの実行時間が短くなっているのを体感しました。 以下の画像は、縦軸が時間で横軸が実行したジョブの番号を示しています。そして、 #1899 以降がGenymotion On Demandを導入した後になっています(※ときどき失敗しているのは、不安定なテストがあるせいです。気にしないでください)。

f:id:litmon:20170822115903p:plain

このグラフから分かる通り、7分近くあったジョブの実行時間が6分弱に収まっていることが分かります。約1分弱の短縮に成功しました!🎉

詳しく状況を見てみると、以下のようになっていました。

  • 端末との接続を確立するまでで 20秒 短縮
  • ./gradlew :cookpad:connectedAndroidTestStagingDebug が 45秒 短縮
  • ./gradlew :cookpad:uninstallAll が 10秒 短縮

なんと、instrumentation testの実行時間が45秒も短くなっているではありませんか! それだけではなく、端末との接続時間や、アプリのアンインストール時間が短縮されていることが分かります。

仮説ですが、OpenSTFは自社のローカルネットワーク上で起動しており、Genymotion On DemandはJenkins Slaveと同じEC2インスタンスのため、ネットワークの接続(apkの転送時間など)に影響があったのではないかと考えています。

安定感がスゴイ!

そんなGenymotion On Demandですが、やはり気になるのは安定性です。 現在、導入して3ヶ月ほど経ちました。弊社のJenkins Slaveは2台あり、内1台はフルタイムで活動しています。そして、Genymotion On DemandのインスタンスはSlaveの起動時に同時に起動し接続しているため、内1台は常に活動したままです。

にも関わらず、5月31日に起動して以来今までGenymotionのエミュレータの調子がおかしくなったことはありません。すごい安定性ですね! これだけ安定して使えるのであれば、ということで、6月の半ば辺りから徐々に既存のJobで使われているOpenSTFをGenymotion On Demandに置き換えていって、今ではほとんどのJobがGenymotion On Demandを使うようになっています。

Genymotion On Demandの制限・注意点

ここまで、Genymotion On Demandの良いところをたくさん挙げてきましたが、いくつか制限もあります。

  • Google Play Serviceが利用できない
  • instrumentation testでタイムゾーンに依存するテストがある場合、失敗する可能性がある
    • 一度これでハマってテストが落ち続けていたので、注意する…
    • エミュレータのタイムゾーンを変更することは可能です

現在、クックパッドの日本のアプリでは上記2つともそこまで大きな問題ではないのですが、プロジェクトによっては導入する際の障害になりうるので、参考になればと思います。

まとめ

OpenSTFを使った実機でのinstrumentation testをGenymotion On Demandを使ったGenymotionエミュレータでのテストに置き換えた結果、CIの実行時間が1分近く短縮されました。また、安定性やスケーラビリティなど、実機で管理していた際の問題点も解消することが出来ました。

クックパッドでは、アプリの開発ももちろんのこと、こういったアプリのビルドやCI周りに対しても様々な取り組みを積極的に行っています。 アプリの基盤の仕組みを整えたり、新たに作り出していける、いきたいAndroidエンジニアのみなさんを募集しています。 もし興味があったらぜひぜひ遊びに来てください。お待ちしています!

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

2nd Hackarade: Machine Learning Challenge

研究開発部の菊田(@yohei_kikuta)です。機械学習を活用した新規サービスの研究開発(主として画像分析系)に取り組んでいます。 最近読んだ論文で面白かったものを3つ挙げろと言われたら以下を挙げます。

以前本ブログで紹介した Hackarade: MRI Internal Challenge ですが、その第二回として機械学習を題材にしたハッカソンが七月末に開催されました。 Hackarade ではエンジニアにとって長期的に有益となる技術を題材にしようという想いがあります。 今回はクックパッドの研究開発部が発足して一年経ち成長したというタイミングも重なることもあり、機械学習こそが時宜にかなったものであろうということでテーマが決まりました。

隆盛を極めている機械学習をほぼ全てのエンジニアが経験するという有意義な会となりましたので、この記事ではその様子についてお伝えします。

第二回 Hackarade の概要

第二回 Hackarade の概要を簡単に紹介します。 同様のイベントを開催しようと考えている方も少なくないと思いますので、参考になれば幸いです。

目標

目標は次のように設定しました。 全エンジニアが参加するイベントなので知識や経験のばらつきが多いことを考慮し、全員に持ち帰ってもらいたいものをベースとしつつ、機械学習に詳しい人にも有益となるよう発展的な内容も盛り込むよう努力しました。

  • 参加者全員
    • 機械学習を自分の言葉で定義できるようになる
    • 機械学習がどのような問題に適用できるのか理解する
    • 自分自身で機械学習のモデルを作る経験をする
    • 機械学習に関連する話題に興味を持つようになる
  • 機械学習に強い興味のある参加者
    • 独力で機械学習の勉強を続けていけるようになる
    • 最先端の機械学習トピックの一端を理解する
    • サービス改善や開発に機械学習を使って貢献できるようになる

時間割

時間割は次のように設定しました。 講義のコマでは私が講義をして、実習のコマではクックパッドのデータを使って機械学習を体験し、ハッカソンのコマでは各自が興味のあるトピックに取り組むという流れで進めました。 機械学習に馴染みが薄い人も多かったので、系統的な講義や実習を多めにして実感を掴んでもらえるよう留意しました。

  • 10:00-10:10 オープニング
  • 10:10-11:00 [講義] 機械学習とは何か
  • 11:10-12:00 [実習] レシピ分類(テキストデータ)
  • 13:00-14:30 [講義] Deep Learning(画像分析)
  • 14:40-16:10 [実習] レシピ分類(画像データ)
  • 16:20-19:00 [ハッカソン] 各自が興味あるトピックにチャレンジ
  • 19:00- パーティー & 成果発表

講義と実習の内容紹介

講義と実習で具体的に何をやったかという内容を簡単に説明します。資料は後日近い内容のものをインターンでも使用してそちらを公開しますので、興味のある方は今しばらくお待ち下さい。 クックパッドには非日本語話者も多いため、説明は日本語で実施しましたが、講義資料や実習資料などは全て英語で作成しました。

全エンジニアが参加する大きなイベントなので、資料作成はかなり気合を入れて一ヶ月前から着手しました。 資料の情報密度は相当高いものとなりましたが、参加者の能力を信じてやり切りました。 結果的には、消化不良の部分も当然あったものの、非常に満足度の高いイベントとすることができました。

講義

講義は「機械学習とは何か」と「Deep Learning(画像分析)」の二本立てでした。

機械学習とは何か

機械学習とは何かという定義から始まり、機械学習を俯瞰できるように以下の内容を説明しました。 機械学習界隈でよく出てくる言葉の意味や関係を説明し、頭の中を整理できるような構成になっています。 また、自己学習ができるように機械学習を学ぶための書籍やウェブ上の有用な情報などもまとめて共有しました。

  • 機械学習が使われている事例
  • 回帰や分類など、どのような問題に機械学習を適用できるのか
  • 機械学習の学習アルゴリズムの種類
  • 機械学習をサービスで活用するためのポイント
  • 機械学習の発展を追うのに有用な情報源

講義では機械学習の “expert” になるための最短経路の話もしました。 当然ここで述べている “expert” は冗談ですが、最低限を経験してみるという意味ではこれくらいの内容が必要かなと考えており、Hackarade でも可視化の部分以外は体験してもらうことにしました。

Deep Learning(画像分析)

Deep Learning に関しては、基礎的な説明の後に CNN にフォーカスして詳しく説明しました。 概念だけでなく具体的な演算としてどんなことをしているかにも踏み込み、Deep Learning の内部では実際に何が行われいるかが理解できるような構成になっています。 クックパッドでは画像分析(に限らずですが)の様々なタスクに取り組んでいるので、その適用事例に関しても共有しました。

  • Deep Learning の定義(なぜ昔のアイデアがうまくいくようになったのか)
  • 表現力、段階的で自動的な特徴量抽出、などの Deep Learning の特徴
  • CNN の動機とその基礎的な構成要素
  • CNN の進化
  • クックパッドにおける CNN の応用事例

一時間半の講義でしたが、内容が凝縮されたかなり濃い講義になりました。 パーセプトロンや誤差逆伝播法のような基礎から、以下のような CNN の進化に関しても少し言及しました。

この後に各モデルの鍵となるアイデアとその意味を説明するスライドが続きます。 時間的に一つ一つを丁寧に説明することは出来ませんでしたが、こうやって主要なモデルをいくつか並べてみるとどんなアイデアが鍵になって発展しているのかが分かり、興味深いですよね。

実習

実習は「レシピ分類(テキストデータ)」と「レシピ分類(画像データ)」の二本立てでした。 分析の環境を効率よく構築するために、準備した Dockerfile を用いて各自のノートPCで docker image をビルドしてもらい、立ち上げたコンテナで jupyter notebook を使って分析をするという形にしました。 そのため GPU は使用しませんでしたが、nvidia-docker を使えば同じスクリプトで GPU を使った分析もできるようになっています。

レシピ分類(テキストデータ)

レシピのタイトルや材料や手順のテキストデータを特徴ベクトル化して、該当のカテゴリ(e.g., ご飯もの、スイーツ)のレシピか否かを当てる二値分類モデルを作成しました。 テキストデータが対象だったため、MeCab を用いた形態素解析、不要な情報を除くための各種前処理、tf-idf を用いた特徴ベクトルの作成などが経験できるように準備をしました。 モデルは scikit-learn の Random Forest と Xgboost の Gradient Boosting Decision Tree を使いました。

そもそも jupyter notebook に不慣れだったり特徴ベクトルが具体的にどのような値になっているか不明瞭であったりで難しい点もありましたが、モデル構築の経験やモデルが正解する場合や間違える場合の具体的な例を見るなどして、実際のサービスで扱っている問題設定と同様のものを経験する機会となりました。

レシピ分類(画像データ)

まずは MNIST のデータを用いて、講義で学んだ MLP や CNN といったモデルを動かしてみました。 簡単にモデルを構築できるように、今回は TensorFlow backend の Keras を使いました。 一通り経験した後には、オリジナルのモデルを構築してその精度を確かめてもらいました。

次にレシピの画像からどのカテゴリ(e.g., パスタ、ラーメン)のレシピかを当てる多値分類モデルを作成しました。 実際の業務と近い分析をしてもらうために、ImageNet で事前学習をした InceptionV3 モデルを fine-tuning するというタスクに取り組んでもらいました。 学習に時間がかかるので画像は600枚程度とかなり少なめにしましたが、それでも10分単位で時間がかかり、最近のモデルは CPU では学習が困難だという実感が得られたのではないかと思います。

それ以外にも、公開されている学習済みのモデルを使えば物体検出などもお手軽に試せるということも軽く紹介し、手元で動かせるようになってもらいました。

CNN は学習は大変ですが、画像は見た目にも分かりやすいので楽しんで取り組んでもらえたように見受けられました。 モデルが間違えた画像を調べることでそもそも答えのラベルが合っているのかを疑問視するという気付きを得られたことも、実際のサービスに適用する場合には重要なので良い経験になったと思います。 また、学習の際の各種パラメタをどうやって決めればいいのかという疑問がたくさん出てきましたが、なかなか難しい話なので私も教えてもらいたいですね。

ハッカソンの紹介

ハッカソンでは各自が興味のあるトピックに対して issue を切り、そこに達成した成果や困難だと感じた点などを記述していく方式で進めました。 二時間半ほどの短い時間で新しいことに取り組むという難しい挑戦だったので、事前にトピックを考えてきてもらったり、取り組みやすそうな様々な問題を準備しておいて提示するといった工夫をしました。 結果として 40 個もの issue が切られ、多くの参加者が楽しんで主体的に取り組んでくれました。

ここではそのうちのいくつかを紹介したいと思います。

正規表現を Neural Network で解く

正規表現エンジンを NN で作れるかということを題材にして、16文字のランダムな文字列を生成し、/a+b+c/ にマッチするか否かを解いてみたという話です。 限られた時間でデータの準備から結果の検証までを行ったお手本のようなトピックでした。 対象が画像ではないですが CNN の kernel を 1*1 にして適用することで精度が上がることも実験していて、パラメタ数と精度の関係やモデルの中でどうやって認識しているのかなどの議論が発生する興味深いものとなりました。

モバイルで自分たちが学習したモデルを動かす

TensorFlow の example をベースとしてクックパッドアプリの料理きろくでも使っている料理/非料理判定モデルをモバイルで動かしてみようという話です。 実務的に興味がありながらもなかなか着手できていなかったこのトピックも、今回のハッカソンによって Android, iOS 共に料理/非料理判定モデルを動かすところまで実装ができ、みんなの興味を惹きました。 あまり機械学習を経験してこなかったモバイルエンジニアも、実習で一通り触ってからタスクに取り組むことで業務に利用していける感覚を掴めたという好例でした。

キッチンカメラの画像で人検出をして人数をカウントする

クックパッドのキッチンには様子を確認する用途のキッチンカメラがありますが、人検出を利用することでキッチンに人が何人いるかをカウントして slack に通知を出すという話です。 機械学習のモデルとしては研究開発部が作成している API があったため、そちらを使用しています。 多くの要素が絡む総合格闘技的なトピックでしたが、スピーディーに実装してみんなが確認できるものが出来上がったので、どうすれば人数カウントの精度が上がるかという議論も含めて盛り上がっていました。

究極のカレーレシピを錬成する

RNN を使ってカレーのレシピを生成してみようという話です。 講義では RNN には触れませんでしたが、torch-rnn を使ってカレーのレシピを生成するモデルを学習し、実際にレシピを生成して出力するというところまでやり切っていました。 生成系は難しいためみんなの笑いを誘うような出力(例えば材料に玉ねぎが三回も出てくる)もありましたが、カレーという特定のカテゴリに絞ったことで材料や手順がある程度理解ができるものであったのは興味深いものでした。

業務と結びつきの深いトピックを考えてみる

プロの作ったレシピとそれ以外を判別する、広告CTRを予測するモデルを構築する、料理教室のレッスンの説明文から特徴ベクトルを作って比較をする、サービス上の重複画像を排除する、アクセスログデータからの攻撃検出、など実際のサービスを意識したにしたトピックも数多く挙げられました。 問題設計やデータセットの構築に時間を要するためすぐに結果を出すというのは難しいものがほとんどでしたが、実際のサービスに活かせそうな部分を考えてみる良いきっかけとなったかと思います。

No Free Lunch Theorem の証明を理解する

話としては何度も聞いたことがあるがどのように証明するかは知らない人が多い No Free Lunch Theorem の証明を理解しようという話です。 硬派なトピックであり、個人的にはこれを選んだ人がいてテンションが上がりました。 評価関数の関数空間を探索アルゴリズムによって分割するという考えは、アルゴリズムの優劣を理論的に論じるのにも有用ですね!

その他

全部は紹介できませんが、その他にも、データセット構築する、新しいライブラリを試してみる、マインスイーパーを解かせる、アニメの速報テロップやL字を判定する、など面白いトピックが盛り沢山でした。 また、公開されているレポジトリを触っていたらバグを発見したため修正 PR を送って OSS に貢献をする人もいました。

これらの成果は美味しい食事を楽しみながら発表をして、歓声や質問が飛び交うとても楽しい時間になりました。 今回の食事のメインは TensorFlow ロゴのライスケーキで、実現が難しいオーダーにも関わらず料理人の方に素敵に仕上げてもらいました。

反省点

概ね上手くいきましたが、以下の点が反省点として挙げられます。

  • 開始時にみんなで一斉にレポジトリを clone して帯域を圧迫してしまった。
  • docker や jupyter notebook に馴染みのない人向けの基本的な説明があるとよかった。
  • 長時間椅子に座って講義を聞いたり実習をしたりするのは大変。
  • みんな楽しみすぎて、ブログに載せるための良い写真があまり撮れていなかった。

まとめ

社内のエンジニアには機械学習に馴染みのない人も少なくありませんでしたが、最高の講義だった、機械学習の系統的な理解が得られて良かった、実際に触ってみることでサービスへの活用方法などがイメージできた、など好評を博しました。 企画側としては限られた時間でのハッカソンが盛り上がるかを特に懸念していましたが、優秀なエンジニアが多いため面白い取り組みをして結果を出すところまで到達する人も多く、実に楽しく有意義なものとなりました。 機械学習は広く深い分野なので一日のイベントでできることには限りがありますが、これを契機に個々人が自発的に機械学習に取り組むようになってくれれば嬉しい限りです。

いかがでしたでしょうか。 クックパッドでは、機械学習を用いて新たなサービスを創り出していける方を募集しています。 興味のある方はぜひ話を聞きに遊びに来て下さい。
クックパッド株式会社 研究開発部 採用情報

スキューのない世界を目指して

こんにちは。インフラストラクチャー部データ基盤グループの小玉です。

先日Amazon Redshift(以下、Redshift)で32TBのテーブルを全行スキャンするクエリを3本同時に走らせたまま帰宅し、クラスターを落としてしまいました。 普段はRedshiftのクエリをチューニングしたり、データ基盤周りの仕組みを慣れないRubyで書いたりしています。

突然ですが、スキュー(skew)という単語をご存じでしょうか。 「skew 意味」で検索すると「斜め」とか「傾斜」といった訳が出てきますが、コンピューティング界隈では「偏り」という訳語が定着していると思います。 さらに、分散並列DB界隈で単にスキューもしくは偏りと言った場合、それはしばしばデータの偏りを指します。

データが偏っているとは

データが偏っているとは、複数ノードで構成される分散並列DBにおいて、各ノードが保持するデータ量(行数)に差異があるということです。

例えば、Node1、Node2、Node3という3ノードで構成される分散並列DBがあり、そこに行数が30のテーブルが一つあるとします。 この場合に、ノード間で行が均等に分散している状態、つまり各ノードが行を10行ずつ保持している状態が、偏っていない状態です。 一方、Node1に3行、Node2に7行、Node3に20行というように、ノード間で保持する行数に差が生じている状態が、 偏っている状態です。

f:id:shimpeko:20170725183536p:plain

データの偏りとクエリパフォーマンス

分散並列DBでクエリのパフォーマンスチューニングを行う際に、データの偏り具合を確認することはとても重要です。 なぜなら、データが偏っている場合、データを多く保持しているノードに引っ張られるかたちで、クエリのパフォーマンスが低下してしまうからです。

なぜパフォーマンスが低下してしまうのか、上記と同様に3ノードのシステムに30行のテーブルがある場合を例に考えてみます。 なお、ここでは仮に1ノードで1行のスキャンに1秒かかることとします。

データが偏っていない場合

まず、データが偏っていない場合、テーブルの全行をスキャンするのにかかる時間は10秒です。 この場合、Node1からNode3はそれぞれ10行ずつ行を保持しているため、各ノードにおけるスキャン行数は10行で、所要時間も10秒になります。 そして、各ノードにおけるスキャンは並列で行われるため、テーブルの全行、30行のスキャンも同じく10秒で終わります。

データが偏っている場合

一方、データが偏っている場合、例えば、Node1に3行、Node2に7行、Node3に20行のデータが保持されている場合は、全行スキャンに20秒かかってしまいます。 この場合、各ノードのスキャン所要時間はそれぞれ保持している行数に応じて、3秒(Node1)、7秒(Node2)、20秒(Node3)になります。 スキャンは並列で行われますが、クエリの結果を返すためには全ノードでスキャンが終了している必要があります。 そのため、一番時間のかかるNode3に引っ張られるかたちで、全体の所要時間も20秒になってしまいます。

さらに極端な例ですが、Node3に30行全てが保持されており、他のノードには1行も無い場合、全行スキャンに30秒かかることになります。 この場合、分散並列DBといいつつも、実際に処理を行うのはNode3だけであり、並列化の恩恵を全く享受できていないことになります。

ここまでで、分散並列DBにおけるデータの偏りと、それがパフォーマンスに及ぼす影響についてなんとなく理解していただけたと思います。 ここからは、弊社で利用している分散並列DBであるRedshiftを例に、ノード間のデータの分散方式と、よくある偏りの原因について見ていきます。

Redshiftにおけるデータの分散方式

RedshiftはEvenKeyAllという3つのデータ分散方式をサポートしています。どの分散方式を利用するかは、テーブル作成時指定することが出来ます。また、Key方式を利用する場合に限り、分散方式の指定に加えて、分散キーとなるカラムを指定する必要があります。

それぞれの分散方式の概要は以下の通りです。 なお、Redshiftは"ノードスライス(以下、スライス)"という単位でデータを保持し、並列処理を行うため、以下では"ノード"に代えて"スライス"という単語を使います。

Even方式

  • ラウンドロビン形式で各スライスに行を出来るだけ均等に割り振る
  • 長所: データが偏りにくい
  • 短所: ジョイン時にデータの再分散が発生しやすい

Key方式

  • 指定されたカラム(分散キーカラム)の値に基づいて、各スライスに行を割り振る。カラムの値が同じ行は、同じスライスへ割り当てられる
  • 長所: 同じ分散キーカラムを持つテーブル同士のジョインでは、データの再分散が発生しない
  • 短所: データが偏る場合がある

Key方式は、他の方式より少し特殊なため補足します。以下はKey方式を指定したCREATE TABLE文の例です。

create table access_log (accsss_time timestamp, user_id int, user_agent varchar(512),....) diststyle key distkey(user_id)
;

distkey(user_id)という部分で、テーブルの分散キーとして、user_idカラムを指定しています。これにより、行がuser_idの値に基づいて各スライスに分散されるようになります。つまり、同じuser_idの値を持つ行は、同じスライスへ保存されるということです。なお、この分散は値そのものではなく、値のハッシュ値に基づいて行われるため、一般的にはハッシュ分散と呼ばれます。

少し話が逸れますが、Key方式には、テーブルのサイズ(ストレージ使用量)が小さくなるという効果もあります。これは、上で述べた通り、分散キーカラムの値が同じ行が、同じスライスに保存されるので、圧縮が効きやすくなるためだと考えています。社内では、分散方式を"Key"から"Even"に変更したところ、テーブルサイズが約2倍になってしまった例もあります。

All方式

  • 各スライスにテーブルの全行を保持する
  • 長所: データが偏らない。ジョイン時にデータの再分散が発生しない
  • 短所: 各スライスにテーブルの全行を保持するため、スライス数×行数のストレージ容量を消費する

このほかにも、Redshiftではサポートされていませんが、レンジ分散も一般的なデータ分散方式の一つです。興味の有る方は調べてみてください。

さて、上記の3つの分散方式を、データの偏りという観点に限って比べると、偏りが生じないEvenやAll方式が優れていると言えます。 しかし、All方式はストレージを多く消費してしまいますし、Even方式は ジョイン時にスライス間でデータの再分散が発生してしまう というデメリットがあります。

ジョイン時のデータ再分散とは

「ジョイン時にスライス間でデータの再分散が発生してしまう」とはどういうことか、以下のクエリを例に解説します。

accsss_logテーブルとusersテーブルをuser_idカラムでジョインしてuser_id毎のアクセス数を集計するクエリ

select
    l.user_id
    , count(*) as pv
from
    access_log l
    inner join users u
    on l.user_id = u.user_id
group by
    l.user_id
;

まず前提として、Redshiftにおいてジョインを実行する場合、結合される行同士、すなわちジョインキーの値が同じ行同士は、同じスライスにある必要があります。上記のクエリで、ジョインキーはuser_idです。よって、user_id=1access_logの行と、user_id=1usersの行は、同じスライスにある必要があります。

もし、ジョイン実行時にそれらの行が同じスライスに無い場合、ネットワークを通じてデータの移動が行われます。これが、データの再分散です。ネットワークを通じたデータの移動は時間がかかるため、できる限り避けたい処理です。

分散方式がEvenのテーブル同士をジョインする場合や、EvenとKeyのテーブルをジョインする場合は、この再分散が必ず発生します。再分散の方法には、両方のテーブルの行をジョインキーの値に基づいて移動する場合と、片方のテーブルの全行を各スライスに移動する場合の2パターンがあります。ただ、いずれにせよ再分散は避けられません。

一方、Key方式を採用し、かつ同じ分散キーカラムを持つテーブル同士のジョインでは、再分散が発生しません。例えば、access_logusersの両テーブルでKey方式を採用し、分散キーカラムとしてuser_idを指定している場合です。この場合、二つのテーブルで同じuser_idの値を持つ行は、同じスライスに保存されています。そのため、再分散無しでジョインが実行出来るのです。

ジョイン時に再分散の発生しないKey方式を上手く利用すると、Redhshift(をはじめとする分散並列DB)の急所であるジョインのパフォーマンスを向上させることが出来ます。そのため、特に頻繁にジョインするテーブルにおいては、データの偏りを考慮する手間を惜しまずに、積極的にこの方式を利用すべきです。

データが偏りやすい分散キーカラムの特徴

Key方式を採用しつつ、偏りを避けるためには、データが偏りにくい分散キーカラムを選択する必要があります。この際に確認するのは、カラムに含まれる値の統計的な特徴です。以下では、データが偏りやすい、避けるべき分散キーカラムの特徴をご紹介します。

ユニークな値の数が少ない

性別カラムでジョインをするからといって、性別カラムを分散キーにしてしまうと、性別の数のスライス数にしかデータが分散しません。クラスタ内に100スライスあっても、数スライスしか使われないことになります。分散キーに指定するカラムは、クラスタのスライス数に対して、十分な数のユニークな値を保持している必要があります。なお、テーブルの行数に対するユニークな値の数の度合いは、カーディナリティー(選択度)と呼ばれています。カーディナリティーが高いカラムは、偏りにくいカラムといえます。

ユニークな値の数を確認するクエリ(大きいほど良い)

select count(distinct column_name) from table;

ある値の数が他の値より多い(少ない)

ユニークな値の数が問題なさそうに見えても、ある値の数が他の値より多い(少ない)と、偏りが発生してしまいます。 例えば、一部の少数のユーザのアクセス数が他のユーザと比べて突出している場合、アクセスログには特定のユーザIDを持つ行の割合が多くなります。これを、ユーザIDカラムで分散すると、アクセス数の多いユーザIDが割り当てられたスライスの行数が、他のスライスより多くなってしまいます。

あるカラムについて特定の値の数の最大を確認するクエリ(1に近いほど良い)

select max(val_cnt) from (select column_name, count(*) as val_cnt from table group by 1);

nullがある

「ある値の数が他の値より多い(少ない)」の中で見逃しがちなのが、nullの数です。null以外の値の数が、ほどよく散らばっていても、例えば全体の10%がnullの場合、10%のデータが同じスライスに割り当てられることになるため、注意が必要です。

(番外)中間データの偏り

分散キーカラムに問題が無い場合でも、クエリによっては、処理中に作られる中間データが偏ってしまう場合があります。中間データに不要な偏りが生じていると思われる場合は、統計情報を更新したり、クエリの書き方を変えたりすることで、生成される実行プランが変わり、偏りを解消出来ることがあります。

Redshiftで、中間データの偏り具合を調べたい時は、STL_QUERY_METRICSや、SVL_QUERY_METRICS_SUMMARYといったシステムテーブルが使えます。これらのテーブルを使うと、スライス間のI/OやCPU使用量の偏り具合を調べることが出来ます。また、AWSのWEBコンソールでも、以下のようにスライス毎の平均所用時間と最大所用時間を確認することが可能です。

f:id:shimpeko:20170725183542p:plain

なお、同様に各テーブルの偏り具合は、SVV_TABLE_INFOというシステムテーブルのskey_rowsカラムで調べることが出来ます。skew_rowsは「最も多くの行を含むスライスの行数と、最も少ない行を含むスライスの行数の比率。」であり、1に近いほど偏りが少ないということになります。アクセスパターンにも依りますが、1.2くらいまでは許容範囲だと思います。

まとめ

この記事では、まず前半で分散並列DBににおけるデータの偏りと、そのパフォーマンスへの影響について解説しました。 また、後半ではRedshiftを例に、サポートされているデータ分散方式とそれぞれの特徴、そしてKey分散方式を採用した場合に偏りの原因となるデータの特徴をご紹介しました。

今回ご紹介したデータの偏りという観点は、並列分散DBだけでなく、Hadoopなどの他の分散システムを使う場合にも、トラブルシューティングやパフォーマンスチューニングの役に立ちます。初歩的な内容でしたが、なにかのお役に立てば幸いです。

最後になりましたが、クックパッドでは共にスキューの無い世界を目指せるデータ基盤エンジニアを募集しています(業務内容は、データ基盤の構築と運用です。詳しくは募集要項ページをごらんください)。