Server::Starter は hot deploy 用の汎用スーパーデーモンで、Perl で書かれています。h2o の起動にも使われているのでみなさんおなじみでしょう!

Server::Starter がやってることは、Server::Starter 側で listen したソケットの fd を環境変数につっこんで子プロセスを起動というものです。子プロセス側では渡ってきた環境変数を読んで、fd について accept すれば良いことになります。

これを node.js でやるには以下のようにすれば良いようです。

//#!/usr/bin/env node
"use strict";

const http = require('http');

const server_starter_port = process.env['SERVER_STARTER_PORT'];
if (!server_starter_port) {
	console.log('SERVER_STARTER_PORT is not set');
	process.exit(1);
}
const fds = server_starter_port.split(/;/).map( (i) => i.split(/=/) );

const server = http.createServer(function (req, res) {
	res.writeHead(200, {'Content-Type': 'text/plain'});
	res.end('Hello World\n');
});

process.on('SIGTERM', () => {
	console.log('server exiting');
	server.close();
});

for (let fd of fds) {
	console.log('listen', fd);
	server.listen({ fd: +fd[1] });
}

起動はたとえば以下のように

start_server --port=5001  -- node server.js 

http.Server の listen() のドキュメントには fd を渡せるバージョンが書いてありませんが、net.Server の listen() がサポートしているので、同様に渡せるようです。

これだけで node.js のプロセスの hot deploy が簡単にできます。

余談:cluster

node.js には cluster というのがあります。これは node.js を複数プロセスで動かすための仕組みなんですが、これの woker プロセス側の listen() は master プロセスと ipc してうまいことやるみたいな感じのようです。

  1. トップ
  2. tech
  3. Server::Starter を node.js のサーバ起動に使う

このサーバはVPS 1台で動いていて、メモリは1GBしかありません。常時メモリ上限まで使いきっており、スワップファイルもそこそこあります。そういうわけで、できるだけメモリ消費をケチりたいのです。

稼働率の低い複数のサービスを1つのプロセスにまとめる

このサーバ上で動いているサービスがいくつかあるのですが、実際のところどれも殆どアクセスはありません。一番アクセスされていてこの日記システムぐらいです。

それらのサービスをそれぞれ別プロセスで起動させておくと、たいへん無駄なので、できるだけプロセス自体を同居させています。

psgi の vhost 化

Plack::Builder が提供している mount() を使うと、vhost を実現できます。

builder {
    mount 'http://lowreal.net' => do {
        my $guard = cwd_guard("lowreal.net/");
        Plack::Util::load_psgi("script/app.psgi")
    };
    mount 'http://env.lowreal.net' => do {
        my $guard = cwd_guard("env.lowreal.net/");
        Plack::Util::load_psgi("script/app.psgi")
    };
};

このようにすると、Host ヘッダに応じて、ディスパッチするアプリケーションを変えることができます。

これを利用して、複数の psgi アプリケーションを1つの psgi アプリケーションにまとめて起動しています。

ただし、起動時 (load_psgi時) にだけ cwd を設定しているため、cwd 依存のコードがあるとうまく動きません。

単にプロセス数も減らせますが、それぞれのアプリケーションで共通で使うモジュールがかなり多いので、それらを fork 前にロードして共有しておくことで、プライベートメモリ使用量を削減しようという意図もあります。

メモリリークや Copy on Write が発生したプロセスを捨てる

上記のまとめた psgi アプリケーションを start_server と plackup 経由で起動しています。

start_server 用には以下のような環境変数を設定しています

export KILL_OLD_DELAY=15
export ENABLE_AUTO_RESTART=1
export AUTO_RESTART_INTERVAL=3600

exec setuidgid cho45 \
        $PERL/bin/start_server --path=/tmp/backend.sock --port=5001 -- $PERL/bin/plackup -p 5001 -s Starlet\
        --max-workers=5 \
        --max-reqs-per-child=500 \
        -a backend.psgi

