hideden.hatenablog.com

はてなぶろぐー。URLなげー。

isuconお遊びチーム(事前社内β組)の設定あれこれ

ISUCONに行ってきました。社内での事前βテストに参加して問題を知っていたので出場はせず。社内β参加を持ちかけられたときは、正直「めんどくせーなw」が素直な感想だったんですが、実際にやってみるとスコアがリアルタイムにわかる&ちょっとずつ自分のスコアが上がっていくってのは楽しくて、わりと本気でチューニングしてしまいました。

さて、本戦でも14時頃からお遊び用としてサーバー一式が解放されたので、大人げも無くそこで112500req/minをたたき出して参加者のやる気を削いだ(・・と懇親会で言われました。色々すいません!)構成について。

今回の課題の場合のチューニングの戦略は何パターンかあると思いますが、普段livedoorBlogでも取っている

  • frontでなるべくcache
  • 更新時はappへ
  • DBへアクセスが行ったら負け

の戦略でいきました。他であまり見かけなかったところとして、cacheの効率を上げるためにfrontのnginxではSSIを使ってます。今回の課題の肝は全ページのサイドバーにあたる「recent_commented_articles」ですが、ページ内にこの部分をレンダリング済みでページ全体をcacheするとコメントpostのたびに全ページのcacheを破棄せねばならず、効率がぐっと落ちます。テンプレートの該当部分を

<!--#include virtual="/recent_commented_articles" -->

とすることでこの部分を分離しています。これにより、コメントpost時のcache更新影響範囲を

  • comment post先articleページ
  • recent_commented_articles部分

だけに限定することが出来ます。frontに使っているサーバーは違うものの、SSIでのページの結合はlivedoorBlogのサイドバー部分を含めたいくつかの箇所で行っています。(本来ならjsでもいいはずなんですが、SEO云々とか色々言われるのでこういう事になってます)

今回のBenchmarkではpostにかかる時間は点数に影響が無いので、article post/comment post時にDB記録とともに更新がある範囲HTML(上記の2ページ分)をレンダリングし、応答を返す前にreverse proxyサーバーに同居しているmemcachedへsetしています。

その他のページに関しては、cache missしたものに関してはオンザフライで生成し、cacheにsetしつつ返しています。レギュレーション的にはサーバー再起動後に1分間の猶予があったので、そこでcacheをウォームアップするものを書けばもっと高速化も可能かもしれませんが、時間が足りなかったのでやっていません。

これにより、app/DBの負荷はぐっと下がってしまうのでapp/DBをチューニングしてもあまり効果は見込めないのですが、最初それに気付くまでに時間がかかり、序盤はappの最適化に時間を費やしてしまいました。今回の課題でPerlではこれのためだけに作られたKossyという簡単なWAFが使われています。これが結構他でも使えそうなくらいよく出来ているんですが、その汎用性の高さのせいであんまり速くないと考え、SCGIというFastCGIのもうちょっとシンプル版のようなものを使ったものに書き直しました。

SCGIを採用した理由としては

  • nginxの公式moduleにngx_http_scgiがあった
  • 使ったことが無かったので使ってみたかったw
  • perlFastCGIは過去(といっても5年以上前)にいい思い出が無かった

こんなところです。他と比較する時間までは無かったので、PSGIと比べてどれくらい差があるのかは不明ですが、CPANモジュールも2006年に更新されたっきりのようなので、、、仕事で使うには色々アレかもしれません。結果的にはこのアプリのrewriteはほとんどパフォーマンスには影響を与えませんでした。社内ircでKossyにいちゃもんつけてすいませんでした!


その他のチューニング項目は

  • mysqlの良くある設定変更
    • innodb_buffer_pool_size系のメモリ設定を増やす
    • skip-name-resolve
    • innodb_flush_log_at_trx_commit = 0
  • 各サーバーのkernelの設定値変更
    • echo 1000000 > /proc/sys/fs/file-max
    • echo 1024 65000 > /proc/sys/net/ipv4/ip_local_port_range
    • echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse
    • echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle

くらいです。社内βは個人戦(っというか誰も手伝ってくれなかっただけ)なのでこの辺に割く時間はあまりなく、mysqlのテーブル構造やindex、アプリからのクエリに関してはいじれて居ません。



nginxの設定は汚いんですが以下に。

worker_processes  4;

events {
    worker_connections  10000;
}

