実行とデプロイ

Tornadoは独自のHTTPServerを提供するため、実行とデプロイは他のPythonウェブフレームワークとは少し異なります。アプリケーションを見つけるWSGIコンテナを設定する代わりに、サーバーを起動するmain()関数を記述します。

import asyncio

async def main():
    app = make_app()
    app.listen(8888)
    await asyncio.Event().wait()

if __name__ == '__main__':
    asyncio.run(main())

このプログラムを実行してサーバーを起動するには、オペレーティングシステムまたはプロセス管理者を構成します。「ファイルが多すぎる」エラーを回避するために、プロセスあたりのオープンファイル数を増やす必要がある場合があります。この制限を上げるには(たとえば50000に設定するには)、ulimitコマンドを使用するか、/etc/security/limits.confを変更するか、supervisordの設定でminfdsを設定します。

プロセスとポート

PythonのGIL(グローバルインタープリターロック)のため、マルチCPUマシンを最大限に活用するには、複数のPythonプロセスを実行する必要があります。通常、CPUごとに1つのプロセスを実行するのが最適です。

これを行う最も簡単な方法は、listen()呼び出しにreuse_port=Trueを追加し、アプリケーションのコピーを複数実行することです。

Tornadoには、単一のパレンタルプロセスから複数のプロセスを起動する機能もあります(ただし、これはWindowsでは機能しません)。これには、アプリケーションの起動にいくつかの変更が必要です。

def main():
    sockets = bind_sockets(8888)
    tornado.process.fork_processes(0)
    async def post_fork_main():
        server = TCPServer()
        server.add_sockets(sockets)
        await asyncio.Event().wait()
    asyncio.run(post_fork_main())

これは複数のプロセスを起動してすべて同じポートを共有させるもう1つの方法ですが、いくつかの制限があります。まず、各子プロセスには独自のIOLoopがあるため、フォークの前にグローバルIOLoopインスタンス(間接的であっても)に触れないことが重要です。第二に、このモデルではゼロダウンタイムのアップデートが困難です。最後に、すべてのプロセスが同じポートを共有するため、個別に監視することが困難です。

より高度なデプロイメントの場合、プロセスを独立して起動し、それぞれ異なるポートでリスンすることをお勧めします。supervisordの「プロセスグループ」機能は、これを実現する良い方法の1つです。各プロセスが異なるポートを使用する場合、HAProxyやnginxなどの外部ロードバランサーを使用して、外部の訪問者に対して単一のアドレスを提供する必要があります。

ロードバランサーの背後での実行

nginxなどのロードバランサーの背後での実行時には、HTTPServerコンストラクタにxheaders=Trueを渡すことをお勧めします。これにより、TornadoはX-Real-IPなどのヘッダーを使用してユーザーのIPアドレスを取得し、すべてのトラフィックをバランサーのIPアドレスに割り当てることを回避します。

これは、FriendFeedで使用しているものと構造的に類似した、ごく基本的なnginx設定ファイルです。nginxとTornadoサーバーは同じマシンで実行され、4つのTornadoサーバーはポート8000~8003で実行されていることを前提としています。

user nginx;
worker_processes 1;

error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    use epoll;
}

http {
    # Enumerate all the Tornado servers here
    upstream frontends {
        server 127.0.0.1:8000;
        server 127.0.0.1:8001;
        server 127.0.0.1:8002;
        server 127.0.0.1:8003;
    }

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    access_log /var/log/nginx/access.log;

    keepalive_timeout 65;
    proxy_read_timeout 200;
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    gzip on;
    gzip_min_length 1000;
    gzip_proxied any;
    gzip_types text/plain text/html text/css text/xml
               application/x-javascript application/xml
               application/atom+xml text/javascript;

    # Only retry if there was a communication error, not a timeout
    # on the Tornado server (to avoid propagating "queries of death"
    # to all frontends)
    proxy_next_upstream error;

    server {
        listen 80;

        # Allow file uploads
        client_max_body_size 50M;

        location ^~ /static/ {
            root /var/www;
            if ($query_string) {
                expires max;
            }
        }
        location = /favicon.ico {
            rewrite (.*) /static/favicon.ico;
        }
        location = /robots.txt {
            rewrite (.*) /static/robots.txt;
        }

        location / {
            proxy_pass_header Server;
            proxy_set_header Host $http_host;
            proxy_redirect off;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Scheme $scheme;
            proxy_pass http://frontends;
        }
    }
}

静的ファイルと積極的なファイルキャッシング

アプリケーションでstatic_path設定を指定することにより、Tornadoから静的ファイルを提供できます。

settings = {
    "static_path": os.path.join(os.path.dirname(__file__), "static"),
    "cookie_secret": "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
    "login_url": "/login",
    "xsrf_cookies": True,
}
application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
    (r"/(apple-touch-icon\.png)", tornado.web.StaticFileHandler,
     dict(path=settings['static_path'])),
], **settings)

