「ZshからRubyにしたら速くなる」 その理由とテクニック

現在取り組んでいるプロジェクトで、パフォーマンスチューニングの一環として当初Zshで書かれていたスクリプトをRubyで書き直すことによって、60倍程度の高速化を実現した。 もちろん、単純に書き換えただけではなく、可能な限りfork/execをしないようにしたり、コストがかかる処理を最小にするなどの工夫を伴って手に入れた結果だが、「ZshでしていたことをRubyに書き換えた」だけでも相当な効果があった。

このパフォーマンスチューニングは単にプログラムを書くだけの人には生まれにくい発想である。 Unix、そしてLinuxのシステムや、プログラミング言語処理系に関する知識がないと考えられない要素が多いのだ。

そこで、この話を解説する。

「ZshよりRubyが速い」そのわけ

根本的な話として、Zshはそもそも遅い処理系だ。 「Zshが遅い」という話はZshのメーリングリストでもちらほら話をされる。 別にBashと比べて遅いということではないのだが(Bashもまた非常に遅い処理系だからだ)、状況によっては速度が問題になる程度に遅い。

Rubyも相当に遅い処理系であると言われていたし、実際かなり遅かったのは事実だ。 それでもZshに比べれば随分早かったのだが。

だが、それ以降、Rubyは高速化に取り組み続けている。対して、Zshはあまり高速化には取り組んでいない。だから、差が開いている。

しかし、理由がそれだけというわけではない。

Zshは純粋なインタープリタである。対して、Rubyはスクリプト言語ではあるがバイトコードインタプリタ型である。 この違いは、syntax errorが起きるタイミングが、Rubyがスクリプトを実行しようとしたタイミングであるのに対し、Zshはその行に到達したときであることからもわかる。

インタープリタ型であれコンパイラ型であれ、ソースコードを機械語に変換しなければならない、という点は変わらない。 その違いは方法とタイミングである。

インタープリタ型言語の場合、「1行ずつ(1コマンドずつ)変換する」のである。 その変換方法はもちろん処理系によって異なるのだが、Zshの場合、complex commandでも複数の文をまとめて変換することはしないし、ループによって繰り返される場合でも一度変換したものを使いまわしたりはしない。

対してRubyは、最初にコード全体を構文木に変換する。 RUby 1.8までは構文木インタープリタによってこれを実行していたが、Ruby 1.9以降はこれをさらにバイトコードに変換し、バイトコードインタープリタ(VM)によって実行するようになった。 バイトコードはRuby専用の機械語のようなもので、VMによって非常に小さなコストで実行できる。 Ruby 2.6からはJITコンパイラも追加され、部分的にCコードを生成し、これをネイティブコンパイラ(例えばgcc)によってバイナリコードに変換する(こともできる)。

これで1行だけのようなコードだとあまり差は出ないし、Zshでは1行だけどRubyでは何十行という可能性もあるので、このようなケースではRuby有利というわけではなくなる。 だが、ループで何度も同じコードを実行するような場合には非常に大きな差になってくる。 今回の場合、テスト段階で500回のループであったことから、大きな差になったということである。 だからループ回数が増えると倍率的にも速度差はさらに開く。

fork/execとコンパイルにかかる時間

Unix関連に少し知識がある人であれば、「forkはコストが重く遅い」というのを聞いたことがあると思う。

だが、この認識にはちょっと注意が必要だ。 というのも、C言語の速度から見た時に「forkする時間があればどれだけ実行できるか」という点を考えるとsystemで外部コマンドを呼び出すとそこだけ局所的に時間がかかる、という状況が発生する。

だが、実際にはfork(2)しても1000分の数秒にすぎない。 どちらかといえばそれよりもexec(2)のほうが重いのだが、それでもせいぜい100分の1秒程度だ。 だから、C言語で書いている場合ですらそれなりに長くなる場合はむしろ実行コストを省略できてコマンドを呼び出すほうが速かったりする。

昔のUnixではfork(2)はもっともっと遅かった。 現在のLinuxにおいてfork(2)が速くなったのはコピーオンライト形式であることの恩恵が大きい。 古典的なUnixではfork(2)は呼び出した時点でプロセスのメモリをコピーしていた。直後にexec(2)する場合はコピーしたメモリの内容は全く使わないのでかなりの無駄だ。

ところが、現在のLinuxにおいてはfork(2)によってメモリはコピーされない。共有されるのである。 そしてforkされたプロセスが共有されているメモリに対して書き込みを行った時に別に領域を確保してそれを変更する仕組みだ。

結果的にfork自体は一瞬に近くなっている。

そして、もうひとつ重要なのが「コンパイル時間」だ。 Rubyは起動時に対象スクリプトの変換を行う。 だが、この変換コストは速くなるに従って増加している。以前は構文木に変換するだけだったのが、1.9からはさらにバイトコードに変換する時間が必要になったし、2.6でJITを使うとさらにCコードを生成してそれをコンパイルする時間まで必要になっている。 つまり、Rubyはだんだん「実行は速くなっているが、実行に着手するまでは時間がかかるようになっている」のである。

これは、例えばechoであれば

% time /bin/echo > /dev/null
/bin/echo > /dev/null  0.00s user 0.00s system 79% cpu 0.001 total

ということになるのだが、Rubyだと空っぽに近くても

% time ruby -e 'nil'         
ruby -e 'nil'  0.04s user 0.02s system 59% cpu 0.089 total

結構時間がかかる。 つまり、一瞬で実行が終わるRubyスクリプトを何度も何度も繰り返して呼び出すと、トータルではかなり時間がかかるわけだ。 もともとのスクリプトは本体はRubyで、呼び出しがZshだったので、20並列で各500回、Rubyによるコンパイルがかかっていた。だから、かなりの時間がかかっていたのだ。

