執行與部署

由於 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(全域解釋器鎖),有必要執行多個 Python 程序才能充分利用多核心機器。通常,最好每個 CPU 執行一個程序。

最簡單的方法是在您的 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())

這是啟動多個程序並讓它們共享同一個連接埠的另一種方法,儘管它有一些限制。首先,每個子程序都會有自己的 IOLoop,因此重要的是,在 fork 之前,沒有任何東西觸碰到全域 IOLoop 實例(即使是間接的)。其次,在此模型中很難進行零停機更新。最後,由於所有程序共享同一個連接埠,因此更難單獨監控它們。

對於更複雜的部署,建議獨立啟動程序,並讓每個程序監聽不同的連接埠。supervisord 的「程序群組」功能是安排此項目的好方法之一。當每個程序使用不同的連接埠時,通常需要像是 HAProxy 或 nginx 的外部負載平衡器,才能向外部訪客呈現單一地址。

在負載平衡器後方執行

當在像是 nginx 的負載平衡器後方執行時,建議將 xheaders=True 傳遞給 HTTPServer 建構函式。這會告訴 Tornado 使用像是 X-Real-IP 的標頭來取得使用者的 IP 位址,而不是將所有流量歸因於平衡器的 IP 位址。

這是一個簡陋的 nginx 設定檔,其結構與我們在 FriendFeed 使用的設定檔類似。它假設 nginx 和 Tornado 伺服器在同一部機器上執行,而四個 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/ 開頭的請求從該靜態目錄提供,例如 https://127.0.0.1:8888/static/foo.png 將從指定的靜態目錄提供檔案 foo.png。我們也會自動從靜態目錄提供 /robots.txt/favicon.ico(即使它們不是以 /static/ 前置詞開頭)。

在上述設定中,我們已明確設定 Tornado 從根目錄提供 apple-touch-icon.png,並使用 StaticFileHandler,儘管它實際位於靜態檔案目錄中。(該正規表示式中的擷取群組對於告訴 StaticFileHandler 請求的檔案名稱是必要的;請回想一下,擷取群組會以方法引數的形式傳遞給處理程式。)您可以執行相同的操作來從網站根目錄提供,例如 sitemap.xml。當然,您也可以透過在 HTML 中使用適當的 <link /> 標籤來避免偽造根 apple-touch-icon.png

為了提高效能,通常最好讓瀏覽器積極快取靜態資源,以便瀏覽器不會傳送不必要的 If-Modified-SinceEtag 請求,這可能會阻礙頁面的呈現。Tornado 使用靜態內容版本控制開箱即用地支援此功能。

若要使用此功能,請在您的樣板中使用 static_url 方法,而不是直接在 HTML 中鍵入靜態檔案的 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=True 傳遞給 Application 建構函式,則應用程式將在偵錯/開發模式下執行。在此模式下,將啟用一些旨在方便開發的功能(每個功能也可以作為個別旗標使用;如果同時指定了兩者,則以個別旗標為優先)

  • autoreload=True:應用程式會監看其原始程式碼檔案的變更,並在任何變更時重新載入自身。這減少了在開發期間手動重新啟動伺服器的需要。然而,某些失敗(例如匯入時的語法錯誤)仍然可能以目前偵錯模式無法復原的方式關閉伺服器。

  • compiled_template_cache=False:樣板將不會被快取。

  • static_hash_cache=False:靜態檔案雜湊(由 static_url 函式使用)將不會被快取。

  • serve_traceback=True:當 RequestHandler 中的例外狀況未被捕獲時,將會產生包含堆疊追蹤的錯誤頁面。

自動重新載入模式與 HTTPServer 的多進程模式不相容。如果您使用自動重新載入模式,您必須避免給 HTTPServer.start 傳入 1 以外的參數(或呼叫 tornado.process.fork_processes)。

除錯模式的自動重新載入功能在 tornado.autoreload 中以獨立模組的形式提供。這兩者可以結合使用,以提供額外的程式碼錯誤防護:在應用程式中設定 autoreload=True 以偵測執行中的變更,並使用 python -m tornado.autoreload myserver.py 啟動它,以便在啟動時捕獲任何語法錯誤或其他錯誤。

重新載入會遺失任何 Python 直譯器的命令列引數(例如 -u),因為它會使用 sys.executablesys.argv 重新執行 Python。此外,修改這些變數會導致重新載入行為不正確。

在某些平台(包括 Windows 和 10.6 之前的 Mac OSX)上,無法「就地」更新進程,因此當偵測到程式碼變更時,舊的伺服器會退出,並啟動新的伺服器。已知這會讓某些 IDE 感到困惑。