http {
    include       mime.types;
    default_type  text/html;
    sendfile        on;
    keepalive_timeout  1;

    upstream scgi_app {
        server xxx.xxx.xxx.xxx:20000;
        server xxx.xxx.xxx.yyy:20000;
        (略)
        keepalive 1000000;
    }

    upstream memcached {
        server 127.0.0.1:11211;
        keepalive 1000000;
    }

    server {
        listen       80;
        server_name  localhost;

        location /images {
            alias /home/isucon/isucon/webapp/staticfiles/images;
        }
        location /css {
            alias /home/isucon/isucon/webapp/staticfiles/css;
        }
        location /js {
            alias /home/isucon/isucon/webapp/staticfiles/js;
        }
        location = /favicon.ico {
            alias /home/isucon/isucon/webapp/staticfiles/favicon.ico;
        }

        location /recent_commented_articles {
            set $memcached_key "/recent_commented_articles";
            memcached_pass memcached;
            default_type text/html;
            error_page 404 =200 @scgi_recent;
        }

        location @scgi_recent {
            scgi_param REQUEST_METHOD "GET";
            scgi_param REQUEST_URI    "/recent_commented_articles";
            scgi_param DOCUMENT_URI   "/recent_commented_articles";
            scgi_param SCGI           1;
            scgi_pass scgi_app;
        }

        location / {
            if ($request_method = POST) {
                return 302;
            }
            ssi on;
            ssi_silent_errors on;
            set $memcached_key $uri;
            memcached_pass memcached;
            default_type text/html;
            error_page 404 =200 @scgi;
            error_page 302 =302 @scgi;
        }

        location @scgi {
            ssi on;
            ssi_silent_errors on;
            scgi_param REQUEST_METHOD $request_method;
            scgi_param REQUEST_URI    $request_uri;
            scgi_param DOCUMENT_URI   $document_uri;
            scgi_param SCGI 1;
            scgi_pass scgi_app;
        }
    }
}

fujiwaraさんも d:id:sfujiwara:20110827:1314460582 で書いておられますが、標準状態ではnginx->memcached間はつど接続/切断されてしまうので、ngx_http_upstream_keepaliveを使って接続を維持しています。おまけでSCGIへの接続もつなげたままにしています。これでだいぶ速度が変わります。

memcached側にpostリクエストを投げるとエラーになってしまうので、scgi側に302statusでfallbackさせています(今回だとpost後には必ず302になるので)。また、404 fallback時に=200がないと404でresponseが返ってしまうのでその辺もいじってます。

/recent_commented_articlesだけ別なのは、SSIでのinclude時のサブリクエスト中は$uri変数が元のリクエストのままになってしまっており、$memcached_keyがおかしくなってしまうのでやっつけ対応です。多分使う変数がまずいんですが、調べる時間もなかったのでこんな事になってます。

普段の業務で使うには色々アレなところも多いですが、今回はとりあえず速度が出れば後は何でもOK!なものという事もあり、適当です。真っ先にiptables -Fした時にはid:tagomorisなどには突っ込まれましたが、まぁそんなもんです。


ちょっと課題が普段blogサービスをやってる人に有利かな?って感じがあり、recent_commented_articlesの実装を見てすぐfrontでcache+SSI戦略を立てることができたので今回の課題ではうまく行きましたが、懇親会などで聞かせていただいた他の参加者の正統なアプリチューニングの話はとても参考になりました。

企画・運営・参加者のかたがたお疲れ様でした!

tiarraをやめてzncにしてみた

Limechatのサーバー設定をいじってる時に、ふとSSL Optionが気になった。tiarra+stoneでやってみるかーと思ってyum search stoneしたところ、見つからない。ソースから入れるかなと思い、openssl-develを入れようとyum search sslしたところ、最下行に

znc.x86_64 : Advanced IRC bouncer

とかいうのを発見。これらしい。

SSL Support 
    Encryption for both the listen port and connecting to IRC servers.
    If your system has OpenSSL, ZNC automatically supports SSL connections.

公式に、↑と書いてあった。せっかくなのでこっちを入れてみることにする。


Installはyumで。SakuraVPSのCent5.5だと、EPELにパッケージがあるらしいので、さくっと追加。

$ rpm -Uvh http://dl.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.rpm
$ sudo yum install znc znc-extra

installはあっさり終わる。



設定にいくまえに注意。zncとtiarraの最大の差が、zncには複数のネットワークを1つにまとめる機能がない事。つまり、これまでtiarraで、

