2014年5月22日木曜日

Amazon RDS for SQLServerはミラーリングだった

ディスクのスナップショットでしょー。データ欠落することもあるんだろーなー。なんて適当なこと言ってマジすいません。

インスタンス立てて確認しました。マジ、ミラーリングでした。調子乗ってすいませんでした。データベース作ってしばらくすると、見慣れたあの表示。

rds1

作りたては、まだミラーリング準備中なんだろね。確認してないけど。

テーブル作ってテストデータ100万レコード作成中にいきなり!

rds2

この見慣れた「プリンシパル、同期済み」が出てきた。

rds3

あー。ちゃんとミラーリングやー。相方のサーバー名も違うやつだし、ウィットネスもいた。

なのに接続先は固定で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/
  1. ターゲットPATHが"/"で始まってない
      <img src="images/logo.gif" />
      <img src="./images/logo.gif" />
      <img src="../rewrite/images/logo.gif" />
  2. ターゲットPATHが"/"で始まってる
      <img src="/rewrite/images/logo.gif" />
  3. 例外的に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とかちょっと触ってみた

Introducing ASP.NET vNext - Scott Hanselman

もうみんな、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 environment)じゃないのね。いい感じ!は!そこは一旦置いとこう。楽しみにとっとくー。

次。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の人じゃないけど、超楽しい。
Microsoft.AspNetの中身、いろいろみていこう。OWINよく知らないけど、これだけ楽しいなら、もう最高ス。
まだIISで動かしてない(KLR上で動かすだけだからビルドじゃなくて実行しただけ)から、Heliosだとどんなbinになるのか気になるしー。App_CodeとかASP.NETでのシステムフォルダの扱い(app_globalresourcesとかさー)がどこまで行けるようになるのかなー。たまらん!

ん?
the assemblies never exist on the disk.
んん!?
・No-compile developer experience
って書いてるね...。まじか。ビビった。

2014年5月9日金曜日

スレッドスロットルなんか普通しなくてもいいのにね

すごくCPUも使うし、たくさんリクエストくるしー、なサイトを動かしてると、リクエストの上限を決め打ちしてしまいたくなるでしょー。そういうこともあるんです。

個別アプリケーションごとにVMがあったりとかー、そういうんだと、そんな時は maxConcurrentThreadsPerCPU とかでー、でしょうか。あ、maxWorkerThreads とかでも。

なんでそんなことが必要なんですか。そもそもCPU Throttlingでいいじゃないですかー。と、いうのもあるんですけど、そもそもマシンパワーのあまりない環境とかだと、返事が返ってこないです。解決方法はいろいろあるなかで、強制的にリクエストをキューから取り出さなきゃいいじゃないと思った結果がSetMaxThreads。

private int ThreadPoolThrottle
{
get
{
return int.Parse(ConfigurationManager.AppSettings["ThreadPoolThrottle"] ?? "0");
}

}

if (ThreadPoolThrottle > 0)
{
// 重いテンプレートの場合これ。
int workers;
int completions;
System.Threading.ThreadPool.GetMaxThreads(out workers, out completions);
System.Threading.ThreadPool.SetMaxThreads(ThreadPoolThrottle, completions);

}

アプリケーションスタート時に実行。


詳しくは↑を。

処理時間がすごく長いわけではないなら(これ重要)、スレッド減らして、っていう方法もありなんじゃないかなー、ないかなー。パフォーマンステストの数値としてはいい感じで。その時の数値はちょっと出しにくくすいません。

public ActionResult Index()
{
var start = DateTime.Now;
var hasher = System.Security.Cryptography.RIPEMD160Managed.Create();
while ((DateTime.Now - start).TotalSeconds < 1)
{
var hash = hasher.ComputeHash(new byte[1024 * 1024]);
Thread.Sleep(10);
}

return View();
}

こんなアクション(1000msの間CPUいっぱい使う、Sleepはちょっとあったほうが本物の挙動に近しいのでなんとなく)があったとして、abで確認してみるとそーねー、てなりますけど、だからなに、ともなります。

SetMaxThreadsで4にしたとき。



abが-n 40 -c 4の時と-c 40の時で同じ、っていうね。マシンパワーないとずいぶん結果は違うけど、まぁ、いいでしょう。

