Windows server上に、Windowsドメイン認証のできるSubversionサーバを構築してハマる。

主な環境
サーバ

CPU Intel(R) Xeon(R) CPU 5160 @ 3.00GHz
Memory 3.00GB
O/S Microsoft Windows Server 2003 Standard Edition Service Pack2
その他 Apache/2.2.13 (Win32), SVN/1.6.4
Compiled in modules:
 core.c
 mod_win32.c
 mpm_winnt.c
 http_core.c
 mod_so.c|

httpd.confに追加

LoadModule sspi_auth_module modules/mod_auth_sspi.so
LoadModule dav_svn_module modules/mod_dav_svn.so
LoadModule authz_svn_module modules/mod_authz_svn.so

<Location /svn>
DAV svn
SVNPath "E:/svnrepository"
AuthType SSPI
AuthName "Authentication request from Subversion."
SSPIAuth On
SSPIAuthoritative On
SSPIDomain MYDOMAIN
SSPIOfferBasic On
Require valid-user
</Location>

クライアント(代表例)

CPU Intel(R) Pentium(R) 4 CPU 2.80GHz
Memory 2.50GB
O/S Windows XP Professional Version 2002 Service Pack 2
その他 TortoiseSVN 1.6.5, Subversion コマンドラインクライアント, バージョン 1.6.4.

一応、こんな感じで基本動作はするようになった。
が、念のため、開発ピーク時の負荷を想定して、20人がかりで一斉にcheckoutをかけてみたところ、Apacheがダウン。
ここから、ハマり道が始まった。

同時接続数が多すぎて捌ききれないのかもしれない

httpd-mpm.confで、以下の値を調整。

<IfModule mpm_winnt_module>
# 150→500
   ThreadsPerChild      500
# 0→10000
   MaxRequestsPerChild    10000
</IfModule>

もう一度同じ負荷をかけたところ、Apacheが落ちなかったので、これで安心と思ったら、しばらくたつとやっぱり落ちた。

どこに負荷がかかっているのだろうか

Windows タスク マネージャでパフォーマンスタブを確認。
PF使用量が非常に高い。
プロセスタブを確認すると、httpd.exeが最大で約2GBも使用している。パフォーマンスモニターのカウンタログで、Processor/Memory/PhysicalDiskの負荷状況を記録すると、Memory/Available MBytesがどんどん落ちていく。CPU/HDDの負荷は許容範囲内。

負荷テストを毎度20人がかりで行うのはしんどい

ので、Perlスクリプトで代用してみる。

use strict;
use warnings;

my $pid;

for(my $i = 0; $i < 10; $i++){
       mkdir($i);
       if ($pid = fork()){
           print "親プロセス(" . $pid . ")です。\n";
       }
       elsif(defined $pid){
           print $i . ":子プロセス(" . $pid . ")です。\n";
           chdir($i);
           system("svn co http://foo/svn --username bar --password baz");
           last;
       }
       else{
               print "親でも子でもないです。\n";
       }
}

同一IPからのアクセスになるが、ちゃんと別々のフォルダにcheckoutされ、負荷テストの状況(メモリ空き容量がどんどん減っていく)を再現できた。処理速度的にはクライアント側のDisk I/Oが律速になっているようだが、問題は再現する。

問題の元がメモリリークならば

何度も負荷テスト→負荷テスト→負荷テスト→サーバのメモリ逼迫→何かの拍子にApacheダウンというのを繰り返してみて、感覚的に"svn coするとどんどんApacheがメモリを喰う"という線が濃厚になってきた。
Windows + Apacheの環境では、AcceptEXが問題になるケースがあるというが、今回の環境では、error.logにはその兆候は出ていない。
(参考)mpm_winnt - Apache HTTP サーバ

メモリリークに対して根本対策をする(hackする?)スキルがないため、プロセスをリセットすることで暫定対策を図る。
httpd-mpm.confで、以下の値を調整。

<IfModule mpm_winnt_module>
# 10000→5
   MaxRequestsPerChild    5
</IfModule>

何度も負荷テスト→負荷テスト→負荷テスト→…と繰り返すと、確かにプロセスがリセットされ、メモリ逼迫が解除された。
svn coの場合は、いくつファイルをダウンロードしようが、コマンド1回で、Request1回とカウントされている気がする(KeepAliveが効いてるせいかもしれない)が、コマンド5回でリセットがかかるかというとそうではないようだ。
おそらく仕掛かりのセッションが全部終わったときに、処理したRequest数が閾値を超えていればリセット、としている気がする。

