1. はじめに
こんにちは、Ops-dataチームの上村(@contradiction29) です。データアナリストとして入社したのち、ダッシュボード作りに没頭し、テーブル作成にのめりこみ、データエンジニアリングに夢中になり、今はデータエンジニア兼アナリティクスエンジニアをやっています。
つい最近、弊社Algoageで運用しているデータ分析基盤のリプレイスを実施しました。その際、私は言い出しっぺ兼リードエンジニア兼ちょっとしたプロジェクトマネージャーのような役割をしていました。いろいろ思うところがあったため、考えをまとめてみようと思います。
2. イントロダクション
チャットブーストCVは「日常生活の中からユーザーの購買・契約行動を後押ししてCVを増加させるサービス」です。
弊社AlgoageはチャットブーストCVに関する業務に注力しており、自分が所属するOps-dataチームではデータの利活用やデータ分析基盤の開発・運用を実施しています。データアナリスト4名とデータエンジニア1名(筆者)で構成されるチームです。
チャットブーストCVが2022年2月にサービスを開始してから3か月後、データレイクとして利用しているS3バケットに初めてのデータが入りました。以来、データ分析基盤はチャットブーストCV本体と併走を続け、社内メンバーやクライアントの意思決定を合理化し続けています。データの量は1.5TB近くになり、BIユーザーの数は100名を超えました。社員数は大体100 ~ 150名なので、社員の半分以上はBIユーザーということになります。
しかし、システムの運用に課題はつきものです。詳しくは後述しますが、さまざまなつらみがエンジニアやアナリストを苦しめました。データ分析基盤として存続していく上では致命傷となる「つらみ」も発見されました。つらみが全くない状態で運用した日は今まで一日もなかったと言っていいでしょう。
運用の段階で溜まった知見を設計に反映させてつらみを解消するとともに、意思決定支援システムとしての価値を存続させるため、私はデータ分析基盤の移行を行うことにしました。データ分析基盤の運用を始めてから1年以上経ちました。知見が溜まった今なら、もっと上手くやれるはずです。データの利用者に対して直接向き合い、価値を提供する役目を持つデータアナリスト、およびBI・アナリティクスエンジニアとしての業務を続けてきたことで、自分の中で、以下のようなことがぼんやりとイメージとして形成されてきました。
- 社内のマーケティングチームをはじめとしたデータのユーザーがデータ分析基盤に何を期待しているのか
- どのような開発フローを構築すれば、素早くデータの利用者に価値を提供できるのか
- そもそも、データ分析基盤はシステムとしてどのような価値を提供するべきか
初期の段階ではなかったイメージが、今は自分の手元にある。やるなら今だ、と思いました。
この文章では、Algoageにおけるデータ分析基盤移行プロジェクトについて、自分が何を考え、どのようなことを実行していったのかを記述していきます。形式としては「移行前のつらみと設計原則編」「設計・構築編」「運用編」の3編構成とします。
データ分析基盤を作ったことがある方ならわかると思うのですが、データ分析基盤は一度構築して終わりではなく、運用の中で進化していくものであり、将来的な発展を見据えた上での設計・構築が必要になります。自分の知る限り、つらみの振り返りから移行後の運用まで全体的に記述した記事は珍しいと思うので、書いてみようと思います。
この「移行前のつらみと設計原則編」では特に設計の前段階の要求定義に焦点を当て、既存基盤を運用していく中ではっきりとしてきた課題を描き、新しい基盤で何を目指すのか、方向性を明確にします。その方向性の元でどのような技術的な選択をしていくのかには踏み込まず、次の記事で細かく書こうと思います。
想定している読者層は「データ分析基盤について興味がある人」とし、登場する技術について細かく記述することは行いません。Amazon AthenaやAWS GlueをはじめとするAWSの分析系サービス群、およびSnowflakeに関しては基礎的な知識があることを想定していますが、適時補足を入れていきます。
構成は以下のようにしていきます。
それでは中身に入りましょう。
3. 旧基盤の反省点
この章では、移行を行う前の基盤について全体像をさっと記述してから、運用上何がつらかったのかについてふりかえっていきます。なお、移行を行う前のS3・Athena・Glueを中心とする基盤を「旧基盤」と呼ぶことにします。
旧基盤の全体像
まずはポンチ絵で旧基盤の全体像を掴みます。
当時のマネージャーから聞いた話ですが、旧基盤を作成した当初、社内にはデータ分析基盤の構築・運用経験のあるエンジニアがおらず、とにかく素早く価値を提供するためQuick and Dirtyの精神で実装したそうです。それから地道に補修を重ね、上記の図ような構成になりました *1。
旧基盤は、役割別に3つのS3バケット「data-lake-raw」「data-lake-formatted」「data-mart」から構成されています。詳細について下記にまとめました。
バケット名 | 中身 | ファイル形式 | データソース |
---|---|---|---|
data-lake-raw | 未加工の生データ | JSON Lines, parquet | Kinesis Firehose RDS(Glue Job経由で取り込み) |
data-lake-formatted | data-lake-rawのデータをフラット化し、ファイル形式を変換して個人情報を脱落させたもの | parquet | data-lake-raw |
data-mart | data-lake-formattedのデータをSQL経由で加工し、データマート化したもの。もしくはスプレッドシートやNotionから取り込まれたデータ | CSVなど | data-lake-formatted スプレッドシート Notion |
- 権限はAWS Lake Formationで管理し、DAG(Directed Acyclic Graph : 依存関係を考慮したタスクの集合)の実行管理はManaged Workflow for Apche Airflow (MWAA)でおこなっている
- GitHubでコードを管理し、GitHub ActionsでMWAA環境へのデプロイを行なっている
全体像をつかんだところで、具体的なつらみの話に入ります。なお、補足しておくと、以下で上げる技術は適切な使い方をすれば十分便利なものであり、つらみを出してしまったのは使いどころが悪かったためです。反省するべきは技術選定を行った我々で、技術そのものは適切な使い方をすれば価値を発揮できるものであることは明記しておきます。
AWS Glue Job (Spark):大型トラック
Amazon Kinesis Data FirehoseからPUTされたJSON Lines形式 *2 のファイルはそのままでは分析に不向きなため、フラット化や個人情報を含むカラムの脱落処理をしたうえで、ファイル形式を列志向フォーマットに変更する必要があります。弊チームでは、この目的を達成するためにSparkのAWS Glue Job (以下Glue Jobと省略)を使用していました。機械学習を用いた処理や複雑なJOINは使っていません。
端的に言うと、上記のようなユースケースであればGlue Jobを利用するのはオーバーでした。Glue Jobは開発者目線で開発しづらく、処理に時間もかかるため「JSONを展開する」程度の処理であれば使うべきではない技術だったのです。
開発しづらい
Glue Job (Spark)はローカルで実行するにはあまりに重く、公式のDockerイメージ*3を使って100万行オーダー以上のデータを処理しようとすれば確実にMacBook Proが火を噴きました*4。AWS上で実行できるNotebook *5 もありますが料金はかかりますし、使い勝手もいいものとは言えません。Glue JobはSparkを便利に使いこなすインターフェースを提供してくれるものの、小回りが利かないため、使うタイミングは慎重に選ぶべきツールです。
時間がかかる
Glue Jobの処理の実行時間は合計で1~2時間ほどでした。詳しくは次の記事で述べますが、同様のデータをSnowflakeのSQLで変換したところ、2分程度で全処理が完了しました。 2分!
徒歩30秒のコンビニに行くために大型トラックを使わない
開発者体験の低さと処理時間の長さにエンジニアの技術的な不慣れさが合わさり、長年の開発でコードは黒魔術と化しました。今年の6月に処理落ちが発生したため、過去に別のエンジニアが書いたGlue Jobのコードを読み解いて修正する作業を私がやっていたのですが、大変つらい思いをしたのを覚えています。ローカル上でのデバッグ実行はできず、処理に時間がかかるため繰り返し実行して検証するのに時間がかかり、苦労しました*6。
しかし、同じ価値を発揮できる、もっと簡単な方法があったのです。処理時間はわずか2分ですし、SQLで書かれているのでチーム内の全員が取り扱えます*7。あの苦労はなんだったのでしょうか?例えるなら我々は、徒歩なら30秒でたどり着けるコンビニに、大型トラックで1時間かけていたのです。乗っていたのがトラックだと気が付かずに。他の種類の車を知らない人がトラックを「トラックである」と判別することは不可能です。今まで何と戦っていたのでしょう?トラックは本当に必要ですか?
移行の結果を先取りしてしまうと、最終的にはdbtを利用したSQLを用いて、Glue Jobで実施していた処理をすべて置き換えることにしました。ほかの要素*8による影響もありますが、処理時間の大半を占めるGlue Jobの処理を省略出来た結果、パイプライン全体の実行時間は180分から6分に短縮され、約30倍の高速化を実現できました。
Amazon Athena:砂漠のオアシス
分析用のクエリエンジン、およびデータマート作成処理のために使っていたAmazon Athena(以下Athenaと省略)はコストパフォーマンスに優れ、リソース管理が楽なツールです。実装されているSQLの関数は豊富であり、たいていの処理はこなしてくれます。
しかし、分析用のクエリエンジンとしては物足りないのに加えて、初期のテーブル設計時のミスが基盤全体のボトルネックになってしまいました。
青すぎた隣の芝生
Athenaのコンソールにはクエリの自動補完の機能がありません。自動補完がなくてもSQLは書けますが、Google Cloud BigQueryやSnowflakeが同様の機能を持っていることを考えると、分析用のクエリエンジンとしては見劣りします。他のエンジンの利用経験があるアナリストからの評判は良いものではありませんでした。最近になって補完機能ができましたが、他のテーブルのカラム名が出てくる、カラム名を出すべきところなのにデータベース名が補完されるなど、精度の観点では実用的と言えません。
Athenaはサーバーレスでクエリを実行でき、管理が楽で、コストパフォーマンスのよいツールです。ただ「BigQueryやSnowflakeでできることを実行できない」という意味では、分析用のツールとしては不便であったと言わざるを得ません。ほかのツールの「芝生の青さ」を知っている者にとっては、Athenaは「不便なツール」に見えたのです。
パーティションの制約とテーブル設計のミス
また、INSERT INTO文を発行する形でデータマート作成時にもAthenaを利用していましたが、パーティションの同時作成数に上限があったのが致命的でした。
弊社ではLINEアカウントを運用する形でビジネスを展開しており、「1つの運用アカウント」ごとに「1つのパーティション」を作成していたため、運用アカウントの数が一定数を超えると既存の処理がすべて停止することが分かったのです。移行開始前の段階ではまだ猶予があったものの、パーティションの上限に達するのは時間の問題であり、この問題をクリアできないとデータ基盤が停止することは明白でした。
対応策としては以下の二つがありました。
- INSERT INTO文を分割する
- すべてのテーブルを一度破棄し、パーティションの定義を変更したうえで再作成する
1はAirflow上の運用に落とし込むのが難しく、2はめんどくさすぎます。詳しくは後述しますが、AWS Glue Data Catalogに由来する問題もあったため、両者ともに本質的な解決策にはならないと判断しました。
エラー復旧のやりづらさ
弊チームのAthena環境ではDELETE文が発行できない*9ため、異常なレコードが見つかった場合の対処にも時間がかかりました。異常なレコードを原因として処理が異常終了した場合、以下のような手順を踏む必要があります。
- まずはdata-lake-rawバケット内にあるデータから異常なレコードを含むファイルを見つける
- 特定したファイルをダウンロードしする
- ローカル上で操作してファイルの内容を変更する
- バケット上に戻す
- Glue Jobの処理からやり直す
この一連の流れを実行するまで時間がかかり、結構つらかったですね...
砂漠のオアシス
データ分析基盤の構築初期段階においては、Athenaは優れたツールでした。サーバーレスのためリソース管理が不要で、コストパフォーマンスに優れ、基本的な操作は問題なくこなせます。使い始めたばかりの頃の自分にとっては、Athenaのクエリ実行能力や、インフラ管理の要らないサーバーレスの特性は、データ分析という砂漠の中のオアシス(救済)のように思えました。
しかし、砂漠のオアシスは一時の救済をもたらしますが、真に緑豊かな土地を求めるならもっと遠くを目指す必要があります。データの規模が増大し、分析の要求が複雑化してくると、頻繁にAthenaを利用するデータアナリストの間で、分析用のクエリエンジンとしてのAthenaの不便さや、機能性の不足に不満がたまるようになりました。
ちなみにですが、3つ挙げた課題のうち、2つめのパーティション数制限はテーブル設計時にもう少し考慮すれば回避できたはずで、最後のDELETE文の発行に関してはApache Icebergなどを使えば回避可能な問題ではありました。
AWS Glue Data Catalog:迷路の地図
AWS Glue Data Catalog(以降Glue Data Catalogと省略)はHive MetastoreのAWS版にあたり、各テーブルのメタデータを保存する頭脳の役割を果たしています。
長くなるため深入りしませんが、以下のようなエラーに悩まされました。Glue Data Catalogというよりは基盤の構造によるものです。
- 少し重めのクエリを投げるとすぐ発生するHIVE CURSOR ERROR (S3 Slow Down)
- 頻繁に発生するHIVE_PARTITION_SCHEMA_MISMATCH
- S3上にファイルがあるのにAthenaからクエリできず、何度もMSCK REPAIRを行う必要があったり、何度やってもパーティションが追加できなかったり...
Glue Data Catalogは迷路の地図のようなものです。示すルートが正しいことを保証しないものの、自分が現在どこにいるのか、またどこに向かうべきかの指示を提供してくれます。しかし、複雑さや頻発するエラーを考慮すると、最初から迷路に足を踏み入れるべきではありませんでした。そもそも迷路に入らずとも同じ目的を達成することは可能であることを考えると、迷路に入りこむメリットはなさそうです。
git-flow:重すぎた剣
Airflowによる開発を行う際はgit-flowで行なっていました。git-flowはdevelopブランチとmainブランチを切り分けてあいのあいの...というものです。詳しくは以下の説明をご覧ください。
なぜgit-flowにしたのか?
AWSのマネージド版Apache AirflowであるManaged Workflow for Apache Airflow (以下MWAAと省略) は環境の立ち上げに30分から1時間程度かかるため、ブランチ作成のたびに開発環境を作成するのは現実的ではありません。 *10。 ローカル開発用の環境 *11 もありますが、重いツールなのでローカルで立ち上げて実行するのもかなり辛く、Macbook Proが火を噴きました(2度目)。スペックの都合上、ローカルでの開発は現実的ではありませんでした。
なので*12、MWAA上に開発用の環境と本番用の環境を二つ用意し、developブランチを開発用環境に対応させ、mainブランチを本番環境に対応させる形で運用していました。本番環境に変更を加える際、「通常通りの運用」として想定されていたのは以下のような流れでした。git-flowの少し変えたバージョンです。
- developブランチからfeatureブランチを切ってチェックアウト
- featureブランチ内で変更を加える
- featureブランチの内容をdevelopブランチにマージ
- mainブランチからreleaseブランチを切る
- developブランチに対するコミットを
git cherry-pick
してreleaseブランチに反映する - releaseブランチの内容をmainブランチにマージ
「本番環境でテストすればいい」
当然面倒なので、次第にmainブランチに直接変更を加えるのが流行るようになり、developブランチとmainブランチの差分が大きくなっていきます。そうなるとますますdevelopブランチ、および開発環境は使われなくなっていきます。やがて「本番環境でテストしよう。なんかあったらgit revert
すればいいだろ、どうせ1日1回のバッチ処理なんだから」というノリになり、誰もdevelopブランチおよび開発環境を使わなくなりました。最終的に本番環境を利用して動作確認をするようになったため、レビューがおろそかになってコードの属人性が高くなったり、きちんと開発環境で動作確認をしていれば防げたようなミスが発生するようになりました。
背景まで説明しておくと、Ops-dataチームはデータアナリストが多数派で、Gitを手足のように使いこなせるメンバーは多くありません。「基本的な操作は知ってるけど、そこまで詳しくはない」レベルの知識でgit-flowの運用は不可能です。メンバーのスキルセットと運用のミスマッチが本質的な原因です。
4. 設計の原則
絶対王政下の圧政と対峙して人権思想が産まれたのと同様に、つらみに正面から取り組むためには「よりどころ」となる価値観が必要です。困難な問題に立ち向かうためには「思想」が必要であり、思想なき改革は「作業」にすぎません。また、技術的な選択にはトレードオフへの対処がつきものであり、トレードオフに対峙したときに寄るべき価値基準を事前に作っておくことには意味があります。
この章では、新しい基盤を作る際の設計原則を提示し、それぞれの原則について旧基盤はどのような点で原則を満たしていなかったのかを書いていきます。「3. 旧基盤の反省点」で書いてはいないつらみも少し出てきます。
なお、ここで記載されていることはすべて筆者の個人的な意見です。
原則1:変化に適応できる設計
データに対するニーズはめまぐるしく変化します。1週間前のミーティングで要求されたダッシュボードが「やっぱいらないわ」と言われることもあれば、既存のニーズに対応した結果として変化するニーズ、状況の変化に伴い突如発生するニーズもあります。現実は常に想像を超えており、利用者のニーズを予測することは不可能です。
であるならば、データのニーズに対応する戦略としては「堅実な予測」よりも「迅速なニーズの充足」に価値を置くべきと考えました。ニーズは放っておくと消失してしまうものですが、消えてしまうニーズの中には「真にユーザーが求めているニーズ」につながるものが隠れています。迅速にデータを提供できれば、そのようなニーズを永続的に満たす仕組みが作れるのです。逆に、遅くなればニーズは立ち消えてしまうでしょう。速さとは価値です。素早くニーズを満たせる仕組みには価値があります。
原則1:データ分析基盤は、 変化するニーズに応え、迅速に価値を提供できるように設計するべき
具体的には、以下のようなことを目指します。
- テーブルの要件定義を始めてから、実際にテーブルを作成するまでの時間を短くすること
- テーブルの定義を変更するまでにかかる時間を短くすること
- 開発フローをより単純にすること
- 開発者体験を向上させること
- メンバー同士で、データマート作成用のSQLのコードレビューをしやすくすること
- テストを容易にすること
旧基盤においては、この価値基準は満たされていませんでした。
- テーブルを作成する主な手段が「mainブランチに直接マージして本番用のMWAA環境で実験する」だったため、テーブルの作成に時間がかかり、レビューも満足に行われなかった。
- テーブル定義の変更が容易ではなかった
- 開発フローは煩雑なものであり、開発者体験も悪かった
- テストは「一回アドホックにSQLで検証して終わり」だった
- Glue Data Catalog由来のエラーが頻繁に発生し、開発を妨げていた
原則2:運用労力の最小化
弊社のようなスタートアップ特有の事情ですが、データ分析基盤の「運用」だけに割ける人手は多くありません。もし仮にデータ分析基盤の運用にかかる手間が非常に重く、エンジニアやアナリストの労力の大半がデータ分析基盤の運用に割かれれば、その分だけ利用者目線での価値提供にコミットすることが難しくなります。
であるならば、プロセスの最適化と自動化を通じて運用にかかる労力を最小限にし、価値提供にコミットできる体制を作る必要があると考えました。
原則2:データ分析基盤の運用にかかる労力が最小限となり、データ利用者への価値提供に集中できるように設計する
具体的には、以下のようなことを目指します。
- エラー発生時のリカバリーにかかる時間を最小化すること
- 新規テーブル作成時や既存テーブルの変更時に発生する「バックフィル・オペレーション」を最適化すること
- コスト監視、クエリ実行監視など、監視業務を自動化すること
- 定常的な運用フローを最適化すること
原則1と同様に、旧基盤においてはこの価値基準は満たされていませんでした。
- 異常なレコードが原因で処理が落ちた場合、取り除いてから処理を再実行するまで長い時間がかかっていた
- Glue Jobの段階でエラーが発生した場合に対応が可能なメンバーが一人しかおらず、危険性の高い状態だった
- Glue Jobの開発のしづらさにより、エラー発生時の修復に時間がかかっていた
原則3:「分析者体験」の最大化
データは複雑な現実世界を読み解くための武器です。利用者の手に渡り、適切な使い方で利用されることにより初めて価値を発揮します。データを分析する際の利便性を犠牲にすればデータは利用されなくなり、価値を失ってしまうでしょう。
であるならば、データの「分析者体験」*13とでも呼ぶべきものを最大化するべきです。データ分析基盤の最上の価値を「データ利用者への「分析者体験」の提供」と定めます。当たり前のことですが、あらためて明示しておきましょう。
原則3:データ分析基盤の利用者への「分析者体験」の最大化を第一の判断基準として設計する
旧基盤においては、分析者の体験を最大限配慮したとは言えないものになっていました。
- Athenaが分析用のクエリエンジンとしては使いづらかったこと
- メタデータの管理が一元化されておらず、情報が古かったこと
- テーブル同士の関係性(Data lineage)*14が把握できず、どのテーブルからとってきたデータなのかわからないこと
おわりに
この文章では、まず移行前のデータ分析基盤のつらみについて、構成する技術ごとに記述したのち、つらみを踏まえたうえで、新しい基盤で何を目指すのかの原則を提示しました。
次の文章では、原則を踏まえた「設計」に焦点を当て、利用した技術の詳細やその検討過程を記述していきます。
*1:旧基盤が全く改修されなかったわけではありません。自分が入社した当時はAWS Step Functionsを実行管理に使っており、Git管理もGitHubではなくGitLabでした。本筋からそれるため、旧基盤の改修については割愛します。
*2:一つの行が一つのJSONレコードとなり、複数行組み合わさったファイル形式のこと。詳細についてはjsonlines.org参照 JSON Lines
*3:Glue JobはAWS公式が配布しているdockerイメージを使ってローカル開発が可能 https://docs.aws.amazon.com/ja_jp/glue/latest/dg/aws-glue-programming-etl-libraries.html
*4:2018年式のIntel Macで実施。また、Glue Parquet形式を利用している場合、ローカルで処理を完結させることがそもそも不可能です
*5:https://docs.aws.amazon.com/ja_jp/glue/latest/ug/notebook-getting-started.html
*6:プロダクト側から発行されるログの形式が複雑であったことも苦労の種となりました。本筋からそれるため、この話は割愛します。
*7:弊チームでは全員SQLが書けます。また、チーム内でGlue Jobを取り扱えるのはこの記事の作者だけでした
*8:例えば、Glue Crawlerの処理をなくしたこと
*9:後述しますが、Iceberg形式だとdelete文を発行できるらしい
*10:後述しますが、dagster cloudでは可能です。 Branch Deployments in Dagster Cloud | Dagster Docs
*11:https://github.com/aws/aws-mwaa-local-runner
*12:今思えば、mainブランチへのプルリクエスト作成時に開発環境にデプロイされるようにすればよかったのかもしれません。github-flowにするのは技術的に可能ではありました。プルリクエストを二つ同時に検証することは不可能ですが、git-flowの状況でもその弱みは変わりません。
*13:ユーザー体験(UX)のデータ版のような概念で、例えば以下のような状態が「データの分析者体験が良い」といえる 1. 必要なデータがどこにあるのかわかる状態 - メタデータが整備されており、ユーザーの近くに配置されている 2. 必要なデータをすぐに利用できる状態 - データの利用者が自分の力でデータを利用できる - クエリが書きやすい - よく利用する分析に関してはダッシュボード化されている 3. データの品質が担保されている - 欠損がない - ユニークであるべきカラムがユニークになっている、など
*14:Data lineageに関してはdbtの公式ドキュメント参照https://docs.getdbt.com/terms/data-lineage