#aaa@freenode, #bbb@freenode, #aaa@ircnet, #bbb@ircnet

みたいにやってたアレができない。が、zncは1プロセスで複数ユーザーが接続出来るのでそれで代用可能。
freenodeとircnetの2つにつなぐ場合は、hideden-freenode, hideden-ircnetのように2つのユーザーを作成するようにする。1つのユーザーには、1つのnetworkのサーバーのみを指定するようにする。




zncを--makeconfオプション付で起動するとウィザードが始まる。nickやらpasswordやらいろいろ聞かれるので主なところを以下に。

$ znc --makeconf
 # portは適当に
[ ?? ] What port would you like ZNC to listen on? (1 to 65535): 6668
 # 今回、SSLが目的なので当然yes
[ ?? ] Would you like ZNC to listen using SSL? (yes/no) [no]: yes
 # pemを自動で作ってくれてとても楽。
[ ?? ] Would you like to create a new pem file now? (yes/no) [yes]: yes

 # global moduleはとりあえず全部noで。
[ ?? ] Load global module <partyline>? (yes/no) [no]: 
[ ?? ] Load global module <webadmin>? (yes/no) [no]: 

 # usernameはircクライアントのLoginNameとして設定する
[ ?? ] Username (AlphaNumeric): hideden-freenode
 # passwordはサーバーパスワードになる
[ ?? ] Enter Password: 

 # adminにしとくと、そのアカウントから色々できる
[ ?? ] Would you like this user to be an admin? (yes/no) [yes]: yes

 # 接続してない時に流れるログの保存行数。チャンネルごと。
[ ?? ] Number of lines to buffer per channel [50]: 

 # yesにすると、接続するたびに保存されてるログがplaybackされる。noだと未読のが1回出るだけ。
[ ?? ] Would you like to keep buffers after replay? (yes/no) [no]: yes

 # ircクライアントから色々管理するやつ。とりあえずこのモジュールだけ入れる。
[ ?? ] Load module <admin>? (yes/no) [no]: yes

 # 1つのユーザーには1つのnetworkのサーバーしか登録しない。別networkの場合はこの後、別のユーザーをつくる。
[ ?? ] IRC server (host only): irc.freenode.net

これで、~/.znc/config/znc.conf ができる。

コンソールから

$ znc

で起動。勝手にbackgroundに回って常駐する。



さっそくLimechatなどのircクライアントから接続。port, login name, server passwordは設定したものを指定。SSLをyesにした場合はSSLも有効に。login nameにzncでつくったuserを指定することで、接続先をわけられる。作成したユーザー分、サーバーを追加する。



tiarraの#aaa@freenodeも悪くはないのだが、もともとirc的に若干無理があってLimeChatからTalk送るときなどに不便。zncのこっちの方が自然で便利かも。今どきのクライアントで複数サーバー未対応なものは少ないし。





zncにも、tiarraと同じく色々Moduleがあるので、ここから探して使うと便利。

もともとtiarraでもlogとってるくらいだったので、今のところlog, keepnick, nickservくらいしか使ってない。

moduleの追加は、ircクライアント上から

/znc LoadModule MODULENAME

 # log moduleの場合
/znc LoadModule log

 # charset moduleの場合
/znc LoadModule charset IRCクライアントの文字コード サーバーの文字コード

とやるだけ。configファイルを直接いじる場合は、

/znc SaveConfig
 ... ~/.znc/config/znc.conf を編集 ...
/znc Rehash

とやる。logモジュールの場合、特に設定もなくloadするだけですべてのチャンネルのlogが

~/.znc/users/hideden-free/moddata/log/#channel_YYYYMMDD.log

に保存される。もろもろのコマンドは

/znc help   ( or /msg *status help )
/msg *admin help

で出てくる。


tiarraに特に不満はなかったものの、

  • 設定ファイルが簡潔
  • SSL対応
  • Moduleが結構豊富
  • 1プロセスで複数ユーザーを使える
  • Moduleを書くのにperlを使えるらしい(modperlモジュール)
  • talkするときに不便じゃない

と、いい感じなのでこのまま乗り換えることにした。

linuxを入れたmac mini server(mid 2010, unibody)で停電後に自動復帰する方法

