この記事は Docker Advent Calendar 2015 14日目の記事です。
Summary
- Pythonで定時処理batchを作りたい場合はparse-crontabを使うと便利&瞬殺で出来るお.
- parse-crontabとDockerの公式Python Imageでbatch実行環境を速攻で作れちゃうぜ!
本題
このネタはPyCon JP 2015のTalk Session「野球Hack!~Pythonを用いたデータ分析と可視化 」の後半戦で披露した、「Dockerとparse-crontabで野球データを取得するbatchを作ったぜ!」ネタの抜粋&詳細解説バージョンとなります.
基本的には私の都合&思いで書いてる部分が多いので意見ツッコミ提案お待ちしておりますm(_ _)m
また、このネタを作るにあたり、
Pythonで定時実行処理を実装する(GAUJIN.JP/ごうじん)
上記エントリーを参考にさせていただきました.
※PyCon JP 2015発表の時点で引用の承諾を頂いております.
お前だれよ
自己紹介
- @shinyorke(Twitter)
- 八重洲方面で働くWeb Engineerであり、野球データをHackする「野生の野球アナリスト」でもある.
- Python, Agile, Lean Startupなどが好物.
- 「野球」「Python」でググると過去の成果がたくさん出てきます.
Dockar歴とか
- 初めて触ったのが2014年冬
- 一度挫折→キッカケがあって2015年夏に再チャレンジ→ハマりにハマり、今ではDockerナシでの開発が苦痛に.
- 上述の通り、今年のPyConの発表でDockerを少しだけ使いました.
- 今冬〜来春にかけて構築する野球データ分析基盤はDocker上で動かす予定です.
- 何が言いたいってDockerメッチャ好き.
背景
PyCon JP 2015、、、の前に行ったXP祭り2015の発表で野球データが必要となり、開発を始めました.
- 野球データのサイト(毎日更新)をスクレイピング&データセットを吐き出すスクリプトをPythonで実装.
- 毎朝定期的に実行してデータセットを取るマイクロサービスが必要に.
- そうだ!Docker上で作る&imageごとクラウドサービスにコピって定時実行すればいいじゃん!(名案).
課題
Docker HubのPythonイメージにcrontabが含まれていない
- 実行すべき処理をPythonで書いて、あとはcrontabを仕込んで放っておけばいい.と、最初は思っていた.
- だがしかし、使う予定だったDocker HubのPython imageにはcrontabが含まれていない!
- ワイ「Dockerの思想上、確かにそうよねー(でもこのままじゃサービス作れないどうしよう...orz)」
転機!〜Pythonだけでcrontabが動かせるだと?
- 適当にググった所、ごうじんさんのブログ「Pythonで定時実行処理を実装する(GAUJIN.JP/ごうじん)」に遭遇
- 手元で写経して実験→うまく動いた\(^o^)/
- Dockerに突っ込んで動かしても問題ない!!!ちゃんと動いてる!!!!これはイケる!!!!!
ちょうどPyCon JP 2015の発表を作ってたのもあり、その場でTwitterで「転載させて!」とお願い→ありがたいことに快諾していただきました.
sample&解説
野球のコードはちょっと複雑(&あんまり解説したくないw)ので、sampleを作りました.
python-crontab-docker-example(GitHub)
推奨環境はPython3.4.x以上です.
ちなみに、現時点(2015/12/14)の最新バージョンPython 3.5.1で問題なく動いております!
Job設定&実行
batchを実行するJobControllerと、次の実行時間&実行までのインターバルを管理するJobSettingsを定義します.
JobController.run()の引数はお馴染みcrontabの設定(* * * * *)です.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import time
import functools
import logging
from crontab import CronTab
from datetime import datetime, timedelta
import math
__author__ = 'Shinichi Nakagawa'
class JobController(object):
"""
ジョブ実行Controller
"""
@classmethod
def run(cls, crontab):
"""
処理実行
:param crontab: job schedule
"""
def receive_func(job):
@functools.wraps(job)
def wrapper():
job_settings = JobSettings(CronTab(crontab))
logging.info("->- Process Start")
while True:
try:
logging.info(
"-?- next running\tschedule:%s" %
job_settings.schedule().strftime("%Y-%m-%d %H:%M:%S")
)
time.sleep(job_settings.interval())
logging.info("->- Job Start")
job()
logging.info("-<- Job Done")
except KeyboardInterrupt:
break
logging.info("-<- Process Done.")
return wrapper
return receive_func
class JobSettings(object):
"""
出力設定
"""
def __init__(self, crontab):
"""
:param crontab: crontab.CronTab
"""
self._crontab = crontab
def schedule(self):
"""
次回実行
:return: datetime
"""
crontab = self._crontab
return datetime.now() + timedelta(seconds=math.ceil(crontab.next()))
def interval(self):
"""
次回実行までの時間
:return: seconds
"""
crontab = self._crontab
return math.ceil(crontab.next())
batch本体
JobControllerをimport、実行すべき処理をPoolしてあとは並列実行します.
sampleは毎週金曜日夜のタモリ倶楽部および、毎日18:00の野球を教える感じで作っています.
注意するポイントとしては、
- DockerのPythonイメージはUTCタイム(デフォ)なので、日本時間では設定しちゃダメ.
- 日本のタイムスケジュールに合わせるなら-9.0hしましょう.
ってことぐらいですね.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
from multiprocessing import Pool
from scheduler.job import JobController
__author__ = 'Shinichi Nakagawa'
# Docker ImageのTimezoneがUTCなので注意!
@JobController.run("20 15 * * 5")
def notice_tmr_club():
"""
タモリ倶楽部の時間だお(東京)
:return: None
"""
logging.info("タモリ倶楽部はじまるよ!!!")
# Docker ImageのTimezoneがUTCなので注意!(大切なので2回言いました)
@JobController.run("00 9 * * *")
def notice_baseball():
"""
やきうの時間を教えるお
:return: None
"""
logging.info("やきうの時間だあああ!!!!")
def main():
"""
crontabを動かすmethod
:return: None
"""
# ログ設定(Infoレベル、フォーマット、タイムスタンプ)
logging.basicConfig(
level=logging.INFO,
format="time:%(asctime)s.%(msecs)03d\tprocess:%(process)d" + "\tmessage:%(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
# crontabで実行したいジョブを登録
jobs = [notice_tmr_club, notice_baseball]
# multi process running
p = Pool(len(jobs))
try:
for job in jobs:
p.apply_async(job)
p.close()
p.join()
except KeyboardInterrupt:
logging.info("exit")
if __name__ == '__main__':
main()
Dockerfile
こちらは単純です.
GitHubのコードをincludeして立ち上げて終了.
# Python crontab sample
FROM python:3.5.1
MAINTAINER Shinichi Nakagawa <spirits.is.my.rader@gmail.com>
# add to application
RUN mkdir /app
WORKDIR /app
ADD requirements.txt /app/
RUN pip install -r requirements.txt
ADD ./scheduler /app/scheduler/
ADD *.py /app/
docker-compose
docker-compose.ymlにdocker起動の設定を書きます.
といってもbatch.pyを動かして終了です.
batch:
build: .
dockerfile: ./Dockerfile
command: python batch.py
container_name: python_crontab_example
起動!!!
docker-compose up(又はdocker run)してこんな感じで動けばOK.
$ docker-compose up
Creating python_crontab_example
Attaching to python_crontab_example
python_crontab_example | time:2015-12-13 13:45:09.463 process:9 message:->- Process Start
python_crontab_example | time:2015-12-13 13:45:09.464 process:8 message:->- Process Start
python_crontab_example | time:2015-12-13 13:45:09.465 process:9 message:-?- next running schedule:2015-12-18 15:20:00
python_crontab_example | time:2015-12-13 13:45:09.465 process:8 message:-?- next running schedule:2015-12-14 09:00:00
まとめ&次の方へ
まとめ
- コンテナの凝集性を高める意味でも、「crontabに依存しない言語独自のcrontab」というアプローチはアリなのかもしれない
- 他の言語でもあれば真似出来そう.
- とはいえ用途とかユースケースは割と限定される&総合的にもっと賢い方法があったら知りたい.
- というわけでご意見ご感想お待ちしています.
次の方へ
@tamai さんよろしくお願いします.