てすと
あいうえおー
isuconお遊びチーム(事前社内β組)の設定あれこれ
ISUCONに行ってきました。社内での事前βテストに参加して問題を知っていたので出場はせず。社内β参加を持ちかけられたときは、正直「めんどくせーなw」が素直な感想だったんですが、実際にやってみるとスコアがリアルタイムにわかる&ちょっとずつ自分のスコアが上がっていくってのは楽しくて、わりと本気でチューニングしてしまいました。
さて、本戦でも14時頃からお遊び用としてサーバー一式が解放されたので、大人げも無くそこで112500req/minをたたき出して参加者のやる気を削いだ(・・と懇親会で言われました。色々すいません!)構成について。
- reverse proxy
- nginx(1.0.5)
- ngx_http_memcached + ngx_http_ssi_filter + ngx_http_scgi + ngx_http_upstream_keepalive(3rd party plugin)
- memcached(1.4.7)
- nginx(1.0.5)
- DB
- mysql(初期構成)
今回の課題の場合のチューニングの戦略は何パターンかあると思いますが、普段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を採用した理由としては
こんなところです。他と比較する時間までは無かったので、PSGIと比べてどれくらい差があるのかは不明ですが、CPANモジュールも2006年に更新されたっきりのようなので、、、仕事で使うには色々アレかもしれません。結果的にはこのアプリのrewriteはほとんどパフォーマンスには影響を与えませんでした。社内ircでKossyにいちゃもんつけてすいませんでした!
その他のチューニング項目は
- mysqlの良くある設定変更
- 各サーバーのkernelの設定値変更
くらいです。社内βは個人戦(っというか誰も手伝ってくれなかっただけ)なのでこの辺に割く時間はあまりなく、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するときに不便じゃない
と、いい感じなのでこのまま乗り換えることにした。
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を貰えるが、多分まずいので素直に。CentOSとGentooで設定ファイルと書式が違うので手で変換する。
[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。
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*1やtcp_tw_reuse*2あたりをいじる必要があるかもしれない。
そして最近なかなか時間が取れなくて、まだproduction環境では導入してないので安定性とかその辺は未知数。暇を見つけてblogの拍手APIとかをこれに置き換えたいなぁ。