八発白中

技術ブログ、改め雑記

Common LispのWebアプリケーションを社内運用してみた

今月の初めに弊社はてなで開発合宿を行いました。2泊3日の合宿の中でチームを組み、テーマを決めて開発をし、最後に各チームがプレゼンをする、というものです。成果物は今後のサービス開発に生かされます。

僕のチームはバックエンドがCommon Lisp、フロントエンドがCoffeeScriptで、お互いが独立していてAPIでのみ通信する設計のWebアプリケーションを作りました。僕とhitode909とswimy1113の3人の最小チームでしたが、最後のプレゼン投票で優勝できました。

↓ 優勝したときの図。

左の灰色のはてなパーカーを着てるのが僕です。

Common Lispで書かれたアプリケーションが社内1位ってのはかなり夢があります。

合宿が終わってからも継続して開発を続けており、そろそろ数週間が経ちました。ので、この辺りでCommon LispでWebアプリケーションを作って運用した話をざっくりまとめておこうと思います。何を作ったかは秘密なのでアプリケーション側の話ではなく、どちらかというと運用側でハマったことと解決法が多めです。

開発環境

処理系はいつも使い慣れているClozure CLを使いました。Emacs、SLIMEは当然のようにセットアップされています。使用ライブラリは以下です。

  • Caveman: Webフレームワーク
  • CL-DBI: DBライブラリ
  • CL-EMB: テンプレートエンジン

Cavemanは、Clackの上に作られた極小フレームワークです。

Cavemanはまあ当たり前のように使うとして、CL-DBIは今回が初の使用でした。いくつか細かいバグを踏みつつ直しつつで開発を進めました。SQLは直書きして流しこむというスタイルで、最初はSQLite3を使ってちまちま動かしていきました。SQLiteからMySQLへの移行も経験しましたが、ソースコードはなんの変更もなく移行することができました。

テンプレートエンジンには一番癖のないCL-EMBを使いました。

ここで少しハマったのはClozure CLではファイルの読み込みの文字コードのデフォルトがlatin-1になっていて、テンプレートに日本語を書くと文字化けすることです。これは.ccl-init.lisp文字コード指定しておくと回避できます。

(setf ccl:*default-external-format*
      (ccl:make-external-format :character-encoding :utf-8
                                :line-termination :unix)
      ccl:*default-file-character-encoding* :utf-8
      ccl:*default-socket-character-encoding* :utf-8)

サーバ構成

アプリケーションサーバとしてHunchentootを5000番で動かし、Apacheをリバースプロキシとして設置しました。こうするとアクセスログはApache側で取れるし、各種設定もしやすいのでおすすめです。

<VirtualHost *:80>
  ServerName i.love.common-lisp.com

  ProxyPass        / http://localhost:5000/
  ProxyPassReverse / http://localhost:5000/
  ProxyRequests    Off
</VirtualHost>

これで i.love.common-lisp.com の80番にアクセスがきたときに5000番にフォワードします。

デプロイ

サーバは前から持っていたさくらVPS 512MBを間借りして使いました。処理系は実行速度を考えてSBCLで動かします。特に処理系依存のライブラリを使っていないので、Clozure CLからの処理系移行に伴う問題はまったくありませんでした。

git pushしてサーバにアップロードし、SSHで繋いでREPLを起動してサーバを立ち上げれば完成です。

……まではいいのですが、その後SSHを切断するとサーバも一緒に落ちてしまいます。どうやらCL処理系はREPLがベースなので標準入力がなくなるとエラーで落ちるようです。Perlなどでははまらないような罠ですね。どうにかデーモン化しないと。

CLのデーモン化にはかなり手こずりましたが、しばらくしてSBCLでsave-lisp-and-dieを使って実行ファイルを作っておくと容易にデーモン化できることに気づきました。

(ql:quickload :my-application)

(defun main ()
  (my-application:start :mode :prod :debug nil :port 5000)
  ;; 起動を維持するために無限ループする
  (loop (sleep 60)))

