Ian Lewis
Ian Lewis is a web developer living in Tokyo Japan. His current interests are in Django, python, alternative databases and rapid web application development. About Me...
  • Django アプリサーバ、gunicorn と fastcgi の比較

    http://gunicorn.org/images/large_gunicorn.png

    概要

    最近、会社では、fastcgi より、 gunicorn を使うのがどう? といわれました。gunicorn を触ったことない僕は fastcgi のロードテストも実際やったことなくて、メソッドについて、(prefork がいいか、 threadedがいいか) の読んでいたものを元にした推測しかできない状態で、知識足りないと思った。

    gunicorn は何かというと、python で作られた WSGI に対応するウェブサーバーです。同期、非同期ウェブアプリ両方対応できますし、作りがよくてかなりスピーディーそうですし、Django アプリを簡単に組み込めますし、python で運用が楽というのがポイントですね。もちろん、エンドユーザーが直接 gunicorn に接続するのではなく、 nginx のローダーバランサーでプロクシーのが一般的だと思っています。

    テストアプリケーション

    ということで、ちゃんとテストしようと思いまして、gunicorn と fastcgi prefork と fastcgi threaded を比較できるテスアプリを作りました。 bitbucket にアップ しましたので、ご参考ください。

    まず、 buildout を使いましたので、以下のコマンドでテストサーバーの環境を作ります。 mysql と nginx も必要なので、インストールしておいてください。

    python bootstrap.py
    ./bin/buildout init -d
    ./bin/buildout
    

    プロジェクトディレクトリのなか、 fastcgi_nginx.conf と gunicorn_nginx.conf ができるので、それぞれのテストをするときに、リンクを/etc/nginx/sites-enabled/ においてください。アプリは同じポートを使っているので、両方のconf ファイルを同時に有効にすることができない。gunicorn を有効にするときに、まず、fastcgi のリンクを /etc/nginx/sites-enabled/ から削除してください。

    mysql の DB は以下のSQLで作れます。

    CREATE DATABASE gunicorn_test CHARACTER SET utf8;
    

    DBを作った後に、syncdb コマンドを実行してください。管理者は特にいらないので、 'no' を入力して大丈夫です。

    ./bin/syncdb
    

    同じポートを使いますし、同時に立ち上げることができないんですが、それぞれのアプリサーバーは以下のように立ち上げます。

    ./bin/runfcgi                         (fcgi prefork)
    ./bin/runfcgi_threaded                (fastcgi threaded)
    ./bin/run_gunicorn -w <# of workers>  (gunicorn)
    

    テストアプリはトップページしかなくて、一つのフォームを持って、一ページくらい(20個)のコンテンツを表示するような処理をします。

    クライアントは以下のように環境をつくれます。

    cd testing/httpclient
    python bootstrap.py
    ./bin/buildout init -d
    ./bin/buildout
    

    クライアントは以下のように実行できます。クライアントのプロセス数と遅延時間を指定できます。遅延時間でテストの期間を指定できます。 デフォールトはプロセス数 500 で、遅延時間 10秒。この設定で、 500 プロセスの処理を10秒の間に伸びます。1秒目は50プロセス、2秒目は50プロセス。。。と言うふうにテストを行います。つまり、1秒間のテスト数はプロセス数わり遅延時間 (500 / 10 = 50)

    ./bin/python run_test.py <host> <# processes> <wait time>
    

    クライアントが行う処理は、トップページを表示し、フォームにテキストデータを入れて、POSTする処理です。なので、1プロセスは3回リクエストをします。 トップページを表示、POST・リダイレクト、トップページを表示 と言う風なテストを行います。なぜなら、ページの閲覧とデータの登録が激しいテストをしたかったわけです。

    テストの環境

    私は今まで知っていたかぎり、theaded はメモリを節約してくれるけど、コアを使いこなせなくて、 メモリが足りるなら、prefork の方がいいという認識でした。 gunicorn もマルチプロセスモデルを使っているので、同じくthreaded より早いはずだが、http の解析は fastcgi より若干遅いかなと思いました。gunicorn の作りが全く別なので、なんとも言えないけど、作りが一緒んであれば、http よりfastcgi が若干早いはず。

    このテストは EC2 上で行い、 ハイCPU ミディアム インスタンス 5台 (サーバー1台、 クライアント4台)。 なぜかというと、複数のコアを使いこなすかどうかをテストしたかったわけです。サーバーインスタンスは2ギガくらいメモリを持っているので、かなりのリクエストを処理するには充分足りるかと思っていました。

    gunicorn は 5ワーカー(リクエストを処理するプロセス)を使ってテストしました。 gunicorn はコア数 * 2 + 1 のワーカーを使うのを おすすめしています

    毎回テストを行う前にDBをクリアしました。

    echo "delete from perftest_mymodel;" | mysql -u root gunicorn_test
    

    テストを行った時に、 1クライアント500グロセス、10秒間にしました。ようするに、 2000ユーザーを同時にアプリをアクセスして、サーバーの処理できるリミットをテストしました。

    ./bin/python runtest xxx.compute.internal 500 10
    

    テストの成果

    fastcgi (threaded)
    ---------------------------
    
    Min time: 0.0689101219177s
    Max time: 23.1616601944s
    Average Time: 8.089163658s
    Errors: 1002 / 2000 (50.1%)
    
    fastcgi (prefork)
    ---------------------------
    
    Min time: 0.0668342113495s
    Max time: 21.9225800037s
    Average Time: 4.586471732s
    Errors: 561 / 2000 (28.1%)
    
    gunicorn
    ----------------------------
    
    Min time: 0.0718009471893s
    Max time: 21.6963949203s
    Average Time: 4.199061027s
    Errors: 293 / 2000 (14.7%)
    

    Min time は最低処理時間 (1プロセス)、Max Time は最高処理時間(1プロセス)、 Average Time は平均処理時間(1プロセス)、Errors は途中でエラーが出て失敗して、処理が出来なかった率とプロセス数。エラーが出たプロセスは時間の計測に入らない。

    まとめ

    fastcgi は思ったより頑張ってましたが、メモリが充分あれば、プロセスモデルを使った prefork メソッドを使うべきでしょうね。平均処理する時間が半分になり、途中でエラーが出る率も半分になりますよね。

    gunicorn の場合は処理時間がfastcgi の prefork と少し早く見えますけど、最低処理時間が少し高くなって、あんまりかわらないんですが、エラー数が prefork よりさらに半分くらいになりました。要するに、gunicorn は fastcgi prefork より多くのユーザーを扱うことができました。 ということは、本当の運用しているアプリケーションにgunicorn がリソースをより効率的に使う可能性が高いですね。 BeProud ではもうちょっと検討するのですが、非同期アプリケーションの仕事も増えていますし、将来にgunicorn を使うのが良さそうに見えます。

    もし、誰かがこのテストを使ったら、他のハードウエア、環境などでは、どういう結果がでるかを聞きたいと思っています。もしくは、テストについてのコメントがあれば、ぜひ宜しくお願いします。

    Send feedback   このエントリーを含むはてなブックマーク はてなブックマーク - Django アプリサーバ、gunicorn と fastcgi の比較
  • Python 温泉 (夏 2010)

    Python 温泉 in 熱海に行ってきました。ま、まだ熱海の旅館なんだけど、早速 Blog を書こうと思っていました。

    結構いい感じで、進んで来ました。会社の AE35 、 と akisutesama と電車に乗ってきました。電車の中、ずっと iPhone 触りつつ

    http://desmond.yfrog.com/Himg35/scaled.php?tn=0&server=35&filename=5r8u.jpg&xsize=640&ysize=640 http://farm2.static.flickr.com/1105/4731986975_91dea2ba68.jpg

    増田さん が持ってかえってきた Singaporeで行った PyCon APAC グッズをジャンケンで割り当てた。後、 清水川先生 が寄付した、 「エクスパート Python プログラミング」の本をジャンケンで配った。

    http://farm5.static.flickr.com/4120/4735457768_8ae67e7c39.jpg

    今回の Python 温泉、割と効率が高くて、いろなことができました。

    Django メールAPIの文字コード周りは余りがあるので、すこし改善しようと思いました。EmailMessage では、エンコーディングを指定したら、その文字コードを使いますけど、 send_mail() 関数はまだメールの文字コードを指定できない。ということで、EMAIL_CHARSET のデフォールトのメール文字コードの設定を増やして、send_mail() に encoding 引数を増やして、パッチを作って、 投稿した

    次に localflavor.jp モジュールにバグ修正 and 機能追加して、パッチを チケットに投稿した

    localflavor.jp.jp_prefectures に Select ウィジェットの選択肢が定義されているのですが、順番は ISO-3166-2 とずれがあったので、直しました。それに、 HiraganaField、KatakanaField、FullWidthField、HalfWidthKatakanaField のフォームフィールドを追加した。普通にフォームに使えるCharField.

    from django import forms
    from django.contrib.localflavor.jp import forms as jp_forms
    
    class MyForm(forms.Form):
        name = forms.CharField(u'名前')
        kana_name = jp_forms.KatakanaField(u'かな')
    

    Django のパッチポリシーに応じて、もちろん、両方とうもテスト付き

    その後に、僕が作った python-disqus-client のパッチを他のデベロッパーからもらったので、新しいバージョンを作って、 pypyにつっこんだ

    その後に、仕事のライブラリの Django 1.2 対応をしたり、 buildbot を立てたりしました。

    皆さん、お疲れ様でした。

    Send feedback   このエントリーを含むはてなブックマーク はてなブックマーク - Python 温泉 (夏 2010)
  • Python StringIO と cStringIO のもう一つの違い

    C で作られた cStringIO は ピュア Python で作られた StringIO モジュールと違うのをみんな知っていると思いますけど、今日、私が知らなかった違いをもう一つ見つけました。

    StringIO では、StringIO のコンストラクターに文字列を渡せば、その文字列に書き込みすることができる。

    >>> from StringIO import StringIO
    >>> writer = StringIO('a')
    >>> writer.seek(1)
    >>> writer.write("b")
    >>> writer.getvalue()
    'ab'
    

    だが、 cStringIO の場合、コンストラクターに文字列を渡せば、StringIO.StringI オブジェクトになって、write メソッドがそもそもないオブジェクトになります。

    >>> from cStringIO import StringIO
    >>> writer = StringIO('a')
    >>> writer
    <cStringIO.StringI object at 0xb74b3530>
    >>> writer.write("b")
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'cStringIO.StringI' object has no attribute 'write'
    

    解決するのがそんなに難しくないけど、AttributeError が出て、ビックリしました。

    解決するのはこう書けばいい。

    >>> from cStringIO import StringIO
    >>> x = 'a' # 既存文字列
    >>> writer = StringIO()
    >>> writer.write(x)
    >>> writer.write('b')
    >>> writer.getvalue()
    'ab'
    

    私は具体的に、やろうとしていたのは、 Django の django.core.files.base の ContentFile を使おうとしていた。だが、どうしても、StringIOのコンストラクターに文字列を渡すので、書き込みには使い物にならなかった。

    >>> from cStringIO import StringIO
    >>> from django.core.files.base import File, ContentFile
    >>> f = ContentFile(None)
    >>> f.write('a')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/home/ian/.virtualenvs/django-test/lib/python2.5/site-packages/django/core/files/utils.py", line 24, in <lambda>
        write = property(lambda self: self.file.write)
    AttributeError: 'cStringIO.StringI' object has no attribute 'write'
    >>> f = File(StringIO())
    >>> f.write('a')
    >>> f.seek(0)
    >>> f.read()
    'a'
    
    Send feedback   このエントリーを含むはてなブックマーク はてなブックマーク - Python StringIO と cStringIO のもう一つの違い
  • Django 1.2 マルチ DB と master/slave レプリ

    Django 1.2 はマルチDB対応ができまして、master/slave レプリにも対応しているのですが、詳しく見るといろな問題が出てきます。

    Django 1.2 のマルチDB対応は どのDBから、読み込むか、どのDBに書き込むかがDBルータで決める。しかし、そのルータで決める時点でリクエストオブジェクトにアクセスできないので、レプリラグのどを自動的に対応するのが難しい。

    レプリケーションを使っている場合、マスターDB に書き込んだ時に、データがスレーブDBまで流れるラグがあるので、DBに書き込んだデータがすぐDBから取れない場合がある (eventual consistency)。

    obj = MyModel.objects.create(content="Hello World")
    obj.content = "Hello World!"
    obj.update()
    
    # 失敗する可能性あり
    obj2 = MyModel.objects.get(pk=obj.id)
    

    こういう場合に対応するには、 QuerySet の using メソッドを使わないとダメです。

    obj = MyModel.objects.create(content="Hello World")
    obj.content = "Hello World!"
    obj.update()
    
    # OK
    obj2 = MyModel.objects.using("master").get(pk=obj.id)
    

    しかし、view コードの中に、DBの仕組みに依存しないといけなくなる。環境 (開発、関連サイトなど)によってDBの仕組みが違う場合もあるだろう。第三者アプリも動かない場合があります。( Django 1.2 自体の contrib モジュールの標準アプリも保証なし)

    上の問題と同じく、POSTして新しく追加したレコードも次のGETリクエストが来た時点では取れない可能性が高い。本当はPOSTした後のGETリクエストは マスター DBを使いたいんですが、 Django 1.2 の仕組みだとそういう処理を導入することが難しい。上と同じく view の中に using() で対応する必要があります。

    どのDBを使うかは QuerySet 単位で、デフォールトDBの名前も固定で、リクエストごとデフォールトDBを変更することもできません。

    今のところは monkey patch しなければ、 Django 1.2 のマルチDBでちゃんと master/slave レプリを使えないのかな。やっぱり微妙ですね。

    Send feedback   このエントリーを含むはてなブックマーク はてなブックマーク - Django 1.2 マルチ DB と master/slave レプリ
  • Django 1.2 の変更のまとめ

    先週、 Django 1.2 が出ました。新しくて、良い機能がいっぱい入っているけども、1.1 からの変更をご紹介しようかと思っています。

    マルチDB

    1.2 では、一番大きい変更は明らかに マルチDB対応 ですね。 settings.pyDATABASE オプションは DATABASES になりました。それで python 辞書で複数のDBを設定する。

    以下のように MySQL、sqlite、PostgreSQL、それぞれ違ってても構いません。

    DATABASES = {
        'default': {
            'NAME': 'app_data',
            'ENGINE': 'django.db.backends.postgresql_psycopg2',
            'USER': 'postgres_user',
            'PASSWORD': 's3krit'
        },
        'users': {
            'NAME': 'user_data',
            'ENGINE': 'django.db.backends.mysql',
            'USER': 'mysql_user',
            'PASSWORD': 'priv4te'
        }
    }
    

    どのデータベースをどの場合に使うかをデータベースルータで決める。 モデルの読み込みの場合のDB、書き込みの場合のDB、リレーションを許可するかどうか、syncdb (テーブル定義)の許可を実装する。

    class MyAppRouter(object):
        """myappアプリケーションのモデルを別DBに保存し、
        DBの操作を制御する"""
    
        def db_for_read(self, model, **hints):
            "'myapp'の場合、'other'というDBを使う。"
            if model._meta.app_label == 'myapp':
                return 'other'
            return None
    
        def db_for_write(self, model, **hints):
            "'myapp'の場合、'other'というDBを使う。"
            if model._meta.app_label == 'myapp':
                return 'other'
            return None
    
        def allow_relation(self, obj1, obj2, **hints):
            "両方のモデルが'myapp'に入っている場合のみにリレーションを許可"
            if obj1._meta.app_label == 'myapp' or obj2._meta.app_label == 'myapp':
                return True
            return None
    
        def allow_syncdb(self, db, model):
            "'myapp' を 'other' DB のみに入れるようにする。"
            if db == 'other':
                return model._meta.app_label == 'myapp'
            elif model._meta.app_label == 'myapp':
                return False
            return None
    

    モデル検証

    モデルのデータを検証すること ができるようになりました。それに、新しいモジュール django.core.validators ができました。

    フォームと同じように full_clean(), clean_fields(), clean(), validate_unique() の 4つのメソッドが追加されました。

    clean_fields(exclude=None) を呼び出すとモデルのフィールドのデータを一個一個検証する。

    clean() はカスタム検証の処理を実装するためのメソッド。このメソッドをサブクラスで実装すれば、カスタム検証ができます。

    validate_unique(exclude=None) はユニークフィールドの検証を行う。

    full_clean(exclude=None) を呼び出すとすべての検証を行います。

    exclude パラメーターで検証を行わないフィールドを指定できる。

    CSRF 対策

    1.1の CsrfMiddleware は正規表現でCSRF用フィールドをフォームに突っ込むことができたが、実装が微妙でJSなどに使えなかったので、Django 1.2でもっと綺麗なAPIが揃えました

    django.middleware.csrf.CsrfViewMiddleware が新しくできました。 このミードールウエアでCSRFのトーケンが生成され、ポストの場合、検証が行われ {% csrf_token %} テンプレートタグでフォームに入れることができている。

    Middleware を使いたくない場合は、 CSRF対策をしたい viewのみに django.views.decorators.csrf.csrf_protect デコレータを使うことができます。

    messages API

    以前、Djangoのユーザメッセージは 「変更しました」とか、「削除しました」とか、一時的なメッセージなのに DB に保存しましたので、微妙だった。

    それで、Django 1.2 では新しい messages API ができました。 DB のかわりにセキュアクッキーとセッションを使えるようになりました。後、メッセージタイプ、info、warning、errorなどが使えるようになりました。

    from django.contrib import messages
    messages.add_message(request, messages.INFO, 'Hello world.')
    
    messages.success(request, u'プロフィールを更新しました。')
    messages.warning(request, u'サービス契約が後3日で切ります。')
    messages.error(request, u'レコードが削除されました')
    

    メールバックエンド

    クラウドサービスでは、メールがAPI で提供されているのが多くて、Django 1.1 では send_mail は使えなかったんですけど、 Django 1.2では メールバックエンド が使えるようになりました。

    標準に入っているのは、SMTP、コンソールに出力するのみ、メールを無視するバックエンドが揃えている。

    カスタムバックエンドを作るために、 django.core.mail.backends.base.BaseEmailBackend を継承して、 send_messages(email_messages) を実装する。 永続コネクションを使う時は open()close() メソッドを実装すれば良い。

    smart if テンプレートタグ

    Django 1.1 以前では、 if テンプレートタグは boolean しか使えなくて、複雑なコンディションが書けなかった。 Django 1.2 から、 ==><andor などが使えれるようになりました。

    {% if a != b %}
        ...
        {% if c == d and e >= f %}
        ...
        {% endif %}
     ...
    {% endif %}
    

    まとめ

    ちょっとしたバグのため、Django 1.2.1 がすぐ出ると思いますが、Django 1.2 は良い新規機能が多くて、是非使って見てください。

    Send feedback   このエントリーを含むはてなブックマーク はてなブックマーク - Django 1.2 の変更のまとめ
  • Django modelformset_factory便利

    Django は複数のフォームのデータを同時に扱えるために、FormSetsというものを用意しているんですけど、実は、ModelFormのFormSetでも使える。クエリーの結果のデータのModelFormを一個一個、一つのページに出すにはこんなコードを書ける。

    from django.forms.models import modelformset_factory
    
    formset = modelformset_factory(
        MyModel,
        fields=('status','content'),
        extra=0,
    )(queryset=MyModel.objects.filter(status=1))
    

    便利

    Send feedback   このエントリーを含むはてなブックマーク はてなブックマーク - Django modelformset_factory便利
  • daemontoolsを使ってdjango fastcgiのデーモンを設定する

    daemontoolsの上にdjango fastcgiを使うのは簡単にできるけど、正しいユーザとして、フォアグラウンドに起動するにはbashとdaemontoolsの設定する必要がある。

    フォアグラウンドに起動するには、daemonize=falseを指定する必要がある。

    それで、起動するデイモンはユーザを指定するオプションがないとrootユーザとして、起動する。runfcgi はそういうオプションがないので、daemontools の setuidgid ツールを使う。

    setuidgidのコマンドになるので、プロセスの標準パイプを正しく接続するには、bashのexecコマンドを使う。

    /service/myapp/run

    #!/bin/bash
    
    BASEDIR="/home/www/"
    PIDFILE="$BASEDIR/app.pid"
    
    exec setuidgid www python /home/www/django-prj/manage.py runfcgi \
        --settings=settings_production method=threaded  port=8001 \
        pidfile=$PIDFILE daemonize=false 2>&1
    
    Send feedback   このエントリーを含むはてなブックマーク はてなブックマーク - daemontoolsを使ってdjango fastcgiのデーモンを設定する
  • DjangoのHTTPS対応開発サーバ

    Djangoの開発サーバはHTTPSを普段に対応してないので、HTTPS対応をどうやって開発すればいいんだろうと思ったら、調べてみた。秘密は開発サーバ、http用とhttps用を二つ立ち上げます。https開発サーバはstunnelでHTTPS対応します。stunnelは普通のソケットをSSL tunnelingをしてくれます。

    このドキュメント を使います。

    stunnel をインストールしてから、pemファイルを作ります。

    openssl req -new -days 365 -nodes -out newreq.pem -keyout /etc/stunnel/stunnel.pem
    

    stunnel の設定ファイルを適当なところに保存します (これから、dev_https)。acceptはhttpsサーバのポート。connectはhttps用の開発サーバのポートになります。

    pid =
    
    [https]
    accept=8002
    connect=8003
    

    stunnel に設定ファイルを指定してdaemonを立ち上げます

    stunnel dev_http
    

    https用の開発サーバを立ち上げます。 HTTPS=on の環境変数を設定しておけば、 request.is_secure()などは Trueをちゃんと返す。

    HTTPS=on python manage.py runserver 0.0.0.0:8003
    

    http用のサーバを立ち上げる

    python manage.py runserver 0.0.0.0:8000
    

    これで、 http://localhost:8001https://localhost:8002 で接続することができる。

    Send feedback   このエントリーを含むはてなブックマーク はてなブックマーク - DjangoのHTTPS対応開発サーバ
  • Django redirect_to はnon-ascii URLに対応してない

    Django は一般的なリダイレクトするビューを django.views.generic.simple.redirect_to に用意していますけど、unicodeのキーワードがあれば、動かないのが最近見つけた。

    Djangoプロジェクトのurls.pyでこういう風にURLの設定を書けます。

    from django.conf.urls.defaults import *
    
    urlpatterns = patterns('django.views.generic.simple',
        url(r'^jp/(?P<tag_name>.+)\;', 'redirect_to', {'url': '/jp/tag/%(tag_name)s'}),
    )
    

    そうした場合、redirect_to は tag_nameに入ったデータをリダイレクト先にURLに入れてくれます。が、そのデータはasciiでない場合、redirect_to の中にtag_nameのデータに入れたURLを urlquoteに特に渡すなど、特に何もしないで、HttpResponseRedirectに渡す。redirect_toはDjango 1.1でこの通り

    def redirect_to(request, url, permanent=True, **kwargs):
        """
        Redirect to a given URL.
    
        The given url may contain dict-style string formatting, which will be
        interpolated against the params in the URL.  For example, to redirect from
        ``/foo/<id>/`` to ``/bar/<id>/``, you could use the following URLconf::
    
            urlpatterns = patterns('',
                ('^foo/(?P<id>\d+)/$', 'django.views.generic.simple.redirect_to', {'url' : '/bar/%(id)s/'}),
            )
    
        If the given url is ``None``, a HttpResponseGone (410) will be issued.
    
        If the ``permanent`` argument is False, then the response will have a 302
        HTTP status code. Otherwise, the status code will be 301.
        """
        if url is not None:
            klass = permanent and HttpResponsePermanentRedirect or HttpResponseRedirect
            return klass(url % kwargs)
        else:
            return HttpResponseGone()
    

    最終的に、この問題が起こる時に、このエラーが出る。

    UnicodeEncodeError: 'ascii' codec can't encode characters in position 8-11: ordinal not in
      range(128), HTTP response headers must be in US-ASCII format
    

    実際は下バグが既にあって、HttpResponseRedirectの中で、asciiでないURLをちゃんとエンコードするはず。

    #11522 Crash on redirect to a relative URL if request.path is unicode

    Send feedback   このエントリーを含むはてなブックマーク はてなブックマーク - Django redirect_to はnon-ascii URLに対応してない
  • 明日からPython温泉

    明日からPython温泉に参加しに行ってきます。課題は下記のとおり

    どこまでできるかわかんないけど、たぶんあまり眠らないでしょう。

    Send feedback   このエントリーを含むはてなブックマーク はてなブックマーク - 明日からPython温泉