だが、「Linuxのforkはメモリが共有され、ほとんど一瞬で終わる」という点を利用すると改善の余地がある。 それは、実行可能なRubyスクリプトをライブラリ化する、という方法だ。

ZshからRubyを呼び出す場合、どうしてもRubyを呼び出すたびにRubyによるコンパイルをかけざるをえない。 当初は10000回コンパイルされていたのだが、500回のループをZshではなくRubyで行うようにすれば20回で済むようになる。だが、それでも20回のコンパイルが必要だ。

しかし、呼び出すスクリプト自体をRubyに変えてしまえば、実行しようとするスクリプトをライブラリとしてロードするという方法がとれるようになる。 ライブラリとしてロードすると、そのコンパイルは呼び出し元スクリプトをロードしたときに行われる。 もちろん、呼び出しの目的は呼び出すだけであり、直接そのライブラリの機能を使うわけではない。だが、この状態からforkすると、「コンパイル済みコードがメモリ上にあるRubyプロセス」が出来上がる。

この時点でスクリプトを実行する方法は「メソッドを呼び出す」(あるいは、その機能を果たすオブジェクトを作ってメソッドを呼び出す)だけである。 繰り返し呼び出すループを書くのも、単にRubyのループを書いて、そこで繰り返しメソッドを呼び出すなりオブジェクトを作るなりすれば良い。 呼び出し元スクリプト側では並列分だけforkしたあと、Process.waitallでもしていればいいわけだ。

これはZshに対して、「Rubyスクリプトのコンパイルが1度だけでいい」「execする必要がない」というメリットをもたらしている。 どちらも結構コストの高い処理であるから、繰り返し実行する場合は非常に大きなコストになり速度を低下させる。「処理自体は軽いのだが果てしなくループする」タイプのスクリプトに対してこの方法は本当に効く。 なぜならば、そのようなスクリプトに対してはコストの高い呼び出しをしているとコストのほとんどは呼び出しで占められ、実行コストは小さいためにスクリプト自体を高速化しようとがんばったところでほとんど無意味だし、逆に呼び出しコストを軽くすると劇的に速くなるからだ。

ループ回数の多い処理でのログ出力

概要

現状、1処理が0.0012秒程度で終わるシステムがある。1回の処理は非常に短いが、これが呼ばれる回数は1処理あたり数百万回にも及ぶ。 速度がそれなりに求められる案件だ。

しかし、その中で何度かログファイルに書き出したいタイミングというのがある。それぞれのログファイルに書き出すタイミングは呼び出しの中では1度だけだ。 だが、「処理時間が短くて」「回数が多い」のに共有のログファイルに書くというのはなかなか鬼門だ。 ファイルに書いている時間に待ち合わせがあると、速度に対してものすごくロスが発生する。

まずは普通に

普通に共有されているログファイルをロックする方針でやってみる。

ログファイルにアクセスできるのは1プロセスだけなので、同時にログに書こうとすると待たされることになる。 同時に書くとファイルが壊れてしまうので、すごく妥当な方法なのだが…

% time ruby test-logging1.rb
ruby test-logging1.rb  20.24s user 676.50s system 1102% cpu 1:03.18 total

1分3秒ということでかなり長い。

ソケットで投げてみる

UNIXドメインソケットにして投げつけてみる。 サーバー側が受け取った後の処理はクライアント側の処理時間には影響しないので、サーバーは投げ捨てる。

クライアントは直接ファイルに書くのではなくUNIXドメインソケットを使う。

待ち合わせは全く発生しないわけではない(ソケットを受付てからスレッドに移るまでの間はブロックされる)が、速くなるはずだと思うのだが。

% time ruby test-logging2.rb
ruby test-logging2.rb  25.51s user 33.05s system 62% cpu 1:33.02 total

1分33秒。却って遅くなってしまった。 やはりソケットに接続するコストが高いようだ。

ただ、並列数としては今回の場合最大24で、繰り返しの間というのは別にソケットをつなぎ直す必要はない設計になっている。 そのため、ループの外側に接続を置くことができる。

1行入れ替えただけだが、効果てきめん。

% time ruby test-logging2.rb
ruby test-logging2.rb  2.91s user 2.63s system 1488% cpu 0.373 total

今度は0.3秒で終わった。 Linuxの場合ソケットへの書き込みはカーネルがバッファするため、この程度の量であればメモリへの書き込みだけで終了する。そのため、かなり高速である。

ちなみに、ファイルの場合共有されているファイルをみんなが開いていたらぐちゃぐちゃになってしまうのでこの方法は使えない。 また、親子関係にある別プロセスについてはファイルディスクリプタ自体は共有されているため、fd番号さえわかっていれば共有できるのだが、 その場合は同じ経路から出ていくことになり、ファイルを共有した場合と同じ問題に直面するため、バグになってしまう。

PIDを使う

私の新しいアイディアは、「ログファイルにPIDを含める」という方法だ。

プロセス自体がマルチスレッドになっていなければ、その瞬間には同一PIDのプロセスはそのプロセスしかいないため、そもそもロックする必要がない。 ファイルのロックはあくまで「同時に」書かれるから必要になるのであって、どれだけ速くても順次であれば何も問題はないのだ。

ファイル名にPIDを含めているため、同時に走っている別のプロセスが同じファイルにアクセスすることはない。 もちろん、これは並列化の方法にプロセスを選択している場合だけだが。

% time ruby test-logging3.rb
ruby test-logging3.rb  12.26s user 22.37s system 1778% cpu 1.948 total

なかなか速い。だが、ソケットとはだいぶ差があるようだ。 しかし考えてみればこれもプロセスをオープンしてからであればなにもループのたびに開き直す必要はない。ソケットと同じ話だ。 そこで、ソケットと同じく順序を入れ替えてみる。

ループのたびにファイルを開くのではなく、予めファイルを開いてループで書いていくようにした。

% time ruby test-logging3.rb
ruby test-logging3.rb  2.52s user 0.13s system 1356% cpu 0.196 total

