smellman's Broken Diary

クソみたいなもんです

MacPorts + Python + Virtualenv + RunSnake (というかwxPython)

gdal2tilesがクソ時間かかるのでプロファイルをしていました。

最初はline_profilerを使ってみたんだけど、 @profile とか書くと普通に動かした時に動かなくなって面倒なので、素直にcProfileを使いました。

% python -m cProfile -o ./profiler_result/1.prof gdal2tiles.py -v -z 0 ~/sampledata/all/temp.vrt ~/sampledata/all/tmp5

これで1.profっていう名前で結果が吐出されるんですが、やっぱりグラフィカルに見たいなーと思って探してみたら RunSnakeRun が良さそう。

とりあえずセットアップします。

% sudo port install py27-wxpython-3.0
% virtualenv-2.7 snake --system-site-packages
% source snake/bin/activate
% pip install SquareMap RunSnakeRun
% rehash

そして動かしてみます。

% runsnake ./profiler_result/1.prof
This program needs access to the screen.
Please run with a Framework build of python, and only when you are
logged in on the main display of your Mac.

怒られました。
これはどうやらpythonwが無いくさいという感じです。

% which python
/Users/btm/(内緒)/dev/snake/bin/python
% which pythonw
/opt/local/bin/pythonw

調べてみたらStack Overflow で osx - Why are Python builds suddenly not Framework builds when using virtualenv? - Stack Overflow という質問があり回答が書いてありました。

% wget --no-check-certificate https://raw.github.com/gldnspud/virtualenv-pythonw-osx/master/install_pythonw.py
% wget --no-check-certificate https://raw.github.com/gldnspud/virtualenv-pythonw-osx/master/pythonw.c
% python install_pythonw.py `which python`/../..
finished!  App bundle created at:  /Users/btm/(内緒)/dev/snake/Python.app

あとは実行してみます。

% runsnake ./profiler_result/1.prof

f:id:smellman:20140206135807p:plain

で、このテクニックなんですが、上記のFixを行う前だと、

>>> import wx
>>> wx.App()
This program needs access to the screen.
Please run with a Framework build of python, and only when you are
logged in on the main display of your Mac.

こうなってしまうので、wxPythonではまってる人も同じ問題で解決できるのではないかと思います。

MapProxy インストールメモ

MapProxyMacに入れようとしたんだけど、ちょいとひっかかったのでメモです。
環境はMountain Lion+MacPortsです。

% virtualenv-2.7 env
% source env/bin/activate
(env) % pip install Pillow
(env) % pip install --no-install PyYAML
(env) % cd env/build/PyYAML
(env) % python setup.py build_ext --include-dirs=/opt/local/include --library-dirs=/opt/local/lib
(env) % pip install --no-download PyYAML
(env) % cd ../../..
(env) % pip install MapProxy

MapProxyが依存してるパッケージを先に手動で入れています。
1つ目はPillow。これはPILの代替として使えるのですが、先にインストールしておかないとPILをインストールしようとしてはまります。
2つ目はPyYAMLです。これは、libyamlが発見できなくて--without-libyamlが有効になってしまうので、手動でlibyamlが入っている位置を指定しています。

というわけで仕事しますね。

Gunicornでsymbolic linkを貼り直しつつ停止なしでデプロイしたい

最近Pythonのプロダクトを扱っていたりします。
GunicornはRuby on Railsでよく使われているUnicornの影響を受けたと思われるプロダクトで、Gunicornは"Green Unicorn"という意味らしいです。

Unicornではよく知られてる手法としてkillによる再起動を使ってほぼ無停止でデプロイ後に再起動をかける手段があります。

Gunicornでも FAQ - How do I reload my application in Gunicorn? にあるように、以下のようなコマンドで再起動ができるそうです。

kill -HUP masterpid

しかしながら、デプロイ時にsymbolic linkを入れ替えるという手法を取った場合にうまく動きません。そもそも、これってソースコードの編集に対応してないんじゃないかなっていう感じです。(検証しているときに書き換えてこれで再起動を掛けても何も変わらなかった)

サーバ自体を停止して、起動するという方法も良いのですが、その方法だと停止なしにはならないし、Ruby on Railsでできてるのになんか負けた気がしたのでちゃんとした方法がないか調べました。

