2009-11-04
JSONPなAPIの負荷対策にngx_http_jsonp_callbackってのを書いてみた
nginx, memcached, server, perl
認証が不要で、結果を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*1やtcp_tw_reuse*2あたりをいじる必要があるかもしれない。
そして最近なかなか時間が取れなくて、まだproduction環境では導入してないので安定性とかその辺は未知数。暇を見つけてblogの拍手APIとかをこれに置き換えたいなぁ。
2009-11-01
nginx+squidで画像キャッシュサーバーの作り方
仕事で画像キャッシュサーバーを構築した時のメモ。大規模事例の設定例が検索してもあまり見つからなかったので同じような境遇の誰かの参考になれば。
規模とアクセス量とアクセスされる画像の種類が多いので、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で確認/設定。
上記設定等は例であり動作未確認です。間違ってたらごめんなさい。
2009-08-01
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」って感じじゃダメだね。
2009-07-01
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はこの場合は全く無関係。紛らわしい名前で萎える。
2009-01-08
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。
.emacsに
;; 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だった。。直しました。