ファイルとソケットのオープンコストの差か、ソケットよりも速くなった。 ちなみに、ソケットを一回だけ開く方法よりも速い。

「forkでの並列処理に追記のログファイルはファイル名にPIDで」、このテクニック、私はどこかで見た記憶はないのだが、かなり有用だと思う。お勧めだ。

素晴らしきRinda

Rindaって

Java生まれのTuppleSpaceのRuby実装である。

Javaの実装はLindaというので、RubyだからRindaということらしい。

Rindaは基本的に分散Ruby(dRuby)のデモンストレーションのようなライブラリのようだが、今(2.5.2)に至るまでずっと標準添付され続けている。

dRubyすらあまり使ったことのある人がいない中、dRubyはPStoreやYAMLStoreなどと同様、 「知っている人は少ないが使ったことのある人はほとんどいない」標準ライブラリになっている。

TuppleSpaceって

TuppleSpaceは共有して置いておけるプールである。

「ホワイトボードシステム」と呼ばれることが多いのでホワイトボードになぞらえるが、このホワイトボードにマグネットシートを貼ったり取ったりできる。 ボードから取ってくるマグネットシートは種類による選別が可能だ。

一般的なソケットと違い、違う種類のやり取りをひとつの経路に簡単に混ぜることができる。

TCPサーバーとして起動できるので、ネットワーク分散も可能だ。

Rindaの概要

Rindaに対して置くことができるのは配列またはハッシュなのだが、「使い勝手はハッシュだが普通は配列」というのが私が感じている空気である。

例えば次のようなオブジェクトを配置する。

このメソッドはTuppleSpaceに接続し、この値を入力する間はブロックするが、受け取り手がいようがいまいが、すぐに終わる。 とにかく一旦TuppleSpaceに置かれるのだ。

そして、別のプロセスが次のようにして受け取る。

nilはワイルドカードになるので、valの値がなんだったかに関係なく受け取られる。 ただし、さらに他のプロセスが

のようにしていたら、この値は受け取らない。[0]に明に指定された値と異なるからだ。

全体リソースの中での曖昧さ

サーバーでは無限にコネクションが維持できるわけではないし、無限にリソースがあるわけでもない。 そのため、処理できるものからどんどん処理していきたい、と考えるのだが、Rindaはこれによって「要請」と「結果」をプールしておくことができる。

例えば、次の例では検索クエリを受け取って、検索を行った結果をTuppleSpaceに置く。 クエリがないときはブロックする。

検索を要請する側がidを発行しておくことで、一意に応答することができる。 この処理を行うプロセスの数によって「並列で検索できるワーカーの数」を制御することができる。 余っているワーカーがあったとしてもそれはRinda::TuppleSpaceProxy#takeによってブロックされるだけであり、「同時最大並列数」だけプロセスを起動しておけば良い。

さらにこれを取っていくワーカーは別のホストでも構わないのだ。

TuppleSpaceによって簡単に分散処理、並列処理が可能なのだが、 これは「入れた順番には出てこない」。処理が大量にたまってしまった場合でも順番を守って処理してほしい場合には適さない。

だが、「手が空いているならば仕事を持ってきて処理する」という形式はなんとも人間的で、かつコンピュータ的にも結構優れたモデルだと思うのだ。 Rindaが有効に機能するように設計することにより、並列処理に伴う難しさをかなり軽減することができる。 そもそも並列処理は厳密さを求めるにはあまり向いていないので、このようなふわっとしたレイヤーをはさむと結構いい感じに動作する。

in action

今回は「ワーカーが処理するデータはそのワーカー単独が処理するディレクトリに分割し、ワーカーは処理が終了したらRinda経由でディレクトリパスを取得する」という方法をとった。

以前はHTMLチャットサーバーの実装で、要求に対して即座にリターンさせるためにRindaを使っていたこともある。

あまり知られておらず、使われる機会もないが、使ってみると結構面白いのではなかろうか。

ウェブサイト全文検索システムの開発

開発経緯

NamazuやGroongaも試したのだが、いまひとつ望むものにはならなかったので、シンプルで美しい全文検索システムを書くことにした。

これは、第一にはGoogle検索に依存しているMimir Yokhamaの検索機能を自前で持つこと、 第二にはChienomiを含むWordPressのシステムの置換えである。

検索システムの開発自体はそれほど難しくないと思うのだが、どのように動作するのが望ましいかということを考えると非常に難しい。 Googleの検索システムは非常に高度なので、それに匹敵するものを作るのは難しいのだ。

だが、ここはPureBuilder Simplyにふさわしいシンプルなものを目指すことにする。

設計その1

とりあえず、grepを使えば話が早いのだが、HTMLだと余計な要素を含んだ検索になってしまう。 HTMLからタグを除去するのは難しくないが、どういうポリシーで除去するのか、いつどうやって除去するのか、などが難しい。

PureBuilder Simplyの構成から言えば、生成時に、Pandocで生成するのが望ましい。

$ pandoc index.md -t plain index.txt

だが、生成時に生成を全く無視してインデックスを生成するのはどうだろうか? そもそもPre pluginsはソースファイルを、Post Pluginsは生成したHTMLファイルを加工するものであるため、本来の目的から逸脱してしまう。 例えばPre Pluginsを使って

とかもできる。

ただ、今のところ検索対象になるようなソースファイルを加工するようなPre plguinsを使っていないため、別にこのようにする必要性はない。

設計その2

あとから処理するためのもの。 .indexes.rbm に基づいて処理を行う方式。

検索

いずれにせよここまでやってしまえば検索は簡単。 grepで検索できる状態なので、シンプルに検索可能。

AND検索の要領としては

ものすごく検索対象が多い場合は、検索対象そのものを絞り込んでいくほうがいいだろう。 だが、プロセス起動回数が増えることを考えると、そのような場合は自前実装のほうが良い可能性が高い。