自宅のあるエリアも今日計画停電で停電に。停電自体は仕方ないにしても、電気が戻ったらサーバーは復帰して欲しい。
OSXなら環境設定ですぐなんだが、あいにくうちのmac miniは完全にlinuxのみなのでこれだけのためにOSX入れなおすのもだるいので調べてみた。

ここにあるとおり、

 sudo setpci -s 00:03.0 0x7b.b=0x19

でコンセント抜き差し後に自動で起動した。設定値の意味とか、自動で起動しないように戻す方法とかまったくわからないので自己責任で。

これで明日から停電があっても心配なくなった!

iTerm2でcmdをAltにするpatch

cmdをAltにするpatchはいくつかあるんだけど*1 *2、自分の好みに合ったのがなかったので書いた。今日の時点での最新版のiTerm2 rev.491で確認。

https://gist.github.com/781481

cmd+ space(入力切替), enter(全画面表示), t(新規タブ), n(新規window), c(copy), v(paste), <-(前のtab), ->(次のtab) 以外の組み合わせの場合だけ左Optを押したことにする。iTerm2の設定で

みたいに左Optを+ESCにしとくといい感じ。左Optとcmdを入れ替えたりという気の利いた事はしてないので、左Optとcmdのどっちを押しても設定どおり+ESCになる。これでcmd+wや左opt+wを押した時にタブ閉じて涙目・・・とかにならなくて済む。

space, enter, t, n, c, v, <-, -> 以外のキーも有効にしたければKey Codes.appで調べて追加。無効にしたければ削るだけ。

iTerm2は描画速くてすばらしいね。ここのとこずっとiTermの遅さに嫌気が差してTerminal.app使ってたけど、仕事でもiTerm2をメインに使ってみよう。

Sakura VPS980のCentOSを再起動一回でGentooに変身させる方法

sakuraのVPS980がとっても快適ですばらしい。調子に乗って自宅サーバーを廃止しNASに置き換え、DNS/mailなんかはすべてVPSにした。部屋も静かで涼しくなって、しかもasahi-netの固定IPもろもろを解約したので月コストもだいぶ下がりいい事だらけ。

ただ、標準で入ってるCentOSが会社と同じでなんとなくつまらない。幸いなことにOS変えてる人がちらほら居る&勝手に変えても(多分)怒られないようなので、もう1インスタンス借りて実験的にGentooにしてみた。

調べてみたらgrub2経由でSystemRescueCdを起動してごにょごにょも出来るらしい。ただ、Web上のシリアルコンソール経由でやるのはちょっとしんどいのでSysRescCd経由はあきらめ、CentOSの中身をごそっと全部入れ替えてGentooにする方法を採用してみた。こういう行為をあまり悩まずに気軽に出来るようになったのはGentoo様のおかげですね。さすがドM仕様。

準備

とりあえず、ここから契約して、root passが書いてあるメール(多分2通目?)がくるまでおとなしく待つ。そのあと、管理画面からVPSを起動してSSH接続。このあと、しばらく新規でSSH接続が確立できなくなるので(理由は後述)、Terminalを2つ起動して念のため予備で2つ接続しとくといい。

つないだら、GentooのInstall用にあらかじめmirror一覧から日本のmirrorを選んでamd64のstage3とportageのsnapshotを/tmpに落としておく。ここでやっておかないとCentOS関連ファイル除去後には落とせなくなるので注意。

[root@wwwXXXXu /]# cd /tmp
[root@wwwXXXXu tmp]# wget http://ftp.iij.ad.jp/pub/linux/gentoo/releases/amd64/current-stage3/stage3-amd64-20100902.tar.bz2
[root@wwwXXXXu tmp]# wget http://ftp.iij.ad.jp/pub/linux/gentoo/snapshots/portage-latest.tar.bz2

CentOSのシステムを掃除する

とりあえず、初期状態のsakuraVPSのCentOSはこんな感じになってます。

[root@wwwXXXXu ~]# ls -F /
bin/   etc/   lib64/       misc/  proc/  selinux/  tmp/
boot/  home/  lost+found/  mnt/   root/  srv/      usr/
dev/   lib/   media/       opt/   sbin/  sys/      var/
[root@wwwXXXXu ~]# mount | grep /dev/hd
/dev/hda2 on / type ext3 (rw)
/dev/hda1 on /boot type ext3 (rw) 

ここから要らないもの(bin, etc, home, lib, lib64 ....など)をまずすべて削除!