長いことリセットがかからないときがある

何度も負荷テスト→負荷テスト→負荷テスト→…と繰り返すと、どうもリセットがかからないことがある。
前回の想像(仕掛かりのセッションが全部終わったときにリセット)が正しければ、何かのセッションが残ったままのはず。
httpd.confで、以下の値を有効化(#を外す)

LoadModule status_module modules/mod_status.so

ブラウザでhttp://foo/server-statusにアクセスすると、Mode of operationが"K" Keepalive (read)のままのやつがいる。
RequestはPROPFIND /CVS HTTP/1.1などとなっていて、svnサービスと関係ないリソースに対して行っている。
CVSの文字から、TortoiseCVSを疑ってみる。
共有フォルダを見たときに、TortoiseCVSが何かのバグで、WebDAVアクセスを試みているのでは?と思い、TortoiseCVS 1.2.2からTortoiseCVS 1.10.10にアップデートしてみた。
すると、PROPFIND /CVS HTTP/1.1は現れなくなった。
が、なぞのPROPFIND投げっぱなしKeep攻撃は他にもまだやってくる。

またMicrosoft

一体なんのプログラムがPROPFIND攻撃をしてくるのか確かめるべく、access.logでユーザーエージェントを記録するようにhttpd.confで、以下の値を変更(common→combind)

CustomLog "logs/access.log" combined

そうして待ち構えたところ、見事にトラップにはまってくれたのがコレ。

xxx.xxx.xxx.xxx - - [21/Oct/2009:12:21:33 +0900] "OPTIONS / HTTP/1.1" 200 - "-" "Microsoft-WebDAV-MiniRedir/5.1.2600"
xxx.xxx.xxx.xxx - - [21/Oct/2009:12:21:33 +0900] "PROPFIND /hoge HTTP/1.1" 405 346 "-" "Microsoft-WebDAV-MiniRedir/5.1.2600"

またMicrosoftか。また、Microsoftか。
server-statusとaccess.logを付き合わせたところ、これが"K" Keepalive (read)のままのやつというのはほぼ疑いない。
Microsoft-WebDAV-MiniRedirでググると、栄えある検索結果第1位がコレ。
Windows XPからファイルサーバへの接続が非常に遅い
こいつだ。間違いない。

お前の相手などしていられない

ということで、UserAgent指定でサービス拒否を行う。

# MicrosoftのWebDAVエージェントからリクエストが来た場合、サービスを拒否
SetEnvIf User-Agent "^Microsoft-WebDAV-MiniRedir/5.1.2600" deny_agent
<Location />
 Order allow,deny
 Allow from all
 Deny from env=deny_agent
</Location>

# MicrosoftのWebDAVエージェントからリクエストについて、Keep-Aliveを許可しない
BrowserMatch "^Microsoft-WebDAV-MiniRedir/5.1.2600" nokeepalive

完全にWindows XP決め打ちだが、もしVistaとか7とかから別のやつがやってきたら、そいつらもブッタKill。
ついでに自分のPCからもこのエージェントを切っておく。
[コントロールパネル]-[管理ツール]-[サービス]を開き、WebClientという名前の虫垂炎を無効にする。

perlスクリプトをドラッグ&ドロップで実行する

処理対象ファイルをバッチファイルにドラッグ&ドロップすると、そのファイルを引数にしてperlスクリプトが実行される。

例:WindowsのパフォーマンスモニタのログをCSVに変換する場合
処理対象ファイル(SystemLog20090122_000004.blg)をバッチファイル(feedme.bat)にドラッグ&ドロップすると、そのファイルを引数にしてperlスクリプト(myscript.pl)が実行され、CSVファイル(20090122.csv)が作成される。

file name: feedme.bat

rem "myscript.pl"を書き換えてご使用ください
echo off
cd /d %0\..
perl myscript.pl %1
pause

file name: myscript.pl

#パフォーマンスモニタのログファイルをCSV形式に変換する
use strict;
use warnings;

my $input       = $ARGV[0];
$input          =~ /SystemLog([\d]+)/;
my $output      = $1 . '.csv';

print '$input: ' . $input . "\n";
print '$output: ' . $output . "\n";
system("relog $input -f csv -o $output\n");

__END__

MS-AccessのテーブルをMySQL(ODBC接続)にエクスポートする

テーブルを格納したファイル(database.mdb)とフォームを格納したファイル(form.mdb)があるときに、database.mdbのデータをMySQL(ODBC接続)にエクスポートした手順を記述します。

実験環境

Clientマシン
Serverマシン
続きを読む

CatalystでAjaxに挑戦してみた

おぞくてダサくてヘボいですが、メモ。

MyApp.pmの記述

use Catalyst qw/ConfigLoader
                               Static::Simple
                               FormValidator
                               Authentication
                               Authentication::Credential::Password
                               Authentication::Store::DBIC
                               Authorization::Roles
                               Session::CGISession
                               Prototype
                               Email::Japanese/;

テンプレートの記述

[% c.prototype.define_javascript_functions %]
<input type="text" name="regist_user_ID"><img src="[% static_root %]/common/img/check.gif" alt="確認" width="36" height="21">
[% c.prototype.observe_field('regist_user_ID',
       {
               url             => '/get_account_info',
               with    => "'search_key=name&id_code='+value",
               update  => 'regist_user_name',
       }) %]
<span id="regist_user_name"></span>

コントローラの記述

sub get_account_info : Global {
       my ( $self, $c ) = @_;

       my @applicants  = MyApp::Model::CDBI::User_detail->search(
               'id_code' => $c->req->param('id_code'),
       );
       my $ret;
       foreach my $applicant (@applicants){
               $ret    = $applicant->get($c->req->param('search_key'));
               Encode::from_to($ret, "sjis", "utf8");  # 文字コード変換処理 ACCESS(Shift_JIS) -> Catalyst(UTF-8)
       }

       if($ret){
               $c->res->output($ret);
       }
       else{
               $c->res->output('該当がありません');
       }
}

Catalyst::Plugin::Email::Japaneseでハマった

Catalyst::Plugin::Email::Japaneseを利用してメール送信する際に、Modelクラスから引いてきたShift-JISの文字列が文字化けしていた。
Template-Toolkitのカスタムフィルタで、Shift-JIS to UTF-8の変換をかけようと、下記サイトを参考に設定を試みたがうまくいかなかった。

へぼへぼCTO日記 - stash による検索結果 : 4件
(ThinkIT) 第5回:テンプレートの作成 (2/2) 設定はconfigオブジェクトにより、リスト15のようにアクセスできます。
Re: (Catalyst) Using a custom TT filter
Catalyst::View::TTでのフィルター作成時のメモ
Catalyst::View::TT::ForceUTF8に設定を握りつぶされる - holidays-l開発ブログ
クイック&ダーティなCatalystチュートリアル
use Template; - 今日のCPANモジュール
Template Toolkit Manual -テンプレートツールキット和訳マニュアル-
Template::Toolkit用独自フィルタを作る - ζ*’ワ’)ζ<うっうー遅レス。
Template Toolkit 使用時 に FILTER を追加する方法 - cooldaemonの備忘録
Template::Manual::Filters