OR検索はもっと簡単で

スペースの取り扱い方とか、case問題とか考え始めると難しい。 ただ、世の中そんな複雑な検索をしている検索エンジンはあまりないし、多分ローカルにそんなもの作ったところで報われないのでこれくらいでいいような気もする。

ANDまたはORではなく自由にANDとORを結合できるようにした場合は、expr自体に評価できるメソッドを追加すると良い。例えば

といった感じである。

あとがき

検索機能の実装自体は難しくないのだが、ChienomiをPureBuilder Simply化するという話になると結構難しい。

既にかなりの記事があり、検索からの流入も多いため、どうしても全記事に対してマップせざるをえない。 これもなかなか面倒だ。

だが、もっと問題Chienomiの記事は書き方が一定でない、ということだ。

Chienomiの記述形式はPOD, RDoc, ACCS2, PureDoc, PureDoc2, PureDoc2::Markdown, Pandoc Markdownがある。 PureBuilder SimplyはPandocでの処理を前提としているため、なんとかしないといけない。

過去記事については諦める方針ならばHTMLとして抜き出すという方法もあるのだが(Mimir Yokohamaでウェブサイトのサルベージでよく使う方法だ)、 できれば避けたいというのもある。

また、タグとカテゴリのつけ方が一定ではないため、これを処理しなければならない。

さらに厄介なのがメディアファイルだ。 WordPressはメディアファイルの使い方が独特だし、そのためにメディアファイルについてはWordPress上で追加する方法をとっていた。 さらにサイト移行時にメディアファイルを紛失したこともあって、結構大規模な作業になると思う。

そのことを考えると一筋縄ではいかない。

それはともかくとして、サイトの検索で非常に複雑な演算子を使いたがる人はまずいない、 どころかサイト内検索なんてほぼ使われていないに等しいので、基本的な検索機能で十分だと思うのだが、それであればこの通り実装はとても簡単だ(例によって設計で稼いだ感があるが)。

というわけでちょっとした実装例、そしてシェルスクリプトサンプルとして役立てば幸いである。

新しいメールフィルタを開発 (Dovecot LDA, Postfix alias/PIPE)

新手のスパムが増えてきたので、追加のスパムフィルタを適用しようとしたのだが、従来のスパムフィルタがちょっと力技過ぎて(まぁ、2時間で作ったものだから)拡張が難しかったので、新しいフレームワークを作った。

GitHubで公開しているけれど まだREADMEを書いていないので利用は難しいだろう。

これ自体は大した話ではないのだが、部分的にとても苦戦したところがあったので、その話をしよう。

旧フィルタはPostfix aliasのPIPEで起動し、通過したものはsendmailでキューに戻すという仕様だった。 これは問題なかったのだが、フォルダへの振り分けも行いたい(特にJunkフォルダに対して振り分けたい)という理由でDovecot LDAを使うことにした。 これはArch Linuxだと/usr/lib/dovecot/dovecot-ldaである。

だが、ここで問題があった。Dovecot LDAはメールボックスに配送する。 一方Postfix sendmailはキューに入れるだけで、メールボックスへの配送はPostdropが行う。

だが、PostfixはPIPEをnobody:nobodyで起動する。 だから、メールボックスに対するアクセス権がなく、Dovecot LDAでメールボックスに配送することができない。

PIPEのユーザーを指定して起動する方法を検討したのだが、公式Wikiに「suidしろ」とか書いてあって絶望した。

ここまで特定するのに随分苦労したが、結局Postfix Sendmail同様、サーバーに対して送信する、という仕組みにすることにした。 rootで起動されているサーバーがDovecot LDAを呼ぶのでroot権限で動かすことができる。

これを実現するためにRubyのUNIXServerインスタンス(socketファイル)を777にしてあげる必要があるのだが、 UNIXServer.openにその機能がないため、

という感じ。

また、ログファイルもnobodyで書き込める必要がある。

メッセージフォームのサポート (Nginx + FastCGI + spawn-fcgi + Rack + Ruby)

あらまし

Mimir Yokohamaでついにお問い合わせ方法として「メッセージフォーム」が追加された。

なにがついになのか、なにをドヤっているのかと思うかもしれない。 まぁ、ドヤってはいないのだが。

実は私はかなり長い間ウェブアプリケーションをほとんど作っていない。 そして、今まで私が作ったウェブアプリケーションは、専用サーバーを持つサーブレットタイプか、もしくはCGIだった。

馬鹿にされがちなCGIだが、利便性は高く、頻繁にアクセスする性質を持たないアプリケーションには適している。

そして、そもそもウェブアプリケーションを作っていなかったのは、私が「事前生成戦略」の研究と実験に注力していたからで、 どちらかといえばウェブアプリケーションからは離れる方向にあった。 そして、ウェブアプリケーションを必要とするとしても大部分は静的ページとして提供できる方式を目指していたため、CGIで十分事足りたのである。

ちなみに、これまでウェブサーバーは

  • Apache
  • lighttpd
  • delegate
  • Nginx

という経過をたどっている。 Apacheは言うに及ばずlighttpdとdelegateはApacheよりもCGIが簡単だったので、「ほぼCGI」だった。

だが、時代は変わった。NginxはCGIをそもそもサポートしない。 私も新しい時代に対応する必要がある。

ちなみに、この作業は次の仕事のための実戦テストという意味合いもあった。

方針を考える

最も話が速いのはFastCGI Wrapである。

NginxはFastCGIをサポートしている。 FastCGIはプログラムをデーモンのように起動しっぱなしにする。

だが、通しで実行するプログラムとデーモンではそもそもの前提が違う。 そのためCGIプログラムをFastCGIとして動かすのはそれなりにハードルが高い。

そこでFastCGI Wrapの登場である。 FastCGIとして利用されるプログラムをFastCGI Wrapにする方式だ。 このラッパープログラムは要求に合わせて都度CGIプログラムをCGIインターフェイス経由で起動する。 結果的にFastCGIの意図は無視して従来型CGIを動作させるようにするというものだ。