・・・と行きたいところだが、いきなり削除するとmvコマンドなんかも使えなくなって詰む。なので、まずbusyboxで現在のCentOSから切り離した形で最低限のコマンド実行が出来るようにする。

busyboxの公式ページから最新版を/tmpなどに落としてstaticでmakeするだけ。busyboxのmenuconfigをするためにncursesのヘッダ類が必要なので、まずそれをyumで入れる。(どうせこの後消えるので細かい事は気にしなくてOK)

[root@wwwXXXXu tmp]# yum install ncurses-devel
[root@wwwXXXXu tmp]# wget http://busybox.net/downloads/busybox-1.17.2.tar.bz2
[root@wwwXXXXu tmp]# tar jxf busybox-1.17.2.tar.bz2
[root@wwwXXXXu tmp]# cd busybox-1.17.2
[root@wwwXXXXu busybox-1.17.2]# make menuconfig

busyboxのメニューが出たら、static linkでbuildするようにと、コンパイルエラーが出るのでiplink moduleのbuildをしない設定だけしてexit。

Busybox Settings  --->
    Build Options  --->
        [*] Build BusyBox as a static binary (no shared libs)

Networking Utilities  --->
    [ ]   Use ip applet (NEW)  ←チェックをはずす 
    [ ]   ip link              ←チェックをはずす

あとは make && make install でめでたくbusyboxを展開したディレクトリ直下の_installにsymlink集が出来上がるので、ここにPATHを通す。

[root@wwwXXXXu /]# export PATH=/tmp/busybox-1.17.2/_install/sbin:
/tmp/busybox-1.17.2/_install/bin:/tmp/busybox-1.17.2/_install/usr/sbin:
/tmp/busybox-1.17.2/_install/usr/bin:$PATH

これで主要なコマンドはCentOSのものではなく、busyboxが優先されるようになるはず。早速、CentOS除去作業を再開。とりあえずいきなり消すのも怖いので、/oldを作ってそこにすべてmvする。(bootは別partitionなので中身だけ移動する)

[root@wwwXXXXu /]# mkdir /old
[root@wwwXXXXu /]# mv bin etc home lib lib64 media misc mnt opt root sbin selinux srv usr var old/
[root@wwwXXXXu /]# mkdir /old/boot
[root@wwwXXXXu /]# mv /boot/* /old/boot/
[root@wwwXXXXu /]# ls -F
boot/       lost+found/ proc/       tmp/
dev/        old/        sys/

dev, lost+found, proc, sys, tmp(busyboxが入ってる) は除外。これでCentOSの痕跡は今起動してるkernelだけに。これ以降、新規でSSHコネクションは張れないので絶対にSSHを切断しないように注意する。切断しちゃった場合は・・・最初からやり直し。VPSって再インストール楽でほんといいね。

Gentooを入れる

ここから先はinstall-minimalで起動してからやるGentooのインストールとさほど変わらない。

stage3, portage-snapshotを入れる

あらかじめdownloadしておいたのを展開して/に移動する。その後、portage snapshotも展開。

[root@wwwXXXXu /]# mkdir /tmp/gentoo
[root@wwwXXXXu /]# cd /tmp/gentoo/
[root@wwwXXXXu gentoo]# tar jxpf ../stage3-amd64-20100902.tar.bz2
[root@wwwXXXXu gentoo]# ls
bin    dev    home   lib32  mnt    proc   sbin   tmp    var
boot   etc    lib    lib64  opt    root   sys    usr
[root@wwwXXXXu gentoo]# mv bin etc home lib lib32 lib64 mnt opt root sbin usr var /
[root@wwwXXXXu gentoo]# cd /usr
[root@wwwXXXXu usr]# tar jxf /tmp/portage-latest.tar.bz2

/devはそのままではCentOSのものがmountされていて書けない。mount -o bindで別のpathにbindしてからdevにコピーする。

[root@wwwXXXXu /]# mkdir /mnt/root
[root@wwwXXXXu /]# mount -o bind / /mnt/root
[root@wwwXXXXu /]# cp -a /tmp/gentoo/dev/* /mnt/root/dev/
[root@wwwXXXXu /]# umount /mnt/root

一時的に使ってたbusyboxとおさらばする。

[root@wwwXXXXu /]# source /etc/profile
[root@wwwXXXXu /]# env-update
[root@wwwXXXXu /]# rm -rf /tmp/busybox*

これでほぼ中身がGentooに。

各種設定をする