で、結局いろいろトライ&エラーしてみた結果。
結論:
Catalyst::Plugin::Email::JapaneseはどうもMyApp::View::TTを使っていないワナ。
MyApp::View::TTとは別途FILTERSの設定が必要。

       my %options = (
               FILTERS => {
                       'sjis2utf8' => \&MyApp::View::TT::sjis2utf8,
               }
       );

       # メール送信
       $c->email(
               To              => $To_field,
#               To              => $c->stash->{'send_to'},
#               Cc              => $c->stash->{'send_cc'},
               Subject => $title . '(#' . $c->req->args->[2] . ')',
               TmplOptions     => \%options,
       );

CatalystアプリをWebサーバにのっけてみる(5) Catalyst on IIS編

Catalystで作ったアプリをIISCGI動作で動かしてみたら、動作が変。htmlの出力が途中で途切れる。

HTTPヘッダを見てみる(ieHTTPheadersで確認)とどうもContent-Lengthが少ない。というか、途切れている部分までのバイト数と一致している。で、いろいろ調べまわった結果、たぶん、結論はこれ、たぶん。

IISがあほうな改行コード変換をしているせいで、CatalystがContent-Lengthを計算した後に、実態のデータ量が増えている。

ばかー、ふぁっきん、さのばびーっち!

