2016年 05月 14日

Server::Starter を node.js のサーバ起動に使う

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 してうまいことやるみたいな感じのようです。

複数の psgi を1つのサーバでサービスするときにメモリをケチる

このサーバは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程度の物理メモリ消費量になります。

Applebot

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

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

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

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

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

2016年 05月 13日

ページャの実装変更

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

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

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

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

余談:この日記の構造

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

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

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

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

2016年 05月 12日

Starlet でプロセス名をわかりやすくしたい

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)
2016年 05月 11日

トラックバックを実装しました


トラックバックを実装しました

といってもサイト内のエントリ間の言及を表示するだけです。いわゆる古代のオープンなトラックバックではありません。

古いエントリに新しいエントリへのリンクがないのは不便だなと思い、とりあえずさくっと LIKE 検索で機能を足してみていたのですが、良さそうなので、ちゃんとリレーションテーブルをこしらえたりしました。

しかし、全体的にページキャッシュ機能を入れているので、単に機能を足すだけでも考えることが増えてしまいます。イメージではトラックバックが送られたページに更新がかかるというだけですが…

キャッシュ処理も見直して、全体的にテストも書いたりしました。キャッシュが入ると複雑性が増す分、独り人力QAには限界があるので、みっちりテストを書くほうがかえって省力化します。

2016年 05月 09日

ブログのキャッシュバックエンドの変更

今までは Cache::FileCache によるファイルシステムキャッシュにしていたけど、いくつか問題があって SQLite にかえた

ファイルシステムキャッシュで困っていたこと

  • なんか遅い
  • キャッシュ無効化の処理のためにキャッシュを生成元とキャッシュのキーをキャッシュ内に格納したいが、アトミックにやるうまい方法がなかった

SQLite 選択

Redis とかを立てるのが機能的には便利そうだけど、リソース的にあんまりサーバプロセスを増やしたくはないので、SQLite とした。

CREATE TABLE cache (
	`cache_key` TEXT NOT NULL PRIMARY KEY,
	`content` BLOB NOT NULL
);

CREATE TABLE cache_relation (
	`id` INTEGER PRIMARY KEY,
	`cache_key` TEXT NOT NULL,
	`source_id` TEXT NOT NULL
);
CREATE INDEX cache_relation_index_cache_key ON cache_relation (`cache_key`);
CREATE INDEX cache_relation_index_source_id ON cache_relation (`source_id`);

CREATE TRIGGER on_cache_deleted AFTER DELETE ON cache BEGIN
	DELETE FROM cache_relation WHERE cache_key = old.cache_key;
END;

CREATE TRIGGER on_cache_related_deleted AFTER DELETE ON cache_relation BEGIN
	DELETE FROM cache WHERE cache_key = old.cache_key;
END;

こんな感じでキャッシュ本体 (cacheテーブル)と、そのキャッシュを生成するのに使ったもののid(文字列)のリスト(cache_relationテーブル)を持って、お互いにトリガーで消しあうようにしておく。

こうしておくと、通常のキャッシュキーによる追加・削除だけではなくて、生成元が更新された時に関連するキャッシュをまとめて消せる。


なお、キャッシュ用のDBファイルは元データと分けた。というのも、元データのDBは毎日バックアップとしてGmailに送りつけているので、キャッシュを含めたくなかったから。

2016年 04月 29日

Adsense のレスポンシブ広告の正しい使いかた

前提知識:レスポンシブ広告といっても、既定の広告サイズからコンテナサイズによって選ばれて表示されるだけで、広告そのもののサイズは固定です。

そのまま使うと、広告がロードされた時にページの高さの再計算が入って、コンテンツがガタガタと動いて大層鬱陶しいです。とりあえず便利そうだから貼ってみると、この挙動でギョギョっとします。

しかし、レスポンシブ広告はCSSで設定する width/height によって正確に対応する広告サイズを入れることができ、この場合は高さが固定になるのでガタガタしません。CSSなのでメディアクエリでサイズを変えられ、レスポンシブサイトと大変相性が良いです。

公式のドキュメントに詳細な設定方法が書いてあります。といっても width/height をコンテナに設定しているだけです。

既にサイトがメディアクエリによってレスポンシブになっているなら、レスポンシブ広告にした場合、広告サイズもCSSで指定するだけでよくなってます。

余談:Adsense の広告コードの修正・改変

「広告コードには一切の変更が認められない」みたいなことが過去に書いてあったような記憶があるんですが (記憶違いかも)、現状では上記の通り、改変が認められるケースがあり、悪意をもってやるようなことじゃなければだいたい大丈夫そうな雰囲気があります。

2016年 04月 28日

ES2015 の iterable/iterator/generator による無限 FizzBuzz (オブジェクト指向編)

ES2015 の iterable/iterator/generator による無限 FizzBuzz | tech - 氾濫原 に続いて、オブジェクト指向っぽく書けるようにしてみました。

ポイントはジェネレータ的なものをラップして常に It というクラスのインスタンスにするところです。

"use strict";

function It(iterable) { if (typeof this === 'undefined') return new It(iterable); this.iterable = iterable; }
It.countup = function* (n) {
	for (;;) yield n++;
};
It.prototype = {
	zip : function* () {
		const iterators = [];
		for (let it of this) {
			iterators.push(it[Symbol.iterator]());
		}
		for (;;) {
			const nexts = [];
			for (let it of iterators) {
				nexts.push(it.next());
			}
			yield nexts.map( (i) => i.value);
			if (nexts.some( (i) => i.done) ) break;
		}
	},
	map : function* (func) {
		for (let value of this) {
			yield func(value);
		}
	},
	cycle : function* () {
		for (;;) {
			for (let value of this) {
				yield value;
			}
		}
	},
	take : function (n) {
		const ret = [];
		for (let value of this) {
			ret.push(value);
			if (!(ret.length < n)) break;
		}
		return ret;
	}
};
It.prototype[Symbol.iterator] = function () { return this.iterable[Symbol.iterator]() };
{
	let wrapGenerator = function (generator) {
		return function () {
			const self = this;
			const args = arguments;
			const iterable = {};
			iterable[Symbol.iterator] = function () {
				return generator.apply(self, args);
			}
			return new It(iterable);
		}
	};
	let generatorConstructor = Object.getPrototypeOf(function*(){}).constructor;
	for (let key of Object.keys(It.prototype)) {
		if (It.prototype[key] instanceof generatorConstructor) {
			It.prototype[key] = wrapGenerator(It.prototype[key]);
		}
	}
}


console.log(Array.from(
	It([
		It.countup(1),
		It(["", "", "Fizz"]).cycle(),
		It(["", "", "", "", "Buzz"]).cycle()
	]).
		zip().
		map( (_) => _[1] + _[2] || _[0] ).
		take(30)
));

Generator は iterator であり、iterable でもある

表題の通りですが、Generator にはいずれの protocol も実装されています。気になるのは iterable の挙動ですが、どうやらレシーバーの Generator 自身を返すようです。

function* count (n) {
	for (;;) yield n++;
}

var c = count(1);
console.log(c.next, c[Symbol.iterator]);
//=> [Function: next] [Function: [Symbol.iterator]]

// iterator protocol
console.log(c.next()); //=> { value: 1, done: false }
console.log(c.next()); //=> { value: 2, done: false }
console.log(c.next()); //=> { value: 3, done: false }

// iterable protocol
console.log(c[Symbol.iterator]().next()); //=> { value: 4, done: false }

console.log(c[Symbol.iterator]() === c); //=> true

iterator protocol (next) で状態をすすめたあとに iterable protocol (Symbol.iterator) で iterator を取得すると、状態は継続されています。