Gentooユーザーにとってはこの辺はいつもどおり。お約束な感じの設定をする。

### /etc/make.conf  (USEフラグとかお好みで)
CHOST="x86_64-pc-linux-gnu"
CFLAGS="-march=core2 -O2 -pipe"
CXXFLAGS="${CFLAGS}"

MAKEOPTS="-j3"
GENTOO_MIRRORS="http://ftp.iij.ad.jp/pub/linux/gentoo/"
SYNC="rsync://rsync.jp.gentoo.org/gentoo-portage"

USE="unicode cjk nls userlocales nptl nptlonly bash-completion emacs perl ruby pcre -samba mmx sse sse2 ssl zlib bzip2 jpeg png sqlite -ipv6 -kde -gtk -gnome -qt -qt3 -qt4 -X -xorg"
LINGUAS="ja"

ネットワークにつなげるようにするために、resolv.confはCentOSのものをコピる。zoneinfoもコピる。locale.genもいじりたければいじる。ついでに忘れないうちにrootのpasswordも設定しておく。

[root@wwwXXXXu /]# cp /old/etc/resolv.conf /etc/
[root@wwwXXXXu /]# cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
cp: overwrite '/etc/localtime'? y
[root@wwwXXXXu /]# passwd
Changing password for root

で、emerge --syncの後、必要なもの(kernel, genkernel, grub, syslog, cron)あたりを入れる。kernelはとりあえず面倒なのでgenkernelで作る。

[root@wwwXXXXu /]# emerge --sync
[root@wwwXXXXu /]# emerge gentoo-sources genkernel grub syslog-ng vixie-cron
[root@wwwXXXXu /]# genkernel all    ←結構時間がかかる

VPSシリアルコンソールの設定、grubの設定、fstabの編集を行う。やらずに再起動しちゃうとこれまた最初からやり直しに。シリアルコンソールの設定はここのとおりにする。

### /boot/grub/grub.conf
default 0
timeout 5
serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1    ←これ
terminal --timeout=10 serial console                            ←これ
hiddenmenu

title Gentoo Linux 2.6.34-r6
root (hd0,0)
kernel /kernel-genkernel-x86_64-2.6.34-gentoo-r6 root=/dev/ram0 real_root=/dev/hda2 console=tty0 console=ttyS0,115200n8r ←これ
initrd /initramfs-genkernel-x86_64-2.6.34-gentoo-r6


### /etc/inittab
co:12345:respawn:/sbin/agetty -h 115200 ttyS0 vt100  (最下行に追加)


### /etc/fstab
/dev/hda1               /boot           ext2            noauto,noatime  1 2
/dev/hda2               /               ext3            noatime         0 1
/dev/hda3               none            swap            sw              0 0
shm                     /dev/shm        tmpfs           nodev,nosuid,noexec     0 0


### grubをinstall
[root@wwwXXXXu /]# grub-install --no-floppy /dev/hda

ネットワーク設定をCentOSからコピーする。DHCPにすると割り当てられたIPじゃないIPを貰えるが、多分まずいので素直に。CentOSGentooで設定ファイルと書式が違うので手で変換する。

[root@wwwXXXXu /]# cat /old/etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0
IPADDR=59.XXX.YYY.ZZZ
NETMASK=255.255.254.0
GATEWAY=59.XXX.YYY.1
ONBOOT=yes


###/etc/conf.d/net
config_eth0=( "59.XXX.YYY.ZZZ netmask 255.255.254.0 brd 59.XXX.YYY.255" )
routes_eth0=( "default via 59.XXX.YYY.1" )

udevとsshdとnet.eth0の起動設定をする。

[root@wwwXXXXu /]# rc-update add udev boot
 * udev added to runlevel boot
[root@wwwXXXXu /]# rc-update add net.eth0 boot
 * net.eth0 added to runlevel boot
[root@wwwXXXXu /]# rc-update add sshd default
 * sshd added to runlevel default

これで再起動する準備が整った・・・はず。勇気を出して再起動する。うまくいけばしばらくするとSSHが起動するはず。あとは/oldを削除すればGentooへの切り替え完了。

CPUが案外速くてかなり安いので、複数契約してdistccとかも楽しそう。

JSONPなAPIの負荷対策にngx_http_jsonp_callbackってのを書いてみた

認証が不要で、結果をJSONPで返してくれるAPI。大体は高速化の為にmemcachedを使用し、cacheが存在すればcacheから、存在しなければDB等から引いてcacheに入れ、その後結果を返す設計になってるはず。