ENABLE_AUTO_RESTART によって、時間経過で自動的にプロセス全体を再起動しています。再起動間隔は AUTO_RESTART_INTERVAL に決まり、この場合1時間ごとになっています。

KILL_OLD_DELAY には新プロセスが起動して、リクエストが受けつけられるようになるまでにかかる時間を余裕をもって指定します。これを指定しないと、プロセス再起動がかかるたびに、モジュールなどのロードが終わるまでの数秒アクセス不能時間ができるので、特に ENABLE_AUTO_RESTART する場合は必須そうです。

これで、もしメモリリークしていても1時間以内に綺麗になることが保証されるとともに、fork した worker プロセスが働いて Copy on Write が起こってプライベートメモリ量が増えたとしても、1時間以内に fork しなおしになるため、共有メモリ量が一定より高い状態を保つことができます。

余談:プライベートメモリ使用量を観測する

Linux のプロセスが Copy on Write で共有しているメモリのサイズを調べる - naoyaのはてなダイアリー にある shared_memory_size.pl が便利でした。共有メモリの割合を表示してくれるのでわかりやすいです。

Starlet のプロセス名をわかりやすくしたかったのも、このスクリプト実行時に指定しやすくするためでした。

こんな感じで使っています。

shared-memory-size.pl `pgrep -f /srv/www/backend.psgi`
PID     RSS     SHARED
29583   55460   47792 (86%)
29584   55760   44688 (80%)
29585   65224   41396 (63%)
29586   56280   44704 (79%)
29587   55352   44660 (80%)
29588   58908   46748 (79%)

起動してすこし経ったあとの状態です。起動直後は90%以上ですが、プロセスが動くごとに少しずつ共有割合が減っていきます。自分の環境で、観測したうちだと60%ぐらいまで下がるようです。

ps -c で見るメモリ使用量には共有分が考慮されていないので、実際の物理メモリ使用量よりもかなり大きく出ます。仮に1プロセスあたり70MBぐらいメモリを消費していても、うち40MBぐらいが共有であることを考えると、worker プロセス1つあたり30MB、5プロセスで150MB程度の物理メモリ消費量になります。

  1. トップ
  2. tech
  3. 複数の psgi を1つのサーバでサービスするときにメモリをケチる

アクセスログを見ていたら Applebot なるものからのアクセスがあった。

https://support.apple.com/ja-jp/HT204683

ほんとに Apple のウェブクローラらしい。ただ

robots の制御指示で Applebot には言及していなくても Googlebot について指定されている場合、Apple のロボットは Googlebot に対する指示に従います。

と書いてあって、それはどうなんだ、という感じがする。

  1. トップ
  2. tech
  3. Applebot

UAからしてなんか怪しいが、210.160.8.236 からくる BOT で、DNSを逆引きすると target.microad.jp. らしい。マイクロアドを使ってるつもりはないんだけど、Adsense 貼るとくるのかな?

まともなクローラのつもりなら、UA をもうちょっとわかりやすくしといてほしいですね。まともな会社なら自社クローラのUAと目的ぐらい書いとくべきだと思います。

今までのページャは良くある ?page=2 みたいな形式でした。これは内部的には offset / limit を使う SQL になります。

変更したページャは /.page/20160509/3 という形式です。20160509 が表示するページ番号、3 がそのページに表示する最大の数です。ページ番号はトップページからのページの場合、日付になっており、内部的には日付ベースの where 句になります。3 は limit です。

これらによって、サーバ側のエントリ表示数のデフォルト設定によらず、URLによって決まる内容が生成されます。よって

  • キャッシュ無効化の負荷が減る
    • 古い形式だと ?page=xxx なキャッシュは1つエントリが増えるたびに全て無効にしないといけません
  • 古いページを表示したときの負荷が減る
    • offset / limit はそのページに至るまでの全てのエントリをソートして辿る必要があるので古いページをページ指定すると不穏な空気が流れます
  • 検索流入したとき見たいコンテンツがないことが防げる