それだけなんすけどね。そのほうがよくなることもありますねー、って。CPU性能足りないときや、スレッドにスロットルかけて、全体最適のほうに倒すとか。設定はやみくもにはできなくて、実環境で調整していかないとうまくいかないんだけど、CPU性能足りないんだけど、リクエスト多くて、チューニングも難しい、って時にはぜひどうぞ。あ、あと、マシン占有できるならconfigでどうぞ。できないときだけね。

2014年5月1日木曜日

CSVを今っぽく

CSVってカンマなのかコモンなのか、っていう話ではなくてですね。
一般的には","カンマで区切るんだと思います。
でも、ダブルクォート使いたいしー、っていうんで"TAB"タブ区切りにしちゃったり。
さらに、用途によっては改行入らないから、ダブルクォート内での改行処理とかなくてもいいよー、っていうのがよくあるパターンかなー、と思ったりもします。

簡単に整理すると

  1. カラム区切りはタブ
  2. カラム内に改行はない
  3. レコードの区切りは改行
  4. 1行目はカラム名を指定するヘッダ
  5. 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でデータつくるじゃん!

ここまでで、もうイメージできた人も多いと思うけど、せっかくなので、コードでどう表現するのかを書いてみます。暇だから。まずは、データのパース。ルールが単純なので、タブやカンマでSplitするだけです。ダブルクォートのエスケープとか、改行込のデータがあるなら、そこは頑張って!

これってコードにすると、

var columns = line.Split(splitChar);

とか、ですね。
1行目ならそこにはカラム名(これの一覧はPOCOにセットするために保持しとく)が入ってて、1行目以降ならそれはデータ。っていうのを、上位層で判定。

これで、レコードをカラムに分割できました。
今度はカラム名の一覧と、モデルクラス(ジェネリックでしょうね)のプロパティをマッピング。

var properties = TypeDescriptor.GetProperties(type);

これで一応POCOのプロパティ一覧(PropertyDescriptorCollection)は取得できましょう。キャッシュするかどうかはお好きに。

あとは、もう単純にくるくるループ回していけば、いいですよね!PropertyDescriptorがあれば、Converter.ConvertFromStringでプロパティにセットできるか判定もね。