この方法は結構出てくるのだが、基本的には既存のCGIプログラムを動作させる話である。

個人的な感覚としては、無駄なプロキシを噛ませるような方法を使ってまでCGIに固執したくない…というか、実はfcgi-wrapってそれなりにめんどくさい。

だったらFastCGI直というのもありかなぁ、と考えるわけだ。

ところが、やっぱりFastCGIはデーモン状のプログラムを想定しているわけで、やはり前提が違う。 要求として割と複雑なのか、デーモン化に関してはspawn-fcgiに担ってもらって、さらにRackを使う、というのがどうやら主流らしい。

だいぶ話が複雑になってきた。

サーバーはNginxである。NginxはFastCGIインターフェイスを経由してFastCGIプログラムにパラメータを渡し、応答を受け取る。

FastCGIプログラムはデーモンである。 Rubyでは次のようにしてFastCGIプログラムを書くことができる。

あるいは、CGIライブラリ互換インターフェイスを使うことで、#each_cgiの中身はまるっきりCGIと同じにすることもできる。

spawn-fcgiはこのデーモン部分を担う。 つまりeachしてる部分を担ってくれるわけだ。

プロセスとしてCGIインターフェイスで起動するわけではないので、fcgiwrapほどの互換性はない。 感覚はCGIに近いが、インターフェイスは意識する必要がある。

Rackはミドルウェアと呼ばれている。これはまずFastCGI抜きで話そう。

Rackはインターフェイスを担っている。 今までプログラムはCGIなり、あるいはFCGIなり、さらには各種フレームワークやサーブレットの様式(例えばSinatraとか)で書いていた。

Rackはこれらの違いを吸収するモジュール設計のものだ。 Rackに準拠したプログラムを書いておけば、たとえ愛用のフレームワークがディスコンになっても、サーバーが変わっても安心、というわけだ。

だが、Rack自身はサーバーではないからサーバーがいるのだが、Rack組み込みのサーバーというのはもう完全にRuby世界の住人だ。 だってRackはRubyのWebアプリケーションインターフェイスだから。

Passengerというソフトウェアがあって、これはwebサーバーのモジュールとしてRackに対応する。 Apacheでは比較的簡単だけれど、Nginxだと結構きつい。

そこでRackに対応したサーバーを立ててサーバーとサーバーでやりとりさせる、という方式がすごく現代的。 直接にRack経由でプログラムとやりとりするのはRackに対応したサーバーだけれど、Rackに対応したサーバーにwebサーバーとしての機能を持たせると大変なので、「本物のwebサーバーに矢面に立ってもらって、RackサーバーはあくまでRack対応に特化」というわけである。

Rackに特化したサーバーとしては(別にRackだけではないんだけど)、Webrick, Mongrel, Puma, Thin, Unicornあたりがある。

しかしRackでやりとりする方法があればいいので、FastCGI + Rackという方法もある。 それはRack側でFastCGI経由で受け取って、応答するためのハンドラが用意されている。

つまり、Unicornのようなサーバーを立てる代わりの手段としてFastCGIが使える。 FastCGIもデーモンを必要とするので別にFastCGIにすることで間に挟まってるものを減らす効果はない。 ただ話が楽になるだけである。

Unicornはむちゃくちゃ速いので、UnicornでUnixドメインソケットを使えば形式とししてはspawn-fcgiでUnixドメインソケットを使っているのと一緒だし、やっていることははるかに高度になる。 これが超モダンなやり方である。

が、あえてのFastCGI。 理由は管理する要素数を減らすためである。必要がないのにいかついものを使うことはしない。 これはサーバー運用のコツでもある。

なお、Rackに関してはかなり情報が少ない。 なんらかのフレームワーク…というか、ほぼRailsのバックエンドとしてのRackの話だけで、Rack単独の話ってない。 そして、FastCGIを使う話もない。これもだいたいなんらかのアプリケーションが「使ってる」あるいは「使わせる」話になる。

なんというか、みんなそんなに自分でプログラム作るってことをしてないのか… 世の中エンジニアたくさんいるのに、WordPressとRailsだけで満足なのか…

そんなわけで情報が猛烈に足りていない中、FastCGIとRackについて勉強することになったわけだ。

なお、Nginxでアプリケーションとやりとりする方法に関してはDiscourceで散々やったので経験済みだ。

なぜRackなのか

もちろんこのことからもわかるようにRackはなくても構わない。 spawn-cgiも使用せず単独のFastCGIアプリケーションを開発するのは容易である。

私が気にしたのはRubyのfcgiライブラリは2013年から更新が止まっているとい点だ。 また、Arch LinuxではfcgiライブラリはAURにもなく

# gem install --no-user-install fcgi

とするよりない。

ベーシックな機構であるFastCGIそのものが廃止になるようなことは考えにくいが、NginxのCGIの扱いのように消極的なサポートへと変遷する可能性はある。 その場合にアプリケーションの書き直しが発生してしまう。

Rackは現在主流であり、新規採用例も多い。 Rackが廃止になると影響を受ける範囲も非常に広いので今後10年は安泰だと思われる。

そこでFastCGI+Rackという構成にしたわけだ。 この場合でもRackはFastCGIをネイティブサポートしているわけではく、fcgiライブラリを使ったハンドラを同梱しているだけなのでfcgiライブラリは必要となる。実はこれを回避したかったのだが、結局はできなかった形だ。

とはいえ、この状態であればFastCGIを捨ててUnicornに移行するのも難しくはない。

とりあえずやってみる

Nginx

location / {
    root /var/www/testapp;
    fastcgi_pass /var/run/fcgi-testapp.sock
    fastcgi_index testapp.rb;
    include fastcgi_params;
}

Rack Application

