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

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

先日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などの他の分散システムを使う場合にも、トラブルシューティングやパフォーマンスチューニングの役に立ちます。初歩的な内容でしたが、なにかのお役に立てば幸いです。

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

時差のあるリモートワークをやってみて

こんにちは、インフラストラクチャー部データ基盤グループの井上寛之(@inohiro)です。私事ですが今年の3月から、時差のあるリモートワークを行っています。今のところ主観的にも、客観的にもうまくいっている状況です。友人・知人にそのことを話すと、「実際のところどうなの?」「どうやってるの?」と聞かれることも多く、今回は日本にいるチームメンバーとの仕事のやり方、また私自身が心がけていることを紹介します。

背景

私が所属している インフラストラクチャー部データ基盤グループは、主にデータウェアハウス(DWH)の開発を行っています。具体的には、サービスのログやユーザーのマスターデータを継続的に取り込み、サービス開発のためのデータ分析や広告配信のためのシステム(DMP)に貢献しています。また、DWHユーザーのアカウントを発行したり、分析的なSQLの相談に対応したりしています。

クックパッドは、現在(2017年3月末時点)62カ国17の言語でサービスを提供しています。日本以外で比較的大きな拠点がブリストル(UK)、アリカンテ(スペイン)、ジャカルタ(インドネシア)に存在し、サービス開発およびコミュニティのサポートを行っています。日本のクックパッドと、海外のクックパッドのコードベースは異なりますが、データ分析のプラットフォームはDWHで統一されています。また、DWHの開発はデータ基盤グループが行っています。そこで、個人スキーマ作成や、新しいデータの取り込みなどの依頼は日本のみならず、世界各地のDWHユーザーから行われます。

前述の通り私は2017年の3月より、家族の都合でアメリカ西海岸に引越しましたが、引き続きデータ基盤グループの仕事を行っています。渡米の前に、部署を変えること(必然的に仕事内容も多少の変化があるはず)を検討しましたが、結局同じチームのままになりました。この辺の細かいところは個人で状況が異なると思うので省略しますが、私にとってはそれまでの仕事を引き続き行うという判断が正解だったと思っています。これまで一緒に仕事をしていたチームメンバーなので、仕事のやり方が理解できていると言うのは大きいです。

肝心のタイムゾーンは太平洋標準時(PST, UTC-8)ですが、現在は夏時間の期間なので太平洋夏時間(PDT, UTC-7)になります。日本が土曜日の朝になったとき、こちらが金曜日の夕方という感じです。ちなみにブリストルやアリカンテのあるヨーロッパが金曜日の夕方になる時、こちらは金曜日の朝になります。

クックパッド社内でリモートワークをやっている人は私だけではありません。個人や家庭の都合でリモートワークをやっている人、またある国では現地にコミュニティマネージャーしか社員がいなく、オフィスがまだ無いのでリモートワークと言う人もいます(アメリカにも現在オフィスはありません)。ただし、それぞれのやり方はほぼ共有されていないのが実情です。

具体的にやっていること

さて、具体的にチームと仕事をどうやっているのか紹介したいと思います。

1. 毎日朝会にビデオ参加する

日本でもチームで朝会を行っていたように、最近全社に導入されたZoom を使って、5分程度の朝会を行っています。日本の11時、こちらの19時なので私にとってはその日やったこと報告、相談する場になっています。

2. 重要なプロジェクトでは週一でビデオ会議をする

日本にいた頃から継続していた複数人で進めているプロジェクトについては、週に1回程度のビデオ会議を行いました。書くまでもありませんが、お互いの認識に相違が生まれないように、また発生してしまった相違を早い段階でなくすことができます。

3. 時差を活かす

時差があることは一見不便な点が多くありそうですが、視点を変えると便利だったりもします。例えば、こちらの昼間は日本の深夜なので、落ちたバッチジョブに迅速に対応することができています。また、前述の通り世界各地にある拠点から発生するデータ関連の依頼に対して、日本の営業時間よりも早く対応することができています(データ基盤に関するチームは現在日本にしかありません)。

4. 柔軟にやる

これはどちらかと言うと考え方になります。チームが必要と思えば、やり方をすぐに変えてみよう、うまくいくやり方を見つけようという考え方です。上で書いた朝会にビデオで参加する、というのは、渡米直後には行っていませんでした。チームのマネージャーとは週1程度でビデオで面談をやる予定を決めていましたが、朝会をやったほうがチームとして仕事がしやすくなると判断し、毎日行うようになりました。

やはりコミュニケーションが課題になるため、柔軟にやらないとどんどん考えていることがズレていってしまうという危機感が常にあります。

心がけていること

また、私個人が心がけていることを紹介します。

1. 仕事が見えるようにする

仕事が進んでいるのが分かるように意識しています。近年では Slack にGithub へのコメントが流れたりするので、ちゃんとやってれば勝手に見えるようになります。しかし、少し早めの段階でプルリクエストしたり、雑に イシューを立てておいたりすることで、より「仕事してますよ」感を出していくことが重要だと考えています。

さらに、これは日本にいたときからの習慣ですが、任意の日報を社内ブログに書いています。実際のところチームメンバーが見ているかどうかは不明ですが、自分のための記録としておすすめできます。

ちなみにチームとしてのタスク管理にはPivotal Tracker を使っています。PivotalTracker を見ることで誰がどのタスクを担当していて、いまどんな状態(着手、未着手、完了、…)なのかが分かります。また2週間に1度はイテレーションミーティングを開催し、そのイテレーション(2週間)に片付けたタスク、次のイテレーションで誰が何をやるのかをチーム全員で見直しています。もちろん私はビデオで参加しています。

2. 脱線を予防する

