データ基盤チーム0人で運用は回るのか?! 前人未踏チャレンジ・クックパッドデータ基盤のすべて2020

技術部データ基盤グループの青木です。

ここ1、2年はなぜか成り行きでBFFをでっちあげたり、 成り行きでiOSアプリリニューアルのPMをしたりしていたので あまりデータ基盤の仕事をしていなかったのですが、 今年は久しぶりに本業に戻れたのでその話をします。

突然の1人チーム、そして0人へ……

今年のデータ基盤チームは消滅の危機から始まりました。

間違いなく去年末は5人のチームだったと思うのですが、 メンバーがイギリスへグローバルのデータ基盤チームを作りに行ったり、 山へ検索システムを直しに行ったり、川へレシピ事業の分析業務をやりに行ったり、 海へ広告のエンジニアリングをしに行ったりするのをホイホイと気前よく全部聞いていたら、 なんと4月から1人だけのチームになってしまいました。

事はそれで終わりません。 恐ろしいことに10月にはわたし自身も育休に入ることになったので、 10月はデータ基盤が0人になることが決まりました。

えっ……マジで……? ヤバない……?

もちろん大変ヤバいです。そんなわけで今年は徹底的な運用改善、 できれば完全無人運用が可能なシステムが最優先目標になりました。

アーキテクチャの概要

まずは前提として、クックパッドのデータ基盤アーキテクチャをざっくり説明しておきます。

f:id:mineroaoki:20201229002438p:plain
クックパッドのデータ基盤アーキテクチャ

中心とするデータベースはAmazon Redshiftです。 2016年から同じサイズのクラスターを使い続けています。

データインポートはマスター、ログ、それ以外の3系統。 各種アプリケーションのマスターテーブルは内製のPipelined Migratorまたは AWS DMS(Data Migration Service)で取り込んでいます。 MySQLがmigrator、PostgreSQLがDMSという使い分けです。 ログにはRedshift Spectrumを使っており、Spectrumへのロードにはこれまた内製の Prismというシステムを使っています。 それ以外のSaaSやDynamoDBのデータについては、アドホックなバッチジョブを Bricolageフレームワークで作ってロードしています。

Redshift内での処理はBricolageを使ったSQLバッチが大半です。 ごく一部はUDFを使ったり他システムへ処理を投げたりしていますが、 9割以上はpure SQLで処理しています。

一方のデータエクスポートも3系統あります。 管理画面などの社内アプリケーション、BIツール(社内標準はTableau)、 それに他システムへのバルクエクスポートです。

バルクエクスポートについてのみ詳細を説明すると、 基本的にはQueuery(きゅーり)という内製のシステムを使っています。 QueueryはHTTPのAPIでRedshiftにクエリーを投げられる薄いシステムで、 内部ではRedshiftのUNLOADを使っています。 アプリケーションはUNLOADされたデータをS3から読むので、 読み込みの負荷をRedshiftから切り離すことができる利点があります。

特にRubyからは、redshift-connectorというライブラリで Queueryを簡単に使えるようにしています。

2020年に行った施策

以上がデータ基盤の概要です。

アーキテクチャは最初に設計した2016年からほとんど変わっていませんが、 5年たったので細部の実装はいろいろと変わってきています。 2020年はさきほど述べたように運用改善が最優先だったので、 そのあたりを中心に対応しました。以下の5本立てでお送りします。

  1. Redshift Spectrumへの移行が(だいたい)完了
  2. Prismの運用改善
  3. ログ定義からのクライアント自動生成
  4. Redshiftのワークロード管理機能の活用
  5. Tableau運用フローの改善

1. Redshift Spectrumへの移行が(だいたい)完了

今年最大の成果はなんと言ってもSpectrum化が「だいたい」終わったことです。

Spectrum化作業はDWHチームのべ5人で交代しながらチマチマやっていたせいで、実に丸3年かかりました。 Redshiftの内部ディスクにあったログテーブルを300本近く捨てたことで、 クラスターのディスク容量は50%を切りました。これまでは常時カツカツで、 80%を越えるたびに過去のデータを消しては凌いでいたことを思うと隔世の感があります。

