なぜこの記事を書いたか
セキュリティ制約が厳しめの企業とかだと外部ストレージの利用やS3などに
顧客情報や個人情報関連を配置するにも各種申請やセキュリティ担保の説明などが必要なケースがある。
もちろんS3などの利用は適切にロールやポリシーの設定など含めて設定次第ではあるが、
それら含めて単純に利用するだけで結構手間(S3の利用ではなく社内の申請など)がかかる。
そんな状況下においても、数十万件のデータを画面からアップロードしたり、
ダウンロードしたいという要望は普通に出る。
今回は、DBは利用している(できる)が
外部ストレージやS3などの利用は前述の通り申請などで少し時間がかかる際に、
10万レコードを超えるような「ちょっと大きめのデータ」を、
外部ストレージなしでDjangoアプリケーションから安全かつ安定的にダウンロードさせるための技術的アプローチを検討してみた。
今回のプロジェクトで利用したのがDjangoなだけで、言語はどの言語でも大体一緒だと思う。
今回のプロジェクトでの制約
- 運用では、画面から10MB〜100MBくらいのファイルをアップロードする
- S3などの外部ストレージの利用には申請などが必要で、利用までに少し時間がかかる
- 申請完了まで待っていられないくらい運用が逼迫しており、すぐにでも対処が必要
- アップロードされたファイルの内容をDBに登録したいが、登録の過程でバリデーションエラーになったレコードと登録できたレコードを一行単位でcsvに出力したい
- 運用的にアップロードの非同期は許容できるが、ダウンロードはできるだけ早くしたい
- WEBサーバー(nginx)のタイムアウト時間は1分に設定されており、変更不可
- 非同期のキュー管理として、Redisを利用している(Amazon SQSとかは使ってない)
- ここに関しては今回の内容としてはどっちでもいい
1. サーバーを止めないための大前提:同期処理の回避
数十万レコードのCSVファイルをオンデマンドで生成する処理は、数十秒〜数分かかる可能性があるため、
(データの内容次第では1分を超えることがある)
ユーザーがダウンロードボタンを押したときに、DjangoのWebプロセス内でこの処理を同期実行する場合、
下記のような問題が発生する
- タイムアウト: ロードバランサーやプロキシサーバーの設定により、処理中にリクエストがタイムアウトする
- Webサーバーのハングアップ: CPUとメモリを大量に消費し、他のユーザーリクエストを処理できなくなる(飢餓状態)
そのため、外部ストレージの有無にかかわらず、大規模データのエクスポートはCeleryなどの非同期タスクキューを利用することがマスト。
下記のイメージ
| 役割 | サーバー/コンポーネント | 処理タイプ |
|---|---|---|
| ユーザーインターフェース | Django View | 同期処理 (リクエスト受付) |
| 重いデータ処理 | Celery Worker | 非同期処理 (CSV生成、DB操作) |
| 仲介役 | Redis (Broker) | メッセージキュー、タスク結果保存 |
2. 全体の処理フローイメージ
画面からファイルをアップロード
↓
<非同期>
<ループ>
バリデーションチェック+DB書き込み
↓
ファイル(CSV)へも書き出し
↓
ファイルをZIP化してDBへバイナリ形式で保存
↓
ファイル取り込み完了通知(ダウンロードボタンの表示)
3. データの永続化:S3を使わない場合の代替手段
外部ストレージが使えない場合、生成されたCSV/ZIPファイルをどこに置くかという問題が発生する。
これが最もコアな課題となるが、データベースへのバイナリ格納が唯一とれる対処法だと思う。
※ 他にもあればぜひ教えて欲しいです。
今回採用した処理フロー
- Workerでの生成: Celery WorkerがDBからデータを取得し、CSVファイルを生成。
- ZIP圧縮: 生成したCSVファイルをメモリ上でZIP圧縮する。
- バイナリ格納: 圧縮したZIPファイルのバイナリをテーブルに保存する。
この案のメリットとデメリット
メリット
- WebサーバーとWorkerサーバーが完全に分離していてもファイル共有の仕組みが不要
- DBに書き込むのでサーバーが分かれていてもDBが共有されていれば問題なし
- DBのアクセス制御でダウンロード権限を管理できるため、セキュリティがシンプル
- ファイルアップロード時の内容とファイル書き出し時の内容の整合性が担保される
- ダウンロードボタンを押した時にDBのデータからファイルを作成するようにすると、データが同じ状態かどうかの担保ができない
- 例えば誰かがSQLを利用してデータを変更していたり、うっかり編集されている場合などを考慮
- ダウンロードボタンを押した時にDBのデータからファイルを作成するようにすると、データが同じ状態かどうかの担保ができない
デメリット
- DBのI/O負荷が非常に高くなる可能性がある
- DBのサイズが肥大化し、バックアップとリストアに時間がかかるようになる可能性がある
デメリットに対しての対処
- ダウンロード可能期限を設け、一定期間が過ぎたファイルはDBから削除する
- DBのコネクションを再利用できるようにプロキシの理由やコネクションプールも場合によっては検討する
まとめ
この非同期・DBバイナリ格納戦略なら、
S3などの外部ストレージが利用できないという厳しい制約の中でも
なんとか大規模データのエクスポート機能を安定して提供することができるかなと思います。
新しい技術は常に生まれるけれど、まだまだレガシーなシステム構成や社内の仕組みでもがいているエンジニアは多いと思うので、
少しでも参考になれば嬉しいなと思います。