余談:この日記の構造

このサイトは「日記」なので、日付単位でエントリがまとめられています。「ブログ」の表示に慣れていると違和感があるかもしれませんが、以下のような違いがあります。

  • ブログは全エントリを通して新しい順に並ぶ
  • 日記は日付単位で新しい順に並び、日付内では古い順に並ぶ

ちょうど、紙に日記を書くときのように、1日の中では連続し、1日単位では独立しているというイメージです。

この挙動は正直初見ではわかりにくいと思うのですが、過去分は1日単位で上から下へ書かれる前提で複数のエントリを構成しているケースがあって、変えるに変えれません。

  1. トップ
  2. tech
  3. ページャの実装変更

子供のおもちゃのチョロQを見ていて不思議に思ったことがあって調べたので書いておきます。

遊びかたの先入観

チョロQみたいなプルバック式のミニカーは、後退するときにぜんまいを巻き上げることで、前進時に動力にするわけです。なので、くるま全体を床に押し付けたまま前後してもぜんまいは巻き上がらず (後退で巻き上がっても前進させた分巻き戻る)、走らないと思っていました。

自分の遊んだ記憶だと、後退するときだけ押し付けて、一旦持ちあげてもう一度後退させていた覚えがあるのです。

いつこの「プルバックの走らせかた」を学んだのか全く覚えていませんが、当然そういうものという認識でいました。そしてそもそも「後退時に巻いたぜんまいで動く」以上の仕組みを知りませんでした。

実際の挙動

子供は「プルバックの走らせかた」を知らず、適当に前後させたらなんかうごくことだけ学習しています。

子供が遊んでるときの挙動を見てるいると、ただ床に押し付けてゴリゴリと前後するだけでもぜんまいが巻き上がり、しかもどう見ても巻いた以上に長い時間回転することに気付きました。

前進時と後退時でギア比が変わる。

Wikipedia の「プルバック式」の項を見ると

ゼンマイを使うプルバック式動力では、後退の時のみギア比の大きい状態になるよう、内部の歯車が浮き上がるようになっており、ゼンマイの力で前進する時だけタイヤを高速回転させるような構造を持つものも多い。

と書いてあって、ギア比が前進と後退で変わるような仕掛けがあるようでした。

「プルバック ギア」とかで検索すると、もうすこし詳しいページも見つかります。構成するギアのうち2つのギアの軸は方向によってずれることで、いずれか一方が噛み合うように調整されているようです。

もしかすると常識なのかもしれないですが、こういう面白い仕掛けがしてあるとは考えていなかったので、びっくりしました。

結局ガルパンは5回ぐらい見てしまった。劇場版がみたい。

ボトルガムを買うと、だいたい2日ぐらいでなくなってしまって金がもったいないという感じになってしまう。

職場にいると無限にストレスが溜まってるので、手の届くところにガムとか食べものを置いておくと無限に食べてしまう。もったいないので、できるだけ買わないで水をガブ飲みしてるけど、口が寂しい。

そういえば、ものすごくイライラしているときは掃除すると多少気が紛れる (やる気になった場合だけだけど)。すこし前にイライラしたときは、ひたすらトイレ掃除をしていた。

掃除には良い点がいくつかあって

  • 満足するまで終わりがない
  • 頭を使わない (普段使っている回路を使わない)
  • 目に見える成果がある

一定の「満足感」は手に入ることがほぼ保証される。そんなものは他にあるか? ネトゲとかは瞬間的な満足感はあるが社会的には虚しい。掃除は同居家族がいるなら社会的な利益もある。

しかし、どういった種類のことでも「やらされている」と思った時点ですべての利点は失われる。他人のために働くということをすると、自分で満足するということがなくなる。成果評価の裁量を他人に奪われた時点で生きてる実感はなくなる。

少しでもストレスが解消されることがら

  • 散歩
  • 掃除
  • 睡眠
  • うざくないお笑い番組 (内村さまぁ〜ずみたいなやつ)
  • 悪役が出てこないアニメ
  • 高徳な主人公のアニメ