実際にやったことは1000本近いバッチジョブをひたすら書き換えるだけの簡単なお仕事です。 書き換えては数値検証、書き換えては数値検証で、検証の時間が一番長かったですね。 横長スプレッドシートと仲良くなれます。

また、移行が完了したことで、内部テーブルにロードするために使っていた 旧システム(strload v2, v3)を捨てられるようになりました。 これでようやくロードシステム3系統をメンテする地獄から解放されます。

2. Prismの運用改善

Spectrum化を進めていくうえで大きな課題になってきたのがPrismの運用のつらさです。

今年はコロナの影響などもあって、3月〜4月ごろにやたらとログの流量が増えており、 しょっちゅうPrismマージジョブのメモリが溢れて死に続ける事故が起きていました。 しかし、その当時のPrismはモニタリングするにもDBを直接見るしかなく、 ジョブのログテーブルもなかったので、ジョブが死んでも何の処理中に死んだのかもよくわからない有様……。

これではさすがにやってられないので、 Prismの前に使っていたstrloadというロードシステムの管理画面を流用し、 2日くらいでコンソールをでっちあげました。

f:id:mineroaoki:20201229002600p:plain
Prismの管理画面

もっとも、管理画面を作ったところで現状が見えるようになっただけにすぎません。 根本的に問題を解決した施策は、遅延ログの扱いを変更したことでした。

これまでPrismはどんなに遅れて到着したログもすべて受け入れて既存パーティションへ マージしていたのですが、今年からはそれを14日で捨てるように変えました。 これはBigQueryもそういう仕様ですし、問題はなかろうということである日突然えいやっと切り替えました。

この点は開発前にはよくわかっていなかったところの1つなのですが、 遅れたログを永久に受け入れていると、ロードシステムの負荷が非常に大きいのです。

例えばプッシュ通知を配信したときに、その処理のためにアプリがバックグラウンドで動く場合があります。 すると端末側のログバッファがいっせいにフラッシュされ、 しばらく休眠していたユーザーも含めて過去のログがまとめて到着します。 すると結果として「プッシュ通知を送るたびに全ログ全期間をマージしなおす」という事態に陥ってしまうわけです。 これは負荷の面でも、コストの面でもさすがに看過できません。

遅延ログを14日で切るようにしたらPrismマージジョブの数が激減して(下図)、 いきなりすべてが安定しました。

f:id:mineroaoki:20201229002627p:plain
6/15から山岳地帯がサバンナに激変

ちなみに、Prismもだいぶ安定してきたので、来年は残りの懸念を潰してオープンソース化するつもりです。

3. ログ定義からのクライアントコード自動生成

今年はiOSアプリのリニューアルという大きな動きがあったので、 そのどさくさに紛れて新しいログの仕組み、通称「大統一アクティビティログ」を導入してもらいました。 この仕組みを使うと、特定のMarkdown形式でログのイベントを定義しておくことで クライアントのロガーとログ定義が自動生成されて、型のズレを根絶することができます。 詳しくは id:giginet の記事「ドキュメントベースの型安全なモバイルアプリ行動ログ基盤の構築」を参照してください。

この仕組みは本当によくできていて、リリース以前にログをちゃんと考える契機になるうえ、 自動的にログのドキュメントが整備されるようになっています。 さらに自動生成システムはAndroidアプリやウェブでもそのまま再利用できたため、 コスパも非常によかったです。

データ基盤側の視点では、この仕組みを導入したことによって、 ログの型が事前に決まるようになった点が最大の利点でしょう。

これまではまず最初にアプリでログ出力が実装されて、 実際にログが届き始めてからログの定義(型)をもらい、 両者のズレをなんとかするというフローでログを運用していました。 しかし当然ながらこの順序では、想定通りのログが出ていなかったり、 データ基盤に設定するログ定義を間違えてしまうことが頻繁に起きます。 するとデータ基盤側でもデータを入れ直すタスクが発生して、 そのたびにいちいち手作業で対応していたわけです。 大統一ログの導入後はこのようなミスマッチも手作業も、いっさいなくなりました。

さて、定義が自動生成されるようになったので、このさい設定の適用も自動化しようということで、 ログ定義をGitHubにコミットしたら自動的に定義を本番適用するツールを開発しました。 このツールができたことによって、ログを追加するには次の3ステップで済むようになりました。

  1. 特定のMarkdown形式でログのイベントを定義する
  2. クライアントのロガーとログ定義を自動生成する
  3. ログ定義を専用のレポジトリにPull Requestしてマージする