後は、DataAnnotationsを指定してるかもしれないから、Validator.TryValidateObjectで、モデル検証。

        public static IList Parse(string text, Action action = null) where T : ModelBase, new()
        {

            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(columns);
                    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で落ち着かせる

とりあえず、VPCならVPN接続(BGP)すると思うんです。

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再接続後には、その上位も再接続しなさい、の理屈が通るもんね。

これ、接続できないんじゃなくて、接続するけど、その後再接続繰り返す、っていうのがさ、アプリケーションから見ただけだと意味わかんないエラーにしか見えなくて、ちょっと解決するのに時間かかった。確認するのに24時間かかったりとかね。とりあえずAmazon VPC+YAMAHA RTX1200の組み合わせなら、今のところ、DPDやめとくのがいいんじゃないか、っていう結論です。

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の場合、コンソールに

wmic /node:"localhost" path Win32_PerfFormattedData_PerfOS_Processor where (Name="_Total")

って、書くと取れますね(localhostならnodeは指定しなくてもいいです)。ほかの項目もこの容量でなんでもとれるけど、プロセス起動になるので、対象サーバーが多いとちょっと辛いかも。

結果は↓。


テキストでとれるので適当にパースしましょう。とりたいフィールド名は指定できるので、必要ない項目は無視で。

System.Managementなら、↓こんな感じで。

ManagementObjectCollection response = null;
var query = "select * from Win32_PerfFormattedData_PerfOS_Processor where (Name=\"_Total\")";
Console.WriteLine(node.Path + " : " + query);

var serverExists = true;
try
{
var ping = new Ping();
var result = ping.Send(node.Path);
serverExists = result.Status == IPStatus.Success;
}
catch 
{
serverExists = false;
}

if (serverExists)
{
try
{
var scope = new ManagementScope("\\\\" + node.Path + "\\root\\cimv2");
var enumerationOptions = new EnumerationOptions
{
Timeout = TimeSpan.FromMilliseconds(_settings.ProcessTimeout)
};
var searcher = new ManagementObjectSearcher(scope, new ObjectQuery(query), enumerationOptions);
response = searcher.Get();
}
catch (Exception ex)
{
// ダメなら無視
Debug.WriteLine(ex.Message);
}
}

雰囲気ね。一度PING飛ばしてちゃんとレスポンスがあるのかどうか確認してるのはManagementObjectSearchはTimeoutを検出しないからデス。どんまい。
これだと、Taskで複数一度に回せて意外とさらさらー、って取り出せます。

続いて、ネットワーク機器の監視にみんな使ってるであろうMRTG。SNMP到達しない機器だろうと、MRTGは見れる、なんていう構成、ありますよね。ないですか。そうですか。あるんです!

その場合、MTRGが吐き出してるログファイルから値を取り出すと楽ちんですね。


あとは、たまに(設定間隔で)集計値が出力されるのでそれは無視するようにしましょう。そうすると、単なるHTTPでログの収集が簡単ポン。

public class MrtgLog
{
public DateTime DateTime { get; set; }
public long InBytesAverage { get; set; }
public long OutBytesAverage { get; set; }
public long InBytesMax { get; set; }
public long OutBytesMax { get; set; }
public long InBytesTotal { get; set; }
public long OutBytesTotal { get; set; }
}
public class MrtgLoader
{
public async Task GetLogFile(Uri uri)
{
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.Range = new RangeHeaderValue(0, 1024);
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();

return content;
}

private static DateTime unixEpocTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public DateTime? UnixTimeToDateTime(string unixTime)
{
long longTime;
if (!long.TryParse(unixTime, out longTime))
return null;

return unixEpocTime.AddSeconds(long.Parse(unixTime));
}

private MrtgLog ParseLine(string line)
{
DateTime? dateTime = null;
var cols = line.Split(' ');

if (cols.Length > 0)
{
dateTime = UnixTimeToDateTime(cols[0]);
}
if (!dateTime.HasValue)
return null;

if (cols.Length == 3)
{
return new MrtgLog
{
DateTime = dateTime.Value,
InBytesTotal= long.Parse(cols[1]),
OutBytesTotal = long.Parse(cols[2]),
};
if (cols.Length == 5)
{
return new MrtgLog
{
DateTime = dateTime.Value,
InBytesAverage = long.Parse(cols[1]),
OutBytesAverage = long.Parse(cols[2]),
InBytesMax = long.Parse(cols[3]),
OutBytesMax = long.Parse(cols[4])
};
}

return null;
}

public IEnumerable ParseLog(string log)
{
if (string.IsNullOrWhiteSpace(log))
return Enumerable.Empty();

var logLines = log.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n');
return logLines.Select(ParseLine).Where(record=> record != null);
}
}

こんな感じで。雰囲気ね。
これはあれです、MRTGでとってるからと言って、MRTGじゃないと見れないのはなんかやだ、って人向けです。

後はNewRelicで収集してるMetricデータの取り出し。REST APIです。


ここに書いてるとおり!
だと、あれなので、curlで取得する場合にどう書きましょうか、っていうのをCPUを例に。これがなんかちょっとわかりにくかったんす。


Applicationじゃなくて、Serversに入ってる値が欲しいんだけど、その場合もagent_idを指定。で、agent_idっていうのは、あれで、"https://api.newrelic.com/api/v1/accounts/:account_id/servers.xml"からとれるidのことだってさ。

そしたら今度は対象Agentで取得できる項目一覧を確認。"https://api.newrelic.com/api/v1/agents/:agent_id/metrics.xml"ね。

まぁ、みんな同じOSなら、内容もたぶん同じだし、項目は最初に確認するときに見る感じで。そうするとCPUの利用率は"System/CPU/User/percent"と"System/CPU/System/percent"っていうのがわかりましたね。

じゃぁ、agent_idを指定してmetricsの値をとります。

"https://api.newrelic.com/api/v1/accounts/:account_id/agents/:agent_id/data.xml?begin=2014-01-29T03:51:00Z&end=2014-01-29T03:52:00Z&metrics[]=System/CPU/User/percent&metrics[]=System/CPU/System/percent&metrics[]=System/Memory/Used/bytes&metrics[]=System/PhysicalMemory/Installed[bytes]&field=average_value"

対象期間を指定して、とりたいmetrics名指定して、fieldはaverage_valueで。
これで、とれるんだけどー、これだとサーバーごとにリクエスト投げなきゃいけないんすよね。んー、いいんだけと、やりすぎは怒られそうじゃないすか。そもそもagent_id複数指定できる的なこと書いてるし(fieldは1個ずつです)。

結果的にAPIが違ったっていう話で。
"https://api.newrelic.com/api/v1/accounts/:account_id/metrics/data.xml?agent_id[]=:agent1&agent_id[]=3agent2&begin=2014-01-29T03:51:00Z&end=2014-01-29T03:52:00Z&metrics[]=System/CPU/User/percent&metrics[]=System/CPU/System/percent&metrics[]=System/Memory/Used/bytes&metrics[]=System/PhysicalMemory/Installed[bytes]&field=average_value"

これなら、一度に全部のサーバーの値が取れますね。
あとは、こうやって集めたデータを、好きなように加工して、好きなように表示するだけで、なんかいい感じに監視できて楽しいです。

楽しいか...?

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の例

SaaSで使った分だけのCDNといえばAmazon CloudFrontがいい感じですよね。

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

すでにコロケーションサービスを利用したりして、データセンターには自前のサーバーがいて。でも、今のデータセンターの利用を拡張していったり、するのもどうかなー、資産化するのもなー。という時にはパブリッククラウドにVPN接続させて、オンプレミスとのハイブリッド。
ハイブリッドクラウドですよね。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 接続が不安定になる問題

接続が不安定、っていうか特定サイトにまったく接続できないんですけどね。

これね、きっとね、先方のネットワーク内もIPSec的な事になってるとね、大きすぎるわー、ってなっちゃうんでしょうね。
ちなみに、今回接続できなかったところって、PINGでMTUチェックできなくてさー。もう、じゃぁ、いくつならいいんだよ!と、思ってたところで、Azureでのはなしが出てたのを思い出したんスよね。

んじゃ、1350にしとこかな、って。

一件落着でした。
自分のところがVPNなら相手のところもVPNっていうのは当然あり得ることですよねー。あれー、おやー、と思ったら、この辺チェックしてみるといいと思います。

2013年12月26日木曜日

ARR Disk Cacheとフィーチャーフォン

Configure and Enable Disk Cache in Application Request Routing : The Official Microsoft IIS Site

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だろうとエラーにならないわけですから、この辺も関係ないんですよね。

つまり、C:\Program Files\IIS\External Disk Cacheにインストールされるecache.dllがですね、ネイティブですけど、文字コード固定なんですよ、きっと。できることなら指定できるようにしてほしい。そもそもQueryStringそのまま信じて処理しないでほしい。ファイル名もエンコードしたものでそのまま生成してくれればいいのに。


UTF-8縛りでいいじゃん!っていうのもね、そうなんですけどー。なかなかさー。
こういう場合はどういう方法とるのがいいのか、悩ましいところですけど、管理手間は増えるけどDiskキャッシュのOn/OffでそれぞれのWebFarmを用意する、っていうのしか手がなさそうなんですよねー。

  http://normal.website
  http://sjis.website
  http://contents.website

とかURLがそもそもわかれる場合は3番目のサイトだけDiskキャッシュOnとかさ。でも、混在させるじゃない、何気に。

  http://composite.website
  http://composite.website/mobile

とかね。そーするとWebFarmのルーティングルールでフォルダ単位でWebFarm区切る。
なかなか、手間かかるところでは、ありますが、最初の定義だけなんでね、その辺は。それよりサーバー利用効率あげたいしー。

と、いうわけで、決め手に欠けるエラー回避(消極的な)しか思いつかなくて、しょんぼりです。

2013年12月5日木曜日

ASP.NET IdentityとTable Per Hierarchy

Getting Started with ASP.NET Identity : The Official Microsoft ASP.NET Site

なかなか面白いですね。

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 ToDoes { get; set; }
    }

    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 ToDoes { get; set; }
    }
}