Requestのほうはインターフェイスに絡むけれど、 Responseは単純に#finishでRackに沿った配列を返すための便利クラス。なくてもいい。

spawn-fcgi

# spawn-fcgi -U http -s /var/run/fcgi-testapp.sock /var/www/testapp/testapp.rb -n

試してるうちは-nつきにしてフォアグラウンドで実行するのが楽

実用的にする

起動スクリプト

forkingなので停止・再起動の制御のためPIDファイルを作る。

Systemd Unit

[Unit]
Description = FastCGI Rack Test Application
After = nginx.service

[Service]
Type = forking
PIDFile = /var/run/fcgi-testapp.pid
ExecStart = /usr/local/sbin/fcgi-testapp.bash
ExecStop = kill $MAINPID

[Install]
WantedBy = multi-user.target

forkingなので$MAINPIDがそのままでは使えないため、PIDFileで指定しておく。 Nginxのあとに起動しておいたほうがいいような気がしたけど、なくても構わない。 アクセスが激しい場合は逆にNginxの前に起動したほうがいいだろう

spawn-fcgi自体にはアプリをリロード、再起動するような機能はない。

おまけ

S-NailがSubjectも本文も、UTF-8をちゃんとエンコードしてくれるのですごくびっくりした。

「mailxとは違うのだよ!!!」ってことか。 さすがSMTPやPOPやIMAPにも対応しているだけのことはある。

ここの部分(MIMEエンコーディング)も自分でやるつもりだったので、かなり省力化された形。

今回の構築は他にも色々やったのだけれど、共有して意味のある部分はこれくらいのものだろう。

PureBuilder Simply 1.4 リリース

PureBuilder Simplyの1.4をリリースした

特に大きな変更点は以下の通りだ。

  • HTML生成前に処理できるPre Pluginsに対応した
  • Pre Plugins/Post Pluginsで環境変数から文書メタデータにアクセスできるようになった

Pre PluginsはPandocにかける前のドキュメントを加工するものである。 Markdownと比べReSTは自由度が低いこと、それぞれのドキュメントフォーマットに基づいて処理しなければならないことから、新たに_docformatというメタ値が追加された。

今回のポイントはPre Pluginsであり、メタデータを渡す仕様は反映していなかっただけで、実は1.2時代からあった。

Pre Pluginsはおもしろいこと書いていないので、どちらかといえばメタデータ渡しの話をしよう。

これはPre Plugionsの一部である。 IO.popenはコマンド群の前に環境変数を置くことができる。 シェル的にいうと

みたいなことだ。

もちろん、同じような手法はRubyでも使えるけれど、ちょっとめんどくさい。IO.popenの利便性は簡便に損なわれてしまう。

かゆいところに手の続くRubyは、ちゃんとそのプロセス用に環境変数を渡す方法を用意してくれているわけだ。

予め環境変数にセットするのと何が違うのか。

まず、自身の環境変数としてセットすると、メモリーを2個分使う。

また、環境変数がそのプログラム自身の制御に影響するケースでは問題が生じる。

さらに、何度もプロセスを起動するたびにセットしてしまうと、ガベージコレクションの問題が出る可能性がある。

結局、子プロセスに対して伝播したいだけの環境変数はこのプロセスに対してのみセットするのが適切、ということになる。 シェルにおいても

ではなく、

あるいは

とすべきである。

PureBuilder Simplyのアップデート (ReST完全対応)

3ヶ月ぶりとなったPureBuilder Simplyのアップデート。

今回はMimir Yokohamaのウェブサイトの新連載(そもそも連載開始にお金かかるので時期未定)でReSTructured Textを使うため、ReSTに完全対応した。

完全対応のポイントは、従来きちんと対応できていなかったdocinfo(Bibliographic Elements)に対応するようにした。

ただし、これは「対応した」という言い方が適切なのかどうかわからない。 ひとつは、自力でdocinfoを解釈する部分が間違っていたのと、仕様の理解自体正しくなかったので適正にした。

File.open([dir, filename].join("/")) do |f|
    l = f.gets
-        if l =~ /:[A-Za-z]+: .*/ #docinfo
-          docinfo_lines = [l.chomp]
+        if l =~ /:([A-Za-z]+): (.*)/ #docinfo
+          frontmatter = { $1 => [$2.chomp] }
+          last_key = $1

        # Read docinfo
        while(l = f.gets)
            break if l =~ /^\s*$/ # End of docinfo
