(1)はこちら、(2)はこちらから。
PSGIミドルウェア
PSGIミドルウェアとは
PSGIアプリケーションとPSGIサーバの間にあり、PSGIアプリケーションをラップすることでリクエストやレスポンスを書き換えるしくみのことをPSGIミドルウェアと呼びます。PlackではPlack::Middleware
という名前空間のもとに実装され、CPANにも数多くアップロードされています。
Plack::Builder
モジュールを利用すると、DSL(Domain Specific Language、ドメイン特化言語)にてミドルウェアの読み込みと設定を行うことができます。
Plack::Builderが提供するbuilderブロックの中で、enable 'ミドルウェア名';として指定してミドルウェア追加します(「Plack::Middleware::」は省略できます)。builderはミドルウェアの読み込みとアプリケーションのラッピングを行い、新しくPSGIアプリケーションを生成します。
以降ではプロダクション環境で使用されるPlack::Middlewareをいくつか紹介します。
Static ── 静的コンテンツの配信
Plack::Middleware::StaticはCSSやJavaScriptなどの静的コンテンツの配信を行うミドルウェアです。Plackのディストリビューションに含まれています。
上記のような設定を行うと、/cssや/jsといったURIにアクセスした際に、rootで指定したディレクトリから対象のファイルを配信します。
ただし、プロダクション環境で静的コンテンツの配信をアプリケーションサーバから行ってしまうと、アプリケーションサーバが本来の処理に集中できなくなってしまい、レスポンス速度の低下につながります。ApacheやnginxといったWebサーバをリバースプロキシとして設置し、静的コンテンツはそこから配信するのが一般的です。
AccessLog ── アクセスログの表示
AccessLogはその名のとおりアクセスログを出力するためのミドルウェアです。
ログのフォーマットにはcombined
、common
、あるいはApacheのmod_log_configと同じフォーマット文字が使えます。
AccessLogミドルウェアでApacheでサポートされる、%D
(レスポンスにかかった時間)を出力したい場合は、Plack::Middleware::AccessLog::Timedが必要となります。
ログの出力先はデフォルトでは$env->{psgi.errors}
が示すファイルハンドルになります。多くの場合は標準エラーです。ファイルに書き出すときはloggerオプションにコードリファレンスを渡します。
File::RotateLogsはログファイルを指定した時間ごとに分割し、古いログファイルを自動で削除する機能を持っています。AccessLogミドルウェアと併せて使うことで、ログのローテーションやディスク溢れの心配なくアクセスログを記録できます。
ReverseProxy── アクセス元IPアドレスの取得
PSGIアプリケーションをApacheやnginxなどのリバースプロキシのもとで動作させた場合、アクセス元のIPアドレスを示す$env->{REMOTE_ADDR}
はリバースプロキシのIPアドレスとなります。同じホスト上にリバースプロキシがあれば127.0.0.1
が格納されます。本来のクライアントのIPアドレスはどうなるかというと、X-Forwarded-For
というヘッダの末尾に追加されて送られてきます。X-Forwarded-For
ヘッダはRFCなどで定義されているヘッダではありませんが、Apache、nginx、Squidなどでサポートされている標準的なヘッダです。
PSGIアプリケーションでは次のようなコードで、クライアントのIPアドレスをX-Forwarded-For
から取得できます。
Plack::Middleware::ReverseProxyはX-Forwarded-For
からIPアドレスを取得し、$env->{REMOTE_ADDR}
の上書きのほか、psgi.url_schemeの調整を自動で行うミドルウェアになります。リスト4ではPlack::Builderの提供するenable_ifを使って、リモートアドレスを確認したうえでReverseProxyを有効にしています。
X-Forwarded-For
ヘッダはクライアント側で簡単に詐称できてしまうので、送られてきた値をそのまま信用するのは危険です。場合によってはIPアドレスでアクセス制限してあるコンテンツに対して不正アクセスが可能な状態になります。リモートIPアドレスがリバースプロキシのIPアドレスかどうか判断し、リバースプロキシからのアクセスの場合のみ、X-Forwarded-For
を信用するようにしてください。
ServerStatus::Lite── サーバ状態の可視化
Starman、StarletといったPrefork型のPSGIサーバにおいて、ワーカの可視化に使われるのがPlack::Middleware::ServerStatus::Liteです。
ServerStatus::Liteにはいくつかオプションがあります。
path
にサーバの状態を表示するためのURIを指定し、allow
にそのURIに対してアクセス許可されるIPアドレスを指定します。もしIPアドレスが指定されていない場合、一切アクセスができません。counter_file
はアクセス数と総転送量を記録するためのファイルです。そしてscoreboard
にワーカプロセスの状態を記録するためのディレクトリを指定します。
HTTPクライアントでpath
に指定したURIにアクセスすると、図4のようなページが表示されます。上からサーバが起動してからの秒数、処理したアクセス数と転送量(KB)、現在リクエストを処理しているビジー状態のワーカ数とアイドル中のワーカ数になります。以降はプロセスの状態でpidごとにリクエスト処理中かどうか、処理中であれば現在処理しているリクエストについての情報、アイドル状態であれば1つ前のリクエストに関する情報がまとめられています。
図5は、ServerStatus::Liteを使って得られた情報をリソースモニタリングツールのCloudForecastでグラフ化したものです。このように可視化することで、適切なワーカ数に調整でき、効率的な運用が可能となります。
PSGIアプリケーションのホットデプロイ
Server::Starterを使ったホットデプロイ
Server::Starterは、サービスを停止することなくサーバを再起動するためのスーパーバイザーデーモンです。Starletの開発者でもある奥氏によってリリースされています。先ほど紹介した中ではStarman、Starlet、Twiggy、Twiggy::Preforkが対応しています。
Server::Starter経由でPSGIサーバを起動するにはstart_server
コマンドを使います。
上記のコマンドを実行すると、Server::Starter(start_server)はTCPポート5000番をListenしたのち、子プロセスとしてStarlet(plackup)をexec(2)
します。このときに、Server::Starterから子プロセスに環境変数経由でListen中のソケットのファイルディスクリプタ[4]が渡されます。
子プロセスのStarletは環境変数からファイルディスクリプタを読み出し、ソケットして開き直します。あとは通常通りワーカプロセスを起動してクライアントからのリクエストに応じます。
アプリケーションへの機能追加などでPSGIサーバの再起動が必要になった際には、Server::StarterのPIDに対してHUPシグナルを送信します。
Server::StarterはHUPシグナルを受け取ると、新たに子プロセスとしてStarlet (plackup)を起動します。新しい子プロセスにもファイルディスクリプタが渡され、同じようにリクエストの処理を開始します。Server::Starterは新しいプロセスが不正終了していないことを確認したのち、古い子プロセスに対してTERMシグナルを送信してプロセスを終了させます。
Server::Starterはこのように再起動処理を行うことで、ユーザからのリクエストを受け付けるプロセスをなくさずに無停止でのアプリケーションデプロイを実現しています。
現場で使われるPSGIアプリケーションの起動方法
Server::StarterとPSGI/Plackを利用してWebサービスを運用している現場では、前節で紹介したstart_server
を次のようにして起動します。
ポイントは2つあります。1つ目はstart_server
コマンドにplackup
コマンドを直接渡さずにシェルスクリプトを使っている点、2つ目はstart_server
の--signalon-hup
オプションとStarletの--spawn-interval
オプションです。
Webサービスの運用中にServer::Starter経由で起動しているStarletのワーカ数を変更をしたいと思っても、start_serverコマンドの引数の1つとして直接--maxworkers
が書かれていると、いくらServer::StarterにHUPシグナルを送っても、起動しているプロセスの引数の変更はされないので、ワーカ数は変わりません。そこで、Starletの起動オプションをリスト5のようなシェルスクリプトにしてstart_server
を起動すると、HUPシグナルを受け取った際にもスクリプトが実行され、Starletの起動オプションの変更が適用できます。
大量のアクセスを受けているサービスでは、デプロイ時のStarletのワーカプロセスのfork(2)
による負荷が問題となるケースがあります。Starletの起動オプションに--spawn-interval=秒数
を追加すると、ワーカプロセスをfork(2)
する際に指定した秒数だけ間隔を開けます。また、USR1シグナルを受信した際に、--spawn-interval
の秒数を空けてワーカプロセスを順次終了させます。
start_server
に追加した--signal-on-hup=USR1
はこのStarletの機能を活用するためのオプションで、HUPシグナルを受け取った際に古いプロセスに対して送られるTERMシグナルを、USR1シグナルに変更します。Server::Starter、Starletにこれらのオプションを追加すると、デプロイ時に古い子プロセスのワーカがゆっくりと減り、同時に新しい子プロセスのワーカが増えていき、最終的に完全に入れ替わるという緩やかな再起動が実現できます。
まとめ
本稿では、仕様策定から4年が経ったPSGI/Plackの実践入門として、PSGIの仕様の振り返り、PSGIサーバとPlackミドルウェア、構築・運用の現場で使われるノウハウをサンプルコードとともに紹介してきました。本稿がPSGI/Plackを活用し、Webアプリケーションの開発・運用の効率を上げる機会となれば幸いです。
さて、次回の執筆者はSongmuさんで、テーマは「cron周りのベストプラクティス」です。