仕事中は完全に一人になることが多く、脱線を予防することが重要と考えています。業務上 2,30 分かかるようなSQLを実行することが多く、待っている間にふらっと SNS などを眺めだしてしまうと危険です。古典的ですが、/etc/hosts ファイルに任意のサービスのドメインを localhost に向けると言うのは結構効き目があったりします(SNSは息抜きに携帯電話から見るようにする)。

また、私は家からフルリモートという働き方だと、経験上なかなかスイッチを切り替えるのに時間がかかると判断し、コワーキングスペースから仕事をするようにしています。

3. 仕事にやりにくさについて率直に聞く

私が仕事をやりやすいと思っていても、日本にいるチームメンバーがやりにくいと考えているならば何か対策をする必要があります。一時的に帰国した時や面談で「実際リモートワークやりにくくないですか?」と率直に聞くようにしています。

まとめ

時差のあるリモートワークから得られた知見や経験について、特に日本にいるチームメンバーとの仕事のやり方、また私自身が心がけていることを紹介しました。

クックパッドに限らず、リモートワーク等の比較的自由な働き方が増えつつあると感じていますが、時差があるとよりコミュニケーションに気を使う必要があると考えています。今までと同じように仕事ができることや、チームメンバーの協力に感謝しています。一方で、私は「リモートだからできない」と言うような、リモートを言い訳にしないように、むしろ時差を活かしたリモートだからこそできることを増やしていきたいと考えています。

他社で同じような状況でお仕事されている方の Tips など教えていただけると参考になります。

Slack 上のエンジニア同士の会話を増やした一つの工夫 + ちょっとした OSS の紹介

こんにちは、技術部開発基盤グループの小室 (id:hogelog) です。

最近エンジニアが全員集まる Slack のチャンネルからデプロイ通知等の機械的な通知を排除したらエンジニア同士のコミュニケーションがほぼ毎日発生するようになり満足しています。自分のような無名なペーペーエンジニアも業界に名を馳せる著名エンジニアもフラットに属しているチャンネルが通知で埋まっていて人間の会話がまったく発生しないなんてもったいないですからね。Slack のチャンネルをどう運用するか会社によって文化の違いがあると思いますが、良い運用は参考にしたいので各社どんどん発信してほしいのでよろしくおねがいします。

さてそんな話で終わっても良いのですが、ここは開発者ブログだしせっかくなので最近開発した Slack 関連のアプリケーションを紹介します。

tokite で GitHub から Slack への通知をカスタマイズ

クックパッドでは GitHub Enterprise (以下 GHE) 上で日々開発やレビューなどをおこなっています。そこで生まれる Issue や Pull Request などを Slack に通知するためのツールとして tokite というツールを実装しました。

https://github.com/hogelog/tokite

今までは主にメールとJasper *1 等を利用して GHE 上でのレビューやコミュニケーションを進めていました。 Slack 通知も利用していましたが、GitHub の Slack 通知はリポジトリ毎の設定しか存在せず、自分が見たい通知のみを特定のチャンネルに流すといったことはできませんでした。

それを解決するため作成したのがユーザ毎のルールで通知を設定できる tokite というツールです。

f:id:hogelog:20170719111939p:plain

tokite のルール

tokite ではユーザ毎に任意個数の通知ルールを設定します。ルールにはそれぞれクエリと通知先等を設定し、どのようなイベントを Slack のどのチャンネルに流したいかを決められます。

社内で運用している tokite に自分が設定しているルールは以下の3つです。

ルール名クエリ解説
opsrepo:tech-dept/ops開発基盤グループのタスク管理リポジトリの Issue を流すルール
呼ばれたbody:/sunao-komuro|dev-infra|hogelog/メンション等で自分のアカウント名、グループが呼ばれた時に流すルール
dev-infra-memberuser:/XXX|YYY|ZZZ/チームメンバーの発言を流すルール

また通知先のチャンネルは自分専用の通知チャンネルとして運用し、全発言が Notification を出すように設定しているためこれらのルールにマッチするイベントが発生すれば Slack 経由で通知が届くようになっています。

tokite の動作

tokite は各リポジトリの Webhook を元に、それぞれのイベントにマッチするルールがあれば Slack の対象チャンネルに通知するという動きをします。 ルールのクエリ実装はなにか既存のライブラリやフォーマットがないものだろうか、と少し探したのですがちょうど良いものが見当たらなかったため parslet *2 という PEG ライクなパーサライブラリを利用しました。PEG ベースのパーサライブラリは使ったことがなかったのですが、ドキュメントも丁寧で使いやすいライブラリでした。 ユーザ認証には GitHub を利用していて Webhook 追加も各ユーザのトークンをそのまま利用しています。

tokite で得られたもの

tokite を使うことでリアクションが必要な GHE 上で発生する同僚の行動 (Pull Request, Issue, Issue Comment) の通知をほぼ Slack に集約し、すぐに気付けるようになりました。

tokite を使う前でもメール、Jasper 等の経路でそれらの行動を観測することはできていました。ただし、僕にとってはそれはどうもかなり意識的におこなわなければできないことで、見逃しも度々ありました。一方で自分は Slack の通知ならばすぐに気づき自然に読み始めることができています*3。tokite による通知だけで完結するという程には至らずメールの通知や Jasper に頼っているところもあるのですが、Slack という経路が一つ増やせたのは割と実装した意味があったなと思っています。

今後の予定

tokite はある方が便利だなと感じていますが、今のところ機能は色々と足りていません。今後も OSS の形で公開しながら改善を続けていこうと思うので、よろしくおねがいします。

https://github.com/hogelog/tokite

*1:http://techlife.cookpad.com/entry/2017/03/14/100000

*2:http://kschiess.github.io/parslet/

*3:何故だろう。Slack のアプリケーションがよくできているからというのが大きい気がしている