モデルはこんな感じですよね。これってDB上どうなるのかっていうと、マイグレーションの結果IdentityUserのプロパティと、MyUserで追加してるプロパティ(HomeTownとTodoes)が展開できるようになりますよね。
で、modelBuilderでToTable("Users")ってなってるので、AspNetUsersテーブルじゃなくてUsersテーブルを利用。そこまではいいですよね。



プロパティに持ってない項目がスキーマにできてるのわかります?



Discriminatorってだれだよ!
ASP.NET Identityの内部で自動生成されたりするんだろうか。って思っちゃいますよね。
違うんです!違う違う!

これねー、EntityFrameworkのCodeFirstの機能なんすよ。


同一テーブルを複数モデルのマッピング先に利用する場合、そのレコードがどのモデルクラスに復元されなければいけないか、を保持する列なんです。問答無用です。まじかよ!ってなりますよね。

なのでテーブルにこの項目がない場合IdentityUserの派生クラスをつかって、既存ユーザーストアにマッピングするのが厳しくなります。

なんでかというと、UserStoreのジェネリックがIdentityUser派生をwhereで指定してるので、IUserから直接だと無理ー。

既存ユーザーストアなんてUserNameとかPasswordHashとかSecurityスタンプとかって名前でテーブル定義してるわけないじゃないですか。