この設定により、/static/で始まるすべての要求がその静的ディレクトリから自動的に提供されます。たとえば、http://localhost:8888/static/foo.pngは、指定された静的ディレクトリからfoo.pngファイルを配信します。/robots.txt/favicon.icoも(/static/プレフィックスで始まらなくても)静的ディレクトリから自動的に提供されます。

上記の構成では、StaticFileHandlerを使用して、apple-touch-icon.pngをルートから明示的に提供するようにTornadoを構成しています(物理的には静的ファイルディレクトリにあります)。(その正規表現内のキャプチャグループは、StaticFileHandlerに要求されたファイル名を伝えるために必要です。キャプチャグループは、メソッド引数としてハンドラーに渡されることに注意してください)。sitemap.xmlなどをサイトルートから提供するために同じことを行うことができます。もちろん、適切な<link />タグをHTMLで使用することで、ルートのapple-touch-icon.pngを偽装することも回避できます。

パフォーマンスを向上させるために、一般的に、ブラウザが静的リソースを積極的にキャッシュすることが推奨されます。これにより、ブラウザはページのレンダリングをブロックする可能性のある不要なIf-Modified-SinceまたはEtag要求を送信しません。Tornadoは、*静的コンテンツのバージョン管理*によってこれを標準でサポートしています。

この機能を使用するには、HTMLに静的ファイルのURLを直接入力するのではなく、テンプレートでstatic_urlメソッドを使用します。

<html>
   <head>
      <title>FriendFeed - {{ _("Home") }}</title>
   </head>
   <body>
     <div><img src="{{ static_url("images/logo.png") }}"/></div>
   </body>
 </html>

static_url()関数は、その相対パスを/static/images/logo.png?v=aae54のようなURIに変換します。v引数はlogo.pngの内容のハッシュであり、その存在により、Tornadoサーバーはユーザーのブラウザにキャッシュヘッダーを送信し、ブラウザが無期限にコンテンツをキャッシュするようになります。

v引数はファイルの内容に基づいているため、ファイルを更新してサーバーを再起動すると、新しいv値の送信が開始され、ユーザーのブラウザは自動的に新しいファイルをフェッチします。ファイルの内容が変更されない場合、ブラウザはサーバーで更新を確認することなく、ローカルにキャッシュされたコピーを引き続き使用するため、レンダリングのパフォーマンスが大幅に向上します。

本番環境では、おそらくnginxなどのより最適化された静的ファイルサーバーから静的ファイルを配信する必要があります。static_url()で使用されるバージョンタグを認識し、それに応じてキャッシュヘッダーを設定するように、ほとんどすべてのウェブサーバーを構成できます。FriendFeedで使用しているnginx設定の関連部分は次のとおりです。

location /static/ {
    root /var/friendfeed/static;
    if ($query_string) {
        expires max;
    }
 }

デバッグモードと自動リロード

debug=TrueApplicationコンストラクタに渡すと、アプリはデバッグ/開発モードで実行されます。このモードでは、開発中に便利なように設計されたいくつかの機能が有効になります(それぞれ個別のフラグとしても使用できます。両方が指定されている場合、個別のフラグが優先されます)。

  • autoreload=True:アプリはソースファイルの変更を監視し、何かが変更されると自動的にリロードします。これにより、開発中にサーバーを手動で再起動する必要が少なくなります。ただし、(インポート時の構文エラーなど)特定のエラーにより、デバッグモードでは現在回復できない方法でサーバーがダウンする可能性があります。

  • compiled_template_cache=False:テンプレートはキャッシュされません。

  • static_hash_cache=False:静的ファイルのハッシュ(static_url関数で使用)はキャッシュされません。

  • serve_traceback=TrueRequestHandlerの例外がキャッチされない場合、スタックトレースを含むエラーページが生成されます。

自動リロードモードは、HTTPServerのマルチプロセスモードと互換性がありません。自動リロードモードを使用する場合は、HTTPServer.startに1以外の引数を渡したり(またはtornado.process.fork_processesを呼び出したり)してはなりません。

デバッグモードの自動リロード機能は、tornado.autoreloadのスタンドアロンモジュールとして使用できます。これらを組み合わせて使用することで、構文エラーに対する堅牢性を高めることができます。実行中に変更を検出するにはアプリ内でautoreload=Trueを設定し、python -m tornado.autoreload myserver.pyで起動して、起動時の構文エラーやその他のエラーをキャッチします。

リロードすると、sys.executablesys.argvを使用してPythonを再実行するため、Pythonインタープリターのコマンドライン引数(例:-u)が失われます。さらに、これらの変数を変更すると、リロードが正しく動作しなくなります。

一部のプラットフォーム(Windowsや10.6以前のMac OSXなど)では、プロセスを「インプレース」で更新できないため、コードの変更が検出されると、古いサーバーが終了し、新しいサーバーが起動します。これにより、一部のIDEが混乱することが知られています。