で、しょうがないのでCatalyst側に対策しました。
site/lib/Catalyst.pmのContent-Length計算部分に、$c->response->bodyの改行文字数を足すように変更。↓これ。

   # Content-Length
   if ( $c->response->body && !$c->response->content_length ) {

       # get the length from a filehandle
       if ( blessed( $c->response->body ) && $c->response->body->can('read') )
       {
           if ( my $stat = stat $c->response->body ) {
               $c->response->content_length( $stat->size );
           }
           else {
               $c->log->warn('Serving filehandle without a content-length');
           }
       }
       else {
                       # Hack to fix fxxk'n IIS return code issue.
                       my $n;
                       my $fh = $c->response->body;
                       $n = $fh =~ s/\n/\n/g;
                       $c->response->content_length( bytes::length( $c->response->body ) + $n );
       }
   }

参考になりました:
2.9 小さな親切大きなお世話?! - CGIの要点整理

CGIがデータを送信するときは,すでに,改行コードが [CRLF] になっているデータを送信すると,"CRCRLF"に変換して送信してしまいます.これにより, CGIが Content-Length で指定したサイズよりデータサイズが大きくなってしまいます.

CGIでHTTPヘッダー「Content-Length」を出力するには?

Windows 環境で、出力する HTML が50行程度ならば、改行コードでしょう。
試してないけど length($data) + $data=~s/\n/\n/g とするか、binmode(STDOUT) かな。

CatalystアプリをWebサーバにのっけてみる(4) Catalyst on IIS編

Catalystで作ったアプリをIIS上でCGI動作で動かしてみたら、動作が変。
例えば、sub move : Path('move')というアクションを作ったときに、http://domain/MyApp_cgi.pl/move にアクセスすると、なぜかdefaultに飛ばされる。

IIS上での動作とApacheサーバ上での動作を比較したところ、IIS環境変数PATH_INFOをスクリプトパスを含んだ形で、CGIスクリプトに渡してくるため、期待する動作になっていないようだ。

  • IIS上で動作した場合:$ENV{'PATH_INFO'} = MyApp_cgi.pl/move
  • Apache上で動作した場合:$ENV{'PATH_INFO'} = move

このIISのPATH_INFOの問題は、結構いろいろなところ*1で地雷になってるらしく、スクリプト側で対処している方が多いので、それに習ってCatalyst::Request.pmで、以下の行(# <- modifiedのある行)を変更。

=head2 $req->path

Returns the path, i.e. the part of the URI after $req->base, for the current request.

=head2 $req->path_info

Alias for path, added for compability with L<CGI>.

=cut

sub path {
   my ( $self, $params ) = @_;

   if ($params) {
       $self->uri->path($params);
   }
   else {
       return $self->{path} if $self->{path};
   }

   my $path     = $self->uri->path;
   my $location = $self->base->path;
   $path =~ s/^(\Q$location\E)?//;

   # hack to fix broken path info in IIS               # <- modified
       if ($path =~ /$ENV{'SCRIPT_NAME'}/){            # <- modified
               $path =~ s/$ENV{'SCRIPT_NAME'}\///;     # <- modified
       }                                               # <- modified
       else{                                           # <- modified
               $path =~ s/^\///;                       # <- modified
       }                                               # <- modified
   $self->{path} = $path;

   return $path;
}

 
 
BBS-サポート掲示板/686 - FreeStyleWiki
6.1.6. PATH_INFO (経路情報) (CGI/1.1 draft 03)
メモ欄に「IIS は正しい PATH_INFO をくれないことがあるそうです。」
blosxomサイトの日本語訳::FAQ - IISのPATH_INFOの問題にも関わらずBlsoxomを動作させるにはどうすれば良いのですか?
CGI アプリケーションの PATH_INFO および PATH_TRANSLATED を使用する
Micro$oftの見解「これは、セキュリティを目的とした仕様」「CGI 仕様に明記されたとおりに PATH_INFO および PATH_TRANSLATED を使用する必要がある場合は、〜〜〜」
MS独自仕様ですか、そうですか。繰り返すこれはバグではない、繰り返すこれはバグではない。

*1:いろいろなところの詳細