もっとシンプルに

CREATE TABLE [dbo].[UserMaster] (
    [Id]       NVARCHAR (128) NOT NULL,
    [Name]     NVARCHAR (MAX) NOT NULL,
    [Password] NVARCHAR (MAX) NOT NULL,
    [Status]   INT           NOT NULL,
    [Description] NVARCHAR (MAX) NOT NULL,
    PRIMARY KEY CLUSTERED ([Id] ASC)
);

こんなもんだったりしないすか。例えばの話ですよ。

じゃぁ、既存のこのスキーマにマッピングするためのFluent定義はどうなるか、っていうと、さっきのコードを回収すると↓こうなります。

    public class ApplicationUser : IdentityUser
    {
        public string Description { get; set; }
    }

    public class ApplicationDbContext : IdentityDbContext
    {
        public ApplicationDbContext()
            : base("DefaultConnection")
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            //base.OnModelCreating(modelBuilder);

            modelBuilder.Ignore();
            modelBuilder.Ignore();
            modelBuilder.Ignore();
            modelBuilder.Ignore();

            modelBuilder.Entity().ToTable("UserMaster");
            modelBuilder.Entity().Property(t => t.Id).HasColumnName("Id");
            modelBuilder.Entity().Property(t => t.UserName).HasColumnName("Name");
            modelBuilder.Entity().Property(t => t.PasswordHash).HasColumnName("Password");
            modelBuilder.Entity().Ignore(t => t.SecurityStamp);

            modelBuilder.Entity().ToTable("UserMaster");
        }
    }

※baseのOnModelCreatingコメントアウトしないと、デフォルトのルールがはいってうるさい。
※あと、Migrationのテーブル消しとかないと、マイグレーションしようとしてうるさいから、テーブルも削除しときましょう。



実行するとこれです。Discriminatorがないです、って怒られます。なんでかっていうと、IdentityUserとApplicationUserが同じテーブルにマッピングされるから。これはねー、難しいと思うんですよ。既存ユーザーストア利用する場合。

Fluent APIで必殺コマンドを使って、Discriminatorを参照しないようにすると、解決できなくはないですけどー。


↑これの、Table Per Hierarcht(TPH)でやってるMapの部分。

そういうことができるカラムが存在してるなら、なんとかなるとは思う。

            modelBuilder.Entity()
                        .Map(m => m.Requires("Status").HasValue(0))
                        .Map(m => m.Requires("Status").HasValue(1));

例えばこうやって。
そうもいかないじゃないすかー。アプリケーションとしてユーザーデータはリードオンリーの部分にだけASP.NET Identityを使うなら、既存カラムの値決め打ちでMapしちゃってもいけるとは思います。が、そうじゃないならどうするか、っていうことになりますよねー。

今のところ思いつくのはあれです、テーブルはそのままに新たにViewを作成して、Viewに'ApplicationUser' as DiscriminatorをSelectに混ぜ込む。更新はINSTEAD OF トリガの使用で乗り切る。くらいでしょうか。テーブルにDiscriminator項目を追加する、か。

他にやりようがあるんだろーかー。
とは言え、ASP.NET Identity奥深し。EF CodeFirstについて知らないと、結構厳しいとは思うけど、何でもありな感じに出来上がってて、とても好感触です。

2013年11月27日水曜日

DataAnnotationsModelValidatorProviderとマルチテナント

1年に1回!
そんな感じですけど、元気にやってます。

マルチテナント花盛りですが、マルチテナントといっても
  • シングルソースシングルデプロイ
    DBが共有か個別かは、別パラメータで調整するのでもいいし。でも拡張は絶望。だってAppDomainにテナント固有コードいれるとかちょっとなんか本末転倒。
  • シングルソースマルチデプロイ
    この場合デプロイのソース割合としては、カスタム2~3%で別アセンブリになってて、97~98%は共通コア。割合は適当だけど、そのくらいなら全然管理できるし落としどころとしてはいいんじゃないかと思う。
  • マルチソースマルチデプロイ
    これが一番厳しいけどとりあえず動かす、っていうのなら、この辺。っていうか運用がマルチテナントなだけでシステムはほぼ個別。コードも10%以上がカスタム。つか、もう、ソースをコピーしちゃってぜ、っていうパターン。
