#序
cronとは、UNIX系OSでバッチ処理に用いるシステムである
cronとは、Webサービスを提供するのに直接かかわりがないが、無いと困るものである
cronとは、テストが通った後でよく動かなくなって焦るものである。。。
普通に動かせば動くスクリプトがどうしてcronで動かすと動かなくなってしまうのだろうか?
#cronは環境変数をセットしない
大体の場合、サーバーのマシンにログインするとたくさんの環境変数がセットされている
どんな環境変数が入っているか知りたいときは printenv コマンドを使うと早い
手元にあったMacBookでprintenvしてみると
> printenv
TERM_PROGRAM=Apple_Terminal
SHELL=/bin/bash
TERM=xterm-256color
TMPDIR=/var/folders/my/69wp99_14f9g5pdfgclt78wh0000gn/T/
TERM_PROGRAM_VERSION=433
TERM_SESSION_ID=027FA716-D538-4223-B54E-C34D1323142B
USER=ming
SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.zv8qNVeG1v/Listeners
PATH=/Users/ming/.rbenv/shims:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/pgsql/bin/:/Users/ming/.nodebrew/current/bin/
LaunchInstanceID=A3D120E3-9757-477B-8D8D-0222AB139DFE
PWD=/Users/ming/Development
LANG=ja_JP.UTF-8
XPC_FLAGS=0x0
RBENV_SHELL=bash
XPC_SERVICE_NAME=0
SHLVL=1
HOME=/Users/ming
LOGNAME=ming
PGDATA=/usr/local/var/postgres/
SECURITYSESSIONID=186a9
_=/usr/bin/printenv
OLDPWD=/Users/ming
といった感じで結構設定されている
この辺が cron だとほとんど設定されていない状態で実行される
特にcronが動かなくなる問題で大半関わる PATH は
なんと /bin と /usr/bin しか登録されていない!
サードパーティー製のアプリケーションはコマンドが
/usr/local/bin
とか
/usr/local/apache22/bin
とか
/var/lib/pgsql/data/bin
とか、意外とあっちこっちに入っていたりして、インストールと同時に PATH に
アレコレ追加されているのがいたって普通の環境である。
なのでサードパーティ製のコマンドとか、下手したらphpとかrubyですら
インストールの仕方によっては動かなくても文句は言えない
pythonに至ってはpython2がこのあたりに入っているので、動いたはいいが
python3で動くライブラリが全部エラーなんてこともよくある
これは動かなくなるわ…!!
##本当にそうなの?っていう方は
crontab -e で次の1行を書いてみよう
* * * * * printenv > ~/env.txt
less ~/env.txt して、あまりの環境変数の少なさに恐れおののくがいい
ちなみに crontab -e って vim で開くんですよねー vim 苦手なんだよな
という人は、
> export EDITOR=/usr/bin/nano
とか適当なエディタのパスを環境変数EDITORに突っ込んでやると vim 以外でも開ける
いや、crontab コマンドのオプションに作ってくれよって感じなんだけど...
###まさか: 何もファイルに書かれないなんてことある?
printenv コマンドがせまーい cron の PATH に含まれていないとしたら
ファイルはできるけど、コマンド失敗して何も出ませんね
> which printenv
/bin/printenv
こんな風に /bin の下とかにあればいいけど、無かったらcrontabにフルパスでコマンド書く
##そういう時どうするのか?
調べたら諸説あり
例えばcrontabに
PATH=$PATH:/usr/local/bin:/usr/ec2-user/bin
* * * * * php myscript.php
cronの設定上に変数を書くことができる
ただ、共用のマシンとかだと既存のジョブにも影響するのでこの方法は取れない
そうなると
* * * * * export PATH=${PATH}:/usr/local/bin:/usr/ec2-user/bin; php myscript.php
みたいに、環境変数をセットしてから処理するように直列に書く
結構長くなってメンテしにくい
* * * * * bash --login -c "php myscript.php"
ログインシェルをサブプロセスで立ち上げてコマンドを -c オプションで渡す
-c でクオーテーションを使ってしまうので、複雑なコマンドを投げる時に悩む
なので落ち着いたところは
source ~/.bash_profile
php myscript.php
という感じのを作って
* * * * * bash myscript.sh
とする方法
メンテしやすくてよいなと思う
ということで、1個目は環境変数をまず疑ってみましょうという話でした
#cronの標準出力は通常と違う
cronの書き方を調べるとよく
* * * * * some command > /dev/null 2>&1
という感じで出てきたりする
これはcronの場合画面がないので、標準出力がメールシステムに流れていく仕様があるためだ
試しに最初の例で
#* * * * * printenv > ~/env.txt
* * * * * printenv
とすると、/var/mail/<your user name>
あたりのファイルに printenv した内容が書き込まれているはずだ
/dev/null に繋ぐとそのまま出力が消えるので
そのタスクの出力をいちいちメールで受け取らなくしている
##標準出力周りは何かある
おかしなことが起きたのでその事象を紹介する
import subprocess
p = subprocess.run(['grep', '-r', 'foo', './doc'])
print(p.stdout)
print(p.stderr)
# -> None, None
シェルのプロセスを立ち上げて grep する python スクリプトを作ってみる
特に引数を渡さないと subprocess.run() は戻り値の .stdout , .stderr に何も格納しない
これはコンソールでも cron でも同じ結果
次に
import subprocess
from subprocess import PIPE
p = subprocess.run(['grep', '-r', 'foo', './doc'], stdout=PIPE)
print(p.stdout)
print(p.stderr)
# コンソールから -> b'./doc/foo.txt:foo\n', None
# cronから -> b'', None
stderr は受けていないので当然 None なのだが、cronの場合の stdout がおかしい
更に
import subprocess
from subprocess import PIPE
p = subprocess.run(['grep', '-r', 'foo', './doc'], stdout=PIPE, stderr=PIPE)
print(p.stdout)
print(p.stderr)
# コンソールから -> b'./doc/foo.txt:foo\n', b''
# cronから -> b'', b'./doc/foo.txt:foo\n'
なぜか cron だと出力が逆転している
import subprocess
from subprocess import PIPE,STDOUT
p = subprocess.run(['grep', '-r', 'foo', './doc'], stdout=PIPE, stderr=STDOUT)
print(p.stdout)
print(p.stderr)
# コンソールから -> b'./doc/foo.txt:foo\n', None
# cronから -> b'./doc/foo.txt:foo\n', None
STDOUT に STDERR を合流させてから出力すると、 cron 経由でも STDOUT に出る
##そんなことあるの
実は centOS 7 でこの現象に遭遇した後、 MacOS X で試してみたら同じ問題は起きなかった
Webで調べると同様のケースが見つかるので環境によって起きる現象だと思われる
##そういう時どうするのか?
どうしたらいいんでしょうね
とりあえず標準出力絡みの引数を変えたパターンを試してみるといいと思います
標準出力系は疑ってみよう
##余談: /dev/null に捨てていいの?
/dev/nullに出力捨てたら椅子なげる人がいるらしい
こわい
まあ何も考えずに結果を捨てていたら、異常に気が付けなくなるからよくない
しかし、わんさか飛んでくるメールなんて誰も見なくなるのが関の山である
個人的には標準出力に頼るよりもスクリプトにロガーを付けておいた方がいいと考えている
とにかく気が付くことが大事なので、こんなのはどうだろう
python something_to_do.py
if [[ $? -gt 0 ]]; then
"なんか裏でえげつないエラー起きてまっせ?" >> /var/www/html/index.html
fi
いやダメだけど
さておき、そろそろ動いてくれるかな...cron
#cronはホームから実行される
たとえば /home/ec2-user/myscript/bin/myscript.php とか作ったとして
コンソールから実行するときって大抵は cd /home/ec2-user/myscript/bin して
php myscript.php すると思う
そうすると php の require は /home/ec2-user/myscript/bin からのパスで解決してくれるので
実質、そのファイルからの相対パスで書いてあれば動いてくれる
<?php
require "../lib/mylib.php"
echo "do something grate"
これは /home/ec2-user/myscript/lib/mylib.php の読み取りに成功すれば動く
Webサーバで動かしている場合も同様に動いてくれる
のだが
##動かないんだこれが
0 * * * * php /home/ec2-user/myscript/bin/myscript.php > /dev/null 2>&1
とcronから動かそうとすると途端に動かない
cron 実行時のカレントディレクトリはホームディレクトリの /home/ec2-user
phpはそこから ../lib/mylib.php 先のパス /home/lib/mylib.php をロードしようとして
エラー終了してしまう
##そういう時どうするのか?
cron実行時にカレントディレクトリを変更するか、ライブラリの参照を絶対パスにすればよい
カレントディレクトリを変更する
0 * * * * bash /home/ec2-user/myscript/bin/myscript.sh > /dev/null 2>&1
cd /home/ec2-user/myscript/bin
php myscript.php
絶対パスでロードする
<?php
$documentRoot = dirname(dirname(__FILE__))
require $documentRoot."/lib/mylib.php"
echo "do something grate"
これはどちらかというとphpの仕様でハマる問題か
#終わりに
cronは動いている
止まっているのはcronではないから、リダイレクトしなければエラーがメールで飛んでくる
メールが読めればエラーが起きている箇所は把握できる
メーラーが死んでいる時は適当なファイルにリダイレクトしよう
インタラクティブ型のデバッガーは使えないけどなー