URL: http://api.example.com/count?user_id=12345&entry_id=12345&callback=hoge
response:
  hoge({"status":"success", "count":1000});

みたいなの。ほとんどの場合cacheにHitするので一瞬でresponseが返るけど、あまりに簡単なお仕事過ぎてそれの為にmod_perlのプロセスを使うのがもったいない。特に1日数千万回アクセスされるようなAPIだと積もり積もってすごい負荷に。

responseに使うJSONをそのままcacheに入れて、Tokyo TyrantにあるHTTPプロトコル実装やnginx + memcached_moduleで返せたらいいのになぁと思ったが、JSONならやれるがJSONPになるとcallback関数名部分は可変なので出来ず、現状callbackを付けるだけの為にmodperlを使ってる。

で、modperlを使用せずにこのcallback関数名を付与の部分だけをnginxのbody filterとして実装したngx_http_jsonp_callbackってのを書いてみた。クエリから関数名を取得してbodyの前後をcallback_func( ... );で挟むだけ。使い方は

  location hoge {
    jsonp_callback        callback;
    jsonp_callback_types  text/javascript;
    # proxy_pass ...
    # memcached_pass ...
    # etc...
  }

こんな感じ。jsonp_callbackにcallback関数名を取得するQueryStringのパラメーター名を指定。jsonp_callback_typesにcallbackを付与するcontent-typeを指定。callback関数名には[a-zA-Z0-9_]{1,255}のみ通します。

さて、これだけじゃ使えないので他の準備。

memcachedが1台だけの場合はmemcached公式のmemcachedモジュールを使えば問題ないけど、基本的に複数台なはず。memcachedは分散がクライアント側の実装に任されている&モジュールによって実装方法がまちまちな為、互換性がある分散方法じゃないと使えない。ngx_http_consistent_hashモジュールとmemcached_passを併用した場合はPHPの分散と同じになるとかなんかドキュメントに書いてあったりした気もするけど会社が全部Perlなので駄目。

で、いろいろ探したところCache::Memcached::Fastと同じ分散をしてくれるnginx-patchedってのを見つけた。

installはgitでmaster-v0.7ブランチを取ってきてコンパイルするだけ。理由は後述するが、ngx_http_upstream_keepaliveもあった方がよさげ。

git clone git://openhack.ru/nginx-patched.git
cd nginx-patched
# get ngx_http_upstream_keepalive, ngx_http_jsonp_callback
cd server
./configure --add-module=../ngx_http_jsonp_callback --add-module=../memcached_hash --add-module=../ngx_http_upstream_keepalive
make; make install

あとはCache::Memcached::Fastとnginxのconfで設定値を合わせればperl側とnginx側が同じcacheを見るようになる。すばらしすぎる。


ベンチを取ってみた。modperl版はWAFを使うと遅すぎて話にならないのでPerlHandler。

mod_perl + memcached

package TestApi;

use strict;
use TestApiCache;
use Apache2::Request;
use Apache2::Const -compile => 'OK';

sub handler : method {
    my ($class, $r) = @_;
    my $req = Apache2::Request->new($r);
    my $key = $req->param('user_id') . '::' . $req->param('entry_id');
    my $json = TestCache->instance->get($key);
    unless ($json) {
        # my $data = Data::Hoge->search(...);
        # $json = JSON::XS->new->latin1->encode({ ... });
        # $cache->set($key, $json, $exp);
    }
    my $callback = $req->param('callback');
    $callback = undef if $callback !~ /^[a-zA-Z0-9_]{1,255}$/;
    $r->content_type('text/javascript');
    if ($callback) {
         $r->print($callback . '(' . $json . ');');
    } else {
         $r->print($json);
    }
    return Apache2::Const::OK;
}

package TestApiCache;

use strict;
use base qw(Cache::Memcached::Fast Class::Singleton);

sub _new_instance {
    my $class = shift;
    $class->SUPER::new({
        servers => [ 'localhost:20000',
                     'localhost:20001',
                     'localhost:20002' ],
        namespace => 'testapp::',
        ketama_points => 150
    });
}


nginx + memcached

