目次
はじめに
新しい機械学習モデルの基盤を構築し、はじめての商用運用を実施したところ、
1回の実行で25万円のSnowflakeのコンピュートコストがかかってしまいました。
コスト増加の原因は、SagemakerからSnowflakeに推論済みデータを格納する処理中ずっとウェアハウスが動きっぱなしだったこと。
それらを回避するために対応策を考えたのですが、環境的な制約が多く実装できず、とはいえコストは下げないとまずいので、諸々回り道をしつつ4万円まで削減(85%カット)できたものの、
「これって本質的じゃないよなぁ...」という虚無感を感じたのが本記事の起点です。
結果論的な議論もしつつ、「同じような環境の制約にぶつかったエンジニアへの参考事例」や「経験者の方の『あるある』」として楽しんでいただけたら幸いです。
結論
- ビッグデータと
pandas.dfやwrite_pandasは相性が悪い - コスト確認大事
- 資格って実務に活きることあるんだ...
1回25万円の請求が来た元々の処理構成
前提:引き継いだノートブックをパイプライン化するのが元々のミッション
もともとの私のタスクは、データサイエンティストから渡された機械学習モデルのJupyter Notebookをリファクタリング・最適化し、SageMakerを用いてパイプライン化・自動実行させることでした。
リリース期限が迫る中、渡されたコードには随所に改善の余地があったため、パイプラインに組み込めるスクリプトへと修正を進めていました。
その中で、推論結果をSnowflakeに書き戻す処理として、以下のような自作関数群が用意されていました。
- 推論結果を Pandas DataFrame に成形する関数
- DataFrameのスキーマに合わせてSnowflake上に テーブル(箱)を作成 する関数
- DataFrameを チャンク(小分け)に分割し、順次格納 していく関数
一見すると「メモリ溢れを防ぐためにチャンク分けまでしていて丁寧だな」と思える実装でした。
ボトルネックの正体はwrite_pandasとウェアハウスサイズ
格納処理のコアには、Snowflake公式のコネクタ関数であるwrite_pandasが使われていました。
write_pandas自体は、内部でParquet変換+ステージング+COPY INTOを行ってくれるため、df.to_sql()等と比較しても小〜中規模なデータであればベストプラクティスとされています。
しかし、今回の商用データは数億単位の超大規模データでした。
これを愚直に「チャンク分け $\rightarrow$ write_pandas」でループ実行した結果、以下のような地獄のコンピュート浪費が発生しました。
[SageMaker] [Snowflake Warehouse]
├─ チャンク①をParquet化 ──(PUT/COPY)───▶ 起動!(秒で完了するが...)
│ (この間、Python側で次チャンク処理) ────▶ 💤 Auto-Suspend待ちでアイドル稼働 (課金継続)
├─ チャンク②をParquet化 ──(PUT/COPY)───▶ 起動!
│ (この間、Python側で次チャンク処理) ────▶ 💤 アイドル稼働 (課金継続)
│ :
└─ (数百回〜数千回ループ) ───────────────▶ 💸 ウェアハウスが数時間ぶっ続けで動いて25万円
- Python側のCPU/IO待ち時間中も、Snowflakeのウェアハウスが常時起動され続けていた。
- 大量データを何度も小分けにして
write_pandasを呼ぶため、細切れのCOPY INTOとオーバーヘッドが大量に発生。 - 結果として、商用の初回実行で 1回あたり25万円 という凄まじい請求を生み出すことになってしまいました。
さらに拍車をかけたのが、ウェアハウスサイズに「2X-Large(32クレジット/時:約1.5万円/時)」という特大サイズを指定していたことでした。
「データ量が多いから処理速度を上げよう」とサイズを大きくしていたのですが、後から検証したところサイズをXSやSに落としても実行時間はほとんど変わりませんでした。
速度のボトルネックがSnowflake側のCPUやメモリではなく、「SageMaker側のPython処理待ち」や「ネットワーク転送・細切れトランザクションのオーバーヘッド」にあったため、コンピュート能力をいくらスケールアップしても単にコストが倍々ゲームで跳ね上がるだけだったのです。
本来やりたかった改善策
当然ウェアハウスサイズを下げることは真っ先に実行。
次に考えたのは、Snowflakeにおけるデータロードの王道構成でした。
SageMakerが推論結果をすでにS3へParquetとして出力しているなら、わざわざPythonで中継する必要はありません。
S3を外部ステージとしてSnowflakeに直接マウントし、一括でバルクロードするのが最速・最強の構成です。
[S3 (推論結果Parquet)]
│
▼ (Snowflakeが直接・並列でバルクロード)
[Snowflake Table]
この構成には圧倒的なメリットがあります。
- オーバーヘッドが最小:中間サーバー(SageMaker等)を介さず、クラウドストレージとSnowflake間で直接転送される。
- 加工処理・メモリ消費がゼロ:すでにS3上にあるParquetファイルをそのまま読ませるため、Pythonのデコード・エンコード処理が不要。
- ウェアハウスの稼働時間が激減:全ノードで並列一括ロードするため、ウェアハウスの起動時間は「数時間」から「数分〜数秒」へと圧縮され、待機時間も最小化できる。
立ちはだかった組織の壁
この王道パターンを実現するには、SnowflakeとAWS S3を安全に連携させる 「ストレージ統合」 を作成するのが大前提となります。
ストレージ統合は、AWSのIAMロールとSnowflakeのExternal IDによる信頼関係を利用するため、コードやSQLにAPIキーなどの認証情報を一切さらさず、最もセキュアに連携できるベストプラクティスです。
イメージは下記のような感じ。
+---------------------------------------------------+
| AWS S3 (推論結果Parquet) |
+---------------------------------------------------+
│
│ (安全なクロスアカウント連携)
▼
+---------------------------------------------------+
| ストレージ統合 (Storage Integration) |
+---------------------------------------------------+
│
▼
+---------------------------------------------------+
| 外部ステージ (External Stage) |
+---------------------------------------------------+
│
▼ (並列 COPY INTO / ウェアハウス最小稼働)
+---------------------------------------------------+
| Snowflake Table |
+---------------------------------------------------+
しかし、ストレージ統合の作成にはSnowflakeの最高権限であるACCOUNTADMIN等の強い権限が必要でした。当然、現場のいち開発者(しかもSESエンジニアの私)にそんな権限はありません。
そこで、手順とメリットを丁寧にまとめてインフラ・管理部門へオブジェクト作成の依頼を出しました。
すると返ってきたのは、衝撃の回答でした。
「ストレージ統合を作成するには社内調整や承認プロセスでかなり時間がかかるかもしれないので、別の方法を考えてもらえますか?」
(心の声:何言ってんねん。別方法を調査・実装してテストする時間があるなら、サクッとオブジェクト作ってもらって王道構成組んだほうが圧倒的に早いし後々も安全やろがい……!)
と思いつつ、私は立場の弱いSESエンジニア。
波風を立てるわけにもいかず、「承知いたしました……」とすごすご引き下がり、別の回避策を考えることになったのでした。
苦肉の策:ローカル落とし+PUT+COPY INTO
ストレージ統合が使えないなら、別のルートを探すしかありません。ここから私の泥臭い試行錯誤が始まりました。
悪あがき①:AWS一時クレデンシャルで直接ロードを試みる
「ストレージ統合がないなら、S3への認証情報をSQL実行時に直接渡せばいいのでは?」と考えました。
もちろん、アクセスキーのハードコーディングはGit管理上論外です。
そこでAWS STSから動的に 一時クレデンシャル を発行し、SQLのCREDENTIALS句に埋め込んで COPY INTO を直接叩く方針を試しました。
これならセキュアだし行けるはず……!と実行したところ、無情にもエラー。
原因を調べて絶望しました。Snowflakeのアカウント設定で、
「外部ステージ作成時はストレージ統合を必須とする」というパラメータであるREQUIRE_STORAGE_INTEGRATION_FOR_STAGE_CREATION = TRUEが有効化されていたのです。
(心の声:ストレージ統合前提の設定にしとるんやったら、最初からストレージ統合作らせろや!!)
と、モニターの前で激しく毒づきましたが、システム的に弾かれる以上どうにもなりません。
これで「S3からの直接ロード」という選択肢は完全に塞がれました。
悪あがき②:write_pandasの裏側の挙動を暴く
ここでやることが思いつかなくなったので、原点に立ち返り、
「そもそも最初に25万円溶かしたwrite_pandasは、裏で何をやっているのか?」という仕組みを調べてみることに。
すると判明したのは以下の事実です。
- メモリ上のDataFrameからローカルに一時的なParquetファイルを生成する
- そのParquetをSnowflakeの内部ステージへPUTする
- 内部ステージから COPY INTO を実行してテーブルにロードする
「……あれ? S3にすでにあるParquetをSageMakerに落として、そのまま内部ステージに PUT して COPY INTO すればいいのでは?」
わざわざPythonのメモリにDataFrameとして展開するから遅いし落ちるのです。
ファイルのまま扱えば、メモリ消費もほぼゼロで済むことにここで気づきました。
内部ステージの選定と「歓喜のOWNERSHIP権限」
Snowflakeは「ステージ(生データ置き場)」と「テーブル(メタデータや統計情報が付与された最適化ストレージ)」が明確に分離されています。
内部ステージには3種類あり、どれを使うかが問題になりました。
- 名前付きステージ:作成権限が必要(弾かれる可能性大)
- ユーザーステージ:誰でも使えるが、管理が煩雑でチーム運用向きではない
-
テーブルステージ:テーブル専用の内部ステージ。最も管理がシンプルでオーバーヘッドが少ないが、対象テーブルに対する
OWNERSHIP権限が必須
「権限で怒られるのがオチだろうな……」と諦め半分で権限を確認したところ、なんと今回の格納先テーブルだけは私がOWNERSHIP権限を持っていました。
「勝てる……!勝てるんだ……!」
最終的に完成した「泥臭い回避ルート」
組み上がった処理フローがこちらです。
[AWS S3 (推論結果Parquet)]
│
▼ ① boto3でSageMakerローカルへダウンロード
[SageMaker ローカルストレージ]
│
▼ ② PUTコマンドでテーブルステージへ転送
[Snowflake テーブルステージ (@%my_table)]
│
▼ ③ COPY INTO で一括バルクロード
[Snowflake Table]
本来のアーキテクチャから見れば「一度SageMakerを経由する」という遠回りをしていますが、
- Pythonメモリを一切使わないためOOMが起きない
- Snowflake内部のバルクロードの恩恵をフルに受ける
- ウェアハウスの起動時間を「数時間」から「数分」へ劇的に短縮
という形で、厳しい制約をすべてクリアしながらコスト削減を達成することに成功しました。
本来はどうすべきだったのかの結果論
今回のトラブルを冷静に振り返ると、「設計」「実装後」「修正時」の3つのフェーズで、それぞれ防ぐ手立てやより良いアプローチがありました。
完全な結果論ではありますが、今後の血肉とするために反省点を整理します。
1. 設計段階:ビッグデータとDataFrameの相性を疑うべきだった
そもそも「ビッグデータを扱うのに、わざわざメモリ上にPandas DataFrameとして展開する」という行為自体がアンチパターンでした。
もちろん、小規模なデータや単一スクリプト内で完結する処理であればwrite_pandasは手軽で素晴らしいツールです。
しかし、数千万〜億行規模のデータを扱うパイプライン設計においては、「Parquetファイルのまま直接クラウドストレージに渡し、PUT/COPY INTOでバルクロードする」というアーキテクチャを最初から選択すべきでした。
また、「データが多いからとりあえずウェアハウスを大きく(2X-Largeに)すれば速くなるだろう」と安易にスケールアップを選択してしまったことも反省点です。
処理のボトルネック(I/Oや待機時間)を見極めずにサイズだけを上げてもコストが爆増するだけだという認識が設計時点で抜けていました。
2. 実装後段階:コスト試算と実行後のモニタリングを怠った
納期が迫る中でリファクタリングを進めていたため、事前の厳密なコスト試算が難しかったのは事実です。
しかし、「一度動かした後に、どれだけのコストが発生しているかを確認しなかった」 のは完全に私の落ち度でした。
Snowflakeの管理者画面はACCOUNTADMINなどの特権がないと見られないため、「自分にはコストが見えない」と確認を後回しにする癖がついていました。
しかし、特権がなくても一般権限で参照できるINFORMATION_SCHEMAやクエリ履歴からコストは簡単に概算できます。
- ウェアハウスサイズごとのクレジット消費率(XS: 1, S: 2, M: 4, L: 8... 2X-Large: 32)を把握しておく。
- 「ウェアハウスサイズを1段階上げるごとにコストが2倍になる」ことを常に意識し、XSやSから小さく試してサイズ変更による速度差を検証する。
- 「処理時間(秒) $\times$ ウェアハウスのクレジット単価」を掛け算し、「これ1回動かすと数千円/数万円飛ぶのでは?」という肌感覚をテスト実行の段階で持つ。
- 開発完了時は、必ずクエリ履歴のトータル実行時間を確認してから本番適用する。
3. 修正時段階:プロパーを説得する努力を放棄して「一人で完結する逃げ道」を選んだ
一番反省すべきはここです。
管理部門から「ストレージ統合の作成には時間がかかる」と言われた際、私は「面倒だから自分だけで完結できる回避策を作ろう」とすぐに引き下がってしまいました。
今回はたまたま対象テーブルの OWNERSHIP 権限を持っていたため、泥臭いワークアラウンドで非機能要件を満たせましたが、もし権限がなければ完全に詰んでいました。
本来やるべきだった動き:
- 「ストレージ統合こそがAPIキーを不要にし、AWSとSnowflake間で最もセキュアに連携できるベストプラクティスである」という公式ドキュメントやセキュリティ上のメリットを整理する。
- 「現在の手法だと中間サーバーを経由するため遅く、将来的にストレージ溢れのリスクがある」というデメリットを定量的に提示する。
- 相手の懸念点(何が足りなくて承認に時間がかかるのか)を逆算し、説得材料を揃えて正攻法を通しにいく。
どんなに立場が低かろうが関係が薄かろうが、システムを長期的に健全に保つためには、「技術的に正しい構成を通すためのコミュニケーションと説明責任」 から逃げてはいけなかったと痛感しました。
感想
記事を書きながら改めて思いましたが、今回「独自の回避策を生み出したのに虚無感がすごい」と感じていた最大の理由は、100%マッチポンプだったからですね。
パイプラインを設計して25万円溶かしたのも私で、その尻拭いに泥臭いワークアラウンドを組んだのも私です。そりゃ虚無にもなります。
その学びの勉強代として(削減できたとはいえ)高額なコストを会社に払わせてしまったのは本当に申し訳なく、今後は二度と同じミスをしないよう肝に銘じて開発に励みます。
とはいえ、SnowPro Core受験してよかった~とかも思いました。
ステージとテーブルの違いすらあやふやだった頃の私なら、今回のトラブルで完全に詰んでいました。
ストレージ統合や内部ステージの仕様、権限の仕組みを事前に知っていたからこそ、制約だらけの環境でも最短で悪あがきができましたんじゃないかなと。
初めて資格勉強が実務の現場でダイレクトに活きたなと感じます。
…それはそうと、
「設定でストレージ統合以外を制限するならストレージ統合するべきなのでは??」
という疑問だけは、今も静かに私の胸の中に残っています。