-            if l =~ /^\s+- / && (docinfo_lines.last.kind_of?(Array) || docinfo_lines.last =~ /^:.*?: +-/) # List items
-              if docinfo_lines.last.kind_of?(String)
-                docinfo_lines.last =~ /^:(.*?): +- *(.*)/
-                docinfo_lines[-1] = [ [$1, $2] ]
-              end
-              docinfo_lines.last[1].push(l.sub(/^\s+- +/).chomp)
-            elsif l =~ /^\s+/ # Continuous line
-              docinfo_lines.last << " " + $'.chomp
-            elsif l =~ /^:.*?: +.*/
-              docinfo_lines.push l.chomp
+            if l =~ /^\s+/ # Continuous line
+              docinfo_lines.last.push($'.chomp)
+            elsif l =~ /:([A-Za-z]+): (.*)/
+              frontmatter[$1] = [$2.chomp]
+              last_key = $1
            end
        end

-          # Convert Hash.
-          frontmatter = {}
-          docinfo_lines.each do |i|
-            if i.kind_of?(Array) #list
-              # Array element
-              frontmatter[i[0]] = i[1]
-            elsif i =~ /^:author: .*[,;]/ #author
-              # It work only pandoc style author (not Authors.)
-              author = i.sub(/:author: /, "")
-              if author.include?(";")
-                author = author.split(/ *; */)
-              elsif author.include?(",")
-                author = author.split(/ *, */)
-              end
+          # Treat docinfo lines
+          frontmatter.each do |k,v|
+            v = v.join(" ")
+            if((k == "author" || k == "authors") && v.include?(";")) # Multiple authors.
+              v = v.split(/\s*;\s*/)

-              frontmatter["author"] = author
-            elsif i =~ /^:(.*?): +(\d{4}-\d{2}-\d{2}[T ]\d{2}[0-9: T+-]*)$/ #datetime
-              key = $1
-              time = DateTime.parse($2)
-              frontmatter[key] = time
-            elsif i =~ /^:(.*?): +(\d{4}-\d{2}-\d{2}) *$/ #date
-              key = $1
-              time = Date.parse($2)
-              frontmatter[key] = time
-            elsif i =~ /^:(.*?): +/
-              key = $1
-              value = $'
-              frontmatter[key] = value
+            elsif k == "date" # Date?
+              # Datetime?
+              if v =~ /[0-2][0-9]:[0-6][0-9]/
+                v = DateTime.parse(v)
+              else
+                v = Date.parse(v)
+              end
+            else # Simple String.
+              nil # keep v
            end
+
+            frontmatter[k] = v
        end

    elsif l && l.chomp == ".." #YAML
        # Load ReST YAML that document begins comment and block is yaml.
+          @extra_meta_format = true # ReST + YAML is not supported by Pandoc.
        lines = []

        while(l = f.gets)

考え方自体に大きな変更があったため、変更行数も多い。 ただし、22165f2よりも8f6e453のほうが以前のバージョンとの違いは少なくなっている。

ここでひとつポイントだ。ReSTの仕様上、authorsa,b,cと書かれた場合は3人の著者になる。a,b,c;と書かれた場合は1人の著者になる。 だが、Pandocは;でのみ分割するので、この仕様に従っている。このため非常にシンプルな仕様だ。

基本的にdocinfoの場合、authorsdateのみが特別扱いされる。 Pandoc的にもそのような仕様になっており、authorsauthorの代わりに書ける点も正式な仕様に従っている。 PureBuilderもこれにならって、それ以外のフィールドについては特別扱いしない。 また、Pandocは仕様にない項目を入れてもエラーにしないため、この点も合わせてある。

この上で、メタデータの解釈はPandocに委ねることにした。 従来は-Mオプションを付加して上書きしていたのだが、解釈の違いからバグにもなったし、Pandocのほうが優秀なのでこのような挙動は取りやめた。

ただし、PureBuilder的にはこれではちょっと困る。 サイトの内容に関する情報をメタデータに記述する風習があるため、メタデータに書ける内容が決められてしまうのは困るのだ。

そこで従来サポートされていた「ReSTでも先頭をコメントにした場合はそのあとYAMLメタデータを書くものとする」という仕様も維持している。 この場合、Pandocは解釈できないため、@extra_meta_formatというインスタンス変数を追加し、これが真の場合のみ-Mオプションでのメタデータを使うことにした。

こうしてReSTも完全対応できることとなった。 asciidocも結構人気があるらしいのだけど、Pandocが入力にasciidocをサポートしていないのでサポートすることはできない。 Textileをサポートしてほしい人がいれば対応は考えなくもないけれど、私が知る限りPureBuilderでTextileを使いたい人はいないはずだ。1


  1. 比較的複雑な構造を書けることがPureBuilderの利点であり、また簡易な記述をしたい人にとってもMarkdownが困ることはないはずだからだ。Textileにはコメント機能があるため、サポートすること自体は不可能ではない。

Mimir Yokohama ウェブサイトの「タグ機能」の仕組み

Mimir Yokohamaのウェブサイトにこっそりとタグ機能が追加された。

だが、PureBuilder Simply自体にはタグ機能がない。 この実現方法は発想力勝負な部分があった。

ドキュメントにデータを持たせる

「記事情報」でも行われている方法として、Markdown YAML Frontmatter内に情報をもたせ、Pandocテンプレートで存在する場合だけエレメントを生成するような手法を取っている。

例えば帯域においては

というYAML Frontmatterが書かれている。 記事情報などは自動的に生成することができないため記事ごとにかかれており、若干執筆コストを上げている面もあるが、 なにしろMimir Yokohamaには力が入っているのでそれくらいどうということはない。 基本的なフォーマットをコピペしてしまえばそれほど難しくない部分でもある。

ちなみに、Pandocテンプレートで

$if(pickable)$
Something
$endif$

とした場合、pickable: no (つまりfalse)ならばここは生成されない。

この追加情報としてtagsが加わったのである。

問題は検索

タグを表示することは簡単だが、普通に考えればタグから同一タグの記事を辿りたいし、タグで検索もしたい。

単純な方法としてGoogleを使うこともできるのだが、それは必ずしもタグつきの記事が上位にくるわけではなく、思ったようには動作しない。 ちゃんと検索機能を用意する必要があったのだが、できればPureBuilder Simplyの枠組みの中で行いたいところである。

PureBuilder Simplyは原則として「MarkdownまたはReST文書から生成する」という前提になっており、 ACCSもindexデータベースからMarkdownドキュメントを生成し、このあとはPandocで処理している。

なのでPureBuilder Simplyの枠組みで処理するためにはMarkdownドキュメントを生成しなくてはいけない。

それなら全てのindexを探し回ってタグを集めればいいじゃない。

ARGV.eachしているので、全ての.indexes.rbmを指定すれば良い。 あとはpbsimply-pandocで処理できるが、タグに登場した.indexes.rbmは実際に記事が存在しているものではなく拾ってほしくないので消しておく。

.indexes.rbmとして書き出すようにした意図の一部に、このように外部からドキュメントデータにアクセスするというものがあった。

これによってドキュメント解析しなくてもメタデータを利用して機能拡張してページに含むことができる。

テンプレートにとうとう限界が

だいぶ魔改造されているPandocテンプレートだが、今回は限界が垣間見えた。

タグクラウドらしくエントリ数の多いタグを大きく表示したいのだが、Pandocテンプレートに計算機能や比較機能がなく、CSSにもないため、 Pandocテンプレートだけでは実現できない。MarkdownにHTMLを直接書くという方法はあるが(Markdown自体はRubyで生成しているため)。

また、URIエンコーディングをする方法はデータを二重に持たせる以外になく、それでもふたつの値を同時にとるイテレータがPandocテンプレートにないため、URIだけでURIエンコーディングをおこなう方法がない。

eRubyを使ってもいいのだが、できれば使いたくない。 現時点ではタグクラウドの大きさ分けはしておらず、URIもURIエンコーディングせずに使用している。

継承の使いどころ

業務のほうでやっていたプログラミングが久しぶりに設計を4回もやる苦戦となった。

苦戦というよりは模索したというほうが近いが、「作るより変更するほうが難しい」の一例となった。

作業としては、出力機能にバリエーションが増えることだった。

入力データは変わらないのだが、出力データに関してはその違いが様々な面で出てくる(文字エンコーディングの違いなのだが、JIS XかUnicodeかという違いでもある)。 そのため入力処理あるいはデータアッセンブルの段階で処理してもいいし、出力時に処理してもいいし…という状態だった。

既に600行を越えているプログラムであるため、あまり変更すると変更量が増えてしんどいしバグにもなりかねない。 かといって単純に分岐してしまうとメンテナンスが困難になり、リファクタリングしておくほうがいい。

大部分は同じだが、変数で済むほどには同じじゃない、という加減が問題だった。

データ用クラスを作る

最初に考えたのがこれだった。

現状、データ自体は複数のインスタンス変数に分かれたHashで保持している。

これをクラスとして独立させ、これに出力機能を与える、というものだった。

この際、データをストアするメソッド(現状ではHash#[]->Array#push)にデータの加工を含めるという方法を考えた。

だが、これは現在とは異なるフローである。現状では入力処理はデータのアッセンブルと出力データのための加工を並行して行っている。 これを実現するためにはこの点も変更しなければならず、一見スマートに見える方法だったが、これは呼び出し側から見ればカプセル化によってスマートに見えるだけで、実際はあまりキレイな実装にはならなそうであった。

一見ゴリ押しに見える現在の設計だが、再考してみると案外合理的なのである。 大幅な変更がリファクタリングとして有効に働くかは結構疑問であった。

また、データオブジェクトだけではなくフロー制御で使われるデータもあり、 データオブジェクトとフロークラスでいくつものインスタンス変数を共有する必要があった。 これはあまり美しくない。

変更点が大きくバグを生じる可能性が高かったため、この方法は断念した。

異なる部分をメソッド分けする

「避けたい」と思いつつもどれくらい複雑になるか考えてみた。

これまでひとつのメソッドに組み込まれていた一部をパーツとして分離し、メソッドとして切り出す。 もちろん、これは悪いことではない。

そして、異なる処理を行うメソッドを追加する。 ここまではまだいい。

だが、そのどちらのメソッドを起動するかということについては従来のメソッド上で条件分岐しなくてはいけない。

別に今後増えることも減ることもないのならばそれでもいい。 だが、今回追加されたJIS機能は「将来的に削除する予定の」機能である。 実際仕様書にも「容易にバイパスできるように」と書いてある。

フラグだけで条件分岐を回避するというだけのコードは明らかに汚くなってしまう。

違いをモジュールで吸収する

「違いが生じる部分をMix-inすることにして、Mix-inするモジュールの選択によって挙動を変えよう」というアイディアが生じた。

これは、出力するたびに入力をやり直すことになるが、フィルタとして機能するようなプログラムではないのであまり問題はない。 入力処理もそこまで極端に重いわけでもない。 入力データのSanity checkや分類にも両者に違いがあるため、実行時は分離したほうが良い考えに思えたのだ。

もちろん、両方生成する場合は非効率的だが、その程度は受容できるだろう。

だが、このアイディアはすぐ適切でないと分かった。 出力系統によって一部だけ変更/追加したい、ということがあるのだが、共通処理から分岐するたびにメソッドの有無をチェックしてあるなら呼び出す…ということになってしまう。 これは明らかにsuperしたい状況であるにも関わらず、だ。

だが、Mix-inされたモジュールのインスタンスメソッドよりクラスのインスタンスメソッドのほうが優先度が高いため、これはうまくいかない。 比較的新しい機能である(といってもRuby 2.0だけれど)Module#prependしても良いのだけれど、これはちょっと違うように思われる。 そもそも正しい継承関係と逆で、設計が歪なのだ。

継承する

「出力の固有特性を持っているほうがサブクラス」であることが自然だと思うなら、サブクラスにすれば良いじゃない!というわけで継承することにした。

従来のクラス名を維持すれば互換性を保つこともできるが、従来型(Unicode)と新機能(JIS)は並列の存在であるため、名称をABCABC::DEFのような関係にはし難い。 そこで、ベースクラスはベースクラスで名称をつけ、UnicodeクラスとJISクラスを用意することにした。

この構造は、共通部分も多いがそれぞれ部分的に違い、また全面的にオーバーライドできるものではないため、 共通処理をベースクラスに記述し、サブクラスからsuperを呼んだり、メソッド中で部分的に異なる点はサブクラスで実装されているメソッドで異なる処理をする。

リファクタリングするならば差異のある部分のメソッド構造を変更し、容易にオーバーライドできるようにする、また処理の一部はコール時のブロックにする、といったことが考えられる。

共通部分があって一部違う挙動を示すものがあるとき、継承を使う、ということ自体はごく当たり前のことだ。

しかし、「従来一本道だった処理で複数の異なる挙動を持つプログラムにする」というときに新たに「共通のスーパークラスを作って」継承を利用する、という発想はなかなか出てこなかった。

しかし結果的には、これぞ継承の使い方という見本のようなものになったな、と思う。