例えば、Ruby on Rails 3.x + Unicorn + capistranoではcapistranounicornで以下のような処理を書いています。
capistranoのdeploy.rbではこんな感じです。

namespace :deploy do
  task :start, :except => { :no_release => true } do
    run "cd #{current_path} ; BUNDLE_GEMFILE=#{current_path}/Gemfile bundle exec unicorn_rails -c config/unicorn.rb -D"
  end

  task :stop, :except => { :no_release => true } do
    run "kill -s QUIT `cat /tmp/#{application}_unicorn_production.pid`"
  end

  task :restart, :except => { :no_release => true } do
    run "kill -s USR2 `cat /tmp/#{application}_unicorn_production.pid`"
  end
end

もう一つunicorn.rbで以下のようなコードを使っています。

before_fork do |server, worker|
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.connection.disconnect!
  end

  old_pid = "/tmp/#{application}_unicorn_production.pid.oldbin"
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      # someone else did our job for us
    end
  end
end

after_fork do |server, worker|
  # the following is *required* for Rails + "preload_app true",
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.establish_connection
  end
end

(この例ではActiveRecordの処理がありますが、今回は使いません)

このような処理を再現すればいいのかと思い調べたところ、以下のスレッドにヒントが有りました。

HUP reloading does not get sys.path updates from .pth files.

このコメントで以下の3つのコマンドを打てばよいという結論が出ていました。

# Reexec a new master with new workers
/bin/kill -s USR2 `cat "$PID"`
# Graceful stop old workers
/bin/kill -s WINCH `cat "$PIDOLD"`
# Graceful stop old master 
/bin/kill -s QUIT `cat "$PIDOLD"`

これを応用してfabricで一気にデプロイできるようにしました。

前提条件として、

  1. /home/hoge/hoge_ve にvirtualenv環境を入れてる(僕の場合はchefで全部やってる)
  2. アプリ自体を /home/hoge/app/(timestamp) みたいにやって、deploy時に /home/hoge/app/hoge にsymbolic linkを貼ってる
  3. /home/hoge/app/.python-eggs は事前にディレクトリを作っておく(あまり意味がないかも?)
# 環境設定
def commonsrv():
    ....
    env.app_name = 'hoge'
    env.home = '~/'
    env.deploy_dir = env.home + 'app/'
    env.app_deploy_root = env.deploy_dir + env.app_name
    env.hoge_pid = '/tmp/hoge_gunicorn.pid'
    env.hoge_pid_old = env.hoge_pid + '.oldbin'

def hogeserv():
    '''hogeサーバーの設定'''
    commonsrv()
    env.user = 'hoge'
    env.hosts = ['127.0.0.1:13022']

def start_app():
    '''
    AP起動
    '''
    cmd = "PYTHON_EGG_CACHE=/home/hoge/app/.python-eggs PATH=$PATH:/home/hoge/hoge_ve/bin/ /home/hoge/hoge_ve/bin/gunicorn main:app -c /home/hoge/app/hoge/gunicorn.nosock.conf.py"
    with cd(env.app_deploy_root):
        run(cmd)

def stop_app():
    '''
    AP停止
    '''
    cmd = "kill -s QUIT `cat " + env.hoge_pid + "`"
    with cd(env.app_deploy_root):
        run(cmd)

def restart_app():
    '''
    AP再起動
    '''
    if exists(env.hoge_pid):
        # Reexec a new master with new workers
        cmd = "kill -s USR2 `cat " + env.hoge_pid + "`"
        run(cmd)
        # Graceful stop old workers
        cmd = "kill -s WINCH `cat " + env.hoge_pid_old + "`"
        run(cmd)
        # Graceful stop old master
        cmd = "kill -s QUIT `cat " + env.hoge_pid_old + "`"
        run(cmd)
    else:
        start_app()

def deploy():
    '''
    デプロイ
    '''
    ....(デプロイの処理とか)
    restart_app()

Gunicornのconfigには以下のように記述しておきます。