大きくストレスが溜まることがら

  • 路上喫煙 (避けようがない)
  • 仕事 (避けようがない)
  • 自分から見て道徳感のない人間の存在 (避けようがない)
  • 道徳観のない人間の発言 (はてブを見なければ、ほとんどエンカウントしない)
  • 他人の儲かった話
  • 自分が直接的にしろ間接的にしろ否定されるようなナニか全般

他人の道徳観に関するストレス (路上喫煙も同じ) は「こんなやつが社会でうまく生きてるのに、なぜおれは社会でこんなに否定されているのか」という感情に基いている。

ps とかで表示されるプロセス名をわかりやすくしたいという話です。

Starman だと $0 に適当な値を入れてくれて、master なのか worker なのかとか、どの psgi が動いているかとかがわかるのですが、Starlet ではそういうことをやってくれないので、1つのサーバで複数インスタンス動かしていると、plackup のプロセスだらけで、どれがどれだかわからなくなります。

app.psgi で $0 で設定すれば良いかと思ったが

app.psgi で $0 設定したらとりあえずいいだろ、と思ってやってみましたが、どうも上手くいきません。app.psgi 内で $0 を warn してみるとそもそも plackup になってもいません。

調べてみると、Plack::Util::load_psgi で使っている _load_sandbox 内で local $0 をしてから app.psgi を do しており、ロード後に元のプロセス名に戻ってしまうようです。

ということで、単純に app.psgi で $0 を書きかえても plackup を使っている場合書きかわりません

Starlet 用のハックを入れる

なんかうまい方法がなさそうなので以下のようなクソハックを app.psgi に入れました。

{
    ### set process name for Starlet

    use Parallel::Prefork;
    my $name = $0; 
    my $orig_new = \&Parallel::Prefork::new;
    no warnings 'redefine';
    *Parallel::Prefork::new = sub {
        my ($class, $opts) = @_; 
        $opts->{before_fork} = sub {
            $0 = "$name (worker)"
        };  
        $opts->{after_fork} = sub {
            $0 = "$name (master)"
        };  
        $orig_new->($class, $opts);
    };  
};

Starlet は Parallel::Prefork を使っているので、前もって use して new を書きかえて before_fork / after_fork を設定します。

before_fork / after_fork は Parallel::Prefork の機能でセットできるフックポイントですfork 前に worker プロセス用の $0 を設定して、fork 後に $0 を master 用の値に変えています。なのでタイミングによっては master プロセスも一瞬 worker 表示になります。

app.psgi ロード時のコンテキストでは $0 には .psgi のファイル名が入っています。これは上記の Plack::Util::_load_sandbox で設定されたものです。ということで、ロード時のコンテキストの$0を保存しておいて、後の $0 の設定に使っています。

app.psgi が Starlet 専用みたいになってキモいですが、Parallel::Prefork をロードする以上の害はとりあえずないのと、自分の場合は開発中に Standalone で動かす以外は Starlet を使うことにしているので問題ありません。

これで以下のようになりました。

cho45     4699  0.0  0.5 185432  5332 ?        S    May11   0:00 /home/cho45/project/COQSO/script/app.psgi (master)
cho45     4700  0.0  2.4 296604 25168 ?        S    May11   0:09 /home/cho45/project/COQSO/script/app.psgi (worker)
cho45     4701  0.0  2.5 296820 26468 ?        S    May11   0:11 /home/cho45/project/COQSO/script/app.psgi (worker)
cho45     4702  0.0  1.8 302304 19136 ?        S    May11   0:11 /home/cho45/project/COQSO/script/app.psgi (worker)
  1. トップ
  2. tech
  3. Starlet でプロセス名をわかりやすくしたい

  • RTT (ping) が約30ms〜50msぐらいの環境
  • * がついているのは http2 の push
  • HTML 内で直接参照しているリソースについてもダウンロードしている (nghttp の --get-assets オプション)