Perl Hackers Hub

第24回PSGI/Plack実践入門―Starman、Starlet、Twiggy、Plack::Middleware、Server::Starter(3)

(1)こちら⁠2)こちらから。

PSGIミドルウェア

PSGIミドルウェアとは

PSGIアプリケーションとPSGIサーバの間にあり、PSGIアプリケーションをラップすることでリクエストやレスポンスを書き換えるしくみのことをPSGIミドルウェアと呼びます。PlackではPlack::Middlewareという名前空間のもとに実装され、CPANにも数多くアップロードされています。

Plack::Builderモジュールを利用すると、DSLDomain Specific Languageドメイン特化言語)にてミドルウェアの読み込みと設定を行うことができます。

use Plack::Builder;
my $app = sub {…};
$app = builder {
  enable 'Plack::Middleware::Lint';
  enable 'Plack::Middleware::StackTrace';
  $app;
};

Plack::Builderが提供するbuilderブロックの中で、enable 'ミドルウェア名';として指定してミドルウェア追加します(⁠⁠Plack::Middleware::」は省略できます⁠⁠。builderはミドルウェアの読み込みとアプリケーションのラッピングを行い、新しくPSGIアプリケーションを生成します。

以降ではプロダクション環境で使用されるPlack::Middlewareをいくつか紹介します。

Static ─⁠─ 静的コンテンツの配信

Plack::Middleware::StaticはCSSやJavaScriptなどの静的コンテンツの配信を行うミドルウェアです。Plackのディストリビューションに含まれています。

builder {
  enable 'Static',
    path => qr!^/(css|js|img)/!,
    root => '/path/to/public';
  $app;
}

上記のような設定を行うと、/cssや/jsといったURIにアクセスした際に、rootで指定したディレクトリから対象のファイルを配信します。

ただし、プロダクション環境で静的コンテンツの配信をアプリケーションサーバから行ってしまうと、アプリケーションサーバが本来の処理に集中できなくなってしまい、レスポンス速度の低下につながります。ApacheやnginxといったWebサーバをリバースプロキシとして設置し、静的コンテンツはそこから配信するのが一般的です。

AccessLog ─⁠─ アクセスログの表示

AccessLogはその名のとおりアクセスログを出力するためのミドルウェアです。

builder {
  enable "AccessLog",
    format => "combined";
  $app;
};

ログのフォーマットにはcombinedcommonあるいはApacheのmod_log_configと同じフォーマット文字が使えます。

AccessLogミドルウェアでApacheでサポートされる、%D(レスポンスにかかった時間)を出力したい場合は、Plack::Middleware::AccessLog::Timedが必要となります。

enable "AccessLog::Timed",
  format => '%h %l %u %t "%r" %>s %b %D';

ログの出力先はデフォルトでは$env->{psgi.errors}が示すファイルハンドルになります。多くの場合は標準エラーです。ファイルに書き出すときはloggerオプションにコードリファレンスを渡します。

use File::RotateLogs;
my $logger = File::RotateLogs->new();

builder {
  enable "AccessLog",
    format => 'combined',
    logger => sub { $logger->print(@_) }
  $app;
}

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から取得できます。

my $xff = $env->{HTTP_X_FORWARDED_FOR};
my ($ip) = $xff =~ /([^,\s]+)$/;

Plack::Middleware::ReverseProxyはX-Forwarded-ForからIPアドレスを取得し、$env->{REMOTE_ADDR}の上書きのほか、psgi.url_schemeの調整を自動で行うミドルウェアになります。リスト4ではPlack::Builderの提供するenable_ifを使って、リモートアドレスを確認したうえでReverseProxyを有効にしています。

リスト4 reverse_proxy.psgi
use Plack::Builder;
builder {
  enable_if { $_[0]->{REMOTE\_ADDR} eq '127.0.0.1' }
    'ReverseProxy';
  $app;
};

X-Forwarded-Forヘッダはクライアント側で簡単に詐称できてしまうので、送られてきた値をそのまま信用するのは危険です。場合によってはIPアドレスでアクセス制限してあるコンテンツに対して不正アクセスが可能な状態になります。リモートIPアドレスがリバースプロキシのIPアドレスかどうか判断し、リバースプロキシからのアクセスの場合のみ、X-Forwarded-Forを信用するようにしてください。

ServerStatus::Lite─⁠─ サーバ状態の可視化

Starman、StarletといったPrefork型のPSGIサーバにおいて、ワーカの可視化に使われるのがPlack::Middleware::ServerStatus::Liteです。

ServerStatus::Liteにはいくつかオプションがあります。

builder {
  enable "Lite",
    path => '/server-status',
    allow => ['127.0.0.1','192.168.0.0/16'],
    counter_file => '/path/to/counter_file',
    scoreboard => '/path/to/scoreboard';
  $app;
};

pathにサーバの状態を表示するためのURIを指定し、allowにそのURIに対してアクセス許可されるIPアドレスを指定します。もしIPアドレスが指定されていない場合、一切アクセスができません。counter_fileはアクセス数と総転送量を記録するためのファイルです。そしてscoreboardにワーカプロセスの状態を記録するためのディレクトリを指定します。

HTTPクライアントでpathに指定したURIにアクセスすると、図4のようなページが表示されます。上からサーバが起動してからの秒数、処理したアクセス数と転送量(KB⁠⁠、現在リクエストを処理しているビジー状態のワーカ数とアイドル中のワーカ数になります。以降はプロセスの状態でpidごとにリクエスト処理中かどうか、処理中であれば現在処理しているリクエストについての情報、アイドル状態であれば1つ前のリクエストに関する情報がまとめられています。

図4 server-status
$ curl http://localhost:5000/server-status
Uptime: 1381942535 (23 seconds)
Total Accesses: 3
Total Kbytes: 0
BusyWorkers: 1
IdleWorkers: 9
--
pid status remote_addr host method uri protocol ss
80060 _ 127.0.0.1 localhost:5000 GET / HTTP/1.1 14
80061 _ 127.0.0.1 localhost:5000 GET / HTTP/1.1 13
80062 _ 127.0.0.1 localhost:5000 GET / HTTP/1.1 13
80063 A 127.0.0.1 localhost:5000 GET /server-status HTTP/1.1 0
80064 .

図5は、ServerStatus::Liteを使って得られた情報をリソースモニタリングツールのCloudForecastでグラフ化したものです。このように可視化することで、適切なワーカ数に調整でき、効率的な運用が可能となります。

図5 server-statusグラフ
図5 server-statusグラフ

PSGIアプリケーションのホットデプロイ

Server::Starterを使ったホットデプロイ

Server::Starterは、サービスを停止することなくサーバを再起動するためのスーパーバイザーデーモンです。Starletの開発者でもある奥氏によってリリースされています。先ほど紹介した中ではStarman、Starlet、Twiggy、Twiggy::Preforkが対応しています。

Server::Starter経由でPSGIサーバを起動するにはstart_serverコマンドを使います。

$ start_server --port 5000 -- plackup \
  -s Starlet -a hello_world.psgi

上記のコマンドを実行すると、Server::Starter(start_server)はTCPポート5000番をListenしたのち、子プロセスとしてStarlet(plackup)exec(2)します。このときに、Server::Starterから子プロセスに環境変数経由でListen中のソケットのファイルディスクリプタ[4]が渡されます。

子プロセスのStarletは環境変数からファイルディスクリプタを読み出し、ソケットして開き直します。あとは通常通りワーカプロセスを起動してクライアントからのリクエストに応じます。

アプリケーションへの機能追加などでPSGIサーバの再起動が必要になった際には、Server::StarterのPIDに対してHUPシグナルを送信します。

$ kill -HUP {pid}

Server::StarterはHUPシグナルを受け取ると、新たに子プロセスとしてStarlet ⁠plackup)を起動します。新しい子プロセスにもファイルディスクリプタが渡され、同じようにリクエストの処理を開始します。Server::Starterは新しいプロセスが不正終了していないことを確認したのち、古い子プロセスに対してTERMシグナルを送信してプロセスを終了させます。

Server::Starterはこのように再起動処理を行うことで、ユーザからのリクエストを受け付けるプロセスをなくさずに無停止でのアプリケーションデプロイを実現しています。

現場で使われるPSGIアプリケーションの起動方法

Server::StarterとPSGI/Plackを利用してWebサービスを運用している現場では、前節で紹介したstart_serverを次のようにして起動します。

$ start_server --port 5000 \
  --signal-on-hup=USR1 \
  -- /path/to/run_app.sh

ポイントは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の起動オプションの変更が適用できます。

リスト5 run_app.sh
#!/bin/bash
exec plackup -s Starlet \
  --spawn-interval 0.1 --max-worker 30 \
  --max-reqs-per-child 1000 --min-reqs-per-child 500 \
  -a /path/to/app.psgi

大量のアクセスを受けているサービスでは、デプロイ時の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周りのベストプラクティス」です。

おすすめ記事

記事・ニュース一覧