(sb-ext:save-lisp-and-die "my-application" :executable t :toplevel 'main)

これで普通に & をつけて実行するとサーバが起動します。

$ my-application &

これでSSHを切断してもサーバは起動したままです。ただ、これだと毎回実行ファイルを作ってサーバを再起動しなければ反映できません。そのため、中でswank-serverを立ち上げてREPLにアクセスできるようにしておきます。

(ql:quickload :my-application)
(ql:quickload :swank)

(defun main ()
  (my-application:start :mode :prod :debug nil :port 5000)
  (swank:create-server :port 4005 :style :spawn :dont-close t)
  ;; 起動を維持するために無限ループする
  (loop (sleep 60)))

(sb-ext:save-lisp-and-die "my-application" :executable t :toplevel 'main)

これで4005番でswank-serverが立ち上がりました。ローカルのEmacsからリモートのREPLに接続するには、ポートフォワードします。

$ ssh -N -f -L 4005:localhost:4005 xxx.xx.xx.xxx

これでEmacsから M-x slime-connect 127.0.0.1 4005 すると本番サーバのREPLにアクセスできます。これでソースをpushした際にモジュールを再ロードするだけで反映できます。

もちろんSLIMEなので、ローカルで編集したファイルの一部分だけをC-c C-cで本番に即座反映することもできます。デバッグにはとても役立ちますが、コミットを忘れてサーバを再起動しなければいけない際にちゃんと立ち上がらない、みたいなことが冗談抜きで本当にあるので注意してください。本当にあります。

最初は上のような書き捨てのスクリプトで何とかやりくりしていたのですが、最近では以下のようなMakefileを用意しています。

build: clean my-application

my-application:
	sbcl --noinform --disable-debugger \
	--eval '(ql:quickload :my-application)' \
	--eval '(ql:quickload :swank)' \
	--eval '(defun main () (my-application:start :mode (or (sb-ext:posix-getenv "CLACK_ENV") :dev) :port (or (parse-integer (string (sb-ext:posix-getenv "SERVER_PORT")) :junk-allowed t) 5000)) (swank:create-server :port (or (parse-integer (string (sb-ext:posix-getenv "SWANK_PORT")) :junk-allowed t) 4005) :style :spawn :dont-close t) (loop (sleep 60)))' \
	--eval '(sb-ext:save-lisp-and-die "my-application" :executable t :toplevel (quote main))'

clean:
	if [ -f my-application ]; then (rm my-application); fi

起動時にCLACK_ENV, SERVER_PORT, SWANK_PORTなどの環境変数を設定すると起動パラメータが変わって便利です。

サーバ構成の変更

社内リリースをしてしばらく運用すると、Hunchentootがたまにレスポンスを返さないことがあるのに気づきました。たまにだけど。Apache Benchをかけてみると、Hunchentootのレスポンスは0.4秒と速いのですが、同時接続が20越えると黙りこむことがあるようです。同時接続が20なんてまあまあよくあることなので、このままでは今後継続して運用し切れないと判断して、別のバックエンドに移行することにしました。

そういう経緯もあり、今月ClackはFastCGI対応しました。これを使ってアプリケーションサーバFastCGIで動くようにし、ついでにフロントサーバもApacheからNginxに移行しました。これでレスポンスタイムは同じくらい高速で、同時接続数は100以上でも安定して稼働できそうです (社内限定リリースなのでまだそこまでの負荷は来ていませんよ)。FastCGIなのでstreamingも可能です。

メモリ使用量

アプリケーションサーバを1週間くらい動かしていると、メモリ使用量がどんどん上がっていきます。SBCLで、何がメモリを食っているかを調べたいときは以下の関数が有効です。

(sb-vm:instance-usage :dynamic :top-n 10)

まあ、512MBのサーバの一角で動かすようなものでもないんですけどね……。

まとめ

最初、会社の開発合宿でCommon Lispのアプリケーションを書くことに、僕も悩みはしたのですが、こういう場では作ってて楽しいものでなければ後悔する、と思ってCommon Lispを選びました。

実際作ってみると、アプリケーションはかなり高速で、だいたい同時接続数が増えても0.5秒程度で返ってくるし、ローカルのEmacsから本番に反映して試す、ということが容易にできるため、問題が発見しやすく、開発のスピードも保つことができました。これは本当です。

開発から社内リリースを経てしばらく運用して思ったのは、SQLite3からMySQLに、もしくはHunchentootからFastCGIに、移行したいと思ったときに容易に移行できることは当然のごとく重要だなぁと思いました。

Hunchentootに依存しているフレームワークはWeblocksとかRESTASとか web4rとかいっぱいありますが、将来Hunchentootではパフォーマンス上問題があると思ったときに移行できないとするとかなり心配です。そういう実体験もあり、今回はClackやCL-DBIの重要性を再度実感しました (宣伝)。

興味がある方はぜひ使ってみてください。

こういうCommon Lispでの運用の話って表に出てこないことが多いので、今後も少しずつ増やしていきたいですね。