このステップはすべて各アプリケーションの開発者が自分で行うことができます。

実はログ定義の本番適用とその後のフォローは、 データ基盤で発生する定期作業の中でも最も頻度が高い作業でした。 この作業を自動化できたことがデータ基盤0人期間を乗り切るための決定打となってくれました。

4. Redshiftのワークロード管理機能の活用

Redshift上のワークロードに関しては、 Concurrency ScalingとUsage Limit、それにAutoWLMを有効にして、 日々襲来する負荷の波をやりすごすことに取り組みました。

Concurrency Scalingは、Redshiftのread onlyクラスターを一時的に増やして、 クエリーの処理キャパシティを上げる機能です。これには当然ながら(?)お金がかかるのですが、 1日1時間の無料枠があるため、1時間だけスケールさせておけば無料で使えます。 そして1時間でスケールを止めるためにUsage Limitを使います。

f:id:mineroaoki:20201229002652p:plain
Concurrency ScalingとUsage Limit

Concurrency ScalingとUsage Limitについてはチームメンバーが書いた記事(英語ですが)があるので、 詳細を読みたいかたはぜひそちらをご参照ください。 結果だけざっくり言うと、コミット待ちの時間が約15%減りました。

最後のAutoWLMはこれまでのManual WLMと違い、CPUとI/Oも配分できるところが特徴です。 Manual WLMではせいぜいメモリしか配分できなかったので、ようやく普通のWLMになったなという印象です。

AutoWLMの設定にはあまりこっていません。 短かそうなクエリーにはリソースを多めに与えて速攻で終わらせる、 長い時間動いているクエリーは徐々にペナルティを増やしてリソース割り当てを減らす、この2つだけです。

様々なクエリーが混在する混合ワークロード環境では、短かいクエリーを早く終わらせることが肝要です。 何も知らないとついうっかり重いクエリーにリソースを割り当てたくなるのですが、実はそれが最もよくありません。 重いクエリーにリソースを割り当てると、大量のリソースがずっと占有されることになり、結果としてすべてのクエリーが詰まります。 むしろ短いクエリーに多すぎるくらいにリソースを割り当てて、とっとと次のクエリーが入る場所を空けさせたほうがパフォーマンスは上がります。

もっともAutoWLMを適用した効果は正直よくわからず、数値では明確に出せませんでした。 体感だとなんとなく待たされることが減っている気がしますが、プラシーボかもしれません。

5. Tableau運用フローの改善

いまのところ、クックパッドでは次のようにTableauワークブックの標準運用を定めています。

  1. 最初はカスタムクエリー(ワークブック埋め込みのSQL)を使って手軽に作る。データソースは抽出にする。
  2. カスタムクエリーをRedshiftのビューに変換する。
  3. ビューが重くなったら蓄積バッチ化する。

Tableauのカスタムクエリーは、作るときには簡単ではあるものの、 一度Serverにアップロードしてしまうと、ワークブックをダウンロードして 開かないと見ることができません。またそのときにRedshiftユーザー名を データソースの個数だけ要求されたりするので使い勝手が最悪に近いです。 できるだけ早くビューにしてしまって、 Tableauワークブックではビューをselectするだけにすべきでしょう。

しかしビューにするためにも結局カスタムクエリーを見る必要があり、 そのためにまたワークブックをダウンロードして20回ユーザー名を入力しなければいけないわけです。 これはあまりにもアホくさいですし、誰もやってくれないので、 カスタムクエリーをS3にダンプする日次バッチを作りました。

またRedshiftのビューを更新するにはバッチユーザーの権限が必要なので、 手作業でやるとわたしがボトルネックになりますし、 手更新はチームでのレビューがやりにくいという問題もあります。 そこで、ビュー定義をGitHubにコミットしたら自動的にビューを作成・更新する仕組みを作りました。 これはほぼ同じ事例をネットで見かけたので、やはりどこも同じことを考えるものですね。

結果、0人期間は乗り越えられたのか?