# -*- coding: utf-8 -*-
bind = '127.0.0.1:5000'
backlog = 2048
...
debug = False
spew  = False
preload_app = True
daemon = True
pidfile = '/tmp/hoge_gunicorn.pid'
user  = 'hoge'
group = 'hoge'
accesslog = '/var/log/gunicorn/hoge-access.log' # /var/log/gunicornを作成しておく
errorlog = '/var/log/gunicorn/hoge-error.log'
loglevel = 'info'
logconfig = None

デプロイはこんなコマンドです。

fab -f hoge.py hogeserv deploy

これで、すでにサーバが起動していたら切り替わるという寸法です。
再起動だけしたいときも、

fab -f hoge.py hogeserv restart_app

としています。

oldbinについてはUnicornとほぼ同様と見て良いでしょう。実際、Gunicornのコードを見ても入れ替えてる処理が見えます。

まだ、これでも問題があったりします。

一つは stop_app したあとに start_app がたまに動かなくなります。何度も叩いてると動くようになるので何かがロックしてるのかもしれない。
それでも、運用時にはそんなに問題にはならないレベルになっています。

もう一つは Gunicorn で unix socket を使った場合はこの手法だとバグがあって使えないみたいです。
USR2 + QUIT deletes unix socket when QUIT on old master
これでかなりの時間ハマりましたorz

ちなみに、上記のkillを使う手法をベースとしてGunicorn向けのラッパーもあったりします。

Rainbow Saddle

こちらも試してみたのですが、daemonとして動かないため利用するのは止めました。あとちょっと不安定っぽい。

PyObjC + Xcode

Using PyObjC for Developing Cocoa Applications with Python というドキュメントを見つけたので、早速試してみたらちょいとはまったのでメモ。環境は以下の通り。

まず最初にビルドができなくてはまった。これは単純にXcodeがPython2.3を呼び出してしまっているために発生していた(.bash_profileの内容は無視されてしまう)ので、ビルドの設定を書き換える事で対応をします。Xcodeの プロジェクト->アクティブターゲット'Development'を編集 を選択してカスタムビルドコマンドのビルドツールを /usr/bin/env から /Library/Frameworks/Python.framework/Versions/Current/bin/python に変更します。単に setup.py を呼び出しているだけですので、このような設定でよいでしょう。
次にはまったのは実行時に以下のような例外が発生するケースでした。

2006-11-25 17:53:37.965 PyAverager[1112] Unknown class `Averager' in nib file, using `NSObject' instead.
(略)
  File "/Library/Frameworks/Python.framework/Versions/2.4/lib/python2.4/site-packages/PyObjC/PyObjCTools/AppHelper.py", line 235, in runEventLoop
    main(argv)
KeyError: 'NSUnknownKeyException - [<NSObject 0x1332840> valueForUndefinedKey:]: this class is not key value coding-compliant for the key calculatedMean.'

Averagerクラスが見つからないため、結果としてキーが見つからずあぼーんしているという状態でした。ただ、Xcode上で Python Class 作っているのになぜ?と思いこんでしまいかなり解決に時間がかかりました。
この問題は Pythonmac-SIG xcode problems に解決のヒントがありました。ここで、次のような記述があります。

So the nib (whatever that is) cannot find the class Averager.
By going to the source of PyAverager I noticed that the Averager module is imported indirectly, as in:

for pythonModule in info[u'Modules']:
__import__(pythonModule)

If I do "import Averager" or "__import__('Averager')" after these lines the application runs successfully. So I guess that info[u'Modules'] doesn't contain 'Averager'.

PyAveragerのモジュールを読み込むところでAveragerを読むように細工をしているんですが、そもそもforループで読み込まれていれば問題がないはずです。というわけでXcodeのよーくみてみるとすごい間抜けな事をしているのに気づきました。Averager.py が"その他のリソース"扱いになっていたんです(涙 XcodeのグループとファイルにあるAverager.pyをClassesのフォルダの中に入れてビルドをしたらあっさり動きました(号泣
ちなみに、Classesのフォルダの中に入れるとはドキュメントのどこにも書いてなかったです。Classesフォルダを選択した状態で、新規ファイル->Cocoa->Python Class を選ぶとちゃんと Classesフォルダの中に入った状態で作られるので、これははまる人とはまらない人がいそうだ(汗