などなど。まぁ、良い悪いも視点しだいなんだけど、デベロップメント視点でいうと、マルチソースは避けたいところですね。

つい先日もStackoverflow.comでおなじみのJeff Atwoodさん話の流れで出てきてましたが、

「コードベースは簡単に再利用できるという発言はよく聞くけど、大きなサイトのコードが本当に再利用できるのは稀。同じコードベースでサイトを3つ運営できるようになったら、本当に再利用できるコードと言える。」

予告ホームランを打とう - ワザノバ | wazanova


最近、必須項目を落としたいっていう"冗談は顔だけにしろよ"っていうオーダーが入ってですね、Validationルールどうしようか、天を仰いだんすよ。もちろんモデルは共通だしー。カスタムモデルも使えるようにはしてるけど、ちょっとそういうのなしね、っていうモデル。DBにべったりなモデル。

で、まぁ、すったもんだですけど、テナントごとに調整できるようにしたのが、↓こんな感じのカスタムModelValidatorProvider。

    public class VoidDataAnnotationsModelValidatorProvider : DataAnnotationsModelValidatorProvider
    {
        private static readonly Dictionary> _disableAttributes = new Dictionary>();

        public static string KeyFormat = "{0}.{1}";

        private static string GenerateRuleKeyName(Expression> _expression)
        {
            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(Expression> _expression, Type attributeType)
        {
            var key = GenerateRuleKeyName(_expression);

            AddRule(key, attributeType);
        }

        public static void RemoveRule(string key)
        {
            _disableAttributes.Remove(key);
        }

        public static void RemoveRule(Expression> _expression)
        {
            var key = GenerateRuleKeyName(_expression);

            RemoveRule(key);
        }

        public static void ClearRules()
        {
            _disableAttributes.Clear();
        }

        protected override IEnumerable GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable attributes)
        {
            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;
        }
    }

太字のところ。GetValidatorsで対象の属性を除外してModelValidatorを抽出。普通にAttributeじゃなくてAdapterで丸めてるので、単純比較はできないんだけど、attributesから除外しちゃえばModelValidatorも除外されるから、あんまり気にしなくてもよろしですね。

これを使うようにGlobal.asaxに登録。登録方法はApp_Codeに配置するなり(マルチデプロイの場合は)、なんだもいいんだけど。

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            // Nullableじゃない基本型の場合にRequiredを付与しないならfalseにしましょう。
            DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;

            VoidDataAnnotationsModelValidatorProvider.AddRule("WebApplication2.Models.RegisterViewModel.UserName", typeof(RequiredAttribute));
            VoidDataAnnotationsModelValidatorProvider.AddRule((o) => o.Password, typeof(StringLengthAttribute));
            VoidDataAnnotationsModelValidatorProvider.AddRule((o) => o.ConfirmPassword, typeof(System.ComponentModel.DataAnnotations.CompareAttribute));

            ModelValidatorProviders.Providers.RemoveAt(0);
            ModelValidatorProviders.Providers.Insert(0, new VoidDataAnnotationsModelValidatorProvider());
        }

こんなのを書いて、標準のプロバイダーを差し替えましょう。
差し替える上に、モデルクラスと無効にしたい属性を指定しておきましょう。
AddRuleで除外したいものを登録するように書いたけど、そこはExpression使えたほうがなんかかっこいいから、ちょっとそんな雰囲気も出してみたよ。

そうすると、この場合カスタムコードはGlobal.asaxのみ。デプロイアセンブリは共通がいいなら、App_Codeにコードファイルをコピーでデプロイ。どっちでも同じですね。

これをVS2013でMVCプロジェクトを新規にテンプレートから作成したものに張り付けて動かすと、ユーザー登録のところで、ユーザ名必須が外れて、パスワード長の制限が外れて、パスワード確認用の一致チェックも外れる。けど、登録自体には失敗するからね。それはバリデーションの位置が違うので今回はほっときます。


わかりにくいけど↑こんな動き。

マルチテナントって楽しいね!

dotnetConf2015 Japan

https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。