Python
Django
pythonDay 7

Django でもバッチ処理がしたい!

Django といえば Python の 有名な WebFramework の一つですが、これをバッチ処理として使ってみた話です。

背景

Django で作った Web システムがあるのですが、これに半リアルタイムで行うバッチ処理を追加することとなりました。具体例には一定間隔ごとにAPIを叩いて内容をDBに入れるような処理ですね。情報の同期やジョブキューの消化などままあるシチュエーションだと思います。

DBにデータを入れるという目的が果たせればいいので、一からシステムを作ってもよかったのですが、データベースまわりなど再度構築するには手間がかかる部分も多いです。データベースとの接続を確保したり、SQLAlchemry のような ORM でモデルを定義したり、 スキーマの管理するために
Alembic を導入したりといろいろ面倒です。

そこで既存の Django の資産を使いまわしてみることを考えました。Django であれば DB の扱いを一貫して任せられますし、言うまでもなく既存の Web 用資産もそのまま流用できます。

懸念があるとすればやはり Django が WebFramework である点ですね。ワーカーのような使い方は主としていないですし、バッチ処理には余計なものが多すぎるかもしれません。とはいえ要件的に速度が求められる処理でもなかったので、メモリをインフラ力で殴れれば問題ないのかな…と思いやってみました。

実際にやってみた

では実際にどうやって Django をバッチジョブとして動かしたか書いていきます。
(サンプルソース交えて説明したかったですが時間が… :bow:)

Model を App として切り出し

既存の資産を有効に活かすため、共通で使える Model を別レポジトリにまとめることにしました。Django には App というプロジェクトを分割する仕組みがあります。View と Model などを垂直に分割してプロジェクト間での再利用を可能にするものです。今回はこれを利用して、Model だけを1つの App に再構成しました。

切り出した App を別レポジトリに移し、Web、Worker それぞれのレポジトリから git の submodule として取り込むようにしました。App として構成しておくと、 INSTALLED_APPS にパッケージへのパスを追記するだけで自身のプロジェクトに取り込む事が可能です。

工夫した点といえば、App レポジトリ単体としても DjangoProject として動くようにしたことです。つまり Model のレポジトリにも manage.py を配置して、そこからでも Django のコマンドが呼び出せるようにしました。 単体でコマンドが動くので他プロジェクトに依存せずマイグレーションを実行できたりと便利です。また、ここで admin.py を作り込んでおくと便利な管理画面が手に入ります。

https://docs.djangoproject.com/en/1.11/intro/reusable-apps/

Django Admin Commands を作成

バッチ処理のエントリーポイントとして、Django Admin Commands でカスタムコマンドを作りました。
Admin Commands というのは manage.py を叩いたときに呼び出せるサブコマンドのことです。Djangoではこのサブコマンドを自由に拡張できます。直接コマンド用のスクリプトを作ってもいいですが、その場合 Django の settings.py を自力でロードさせる必要があったりするので、素直にこの AdminComands を使うのが楽です。引数無しで呼び出せば利用できるサブコマンドがリストアップされるので、とりあえず Django のコマンド叩けばよくなります。
https://docs.djangoproject.com/en/1.11/howto/custom-management-commands/

ここでは 1 コマンド叩くと 1 イテレーション分動いて終了するようなコマンドを作りました。 Celery のようなものを入れてスケジューラーごとコマンドで起動する方法もありますが、 1 イテレーションごとにタスクを分けておくことで手動での実行も出来ますし、必要に応じてオプションを渡すことも可能です。また後述する Docker 運用の都合上、フォアグラウンドで明瞭に動いてくれたほうが嬉かったりします。

常時動かす

バッチ処理のコマンドを作ったらあとはこれを断続的に動かすだけです。サーバーを自分で建てて管理する場合は cron を使ったりすればいいと思いますが、弊社のインフラは概ね Amazon ECS のクラスタで構築されているので、そちらでジョブ管理することにしました。

ECS 上でタスクの起動方法は大きく分けて二つあり、 一つはサービスとして常時動かす方法、そしてもう一つは必要なタイミングで都度起動してあげる方法です。サービスとして起動した場合、ECS が自動的にタスクの死活管理をしてくれます。コンテナが終了すると自動で別のコンテナを立ち上げてくれるので、Webサーバーなどの用途でよく利用します。後者の場合は外部から起動してあげる必要があります。代表的なのは CloudWatch のスケジューラーですね。 cron みたいな時間を指定しての実行が可能になります。ただ CloudWatch の場合、分単位での制御になります。秒単位でタスク起動したい場合には利用することが出来ません。

今回は数十秒に 1 回くらいの頻度で動かしたかったので、サービスとして動かすことにしました。先程定義したサブコマンドを使って

/bin/bash -c "while :; do python path/to/manage.py $(サブコマンド名); sleep 15; done;

のように Sleep を挟んで無限ループする形でサービスを作ってあげれば完了です。Sleep 使うのは結構乱暴な気もしますが、厳密なタイミング調整が不要であればうまく行きます。Python としては都度処理が終了するので余計なメモリリークなどは起きないですし、ループが正常に回っているかは ECS 側が管理してくれるので安心です。なお、Python側で処理がロックしてしまうと実行間隔が思い通りにならないという欠点はあるので、1イテレーションが長くなり過ぎないように工夫する必要はあります。

所感

よかったこと

  • Django でも無事バッチ処理が動くように
    • メモリがネックになることもなくちゃんと動きました
    • 管理画面とかも同時に手に入るのが便利です
  • 資産の共通で開発が高速化
    • セットアップが楽々
    • スキーマ変更等にも対応しやすいです

微妙な点

  • submodule 切ると階層が深くなりがち
    • 独立 App としても動くようにしたため、使う側は worker.ext.models.core.models みたいな階層になってしまった
  • 起動までがちょっと遅い
    • Django Admin のサブコマンド呼び出しから処理が始まるまで若干ディレイがあります
      • DB へのコネクションを貼ったりする時間も含まれるのでひとえに Django のせいではないかも

印象としては Django でバッチ処理するのも悪くない感じです。
開発効率を優先するなら使ってもいいのではないでしょうか。