以上が今年やってきたことです。 ほぼ全方位にわたってとにかく手作業を減らし、そもそも定期的なメンテ作業が発生しないようにすること、 開発者にセルフサービスで問題を解決してもらえるようにすることに注力しました。

結論を言えば、データ基盤0人期間はなんとか乗り切ることができました。 実際には2、3回ちょっとした問題が起きたのですが、 元データ基盤のメンバーたちが首尾よく解決してくれたのでノーカンです。 完全無風とはいきませんでしたが、乗り切れたのでよしとしましょう。 元メンバーのみんなには感謝です! 今度おごります。

アーキテクチャ選定で後悔していることと、していないこと

ところで、この年末でクックパッドのデータ基盤は開発開始からほぼ丸5年を迎えます。 この節目に、アーキテクチャの選択について後悔していることと、していないことを総括したいと思います。

後悔していないこと

まず、Redshiftを選んだことは後悔していません。

一時期はBigQueryがうらやましすぎて、 他社のデータ基盤の人にBigQueryの話を聞くたびに 「ソーダヨネービッグクエリーベンリダヨネースゴイスゴーイ」 を無表情で連呼する機械と化していましたが、 まあいまでもうらやましい点もあるんですが、 トータルで見れば現状は悪くないなと思うようになりました。 あと5年戦って10年までいけそうな気がしています。

第一に、クックパッド全社のアーキテクチャを見たときにRedshiftは最もシンプルかつ安価なソリューションです。 やはりアプリケーションとデータ基盤をAWSで統一できるという点は非常に大きいと思います。 人間が分析をするだけならばデータ基盤を外出しにしてもたいして問題はないと考えていますが、 他システムとのやりとりが増えてくると、認証の複雑化なども含むデータ移動のコストがばかになりません。 これからますますデータ基盤と他システムとのデータ連携パスが増える一方であることを考えると、 アプリケーションとデータはできるかぎり近くの、連携が容易な場所に置くべきでしょう。

第二に、SpectrumやPartiQLによって、 ログをRedshiftで扱いやすくなったことが挙げられます。 もし仮にこれらの新機能がないままだったら、さすがに後悔していたでしょう。 Redshiftは次々に新機能がリリースされるので、いま困っていることでも 少し待っていたらどうにかなるのではないかという謎の安心感があります。

最後に、Redshift Federated Queryの存在が挙げられます。 Federated QueryはアプリケーションのDB(MySQLやPostgreSQL)に Redshiftから直接接続してクエリーすることができる機能です。 これはAWSでシステムを統一してこそ活用できる機能なので、 クックパッドにとってはまさに狙い通り、待望の機能でした。 今年はPostgreSQL限定だったので試験運用にとどまりましたが、 来年はいよいよMySQLサポートがやってくるので、大々的に使っていくつもりです。

来年はRA3ノードも導入する予定ですし、まだまだRedshift周辺は楽しめそうです。

後悔していること

逆に最も後悔した選択はTableauです。

この記事でもTableauのカスタムクエリーをダンプする仕組みなどについて述べましたが、 そもそもこれはTableauのダメなところをカバーする仕組みであって、 こんなロクでもない機能を実装しなければいけない時点でもうダメです。

運用面では、共有に向いていないデータソースの仕組みと、 抽出(extract)更新の管理機能が弱すぎる点が癌です。 利用者側から見るとコラボレーションと共有の機能が貧弱すぎます。 総じてTableau Serverの機能不足が目立ちますね。

ちなみに、以前に利用していたRedashは手軽さは最高によかったのですが、 データ更新ジョブの実行ログが貧弱である(というかない)こと、 クエリーの並列実行でキューが壊れまくることが課題でした。 いったい何回Redis(クエリー実行キューがある)をflushdbしたかわかりません。

データ基盤チームは仲間を募集しています

さすがに1人チームは無理があるということがわかったので、来年からはメンバーが1人増えることになっています。 もう1人くらいは社内から増やせそうな気がしますが、 できればさらにもう1人ほしいので、データ基盤チームでは仲間を大募集しています。 データ基盤を整備するどさくさに紛れて新しいシステムを開発したい人はぜひご応募ください。 以下のページの「データエンジニア」がデータ基盤チームです。

クックパッド採用情報 https://info.cookpad.com/careers/jobs/?jobs=engineer