2015年4月13日月曜日
2014年5月22日木曜日
Amazon RDS for SQLServerはミラーリングだった
ディスクのスナップショットでしょー。データ欠落することもあるんだろーなー。なんて適当なこと言ってマジすいません。
インスタンス立てて確認しました。マジ、ミラーリングでした。調子乗ってすいませんでした。データベース作ってしばらくすると、見慣れたあの表示。
作りたては、まだミラーリング準備中なんだろね。確認してないけど。
テーブル作ってテストデータ100万レコード作成中にいきなり!
この見慣れた「プリンシパル、同期済み」が出てきた。
あー。ちゃんとミラーリングやー。相方のサーバー名も違うやつだし、ウィットネスもいた。
なのに接続先は固定でfailover partner指定とかないんだぜ!そこはエンドポイントが受け付けて、そこから先にうまいこと繋げてるんだろね。
ここから先はビールでマッチョなお方、おねげーします。
2014年5月21日水曜日
URLRewriteでほとんどすべてCDNに向ける
HTMLの書き換えルールをURLRewriteでセットすれば、環境設定だけでCDN向けたりできるよー、っていうのを書いたのはいつの話だろう...。
URLRewrite+CloudFrontでパフォーマンスを取り戻す http://takepara.blogspot.jp/2011/12/urlrewritecloudfront.html
いまや、猫も杓子もCDN使うじゃないですか。
CloudFront/KeyCDNもHTTP Methodうまいこと処理できるようになってるから、オリジンにガッツリかぶせて(サイトの公開URLをCDNに向けてすべてのリクエストをCF経由オリジン行き)しまうっていうアプローチもありですし。
とはいえ、トラフィックあれだしー、証明書もなんだしー、お金かかるっしーなっしー、だとやっぱりURLRewriteですよね。
でもね、前のエントリの方法だと、SRC属性がちゃんと"/"で始まってることを条件にしてたんです。そうは問屋が卸さない書き方をすることもあるですよね。ルール厳しいと比較的低コストで最適化できるからいいのにー、という理屈は通りませんよ。
なので、こうなったらIMG/SCRIPT/LINKガッツリ全部書き換えてやる!でも、絶対参照やスキーム相対でホスト名込のものはそのまま残す。
前回の方法はSRC属性(ターゲットPATH)のみのパターンマッチでやってたけど、今回はリクエストURLも参照することでより広範囲に適用。もちろんパターンマッチはするんだけど、それはホスト名指定を救いたいから、という意味で、特定パスのマッチとはちょっとアプローチが違います。
考えられる組み合わせとして、リクエストURLが"/"で終わってるか、終わってないか。ターゲットPATHが"/"で始まってるか、始まってないか。の、組み合わせで4パターン。だけどターゲットPATHが"/"で始まってる絶対パス指定はリクエストURLにかかわらず、絶対パスなので実質3パターン。
- リクエストURLが"/"で終わってない
http://localhost/rewrite - リクエストURLが"/"で終わってる
http://localhost/rewrite/
- ターゲットPATHが"/"で始まってない
<img src="images/logo.gif" />
<img src="./images/logo.gif" />
<img src="../rewrite/images/logo.gif" /> - ターゲットPATHが"/"で始まってる
<img src="/rewrite/images/logo.gif" /> - 例外的にhttp/httpsで始まってるものと、"//"のスキーム相対で始まってるもの
<script type="text/javascript" src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="//code.jquery.com/jquery-1.10.1.min.js"></script>
なので、3パターンの組み合わせに対して、Rewriteするルールを作成すれば、ターゲットPATHそのものに対するパターンマッチを行う必要がなくなる(特定のパスを指してるものだけをCDNに向けるようなRewriteをしない)ので、コーダーの書き方に依存したRewriteにならないはず。
これらのコンテンツがRewriteで正しく表示されるように考えたルールが↓これ(CDNのホスト名はlocalhost扱い)。
<?xml version="1.0"?>
<configuration>
<system.webServer>
<rewrite>
<outboundRules>
<rule name="HttpCdnPathAbsolute" preCondition="html" enabled="true">
<match filterByTags="Img, Link, Script" pattern="^/[^/]+(.*)$" />
<action type="Rewrite" value="//localhost{R:0}" />
<conditions>
<add input="{HTTPS}" pattern="off" />
</conditions>
</rule>
<rule name="HttpCdnPathRelativeSlashEnd" preCondition="html" enabled="true">
<match filterByTags="Img, Link, Script" pattern="^((?!/.*)(?!//.*)(?!http.*)(?!https.*))(.*)$" />
<action type="Rewrite" value="//localhost{PATH_INFO}{R:0}" />
<conditions>
<add input="{HTTPS}" pattern="off" />
<add input="{REQUEST_URI}" pattern="/$" />
</conditions>
</rule>
<rule name="HttpCdnPathRelativeUnslashEnd" preCondition="html" enabled="true">
<match filterByTags="Img, Link, Script" pattern="^((?!/.*)(?!//.*)(?!http.*)(?!https.*))(.*)$" />
<action type="Rewrite" value="//localhost{PATH_INFO}/../{R:0}" />
<conditions>
<add input="{HTTPS}" pattern="off" />
<add input="{REQUEST_URI}" pattern="[^/]$" />
</conditions>
</rule>
<preConditions>
<preCondition name="html">
<add input="{RESPONSE_CONTENT_TYPE}" pattern="text/html" />
<add input="{RESPONSE_STATUS}" pattern="^[45].*$" negate="true" />
</preCondition>
</preConditions>
</outboundRules>
</rewrite>
</system.webServer>
</configuration>
↑これはHTTPの時だけCDNにっていう設定なのでconditionsにHTTPSが”off”って入ってる。HTTPSも同じホストに書き換えていいなら、そこ削除で。HTTPSの時は違うホスト名にしたい場合、同じ組み合わせをもういっちょHTTPS用に登録してね。
何もしなかった時のレンダリング結果
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rewrite Test</title>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="//code.jquery.com/jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="./jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="../rewrite/jquery-1.10.1.min.js"></script>
</head>
<body>
<img src="/rewrite/images/logo.gif" /><br />
<img src="images/logo.gif" /><br />
<img src="./images/logo.gif" /><br />
<img src="../rewrite/images/logo.gif" /><br />
</body>
</html>
フォルダ構成としては
/rewrite
/rewrite/index.cshtml
/rewrite/images
/rewrite/images/logo.gif
これをイメージ。
リクエストURLが"/"で終わってない時のレンダリング結果
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rewrite Test</title>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="//code.jquery.com/jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="//localhost/rewrite/index.cshtml/../jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="//localhost/rewrite/index.cshtml/.././jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="//localhost/rewrite/index.cshtml/../../rewrite/jquery-1.10.1.min.js"></script>
</head>
<body>
<img src="//localhost/rewrite/images/logo.gif" /><br />
<img src="//localhost/rewrite/index.cshtml/../images/logo.gif" /><br />
<img src="//localhost/rewrite/index.cshtml/.././images/logo.gif" /><br />
<img src="//localhost/rewrite/index.cshtml/../../rewrite/images/logo.gif" /><br />
</body>
</html>
リクエストURLが"/"で終わった時のレンダリング結果
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rewrite Test</title>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="//code.jquery.com/jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="//localhost/rewrite/jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="//localhost/rewrite/./jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="//localhost/rewrite/../rewrite/jquery-1.10.1.min.js"></script>
</head>
<body>
<img src="//localhost/rewrite/images/logo.gif" /><br />
<img src="//localhost/rewrite/images/logo.gif" /><br />
<img src="//localhost/rewrite/./images/logo.gif" /><br />
<img src="//localhost/rewrite/../rewrite/images/logo.gif" /><br />
</body>
</html>
リクエストURLが"http://localhost/rewrite/index.cshtml/"となってる時のレンダリング結果
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rewrite Test</title>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="//code.jquery.com/jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="//localhost/rewrite/index.cshtml/jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="//localhost/rewrite/index.cshtml/./jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="//localhost/rewrite/index.cshtml/../rewrite/jquery-1.10.1.min.js"></script>
</head>
<body>
<img src="//localhost/rewrite/images/logo.gif" /><br />
<img src="//localhost/rewrite/index.cshtml/images/logo.gif" /><br />
<img src="//localhost/rewrite/index.cshtml/./images/logo.gif" /><br />
<img src="//localhost/rewrite/index.cshtml/../rewrite/images/logo.gif" /><br />
</body>
</html>
明らかに参照パスがおかしい(階層が一個浅い)んだけど、この場合も救いたいとなるとかなりヘビー。なんでかというと、階層構造を無視してる指定になってるから。 現状ではこういうリクエスト時に末尾の"/"を削除したURL(canonical url)にリダイレクトするようにすればうまくいくぜ!たぶん。きっと。
2014年5月14日水曜日
KRuntimeとかMVCとかちょっと触ってみた
もうみんな、ASP.NET vNEXTっていうか、.NETでの開発方法の変更が気になって仕方ないすね。
ここまで変えてくるのかよ!っていうくらい感動的。.NET初めて出てきた時くらいのワクワクですねー。
とりあえず、KVMつかってみよー。
https://github.com/aspnet/home
に書かれてること素直にやるだけす。
cloneしてkvmsetup。
PowerShellでもCmdでも。それぞれ別々にね。
kvm install なんちゃらかんちゃらで .kreにランタイムをごっそり。MyGet。
で、Samples/ConsoleAppでkvm restore。これで、一個上のpackagesに色々ごっそり。MyGet。
あとはもうk run。
すげー!Hello World。binがないー!!し、Tempにcscなら吐き出すはずのものがないー!
あ、SET KRE_TRACE=1ね。
klr.exeさんよー!中でRoslynだねー。
KRuntime structure
https://github.com/aspnet/Home/wiki/KRuntime-structure
次。HelloWeb。k runじゃなくてk web。
StaticFilesモジュール。いや、StaticFileMiddleware。これもpackagesにMicrosoft.AspNet.StaticFiles.dllとしてMyGet。
Task Invoke(HttpContext context)なんだけど、そうでしたっけ?
Task Invoke(IDictionary
次。HelloMvc。
同じですね。k webですね。
うほ。うごきよる。しかもRazorリポジトリにごっそり入ってるから、移行中なんだね。すげーなー。
Microsoft.AspNet.Mvc.Razorのソースみたら、ICompilationServiceの実装がRoslynCompirationService。BuildManager使わないんじゃん!kprojって拡張子気になるけどおいとこ。やべー。興奮してきた。Razorはまだcshtmlしか実装してないみたい。vbhtmlのコードがなかった。
Microsoft.AspNet.Mvcがどこまで移行してるのか、試しに、HomeControllerとIndex.cshtmlをいじってみたよ。
Controllers/HomeController.cs
Views/Home/Index.cshtml
ModelBindするかなー。って程度の確認ですけどね。
するがな!
すごいがな!
ブルブルしてきた。超ブルブルしてきたSystem.Web完全に分離できてるー。かっけー。そんなの当分先になると思ってたのにー。MVC6でも参照なくすんかいー。
ちょっと、こう、やべし。超楽しい。どこかのLINQの人じゃないけど、超楽しい。
まだIISで動かしてない(KLR上で動かすだけだからビルドじゃなくて実行しただけ)から、Heliosだとどんなbinになるのか気になるしー。App_CodeとかASP.NETでのシステムフォルダの扱い(app_globalresourcesとかさー)がどこまで行けるようになるのかなー。たまらん!
ん?
the assemblies never exist on the disk.
んん!?
・No-compile developer experience
って書いてるね...。まじか。ビビった。
2014年5月9日金曜日
スレッドスロットルなんか普通しなくてもいいのにね
個別アプリケーションごとにVMがあったりとかー、そういうんだと、そんな時は maxConcurrentThreadsPerCPU とかでー、でしょうか。あ、maxWorkerThreads とかでも。
なんでそんなことが必要なんですか。そもそもCPU Throttlingでいいじゃないですかー。と、いうのもあるんですけど、そもそもマシンパワーのあまりない環境とかだと、返事が返ってこないです。解決方法はいろいろあるなかで、強制的にリクエストをキューから取り出さなきゃいいじゃないと思った結果がSetMaxThreads。
private int ThreadPoolThrottle
{
get
{
return int.Parse(ConfigurationManager.AppSettings["ThreadPoolThrottle"] ?? "0");
}
}
{
// 重いテンプレートの場合これ。
int workers;
int completions;
System.Threading.ThreadPool.GetMaxThreads(out workers, out completions);
System.Threading.ThreadPool.SetMaxThreads(ThreadPoolThrottle, completions);
}
2014年5月1日木曜日
CSVを今っぽく
一般的には","カンマで区切るんだと思います。
でも、ダブルクォート使いたいしー、っていうんで"TAB"タブ区切りにしちゃったり。
さらに、用途によっては改行入らないから、ダブルクォート内での改行処理とかなくてもいいよー、っていうのがよくあるパターンかなー、と思ったりもします。
簡単に整理すると
- カラム区切りはタブ
- カラム内に改行はない
- レコードの区切りは改行
- 1行目はカラム名を指定するヘッダ
- 2行目以降がレコード
このくらいの緩いルールで実装するっていうのを前提にしてみようと思います。
ところで、CSVって1行目からデータ派?それとも1行目はヘッダー派?ヘッダーが入ったらCSVじゃないよー、とかっていう原理主義もあろうかと思うけど、そこは気にせずにぜひ。1カラム目がHならヘッダーレコード、Dならデータレコード、とかっていう構造をもつものはそもそもそれに適したJSONやXMLがいいので、その話も今回はないってことにしてください。
前置きなげー!
本題は、CSVを可変長カラムにするほうがみんな幸せなんじゃない?っていうのとModel Validationで検証するほうが何かと楽ちんじゃない?です。
可変長カラムっていうのはどういうことかっつーと、そもそもCSVで取り込むデータを何らかのPOCOに入れて保持するっていうのを前提に、そのあとデータストアに反映するとか、そのあとの処理に流す、っていうふうに考えると、POCOならコンストラクタで初期値セットするなり、何もしなかったら型の初期値が入ってるから、そのままの値を次の処理に渡せばよろしい、っていう使い方でよければ、そもそもCSV内にそのカラムなくてもいいよね!
で、Model Validationって言ってるのは、POCOにデータいったん入れるんなら、そのタイミングでDataAnnotations使って、モデル検証しちゃえばいいよね!
ビジネスルールを適用して、さらなる検証を行いたい(特定のIDがデータストアに存在するのかとか、外部サービスに依存してるものはModel Validationで行わない)場合は、さらに上位のレイヤで処理すれば、いろいろ疎結合だし、テストも楽ちんでしょーがー。使いまわせるしね。
そんなのそうしてますから!っていういまどきの方々は面白くない話なんですけど、身近なところでは、そーなんだー、という話にもなったので。
なんで、CSVを固定カラムじゃないほうがいいと思ったのか、っていうのは(一般的じゃない理由かもしれないけど)データストアの拡張とか、処理の拡張に対して、CSVフォーマットが紐づいててほしくない、影響範囲に含めたくないから。っていうのと、マルチテナント展開するシステムだと、外部で生成されるCSVを、自システムに取り込む、っていうフローがほとんどだけど、機能拡張のたびに外部での生成方法変えて、とは言いずらい。し、言ってたらペースが外部に依存して、すごく遅くなるから。
CSV作る人(そこをシステム化してるかどうかはあまり重要じゃないですね)と、そのCSVを自システムに処理させる、っていう業務を、いろんな意味で疎結合。の、イメージす。
んー、うまく言えてないすね。さーせん。ようするにフォーマットが疎じゃないと不便だったから、です。んじゃ、CSVにするなよ!みたいなね。でも、Excelでデータつくるじゃん!
ここまでで、もうイメージできた人も多いと思うけど、せっかくなので、コードでどう表現するのかを書いてみます。
これってコードにすると、
var columns = line.Split(splitChar);
とか、ですね。
1行目ならそこにはカラム名(これの一覧はPOCOにセットするために保持しとく)が入ってて、1行目以降ならそれはデータ。っていうのを、上位層で判定。
これで、レコードをカラムに分割できました。
今度はカラム名の一覧と、モデルクラス(ジェネリックでしょうね)のプロパティをマッピング。
var properties = TypeDescriptor.GetProperties(type);
これで一応POCOのプロパティ一覧(PropertyDescriptorCollection)は取得できましょう。キャッシュするかどうかはお好きに。
あとは、もう単純にくるくるループ回していけば、いいですよね!PropertyDescriptorがあれば、Converter.ConvertFromStringでプロパティにセットできるか判定もね。
後は、DataAnnotationsを指定してるかもしれないから、Validator.TryValidateObjectで、モデル検証。
public static IList
{
var lineNumber = 0;
var mappings = new ColumnMapping[] { };
var result = new List
foreach (var line in LoadLine(text))
{
var rowErrors = new List
var columns = ParserHelper.SplitLine(line, SplitChar);
lineNumber++;
if (lineNumber == 1)
{
// 1行目のヘッダからマッピングカラム判定
mappings = GenerateColmunMappings
continue;
}
var model = new T() { IsValid = false, LineNumber = lineNumber - 1 };
result.Add(model);
// マッピングと数が一致してない行はエラー
if (columns.Length != mappings.Length)
{
rowErrors.Add("ヘッダのカラム数と一致しません");
continue;
}
// カラム値をマッピング
foreach (var map in mappings)
{
var value = columns[map.Index];
// 無効カラムはスキップ
if (!map.IsValid)
continue;
// 値の正規化
value = ParserHelper.NormalizeString(map.PropertyDescriptor, value);
// 値の型変換可能かチェック
if (!ParserHelper.IsValidValue(map.PropertyDescriptor, value))
{
rowErrors.Add("カラムの型が適切ではありません(" + map.HeaderName + ")");
}
else
{
map.PropertyDescriptor.SetValue(model, ParserHelper.ConvertFromString(map.PropertyDescriptor, value));
}
}
// DataAnnotationでモデル検証
var validationResults = new List
ModelValidator.Validation(model, validationResults);
rowErrors.AddRange(validationResults.Select(vr => vr.ErrorMessage));
model.IsValid = !validationResults.Any();
if (!model.IsValid)
{
model.ErrorMessage = string.Join("\n", validationResults.Select(vr => vr.ErrorMessage));
}
// なんかやりたかったらどうぞ
if (action != null)
{
action(model);
}
}
return result;
}
で、これのいいところはヘッダにカラム名してして、その名前をキーにプロパティにセットするから、CSVのフォーマットとして並び順が全く重要じゃなくなる。悪いところ?まぁ、いいや。いいように考えましょう。
あと、今回のコードではPOCO中心でCSVファイルのカラム有無を判定するような処理いれてないす。POCOの値だけを頼りにカラムの更新を判断して処理する場合、たとえばデータストアに反映するなんてときに特定カラムのみ更新する、とか、値をNullにするのか無指定なのか判断できないとちょっと不便かもー?
takepara/ModernCsv
あと、他にも変な機能もあるけど、興味があるなら...。
2014年4月28日月曜日
Amazon VPCをYAMAHA RTX-1200で落ち着かせる
Amazon Virtual Private Cloud (Amazon VPC) 設定例
グローバルではどうか知らないですけど、国内ならYAMAHAルーター使ってるひと、結構いると思うんですよね。安いですし。
んで、RTX1200使って接続させるんです。設定自体は簡単だし。
が、これが、なかなか曲者で。
気づきはVPCからオンプレのDB接続に失敗して、処理が正常終了しないことが、たまにある、です。
そもそも、ちゃんと検証して、どういうことが起きるのか確認しときなさい、っていうのはもうそりゃそうですよね。
ちなみにDB接続をリトライする処理を入れる、じゃ問題は解決しないんすよね。
結論から言うと「VPNアップダウン繰り返すなら、SAキープアライブにIPSec DPD使わずにICMP Echo使いましょう」です。
IPsec DPD
18.19 IKE キープアライブ機能の設定
いずれもIKEの生存確認のための機能だけど、AWSやらYAMAHAに問い合わせしたり、ログから判断した結果、DPDだとレスポンスが返ってこないことが、結構あってDOWN/UPを繰り返す。そのタイミングでアプリケーションからみたConnection Poolは、切れてないと思ってるから接続を使いまわそうとする。けど、実質再接続されちゃってるもんだから、データは到達しない。
と、いう状況なので、アプリケーションから対処しようとするなら、接続がロストしたと判定した場合はClearAllPoolでプールを全部クリアしたのちの再接続、という処理にしましょう。この辺TDS ProxyのSQL Databaseでの挙動とは違う(そこから先が切り替わったから再接続、って意味では同じだけど、直接の接続先が変わるわけではない)ので対応方法も違うことになりましょう。
SqlConnection.ClearAllPools Method (System.Data.SqlClient)
と、いっても、既存システムをそのままクラウドに持っていきたいのにソースいじるの?っていうのはちょっと納得できないすよね。もちろん、そういう風に最初から作っておけば、いいんですけど、それはまぁ、ねー。
どういう挙動をしていたか、ルーターのsyslogを監視してたら、一日のうち何度もDOWN/UPを繰り返してるんです。なんで、繰り返すんだろー、と思って、syslog debug onで様子見。完全に不定期。規則性が見つからない状況で繰り返すんですけど、どうもキープアライブのところで、返事が返ってきてないことがちょくちょくあってシーケンスエラーが出てる。2本あるトンネルのうち、片方だけ。
標準のコンフィグ設定で指定されてるトンネルを入れ替えて様子みてると、入れ替え前と同じトンネルにつながってるほうだけ相変わらず変な挙動。
ふむ。いろいろ試してみたんすよね。コンフィグ変えたり(mssとかさー)。でも、少し落ち着いたかな、と思いきやすぐまたDOWN/UP。なんど解決したと夢見たことか。
それもあれですよね、SAの有効期限がISAKMPとIPSec別々に指定してて方や初期値で8時間のままだったりして、なんか、うまく動いてるように見えただけ、的な?こちらの設定都合じゃないところでDPDの返事がないから、関係ないのにね。
どうにもこうにもキープアライブ怪しい、って思ってたところAWSのサポートから、ICMP Echoにしてみてください、って返事が来た。理由を教えてほしい、ルーター側の問題なのかAWSの実装なのか知りたいから、っていうと「わかりません」ですって。そういうこともありますよね。
YAMAHAにも同じ問い合わせしたんだけど、返ってきた答えは「わかりません」でした。
これね、しょうがないかな、とは思うんですよね。組み合わせすべてテストするなんて、現実的じゃないしね。
接続の向きはいいとして(VPN通さないようにして、接続テストプログラム動かしても再現しない、っていう検証後の話です)、IPSecでのセッション管理について詳しくないですけど、鍵とセッションは紐づいてるんでしょーね。それなら、IPSec再接続後には、その上位も再接続しなさい、の理屈が通るもんね。
2014年1月31日金曜日
サーバーの監視を楽しむ
今は黒背景にしてるけど、気分で白にしたり、クリスマスバージョンにしたり。これをオフィスに大きなテレビに出してるんですよね。
で、もとになるデータをどうやって取得するのか、っていうのがいろいろなんですよ。
DBから業務データを集計したものだったり、単純にサーバーのCPUとかメモリとかだったり、サイトごとのリクエスト数、コネクション数とか。データセンターのスイッチのトラフィックとかもね。
エージェントベースのものでいけるのもあるし、それだと面倒だからWMIでとったり、SQLだったりさー。NewRelicも最近使いつつ(Server Monitor)。でも.NET Agentでのアプリケーションプロファイルは極悪なほどパフォーマンスが悪くなるので、テストの時だけね。あれ、常時運用で仕掛けてるひといるのかな。お金がたんまりあってはサーバーたくさんでハニーポット的に一部のサーバーだけロードバランさの割り当て率下げて、とかならやれなくはないかもだけどー。マルチテナントきりきりだとちょっとなー、と思うんす。そーでもないの?
で、いろんなところから取得したデータをMongoDBに入れて、画面に出すときには、MongoDBからとってきて表示。MVCアプリでjqPlotでレンダリング。
取得と保持と表示を分けてるのは、まぁ、ね。いろいろとりたいし、いろいろ出したいのと、取得したものをそのままの精度で保持しときたいから。MongoDBはしょぼいサーバーで動かしてるけど、がんばってくれるんす。
ちなみにこれとは別のMongoDBクラスタでトレース情報はとってるけど、そっちはアプリケーションログなんで、また別のモニターに出たりしてて。そっちはそっちで重要だけど、なんか、あんまり楽しくない。
ひょっとして、監視っていうのが需要の少ないものなのかもしれないけど、どうなんでしょうね。
まぁ、いいか。
WMIで対象データを抽出する場合は、WMIで取得したい項目もクラス名調べたりいろいろ面倒だけど、抽出自体はとても簡単で、コンソールからwmicっていうの取得するか、コードでSystem.Managementのクラス使ってちょこちょこ取得。
これのいい点は対象サーバーのCPUをほとんど使わないところ。必要なものだけしかとらないしね。辛い点は外部からの監視だから対象サーバーに到達しないとか、忙しくて無視されるとちゃんととれないところ。でも、そのときは前後の状態から人が目で見て判断できることがほとんどだから、そんなに気にしない。気になる人には向いてないですね。
CPU利用率を取得するならwmicの場合、コンソールに
2013年12月30日月曜日
棟梁と頭領
Amazon.co.jp: 木に学べ―法隆寺・薬師寺の美 (小学館文庫): 西岡 常一: 本
すごいですよ。500年後の1尺を考慮する宮大工の話。
前半は法隆寺と薬師寺の話でぽかんとするんだけど、後半から宮大工とは、っていう話から自身の話(見習いとか弟子入りの話とか、継承の話とか)になってすごい面白かったんですよ。
学ぼうという心がないと、ただ仕事をするだけになってしまうんです。どんなに修行しても、みんなが宮大工になるわけじゃない。ほとんどの人が宮大工に達しないんだって。
みんな技術者ですのや。技能者がおりませんのや。仕事をする人がおらん。これ、なかなかすごいところをついてると思うんですよね。宮大工の話ですよ。でも、神仏について理解できない、木について考えない、そういうことの積み重ねで技術はわかっても、宮大工の心はついてこない。設計の基本の考えがわからない、っていうところに行きつくんだって。
本質を知らずに、形だけを追い求めるのを文化だと、勘違いしている人が仰山おりますからな。なんていうか、目に見えるものだけが文化を形作るわけじゃないじゃない、ていうさ。自分の仕事に置き換えるのなんてもうおこがましくて恥ずかしいですけど、サイエンスも大事だけど、実現することがなんなのか(人に関係ないシステムなんて作らないのに、人を無視して技術がどやこやなんてね)をちゃんと見極め、何を見て何をするのか、何に価値があって、何が人に影響を及ぼして、どうすることが単なる搾取システムじゃないのか。先は長いぜ!
でも、あれだって、それもこれも「心がけの問題」だと。
この本の中で棟梁が頭領を束ねて、それぞれのプロフェッショナルに仕事は任せてる、ていうくだりがあるんですね。すごいなー、って。棟梁ですよ。西岡さんが指す棟梁はもう薬師寺でなくなるとまで言ってます。すごい話です。すごい世界です。
で、先日のMTUの話ですけどー。
GRE および IP セキュリティでの IP フラグメンテーション、MTU、MSS、および PMTUD の問題の解決 - Cisco Systems
さすがCISCOさんです。超詳しく書いてます。
フラグメントダメってなってたり、ファイアウォールでICMP全拒否してると最適値に近づけないから、ダメになっちゃう。
pingでMTUサイズを調査する - @IT
この辺でも。
単にIPSecだけが問題になるわけじゃなく、ファイアウォールとの絡みもあってってことになるわけですねー。
しかも、フラグメント処理できたとしてもルーターがてんやわんやになるので、やっぱり調整したほうがいいね、っていうことになりますね。
いやはや。
2013年12月29日日曜日
組み合わせると安くなる CDNの例
Amazon CloudFront(コンテンツ・ストリーミング配信ネットワーク) | アマゾン ウェブ サービス (AWS 日本語)
CDNの老舗 Akamai には憧れます。F5 Big-IPくらい憧れます。
ほかに気軽に始めることのできるCDNってどんなものがあるんでしょうね。
Compare CDN | CDN Comparison, CDN Performance, CDN Providers
いっぱいあるなー。
パフォーマンスももちろんだけど、可用性が気になるところです。さらに言えば、先日の仕入れの話に戻って、お値段なんていうのももちろん気になりますよね!
CDNの機能として、ぜひとも提供しておいて欲しいものってどんなものでしょう。
- オリジン指定での透過アクセス(PUSHしない)
- HTTP GET以外のリクエストも転送
- ワイルドカード証明書でのHTTPS
- カスタムドメイン証明書でのHTTPS
- ログの取得
とかかなー。
あとは、そうねリアルタイムレポートとか、自動フェールオーバー(エッジ間とかで)とか、Cookie対応、CORS、Dynamic IP Restrictみたいな動的にDoS判定して拒否(これはぜひとも欲しいけど、こんなことすると商売あがったりだぜ、という気もしなくもなく)とか?
なんにせよ日本にエッジがないことにはちょっと辛いですよね。
そんなあなたにおすすめなのが↓ここ。
KeyCDN - Content Delivery Network
なんか、聞いたことないよー、ってなるでしょ。スイスで頑張ってるベンチャーだってさ。
不思議なことに日本にもエッジ(POP:Point Of Presence)があってね。
とにかく安い。
しかもプリペイ。事前にチャージしとく。チャージが尽きたら止まるのかよ!って、そーなんだけど、すぐには止まりません。と、言われました。
なんにせよですよ、とにかく安いんですよ。
KeyCDN - Content Delivery Network
トラフィック$0.04/GB、ストレージ$0.9/GB。ストレージはちょっと謎なんだけど、ログの保持料と、たぶんキャッシュ。ただ、何にどのくらい使ってるのかはわからない。けど、まぁ、変な感じになってはないです。
月額10TBで$400、CloudFrontだとトラフィックだけで、その倍額なうえにリクエスト数にも課金なので、いくらお安いとはいえ、KeyCDNと比べると高いなー、って思える。ドメインいくつあろうとエイリアスで別名どんだけつけようと値段はトラフィック(とストレージ)のみっていうわかりやすい価格設定。
ただね、このKeyCDN。ちょいちょい落ちる。結構がっつり。最近はそんなことないけど、これがびっくりするくらい最初は落ちまくってた。
いくら安くてもこれじゃー、ちょっと。って思ったんだけど、そこでふと思ったですよ。
Amazon Route 53でフェールオーバーさせちゃえば、そこそこ乗り切れるんじゃね?って。
ワイルドカード証明書(カスタムは高いのよー)でのHTTPSはあきらめなきゃいけないけど、HTTPでのアクセスが圧倒的に多い場合、それだけでもKeyCDNに飛ばして、HTTPSはCloudFront。KeyCDNがおちたら、CloudFrontにフェールオーバー。そういう構成にすればいいじゃんねー。
Amazon Route 53(SLA100%のDNSサービス) | アマゾン ウェブ サービス(AWS 日本語)
別名つけないといけないから、それようにドメイン(サブドメインでも)用意しておいて、KeyCDN/CloudFrontのDNS名をそれぞれCNAMEで登録。
でもって、Route 53のヘルスチェック機能を使いたいんだけど、CDNがIP直さしなわけないので、別途固定IPをふれるサーバーから、Reverse Proxyさせて、KeyCDNへのアクセスを固定IPで見えるように(ヘルスチェック専用ね)。
CloudFrontは一応SLAもあって99.9%で頑張るって言ってるんで、そこはもうそれ。それで!
万が一でもCloudFrontがえらいことになったら(CDN参照はUrlRewriteのoutboundRulesに書いてHTMLそのものの書き換えで参照切り替えるようにしてるから)人力でWeb.Config書き換えてCDNをOffにする。
トラフィックさばけるのか怖いけど、サービス停止のほうがもっとこわいー。まぁ、2つの会社のサービスが同時に落ちることはそんな確率も高くないだろうから、なんとかなんじゃん。どうせSLA100%なんておかしな契約をすることなんてありえないんだしね。こういうところでもバランス大事だと思います。
やってることは単純だけど、文字にするとわかりにく。
つまり、組み合わせていいとこどりすれば、CDNの費用を5分の1とか10分の1に落とせるよ!CDNのサービスだって落ちるのを前提にして組み合わせることで、身の丈に合った費用対効果を出せて、うへへな気持ちで年を越せます。
これでなんとか5エントリー...。
来年はもう少し書きたいです...。
書かないと忘れちゃうから。
2013年12月27日金曜日
ハイブリッドクラウドとMTU
ハイブリッドクラウドですよね。IaaSとしてのハイブリッドだけじゃなく、SaaS利用でのハイブリッドももちろん最大限活用していくのが重要だと思いますが、今回はIaaSで。
ちょっと図にするのは苦手なので拝借したものを貼り付けますー。
Amazon Web Services Blog: Introducing Amazon Virtual Private Cloud (VPC)
こんな感じのイメージですよね。
データセンターからパブリッククラウド(とりあえずAWSでどうぞ)へVPN(IPSec)を利用して経路確保。
で、プライベートアドレスを使った、データセンター拡張の出来上がり!
もともとデータセンター使ってて、がっつり移行する場合は、気にしなくても意外といけちゃうんですけど、まぁ、ね。IaaS上のサービスがいかにハイパフォーマンスになっても、利用料だといってもですよ、コンピューティングリソースは仕入れです!仕入れ値が閾値を超えない範囲なら全然大活用するんですけど、微妙だなー、オンプレもあるしなー、っていうときはどっこいせと全面クラウド移行しないで、ハイブリッドが都合いいと思うわけです。もちろん値段だけじゃないですけど、値段気にしなくていいならね、そもそも自分たちでデータセンタ作るとか、そういうところまで行き着くとおもうので、その辺バランス大事ですね。
でですよ、提供してるサービスがさらに外部サービスを呼び出す場合にですね、AWS VPCから直接外に向かう方法と、データセンター経由する方法とあると思うんですけど、シンプルなのはデータセンター経由させる方法でしょうか。既存の拡張という意味で。トラフィックが問題になったり、いろいろあるなら、VPCからNATインスタンス(NAT インスタンス - Amazon Virtual Private Cloud)よろしく。
ここで、ちょっとあれです。外部サービスでHTTPSのものをVPC内からVPN経由で呼び出す際に、全然接続できない問題が起きることがあります。ありました。起きました。
面白いですよね。
データセンター内のHTTPSサーバーにはつながるから、ふむー、ってなんたんで、しょうがないからNetwork Monitorで確認してみたんすよ。
SSL (Secure Sockets Layer) と TLS (Transport Layer Security)
ね。うまく接続できるサイトと出来ないサイトの違いはServer Helloが返ってこないところ。
上のフローが正常に接続できるとき。下がダメなとき。ハンドシェークの途中、ダメなときはServer Helloが返ってきてないでしょ。
Client Helloの後の、TLS:Continued Data見てみると、TCP: Segment Lostってなってるあるよ。
12.04 - Can't connect to certain HTTPS sites - Ask Ubuntu
なんだ、つまり、curl -vでやれば確認できたのね。じゃぁ、これどうしちゃえば、いいんすか。
おぉー。Client Helloでとまりよった。
TCP PREVIOUS SEGMENT LOST #REALLY RARE CASE# - Wireshark Q&A
と、いうわけでMTUです。
Windows Azure の VPN 接続が不安定になる問題
接続が不安定、っていうか特定サイトにまったく接続できないんですけどね。
2013年12月26日木曜日
ARR Disk Cacheとフィーチャーフォン
ARRを導入してるならぜひとも使っておきたい機能ですね。
Disk Cacheの有無で内部トラフィックや内部サーバーはずいぶんと楽になります。もちろんCDNは使うんですけど、CDNを使うかどうかにかかわらずです。
Using Compression in Application Request Routing : The Official Microsoft IIS Site
ちなみにDisk CacheをOnにした場合、ScavengerっていうのがDiskのクリーニングをするんですが、マルチテナントだとそのIOがてーへんなことになるので、極力Flash Driveを使うようにしましょう。コンテンツがそんなに多くなくて問題にならない(確認しつつ)HDDでも全然さばけます。
サーバーファームにしてなくてもARRにするだけで接続をうまく制御できるので、おすすめです。って話は前と変わらないんですけど、今回はちょっと決定打が見当たらない問題についてです。
そもそもDisk CacheをOnにするときってWebFarm単位で指定するんですけど、その際にフィーチャーフォンなんかのutf-8でリクエストを受け取れないものがある場合、残念なことに502.3でエラーです。
URL引数に"?日本語"っていうのをShift_JISでURL Encodeして渡してます。これUTF-8だと問題ないんです。どういうことかというと、キャッシュされてるものをみるとなんとなくわかるんですけど。
キャッシュしたファイル名にURL引数の値がそのままくっついて生成されます。もちろんキャッシュするさいにQuery StringをIgnoreにすると関係なくなるんですけどね。
Configure Caching with Query String Support in Application Request Routing : The Official Microsoft IIS Site
で、じゃー、system.webのglobalization(globalization 要素 (ASP.NET 設定スキーマ))で指定しとけばいいじゃん、て思うでしょ?
ダメなんすよ、タイミング的に先にキャッシュの有無を確認してからだし、そもそもARRのアプリケーションプールはマネージコード無しだしー。先にDiskをチェックするからこそ!ですよね。
要求トレースしとくと、エラー出てますよね。
気持ちはわかりますよ。Windows 用の Http.sys レジストリ設定とかでFavorUTF8とかつっても、ARRがエラーです、って言ってんだから。そもそもDiskキャッシュOnにしなければ、Shift_JISだろうとエラーにならないわけですから、この辺も関係ないんですよね。
2013年12月5日木曜日
ASP.NET IdentityとTable Per Hierarchy
なかなか面白いですね。
SimpleMembership, Membership Providers, Universal Providers and the new ASP.NET 4.5 Web Forms and ASP.NET MVC 4 templates - Jon Galloway
Membershipそのものをいろいろやっていこうとしてたところ、やっぱり筋がよくないなー、ってなってしまったのか。
認証と承認、アプリケーション機能を明確に分離してしまったほうが、いいんじゃないか。っていうところに行き着いて、外部認証(OAuth)なんかも取り込んで行きたいし、Claimだしー、みたいなのを考えるとー、っていうところでしょうか。
The good, the bad and the ugly of ASP.NET Identity | brockallen
気持ちはわかりますが、アプリケーション機能を除外して認証に特化するUserManager系列と、承認は既存パイプラインに乗せて、OWINやらHttpModuleやらで個別に実装するか、標準のものを利用するか、でずいぶんと筋のいい発展だと思います。
んじゃ、いつどこでMembershipProviderからASP.NET Identityに乗り換えるんでしょう。どういうパターンで乗り換えましょうね。
- 新しいプロジェクトで新しいユーザーストアに適用。DBは新規。
- 新しいプロジェクトで既存ユーザーストアを利用。DBは既存。
- 既存プロジェクトで新しいデータストアを再構築。DBは移行。
- 既存のプロジェクトで既存ユーザーストアを利用。DBは既存。
とかー。
新規にDB作ったり移行する分には簡単ですよ。きっと。かってにマイグレーションされてDB出来たり、Migrating an Existing Website from SQL Membership to ASP.NET Identity : The Official Microsoft ASP.NET Site こういうのもあって、移行も楽ちん。この辺はいろいろ情報もあるしちょうと小野さんが遊んでるようなので、そちらのエントリーを。
問題は既存のDBを利用するパターンでしょうか。しかもMembershipを使ってない時なんていうのはもう完全にFluent API(Entity Framework Fluent API - Configuring/Mapping Properties & Types)でのマッピングがわからないと手のつけようがないでしょう。
例えばこのサンプル。そのままだとちゃんと動かないんだけど。
rustd/AspnetIdentitySample
namespace AspnetIdentitySample.Models
{
public class MyUser : IdentityUser
{
public string HomeTown { get; set; }
public virtual ICollection
}
public class ToDo
{
public int Id { get; set; }
public string Description { get; set; }
public bool IsDone { get; set; }
public virtual MyUser User { get; set; }
}
public class MyDbContext : IdentityDbContext
{
public MyDbContext()
: base("DefaultConnection")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity
.ToTable("Users");
modelBuilder.Entity
.ToTable("Users");
}
public System.Data.Entity.DbSet
}
}
モデルはこんな感じですよね。これってDB上どうなるのかっていうと、マイグレーションの結果IdentityUserのプロパティと、MyUserで追加してるプロパティ(HomeTownとTodoes)が展開できるようになりますよね。
で、modelBuilderでToTable("Users")ってなってるので、AspNetUsersテーブルじゃなくてUsersテーブルを利用。そこまではいいですよね。
modelBuilder.Entity
.Map
.Map
例えばこうやって。
そうもいかないじゃないすかー。アプリケーションとしてユーザーデータはリードオンリーの部分にだけASP.NET Identityを使うなら、既存カラムの値決め打ちでMapしちゃってもいけるとは思います。が、そうじゃないならどうするか、っていうことになりますよねー。
今のところ思いつくのはあれです、テーブルはそのままに新たにViewを作成して、Viewに'ApplicationUser' as DiscriminatorをSelectに混ぜ込む。更新はINSTEAD OF トリガの使用で乗り切る。くらいでしょうか。テーブルにDiscriminator項目を追加する、か。
他にやりようがあるんだろーかー。
とは言え、ASP.NET Identity奥深し。EF CodeFirstについて知らないと、結構厳しいとは思うけど、何でもありな感じに出来上がってて、とても好感触です。
2013年11月27日水曜日
DataAnnotationsModelValidatorProviderとマルチテナント
そんな感じですけど、元気にやってます。
マルチテナント花盛りですが、マルチテナントといっても
- シングルソースシングルデプロイ
DBが共有か個別かは、別パラメータで調整するのでもいいし。でも拡張は絶望。だってAppDomainにテナント固有コードいれるとかちょっとなんか本末転倒。 - シングルソースマルチデプロイ
この場合デプロイのソース割合としては、カスタム2~3%で別アセンブリになってて、97~98%は共通コア。割合は適当だけど、そのくらいなら全然管理できるし落としどころとしてはいいんじゃないかと思う。 - マルチソースマルチデプロイ
これが一番厳しいけどとりあえず動かす、っていうのなら、この辺。っていうか運用がマルチテナントなだけでシステムはほぼ個別。コードも10%以上がカスタム。つか、もう、ソースをコピーしちゃってぜ、っていうパターン。
つい先日もStackoverflow.comでおなじみのJeff Atwoodさん話の流れで出てきてましたが、
「コードベースは簡単に再利用できるという発言はよく聞くけど、大きなサイトのコードが本当に再利用できるのは稀。同じコードベースでサイトを3つ運営できるようになったら、本当に再利用できるコードと言える。」
予告ホームランを打とう - ワザノバ | wazanova
最近、必須項目を落としたいっていう"冗談は顔だけにしろよ"っていうオーダーが入ってですね、Validationルールどうしようか、天を仰いだんすよ。もちろんモデルは共通だしー。カスタムモデルも使えるようにはしてるけど、ちょっとそういうのなしね、っていうモデル。DBにべったりなモデル。
で、まぁ、すったもんだですけど、テナントごとに調整できるようにしたのが、↓こんな感じのカスタムModelValidatorProvider。
public class VoidDataAnnotationsModelValidatorProvider : DataAnnotationsModelValidatorProvider
{
private static readonly Dictionary
public static string KeyFormat = "{0}.{1}";
private static string GenerateRuleKeyName
{
return string.Format(KeyFormat, typeof(T).FullName, ExpressionHelper.GetExpressionText(_expression));
}
public static void AddRule(string key, Type attributeType)
{
if (!_disableAttributes.ContainsKey(key))
_disableAttributes.Add(key, new List
_disableAttributes[key].Add(attributeType);
}
public static void AddRule
{
var key = GenerateRuleKeyName(_expression);
AddRule(key, attributeType);
}
public static void RemoveRule(string key)
{
_disableAttributes.Remove(key);
}
public static void RemoveRule
{
var key = GenerateRuleKeyName(_expression);
RemoveRule(key);
}
public static void ClearRules()
{
_disableAttributes.Clear();
}
protected override IEnumerable
{
var exceptAttrs = attributes;
if (metadata.ContainerType != null && !string.IsNullOrWhiteSpace(metadata.PropertyName))
{
var disableKey = string.Format(KeyFormat, metadata.ContainerType.FullName, metadata.PropertyName);
if (_disableAttributes.ContainsKey(disableKey))
{
exceptAttrs = attributes.Where(attr => !_disableAttributes[disableKey].Contains(attr.GetType()));
}
}
var validators = base.GetValidators(metadata, context, exceptAttrs);
return validators;
}
}
2012年12月28日金曜日
なんかかんか
すっかりブログ、書いてないね。前回は7/30だってさ。
もう今年も終わっちゃうねー。
今年もいろいろありました。
来年もいろいろあるでしょうね。
書きたいモチベーションが上がってきたら、ブログ書く。きっと書く。いろいろと。
Cookieのキャッシュで認証がとんでもないことになるとか、YSOD誰が出すんでしたっけ、とか?
いやいや、そーじゃなくてゴルフ始めたんですよ。今まで使ってなかった筋肉使って、さらなるパンプアップ!ベン・ホーガンってすごいね。
ホッケーも楽しくやってるんだよ。新しい友だちできたり、外人が「ガイコクジンと言え!」とか面白い。
仕事はなんかよくわかんなくなってきたから一旦整理する。何する人なのかイマイチ中途半端な事になってるし。
あとはー、そーだなー。今後も楽しくやってくよ!
2012年7月30日月曜日
外部のWebサービスを呼び出す側の簡単なユニットテスト
外部システムとの連携で、テストしにくいこといろいろあると思うんだけど、その辺について先日会社でこうするといいんじゃないのー?って話した内容のおさらい。
外部システムって言っても、同一システムの一部であることも含めます。とにかく、テストをしたいと思ってる部分から接続する必要のあるものすべて。社外か社内かはあんまり関係なく。WCFかASMXかRESTかすら関係なく。ホントはDBもだけど、今回はHTTPベースのWebサービスということで。
まずは、わかりやすくWCFでWebサービスを作ってみます。別にWebAPIでもいいしー。なんでもいいんだけどー。用意するのはVS2010とFiddlerとブラウザとテキストエディタ。
[ServiceContract]
public class WebScraper
{
[OperationContract]
public string ExtractPageTitle(string url)
{
var title = "-unknown-";
var client = new HttpClient();
var html = string.Empty;
client.GetStringAsync(url)
.ContinueWith(response =>
{
html = response.Result;
})
.Wait();
var match = Regex.Match(html, "<title>([^<]+)</title>");
if(match.Success)
{
title = match.Groups[1].Value;
if (!string.IsNullOrWhiteSpace(title))
title = HttpUtility.HtmlDecode(title);
}
return title;
}
}
なんでもいいんだけど。こんなの。外部Webサービスのつもりで。URLを指定して呼び出すと、ページタイトル取得する、っていう意味不明なWebサービスがありました。として。
WcfTestClientで実行してみて動くのを確認。大丈夫だね。
これを呼び出すMvcアプリケーションも用意。HomeControllerしかいないよ。
private string Logic(string value)
{
return new string(value.Reverse().ToArray()).ToUpper();
}
public ActionResult Index(string url)
{
ViewBag.Message = "URLを入れるとページのタイトルを取得するよ";
if(!string.IsNullOrWhiteSpace(url))
{
var service = new ApiService.WebScraperClient();
var title = service.ExtractPageTitle(url);
ViewBag.Message = Logic(title);
}
return View();
}
Indexアクションでは、サービス参照の追加で作成したサービスクライアントクラスを実行してページタイトル取得した後に、反転して大文字にするロジックが含まれてるつもりです。
はいはい。雰囲気出てますね。
このIndexアクションのテストを書いてみました。
[TestMethod]
public void Index_マイクロソフト()
{
// Arrange
HomeController controller = new HomeController();
// Act
ViewResult result = controller.Index("http://www.microsoft.com") as ViewResult;
// Assert
Assert.AreEqual("NOITAROPROC TFOSORCIM", result.ViewBag.Message);
}
[TestMethod]
public void Index_いろいろ()
{
// Arrange
var list = new[] { "http://www.google.com", "http://www.twitter.com", "http://takepara.blogspot.jp" };
// Act
var result = list.Select(url => new HomeController().Index(url) as ViewResult).ToArray();
// Assert
Assert.AreEqual("ELGOOG", result[0].ViewBag.Message);
Assert.AreEqual("RETTIWT", result[1].ViewBag.Message);
Assert.AreEqual("!だらかれこはみし楽お", result[2].ViewBag.Message);
}
これといって変なこともなく。HomeControllerのインスタンス作ってUrlを引数にIndexを呼び出したら、ページタイトルが反転して大文字になってるかどうかを確認してるだけです。
外部サービスがですね、ホントに社外のもので、そこのロジックがどういうものかは知らないっていうことになると、テストしたい部分ってどのになりますかね。通信経路ですかね?それとも、外部サービスのロジックですかね?ユニットテストでまわしたいとすると、そこってテストしたい部分じゃないと思います。自分書いたコードじゃないし、環境だし。
なので、ユニットテストしたいのは反転して大文字にするロジックですよね。たぶん。とはいえ、すでに存在するコードのテストをしたい、っていう場合もあるでしょう。みんなTDDなわけでもなく。そんな時はMVCを使って簡単なFakeサーバー。HTTP(S)だしー。なんでもいいでしょー。
実際の通信内容を保持しといて、リクエストのたびにその値を返せば開発環境で完結したテストが実行できて便利。何パターンか用意しておけばいいしね。その辺はテストしたい対象に合わせて良しなに用意しましょう。
今回は、上記テストがパスするように用意します。エラーのパターンがないのはご愛嬌。
public class FakeController : Controller
{
private static string RequestBody(HttpContextBase context)
{
context.Request.InputStream.Position = 0;
var reader = new StreamReader(context.Request.InputStream);
return HttpUtility.UrlDecode(reader.ReadToEnd(), Encoding.UTF8);
}
private Dictionary<Func<HttpContextBase, bool>, Func<ActionResult>> _responseHandlers =
new Dictionary<Func<HttpContextBase, bool>, Func<ActionResult>>
{
{
(context) =>
context.Request.CurrentExecutionFilePath == "/HttpApiServer.Fake/WebScraper.svc" &&
RequestBody(context).Contains("www.microsoft.com"),
()=>new FilePathResult("~/App_Data/www.microsoft.com.txt", "text/xml; charset=utf-8")
},
{
(context) =>
context.Request.CurrentExecutionFilePath == "/HttpApiServer.Fake/WebScraper.svc" &&
RequestBody(context).Contains("www.google.com"),
()=>new FilePathResult("~/App_Data/www.google.com.txt", "text/xml; charset=utf-8")
},
{
(context) =>
context.Request.CurrentExecutionFilePath == "/HttpApiServer.Fake/WebScraper.svc" &&
RequestBody(context).Contains("www.twitter.com"),
()=>new FilePathResult("~/App_Data/www.twitter.com.txt", "text/xml; charset=utf-8")
},
{
(context) =>
context.Request.CurrentExecutionFilePath == "/HttpApiServer.Fake/WebScraper.svc" &&
RequestBody(context).Contains("takepara.blogspot.jp"),
()=>new FilePathResult("~/App_Data/takepara.blogspot.jp.txt", "text/xml; charset=utf-8")
},
};
public ActionResult Api()
{
var handler = _responseHandlers.Where(_ => _.Key(HttpContext)).Select(_ => _.Value).FirstOrDefault();
return handler !=null ?
handler() :
new HttpStatusCodeResult(500);
}
}
リクエストに合わせて結果を返すだけなんですけどね。ローカルにファイルを作成するために、一度Fiddlerで一通り動かして、データを取得しておきましょう!今回の場合だとWebサービスをIISで動かすと都合がよかったです。Fiddlerの都合で。
<endpoint address="http://localhost/HttpApiServer.Real/WebScraper.svc" binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_WebScraper" contract="ApiService.WebScraper" name="BasicHttpBinding_WebScraper" />
こんな感じのendpointだったものを以下のようにFiddler通すように変更。
<endpoint address="http://ipv4.fiddler/HttpApiServer.Real/WebScraper.svc" binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_WebScraper" contract="ApiService.WebScraper" name="BasicHttpBinding_WebScraper" />
そうすると、www.microsoft.comへを指定した、API呼出しのRequestとResponseは画像のような結果になります。ここから、ResponseのBody部だけ抜き出して、ファイルに保存しておき、それをリクエストに合わせてレスポンスするのがFakeサーバーの役目です。
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><ExtractPageTitleResponse xmlns="http://tempuri.org/"><ExtractPageTitleResult>Microsoft Corporation</ExtractPageTitleResult></ExtractPageTitleResponse></s:Body></s:Envelope>
↑たとえばこれをwww.microsoft.com.txtっていうファイルにしておきます。どんなパスを指定してもFakeControllerのApiアクションにリクエストが届くようにルーティング登録とWeb。configで.svcの拡張子を外しておきましょう。.svc外しとかないとSystem.ServiceModel.Activationが横取りするのでApiアクションにリクエストが届かず、404エラーになっちゃいます。
public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"FakeServer", // Route name
"{*path}", // URL with parameters
new { controller = "Fake", action = "Api" } // Parameter defaults
);
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
}
↑Global.asaxね。MapRouteだけいじってます。
<system.web>
<compilation debug="true" targetFramework="4.0">
<buildProviders>
<remove extension=".svc"/>
</buildProviders>
<assemblies>
:
</assemblies>
</compilation>
<pages>
<namespaces>
:
</namespaces>
</pages>
</system.web>
↑web.configね。buildProvidersで.svcを削除してます。一通りファイルを用意して、テストのendpointをこのFakeサーバーに向けて実行します。
<endpoint address="http://localhost/HttpApiServer.Fake/WebScraper.svc" binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_WebScraper" contract="ApiService.WebScraper" name="BasicHttpBinding_WebScraper" />
ここまでが既に作成したものに対して、テスト実行したいときにとる手法で、WebサービスのFakeパターンですが、ユニットテストっていう意味ならテスト対象のIndexアクションが本当に通信するコードを呼び出さないほうが都合がいいと思います。
なので、これから作成するなら、Webサービスへのアクセスを抽象化し、ユニットテストで差し替えるようにすると、本来テストしたい部分に特化したテストができていいと思います。例えば以下のようにControllerのコードを変えてみます。
public interface IApiAdapter
{
string Execute(string url);
}
public class HttpApiAdapter: IApiAdapter
{
public string Execute(string url)
{
var service = new ApiService.WebScraperClient();
return service.ExtractPageTitle(url);
}
}
public class HomeController : Controller
{
public static Func<IApiAdapter> ApiAdapterFactory = () => new HttpApiAdapter();
private string Logic(string value)
{
return new string(value.Reverse().ToArray()).ToUpper();
}
public ActionResult Index(string url)
{
ViewBag.Message = "URLを入れるとページのタイトルを取得するよ";
if (!string.IsNullOrWhiteSpace(url))
{
var adapter = ApiAdapterFactory();
var title = adapter.Execute(url);
ViewBag.Message = Logic(title);
}
return View("Index");
}
}
IApiAdapterと、その実装であるHttpApiAdapterを用意して、インスタンスはファクトリメソッドから実行するように書き換えました。
テストのほうも同じく書き換えます。まずはFakeApiAdapterの追加。
public class FakeApiAdapter : IApiAdapter
{
public string Execute(string url)
{
var list = new Dictionary<string,string>
{ {"http://www.google.com","Google"},
{"http://www.twitter.com","Twitter"},
{"http://takepara.blogspot.jp","お楽しみはこれからだ!"}, };
return list[url];
}
}
あとはTestInitializerでファクトリメソッドのFuncデリゲート書き換え。
[TestInitialize]
public void Initialize()
{
HomeController.ApiAdapterFactory = () => new FakeApiAdapter();
}
以上!
これで実際のプログラムにアクセスするときは、ClientBaseのWCFクライアントクラスから実Webサービス呼出しをするし、ユニットテストからは呼んだつもりで偽アダプター(Stub)が結果を返します。
準備するの面倒かもしれないけど、この辺もちゃんとテストしましょー。なるべく環境依存しなくて済むようにね。
dotnetConf2015 Japan
https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。
-
How to iterate through objects in ViewData via javascript on the page/view? - Stack Overflow この質問の回答としては、ページにscriptタグを書き、その中でJavaScriptを書き...
-
Working with SSL at Development Time is easier with IISExpress - Scott Hanselman Hanselmanさんのエントリに書かれてる通りデス! IIS ExpressでSSLを有効にしたデバッグだと無効...
-
ASP.NET MVC4 beta面白いですねー。 特にRESTfulなサービス実装を容易に実現するために導入されたApiControllerは強烈。 WCFのプロダクトとして開発が進められていたWCF Web APIが、名前を変えてASP.NET Web APIとして生まれ...






