# nginx.conf
user  www-data;
worker_processes  1;
events {
    worker_connections  2048;
}
http {
    include       mime.types;
    default_type  text/html;
    tcp_nopush     on;

    upstream memcached_cluster {
        # ketama_pointsはCache::Memcached::Fastで指定したものと同じにする
        memcached_hash  ketama_points=150;
        # serverの並び順やweightも同様
        server localhost:20000;
        server localhost:20001;
        server localhost:20002;
        keepalive 300;
    }
    server {
        listen       10080;
        server_name  localhost;

        location /count {
            default_type             text/javascript;
            # callbackの設定
            jsonp_callback           callback;
            jsonp_callback_types     text/javascript;
            # memcachedのkey設定。$arg_QUERY_NAMEでQueryAtringsをnginxがparseしてくれた
            # 結果を使えるので便利。便利すぎ。
            set $memcached_key       "$arg_user_id::$arg_entry_id";
            set $memcached_namespace "testapp::";
            memcached_pass           memcached_cluster;
            # 存在しない時はbackendのmodperlにリクエストを送る
            error_page 404         = @fetch;
        }
        # backendの設定
        location @fetch {
            proxy_pass http://localhost:8080;
        }
    }
}

nginxの場合は、単体ではcacheからのread onlyなので、cacheにHitしなかった場合はbackendのmodperlに飛ばす。ただ、今回は事前にsetしておきcacheにHitしなかった場合は省略した。

結果

##### modperl
$ lwp-request 'http://192.168.0.1:8080/count?user_id=100&entry_id=100&callback=cb_func_hoge'
cb_func_hoge({"status":"success", "count":1000});

##### nginx
$ lwp-request 'http://192.168.0.1:10080/count?user_id=100&entry_id=100&callback=cb_func_hoge'
cb_func_hoge({"status":"success", "count":1000});

# 当然取得結果は同じ
##### modperl
$ ab -c 100 -n 100000 'http://192.168.0.1:8080/count?user_id=100&entry_id=100&callback=cb_func_hoge' 

Time taken for tests:   29.795265 seconds
Requests per second:    3356.24 [#/sec] (mean)
Time per request:       29.795 [ms] (mean)
Time per request:       0.298 [ms] (mean, across all concurrent requests)
Transfer rate:          966.93 [Kbytes/sec] received


USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
www-data 21833  0.2  0.6  30020 12588 ?        S    02:43   0:00 /usr/sbin/apache2 -k start


##### nginx
$ ab -c 100 -n 100000 'http://192.168.0.1:10080/count?user_id=100&entry_id=100&callback=cb_func_hoge' 

Time taken for tests:   11.729462 seconds
Requests per second:    8525.54 [#/sec] (mean)
Time per request:       11.729 [ms] (mean)
Time per request:       0.117 [ms] (mean, across all concurrent requests)
Transfer rate:          1473.64 [Kbytes/sec] received


USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
www-data 21921  1.3  0.0   4976  1904 ?        S    02:45   0:01 nginx: worker process 

PerlHandlerも十分早いが、nginxだと大体それの2倍くらい。それよりもメモリ消費量。modperlはAPI専用ではない事が多いと思うが、その場合1プロセスあたりメモリを数十MB食ってるケースも普通にあるはず。APIにアクセスが大量に来るとそんな巨大プロセスがmpm_preforkで複数起動するが、nginxはイベント駆動なので数MBのプロセスが数個起動するだけ。cache hitした場合はmodperlのプロセスを消費しなくなるので、だいぶうれしい。

また、普段うちの会社で業務で使う構成の場合frontのapacheとbackendのmodperlは別のサーバーなので、負荷の高いmodperlサーバーから比較的余裕があるfrontサーバーに負荷を移動できる点もうれしい。


ただいくつか注意。

modperlではmemcachedへのconnectionを都度切らずに使い回したりするが、nginxは都度切断するのでベンチマークのような大量のconnectionが一気に来るとシステムがTIME_WAITなsocketで埋まってたりする。通常1台あたり秒間数千ものアクセスが来る事はほぼあり得ないとは思うが、ngx_http_upstream_keepaliveを使ってconnectionをある程度使い回したり、kernelのtcp_tw_recycle*1tcp_tw_reuse*2あたりをいじる必要があるかもしれない。


そして最近なかなか時間が取れなくて、まだproduction環境では導入してないので安定性とかその辺は未知数。暇を見つけてblogの拍手APIとかをこれに置き換えたいなぁ。




 
  

*1:ロードバランサ配下だったりするとかなりややこしい弊害もあるので注意。詳細はgoogle先生に。

*2:これも注意?