nginx+squidで画像キャッシュサーバーの作り方
仕事で画像キャッシュサーバーを構築した時のメモ。大規模事例の設定例が検索してもあまり見つからなかったので同じような境遇の誰かの参考になれば。
- ピーク時のトラフィックは数Gbps
- 画像総容量は数十TB
- バックエンドのstorageが複数種類
規模とアクセス量とアクセスされる画像の種類が多いので、squidでdisk cacheを使用するとCOSS等を使用してもdiskIOで詰まる為、全てon memory cache。cache容量を確保する為に必然的にcacheサーバーの台数も数十台。
1. squidをsibling構成で並列に並べる
cache_peer 10.0.1.1 sibling 80 3130 no-query no-digest proxy-only cache_peer 10.0.1.2 sibling 80 3130 no-query no-digest proxy-only cache_peer localhost parent 8000 0 no-query originserver
自分がcacheを持っていなかった場合、ICPでsiblingしてるpeer全てに問い合わせ、cacheを保持してるpeerが見つかった場合はそこから取得して転送。無かった場合のみparentに取得しに行く方式。設定自体は一番簡単。
- よくやる設定なので実績もあってなにかと安心
- キャッシュを削除する際、全台にPURGEリクエストを送る必要がある (=台数が多いと大変)
- configが1台1台で微妙に違うのでdeployめんどくさい
特に数十台並列に並べているとアプリケーション側で行うとpurgeだけで時間がかかるので、purge jobをqueueに投げるとかめんどくさい事をする羽目に。
squid3.1からcache_peerに『htcp-forward-clr』というoptionが追加されていて、ICP(Internet Cache Protocol)の代わりにHTCP(HyperText Caching Protocol)を使えば、ICPには無かったキャッシュを削除するCLRリクエストを送ってくれるらしい。(未検証)
HTTP経由でPURGEを送った場合にもそれをHTCPのCLRとしてsibling先に送ってくれるのを期待したけど駄目だった。HTCPを話すPerlのモジュールも見あたらなかった上に、バイナリなプロトコルで自分で書くには面倒。まだunstableなsquid3.1のみでしかサポートしてないという事もあり断念。
ほんとはsiblingでCARPがやれれば一番いいんだけどなぁ。siblingで全台に問い合わせるのも無駄だし、IDC的な事情でmulticastも使いづらい。Cache-Control: max-age=1とかを連射された場合に複数台に同じファイルがcacheされるのもちょっと微妙。
2. squidを2段構成にしてCARPを使った分散を行う
1段目 cache_peer 10.0.1.1 parent 80 0 no-query carp proxy-only cache_peer 10.0.1.2 parent 80 0 no-query carp proxy-only 2段目 cache_peer localhost parent 8000 0 no-query originserver
1段目のsquidでCARPによってURLベースのhashingで特定のURLに対する2段目のcacheサーバーを特定の1つに決める方法。URL単位かつ1段目はbalancingだけを行いcacheしない為、キャッシュを削除する時は1段目のどれかに対しPURGEリクエストを1回送ればいい。
- PURGEが楽
- cacheも重複しない
- 同一サーバーにsquidを2つ起動するのが微妙な為、サーバーを分けるしかない
- 1段目はキャッシュしない為、全トラフィックが1段目を通過する事になる
- 貧乏性なので1段目のメモリがもったいなく思えてしょうがない
squidを2つ起動しちゃ駄目って書いてあるわけではないんだけど、なんとなく気分的に。それなりに負荷あるし。1段目のsquidは普通にproxyとして動作するので、当然全てのデータが通過する。たとえば画像のトラフィックが3Gbpsあった場合、1段目のサーバー群全体でバックエンドから3Gbpsで受信し、クライアントに3Gbpsで送信する事になり、送受信が同じスイッチを経由してたりすると、、、とか、上流と下流で分けるのはそれはそれでラック的に、、、とかなんか微妙らしい。
その辺は専門外なので良くわからないけど、1段目のメモリがもったいないって事だけはわかる。
3. 1段目をnginx、2段目にsquidを置いて同居
ちょっと長いけどconfig例
user nginx nginx; worker_processes 4; error_log logs/error.log; pid logs/nginx.pid; worker_rlimit_nofile 200000; events { use epoll; worker_connections 10000; # max_connections = worker_processes * worker_connections / 4 (reverce proxy) } http { include mime.types; default_type text/html; sendfile on; tcp_nopush on; send_timeout 10; keepalive_timeout 5 3; keepalive_requests 30; output_buffers 1 64k; postpone_output 1460; client_header_timeout 5; client_body_timeout 5; client_body_temp_path /tmp/nginx/client_temp 1 2 3; client_max_body_size 10m; client_body_buffer_size 32k; client_header_buffer_size 2k; large_client_header_buffers 4 8k; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log logs/access.log main; # consistent hash balancing upstream image-cache { consistent_hash $uri; server 10.0.1.1:3128; server 10.0.1.2:3128; server 10.0.1.3:3128; } # failover (normal balancing) upstream image-cache-failover { server 10.0.1.1:3128; server 10.0.1.2:3128; server 10.0.1.3:3128; } # dispatcher upstream image-cache-dispatcher { server 10.0.1.1:10080; server 10.0.1.2:10080; server 10.0.1.3:10080; } server { listen 80; server_name img.example.com; expires 30d; proxy_connect_timeout 5; proxy_send_timeout 5; proxy_read_timeout 10; proxy_buffering off; proxy_set_header Host $host; proxy_set_header X-Real_IP $remote_addr; # squidの付けるヘッダを消す proxy_hide_header X-Cache; proxy_hide_header X-Cache-Lookup; proxy_hide_header X-Squid-Error; proxy_hide_header Warning; proxy_hide_header Via; # squidが死んでた場合に502になるのでその場合はfailover error_page 502 = /failover; location / { # rewriteでquery string削る if ( $is_args ) { rewrite ^(.*)$ $1?; } # GET, HEAD, PURGEの場合はsquidへ if ( $request_method = GET ) { proxy_pass http://image-cache; break; } if ( $request_method = HEAD ) { proxy_pass http://image-cache; break; } if ( $request_method = PURGE ) { proxy_pass http://image-cache; break; } # 画像サーバーはPOST受け付けてないよ if ( $request_method = POST ) { return 405; } # その他(WebDAV的なのとか)はsquidを迂回してapacheへ proxy_pass http://image-cache-dispatcher; } location /failover { if ( $is_args ) { rewrite ^(.*)$ $1?; } if ( $request_method = GET ) { proxy_pass http://image-cache-failover; break; } if ( $request_method = HEAD ) { proxy_pass http://image-cache-failover; break; } if ( $request_method = PURGE ) { proxy_pass http://image-cache-failover; break; } if ( $request_method = POST ) { return 405; } proxy_pass http://image-cache-dispatcher; } } }
1段目のsquidをnginxに置き換えて同居させたもの。upstreamへの分散は通常均等に分散されるのだが、3rd party pluginのngx_http_upstream_consistent_hashを使えばCARPのような事がやれる。そのままだとバックエンドのsquidが落ちた場合に一部URLだけ見られなくなってしまうので、その場合のみconsistent hashingな分散を諦めてランダムにどれかのサーバーにrequestが行く。
- なんとなく最近nginxが流行ってる気がする
- nginx軽い(メモリ使用量もこれだけやるなら10MB以下)
- いっぱい接続しても大丈夫らしい
- ngx_http_limit_zone_moduleやngx_http_limit_req_moduleで同時接続数やリクエスト頻度を制限出来たりもするらしい
- load-balancerから全cacheサーバーにトラフィックが分散するのでネットワーク的に楽っぽい
って感じで最終的に3番になりました。
いくつか注意点
client_header_buffer_size 2k; large_client_header_buffers 4 8k;
いくつかの携帯電話や、何らかの理由でCookieを食いまくってる場合(画像サーバーでcookie食うのはトラフィックの無駄なのでよくない。ドメイン分けてcookie撲滅すべき。)とかに、リクエストヘッダが肥大化している場合があるので、それを見越したサイズにしないとエラーになる。auとかauとかauとかで。
大量のconnectionがある場合はsquidのコンパイル時に
ulimit -n 100000 ./configure --with-descriptors=32768 ...
などとしておく。ulimitする前にconfigureすると1024とかに戻って萎える。システム全体のfile descriptorの最大はlinuxの場合は/proc/sys/fs/file-maxで確認/設定。
上記設定等は例であり動作未確認です。間違ってたらごめんなさい。
SoftBank Mobileの携帯用GatewayをPCで通る方法のメモ
2009-08-02 15:10:00 iPhone使わない方法を追記
iPhoneを色々いじってる過程でやってみたら出来たのでメモ。さほど悪い事は出来ないと思うけど、色々自己責任で。
iPhoneとSBMガラケーでは全く別のネットワークを使用しているため、通常iPhoneからは公式サイトやIPでアクセス制限をかけてる勝手サイトは見る事が出来ない。特に見る必要も無いのだが、実験としてやってみた。
iPhoneは通常 "smile.world" というAPNに接続している。一方、ガラケーはググって見たところ "mailwebservice.softbank.ne.jp" というAPNに接続しているらしい。っと言うことは、iPhoneの接続先をこれに変えてしまえばiPhoneもSBMガラケー側のネットワークに入れる・・・はず。
用意するモノ
まず通常の黒SIM + APN書き換えでやってみたところ、黒SIMの契約ではこのAPNに接続する事が出来なかった。まぁ当然と言えば当然。なのでSIMUnlock iPhoneを使用して銀SIMにて試す事に。SIMUnlock or 香港版iPhoneの入手についてはググれば情報源がいっぱいあるのでそちらを参照。
ひとまず銀SIM側のネットワークに変なデータをなるべく流さないよう、メールのPush等を全部OFFにする。その後、黒SIMを取り外して再起動。再起動後、WiFi経由でAPN設定変更用の.mobileconfigを読み込ませる。
.mobileconfigはどこかのサーバーにUploadしてSafariで開くか、メールに添付して自分自身に送信して開けばOK。今回使用したのはこれ。一応APNのpasswordは伏せておいたので[APN_PASSWORD]の部分を書き換えた上で使用する。不思議な事にAPN名の"mailwebservice.softbank.ne.jp"でググるとX01HT用などのページがいっぱいHitするのでそこに書いてあるのを使う。
.mobileconfigのプロファイルを適用したら、銀SIMをiPhoneに挿入し認識させ、「設定 > 一般 > ネットワーク > テザリング」でテザリングをONにする。接続方法はBluetoothでもUSBでもOK。iPhoneをPCに繋いで、iPhoneのステータスバーが青く変わりテザリング状態になればほぼ完了。これでPCがSBMガラケーとテザリング経由で同一ネットワークに参加しているはず。
SBMの通常の携帯はWAPなので、そのまますべてのTCP/IPの通信が通るわけではない。どうやら特定のGatewayサーバーを経由してしかWebは見る事が出来ない模様。FirefoxでProxy設定とUserAgent設定を変更出来るAdd-onをインストールして
proxy: sbwapproxy.softbank.ne.jp:8080 UserAgent: SoftBank/1.0/705NK/NKJ001 Series60/3.0 NokiaN73/3.0650 Profile/MIDP-2.0 Configuration/CLDC-1.1
に設定。UserAgentはどうやらNokiaかSamsungの端末のもので無いと「お客様の端末からはご利用になれません。(WJ46140E)」とエラーが出てはじかれる。SHやNの端末はProxyを使用せずにアクセスしてるのかもしれない。
あとは http://ptl/menu/ にアクセスすれば普通にYahoo!ケータイのメニューにアクセス可能。メニューリストやMy Softbankなんかも普通に閲覧出来る。実際のアクセスヘッダ/REMOTE_ADDRはこんな感じになる。
REMOTE_ADDR 123.108.237.27 REMOTE_HOST w21.jp-t.ne.jp GET / HTTP/1.1 Connection: close Accept: */* Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7 Accept-Encoding: identity Accept-Language: ja,en-us;q=0.7,en;q=0.3 Host: ******* User-Agent: SoftBank/1.0/705NK/NKJ001 Series60/3.0 NokiaN73/3.0650 Profile/MIDP-2.0 Configuration/CLDC-1.1 x-jphone-color: C262144 x-jphone-display: 240*320 x-jphone-msname: 705NK x-jphone-region: 44020 x-jphone-uid: xxxxxxxxxxxxxxxx
x-jphone-uidはネットワーク側で付与されるものなので、MySoftbankで通知をONにしてあれば当然出る。UAはFirefox側で設定したものがそのまま渡される模様。
携帯端末の設定で製造番号を付与する設定にした場合、製造番号としてSNの後にIMEI番号が付く。
User-Agent: SoftBank/1.0/705NK/NKJ001/SN000000000000000 Series60/3.0 NokiaN73/3.0650 Profile/MIDP-2.0 Configuration/CLDC-1.1
これのみでクイックログインを処理しているサイトは偽装すればログイン出来てしまうので危険かもしれない。特にUserAgentからIMEI番号を正規表現などで抜き取ってそれだけを利用している場合は、この方法を使えば偽装可能。uidの方を使うようにした方がいいかも。
ちなみにIMEI番号はほとんどの携帯のダイヤル画面で *#06# と入力すると調べる事が出来る。
ついでにiPhoneでOpenSSHを立ててログインして見てみた。テザリング中のiPhoneにUSB経由でSSHするには ssh root@192.168.20.1 でいける。(多分)
hideden-ipn:~ root# ifconfig lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384 inet 127.0.0.1 netmask 0xff000000 en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500 inet 192.168.0.88 netmask 0xffffff00 broadcast 192.168.0.255 ether xx:xx:xx:xx:xx:xx pdp_ip0: flags=8011<UP,POINTOPOINT,MULTICAST> mtu 1450 inet 10.xxx.xxx.xxx --> 10.xxx.xxx.xxx netmask 0xffffffff pdp_ip1: flags=8011<UP,POINTOPOINT,MULTICAST> mtu 1024 pdp_ip2: flags=8011<UP,POINTOPOINT,MULTICAST> mtu 1024 pdp_ip3: flags=8011<UP,POINTOPOINT,MULTICAST> mtu 1024 en1: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500 inet 192.168.20.1 netmask 0xffffff00 broadcast 192.168.20.255 ether xx:xx:xx:xx:xx:xx
[hideden@hideden-macbook]$ host sbwapproxy.softbank.ne.jp sbwapproxy.softbank.ne.jp has address 172.24.168.97
なので、この周辺を探せばNokiaのUA以外でも通れるProxyとかあるのだろうか。この辺で飽きたので断念。
(2009-08-02 15:10追記)
↑のようなややこしい事をする必要は全くなかった。普通にSBMの携帯をUSBでPCにつないでダイアルアップすれば同じ事が出来た。
OSXの場合はこんな感じ。
Winとかの場合は適切なモデム定義をインストールするか、接続先APNをATコマンドで設定するとかする必要があるかも。
AT+CGDCONT=1,"IP","mailwebservice.softbank.ne.jp"
とか。
こんだけあっさり出来ちゃうと、製造番号を認証に使うのは危ないな−。昔作ったヤツがちゃんとx-jphone-uidを使ってるか確認しよう。「IPでアクセス制限かけてるから、認証とかある程度適当でも大丈夫だよねwww」って感じじゃダメだね。
squid 3.1をreverse proxyとして使ってる場合にoriginサーバー側のbasic認証を使う方法
[squid]---[apache:1]---[apache:2]
という構成の[apache:2]で特定のHTTP methodの場合だけかけてあるBASIC認証がsquidを経由すると全然通れなくなって、[apache:1]でmod_rewriteの[P]使ってるからダメなのかと疑ってみたり、tcpdumpで調べて「なんでsquidの後ろでAuthorizationヘッダが消えるんだよ!!」とムカついてたりしたんだが、単純にsquidの設定が足りなかった。
cache_peer 10.0.0.x parent 8080 0 no-query login=PASS originserver
"login=PASS" が必要らしい。普通にドキュメントに書いてあった。ドキュメントは隅々までちゃんと読むべき。
header_accessやrequest_header_accessはこの場合は全く無関係。紛らわしい名前で萎える。
emacs + trampで多段SSHで接続したサーバー上のファイルを直接編集する時のメモ
会社のサービスはローカルに開発環境を構築するにはでかすぎてだるいので開発サーバー上で開発してるのだが、この開発サーバーが非力すぎて誰かがsvn操作とかでdisk IO使い出すとemacsまで巻き込んで固まる。。。
で、周りの人に聞いてみたらemacs+trampでやってる人が居た。昔チラッと使った時はFUSEのsshfsでマウントした方が何かと便利じゃね?っと思ってすぐ使うのを辞めた気がするので忘れてた。せっかく教えてもらったので真似してみる。
ローカルの環境はOSX Leopardで、ターミナル上のemacs使用。trampはMacPortsとかに無いっぽいので自前で入れる事にした。
trampの現在の最新版は2.1.14。ここから落とした。
wget http://ftp.gnu.org/gnu/tramp/tramp-2.1.14.tar.gz tar zxf tramp-2.1.14.tar.gz cd tramp-2.1.14 ./configure --with-lispdir=~/.lisp make make install
とかで~/.lispにInstall。
;; tramp (require 'tramp) (setq tramp-default-method "ssh")
って書いて終わり。あとは C-x C-f /my.server.example:~/hoge.txt とかやるとmy.server.exampleに接続して直接ファイルを編集できる。とっても便利。
会社の開発サーバーへは直接接続できず、いくつか踏み台を経由して接続している。仮に下のような感じとする。
localPC -> GW1 server (gw1) -> GW2 server (gw2) -> dev server (hoge.dev)
MacFUSEのsshfsを使ってマウントするのも踏み台を経由すると結構めんどくさくてやってなかったんだが、trumpはその辺賢いらしく踏み台経由の編集が出来るらしい。ググったところ、/multi:ssh:GW1.server:ssh:GW2.server:ssh:dev.server:/tmp/hoge.txtとかやれって記述とかtramp-multi-connection-function-alistをごにょごにょみたいなのを見つけたんだが、どうやらこれは古くて最新版じゃ方法が違うらしい。和訳マニュアルも古いままのようだ。
上の構成の場合はとりあえずこんな感じでやれた。
; *.devに接続するにはgw2を経由する。gw2にはuser: dev001でログインする。 (add-to-list 'tramp-default-proxies-alist '("\\.dev" nil "/ssh:dev001@gw2:")) ; gw2に接続するにはgw1を経由する。user: hideden。 (add-to-list 'tramp-default-proxies-alist '("gw2" nil "/ssh:hideden@gw1:"))
この設定をした後、C-x C-f /hoge.dev:/tmp/hoge.txt とやると local -> gw1 -> gw2 -> hoge.devとちゃんとたどってくれた。とっても便利。
途中でsudoを挟む場合は以下。
;;途中でsudoする場合 ;; hideden@local -> ssh hideden@gw1 -> sudo ssh root@gw2 -> ssh root@hoge.dev ; *.devに接続するにはgw2のrootでなきゃだめ (add-to-list 'tramp-default-proxies-alist '("\\.dev" nil "/ssh:root@gw2:")) ; gw2に接続するにはgw1でsudoしなきゃだめ (add-to-list 'tramp-default-proxies-alist '("gw2" nil "/sudo:gw1:")) ; gw1にはhidedenでログインするけど、その後はrootとして行動する???? (add-to-list 'tramp-default-proxies-alist '("gw1" "\\`root\\'" "/ssh:hideden@gw1:"))
想定した通りに動くんだけど、なんかOptionの解釈が間違ってるような。。ちゃんとマニュアル読めって事かな。。
あと、リモートホストがzshでtrampがうまくプロンプトを見つけられない場合は.zshrcに
case "${TERM}" in dumb | emacs) PROMPT="%n@%~%(!.#.$)" RPROMPT="" unsetopt zle ;; esac
とか書いておけばうまくいった。zshのline editorが色々邪魔してるらしく、unsetopt zleが大事らしい。
(090111追記)
trump -> trampだった。。直しました。
jTemplatesのforeachの中から元の$Tを参照する方法
jQuery+jTemplatesでごにょごにょやってて、ちょっと悩んだのでメモ。
データ
var data = { status: 'success', base_url: 'http://localhost/entry/edit/', entries: [ { id: "1", title: "title1" }, { id: "2", title: "title2" } ] };
テンプレート
{#if status == 'success'} <ul> {#foreach $T.entries as entry} <li><a href="{$T.base_url}{$T.entry.entry_id}">{$T.entry.title}</a></li> {#else} <li>no entries.</li> {#/foreach} </ul> {#else} failed to load. {#/if}
これ素直に動きそうだけど動かない。{#foreach}の外での$Tと{#foreach}の中での$Tがどうやら別物のよう。{#foreach}内では"$T.entries as entry"で指定した$T.entryしか見えないので{$T.base_url}が空白になる。
マニュアルを一通り見たけどそれらしい解説もないのでググってみると
http://www.memorycraft.jp/2008/11/jtemplatesforeach.html
という記事を発見。確かに{#param name=org value=$T}とか書いておくと{$P.org.base_url}で参照できるっぽいのだが、なんだか気持ち悪い。
jTemplate 0.7.5のソースをざっと眺めてみたところ、{#foreach}に渡すoptionとしてextDataなるものがあるのを発見。
以下のような感じで
{#if status == 'success'} <ul> {#foreach $T.entries as entry extData=$T} <li><a href="{$T.base_url}{$T.entry.entry_id}">{$T.entry.title}</a></li> {#else} <li>no entries.</li> {#/foreach} </ul> {#else} failed to load. {#/if}
"{#foreach $T.entries as entry extData=$T}" という感じでextData optionに$Tを渡しておくと、うまい具合に{$T.base_url}も取れるようになった。
・・・使い方として正しいのかどうかは不明。
extDataはjTemplates 0.7以上で実装された{#for}を内部的に{#foreach}構文に変換するopFORFactoryで使ってるだけのようで、
var extData = (this._option.extData !== undefined) ? (eval(this._option.extData)) : {};
と普段は使われてないような気がする。とりあえず特に弊害はなさそうだと勝手に判断する事にした。
iPhone3GをJailBreakするときの注意点
iPhoneはデータ取得方法にプッシュを指定しておくと、wifiエリア内でwifi接続していても3G圏内であれば常時3G経由のglobalIPを保持している。この状態だと、画面ロック状態でも3G経由のglobalIPできちんとping等に応答する。*1
で、事実上プロバイダが1社で126.240.0.0/12が割り当てられる。このIPアドレス帯域のPort22が開いてるSSHサーバーは、ほぼ確実にJailBreakしたiPhoneであると推測でき、password変更してない場合は例のpasswordで入りたい放題という事になる。
しかもこれ、wifi経由と違って画面ロック状態(バックライト消灯・画面非表示)でも接続可能なのでうっかりSSHを立ち上げたまま放置すると・・・・・。*2
wifiエリア内でしかSSHなんて使わないからpassword変更しなくても大丈夫とか思ってると痛い目にあう可能性が。
プッシュに指定してなくともフェッチ間隔でやはり勝手に接続してしまうので、当然JailBreakするくらいだからリスクも把握の上だろうが、OpenSSHを立ち上げる前に3G接続をOFFにしてせめてpassword変更すべき。出来ればsshd_configでpass認証をOFFにしとくといい。また、不要な時は極力立ち上げない。
SSHで入る事ができれば電話帳等の個人情報も抜き放題になってしまう。
それ以外にも、Cydiaで入れられるErica Utilitiesはbeta11だが、最新のbeta13だとfindmeなるコマンドが出てる。これを使うとGPSで現在の座標をコマンドラインで取得できる。また、recAudioを使えば本体マイクから周囲の音を録音できる。しかも両方共画面ロック状態でも画面上の確認無しで可能。
気軽な気持ちでJailBreakすると、気づかずに自分の交友関係をばらまきつつ、現在位置を追跡できる盗聴器を持ち歩いてるような状態になるので注意。
まぁ今のところ危険性に気づけないような人がJailBreakする利点は特に無いだろうけど。
また、JailBreakしてないユーザーでもWebDAVやFTPを利用したファイル共有ツールがAppStoreに結構上がっているが、これらが立ち上げたport8080等の簡易HTTPサーバーも3G経由でアクセス可能(画面ロック時でも)なので面倒だからと言ってpassword無しで開放してると色々怖いかも。
あんまり人に勧める気にはならないけど、iPhoneって楽しい端末だね。
repcachedのメモ。
ある程度でかいサービスでDailyCount的な事をやらなきゃいけなくなって、DBに突っ込むとログのお掃除や負荷が大変だからmemcachedでやりたいけど、落ちたらウザいしなー・・・って考えてたらrepcachedを思い出した。YAPC2008でもmixiのkazeburoさんが紹介してたみたい。まぁ寝坊して聞けなかったんだけど。。。
で、ちょっと色々やってみたメモ。まださほど検証してない。
Installとかはほとんど公式のとおりに。memcachedと共存させるためにバイナリ名かえてみた。
./configure --enable-replication --program-transform-name=s/memcached/repcached/ make make install
で、起動。console2つ開いて-vつけて情報表示させつつ。
repcached -v -m 64 -c 64 -u memcached -p 11212 -x 127.0.0.1 # repcached-A repcached -v -m 64 -c 64 -u memcached -p 11213 -x 127.0.0.1 # repcached-B
こうすることでMultiMasterになる。2つのmemcachedの保持するデータは同一になるはず。
本来、Cache::Memcached*のモジュールはserversに複数渡すと特定のhashアルゴリズムでデータを格納するサーバーを選び、重複して登録されることはない。だがrepcachedでレプリケーションするとすべてのrepcachedが同内容になるため、この周りの挙動を確認してみる
あらかじめ
$cache->set("k".$_, "v".$_, 3600) for (1..20);
でデータをセットしておき、
#!/usr/bin/perl use strict; use Cache::Memcached; my $c = Cache::Memcached->new({ servers => ['127.0.0.1:11212', '127.0.0.2:11213'], }); for (1..30){ print $c->get('k'.$_) for (1..20); print "\n"; sleep 1; }
ってな感じの簡単なスクリプトを実行しつつ、repcachedをCtrl+Cで落としたりして見てみた。
結果はこんな感じ。
Cache::Memcachedの場合 v1v2v3v4v5v6v7v8v9v10v11v12v13v14v15v16v17v18v19v20 # Ctrl+Cでrepcached-B停止 v2v3v4v5v6v7v8v9v10v11v12v13v14v15v16v17v18v19v20 # <-落ちて初回のgetのみundefに v1v2v3v4v5v6v7v8v9v10v11v12v13v14v15v16v17v18v19v20 # <-以降はrehashされてrepcached-Aを見にいくが # 複製されてるので全部取れる # repcached-B起動 v1v2v3v4v5v6v7v8v9v10v11v12v13v14v15v16v17v18v19v20 # <-復帰後しばらく(20-30sec)すると2台に戻る Cache::Memcached::Fastの場合 v1v2v3v4v5v6v7v8v9v10v11v12v13v14v15v16v17v18v19v20 # Ctrl+Cでrepcached-B停止 v2v5v7v9v10v12v15v17v19 # repcached-Aにあるものだけ取得できる v2v5v7v9v10v12v15v17v19 # repcached-B起動 v1v2v3v4v5v6v7v8v9v10v11v12v13v14v15v16v17v18v19v20 Cache::Memcached::libmemcachedの場合 v1v2v3v4v5v6v7v8v9v10v11v12v13v14v15v16v17v18v19v20 # Ctrl+Cでrepcached-B停止 # ここでScript実行停止。たまにSegmentation faultでcoredumpが・・
なんかこんな感じになった。get_multiでも当然同じ感じなので略。また、例ではrepcached-Bを落としているが、repcached-Aを落としてもMultiMasterなので当然同様の結果になる。
Cache::Memcached::FastはPODにもno_rehash optionはサポートせず、サーバーの応答が無くなってもrehashはしない実装になってるって書いてあるので当然こうなる。一部サーバーが落ちている間はおそらくkeyによって保存できない状態になると思われる。
Cache::Memcachedはサーバーが落ちると20-30秒間接続リストから除外し、no_rehashオプション無しの場合は1台としてハッシュを再計算してくれるため、全部取れる。ただし、落ちた最初の1回のみ取得に失敗してundefになる。set_cb_connect_fail等でうまくやれるかなと思ってソースを見たがよくわからなかった。接続中のsocketがいきなり死んだ場合に再取得するような用途では使えないっぽい(未検証)
Cache::Memcached::libmemcacheはなぜか突然終了した。数回CoreDumpも吐いた。ちょっと謎。
複数memcachedサーバーな構成で1台接続中にコケるとそれ以後動かなくなるのか、もしくは接続中だったプロセスのみ落ちて最初から接続できない場合は除外されるのかもしれない。ただサービス側では::Fastを使っているため、::libmemcachedについてはほぼ未検証。
そもそもmemcachedのためのモジュール群なのでrepcachedのMultiMasterな挙動が考慮されてるわけでは無いので当然なのだが、Cache::Memcachedを使えばrepcachedの片側が不意に落ちたとしてもキャッシュ内容を失わずサービスを継続するという挙動を実現できるっぽい。
落ちた後のレプリケーションは自動で行われ、ドキュメントによるとレプリケーション完了までは接続を受け付けないらしいのでdaemontools等による自動再起動でも運用上問題はあまりおこらなそう。
何より、-vでdebug情報を出力するとレプリケーション時のメッセージが
marugoto copying.
とかになってるのがポイント高かった。別に動作とはまったく関係ないけどw
incr等もサポートしてるので
$cache->set("count::20080605::hideden", 1, 3600*48); $cache->incr("count::20080605::hideden", 1); $cache->get("count::20080605::hideden");
ってな感じで過去ログが不要な『昨日のアクセス数』『本日のアクセス数』なんかを出す部分で使ってみるといい感じかも。ただし、メモリが溢れる状況だとレプリケーションしててもデータは消えていってしまうので、memcached-tools等で監視しつつ運用する等はすべきかも。
mysqlでdriver=memoryとどっちの方が(使い勝手|パフォーマンス)いいんだろうか。ま、memcachedのシンプルさは十分魅力なんでこれはこれでやってみよう。