Upgrade to Pro — share decks privately, control downloads, hide ads and more …

.NET最先端技術によるハイパフォーマンスウェブアプリケーション

 .NET最先端技術によるハイパフォーマンスウェブアプリケーション

2013-06-08, Build Insider Offline #1

Yoshifumi Kawai

June 08, 2013
Tweet

More Decks by Yoshifumi Kawai

Other Decks in Technology

Transcript

  1. 2 自己紹介 • @仕事 • 河合 宜文(Kawai Yoshifumi) • 株式会社グラニ

    取締役CTO • 技術的な目標としては、C#で日本を代表する会社にする! • @個人活動 • Microsoft MVP for Visual C# • Web http://neue.cc/ • Twitter @neuecc • JavaScriptにLINQ to Objectsを移植したライブラリ作ってます • linq.js - http://linqjs.codeplex.com/
  2. 4 ソーシャルゲームの規模感 • 普通のウェブアプリケーション • ただしユーザーの1クリックの度にDB更新が入るなど負荷が高い • 1ユーザーのPV数、滞在時間も通常のウェブに比べて長い • ピーク時5000リクエスト/sec以上

    • デイリーで1億リクエスト以上 • 非常に高負荷のかかるウェブアプリケーション • 神獄のヴァルハラゲートはPHPで動いてる(所謂LAMP) • え? • え?え?
  3. 5 PHP→C# • なぜPHP? • 諸事情あって • 現在C#に全面移行作業中 • この発表までには間に合いませんでした!

    • なので実例、ではないですが、まあPHPで実績ありますので…… • リリース後にはReal Worldな実例としてまたどこかで
  4. 6 なぜC#? • パフォーマンス上の問題 • 圧倒的な皮下脂肪率 • 台数増えすぎによる影響 • 但しCakePHPという

    クソ重いフレームワークのせいもあり • 開発効率の問題 • コンパイルエラーで発見できる ものが見落とされる • 何でもハッシュに詰めるしかないのでIntelliSenseが利かなすぎる • 貧弱なコレクション処理(LINQがない!) • その他その他、あげればキリがない
  5. 11 基本構成 • Windows Server 2012(EC2) • AWSのWindows Serverインスタンス •

    IIS8 + .NET Framework 4.5 • RDS(MySQL) • AWSのマネージドなデータベースサービス • RDSにはSQL Serverもある • 1から作るのだったらSQL Serverを選ぶ、今回はPHPからの移植なので • Redis(EC2 Amazon Linux) • インメモリ型KVS • キャッシュ・セッション・その他NoSQL的な使い方
  6. 13 なぜリレーショナルデータベースを使うのか • NoSQLでいい? • Azure Table, Riak, Dynamoなど無限にスケールするし? •

    機能面では満たせるかもしれないが、依然として選べない • 少なくともヴァルハラゲートの規模で、何とかなっている • 水平分割が始まったらさすがに苦しいのですが、まだ垂直だけで済んでる • 利点 • ちょっとSQL叩いてのカジュアルな解析 • データの弄りやすさ • 周辺ツールの充実具合 • を、補足できるだけの仕掛けがない限りはRDBを選択する
  7. 14 DB側の性能問題対策のための垂直分割 • 一台では負荷に耐えられないので機能単位での垂直分割 • ユーザー情報/ギルド情報/バトル情報、みたいな分け方 • 現在6分割 • テーブルが物理的別DBに分かれるため外部キーが張れない

    • よって一切、外部キーは使っていない • クエリに若干の制限(DBを超えたジョインが不可能) • 水平分割は無限にスケールするが最終手段として極力避ける • 記述可能なクエリにかなり制限がかかる • アプリケーション側での分割制御の手間がかかる • アドホックなクエリでの集計が不可能になる
  8. 17 Micro-ORM • DataRow => Objectへの変換だけを担うもの • グラニではDapperを採用 • https://code.google.com/p/dapper-dot-net/

    • 文字列で生SQLを書いて<T>にマッピング、それだけ • 非常に高速 • Dapperだけだとプリミティブすぎるので簡単な上モノは用意しています • Dapperのシンプルさを損ねないよう、やりすぎないようシンプルに var dog = connection.Query<Dog>("select * from dogs where id = @id", new { id = 100 });
  9. 19 コネクションへの型付け • 物理的に台が異なるので、それぞれの台に対して型で分ける • 単純ですがミス防止やドキュメント的な意味で効果アリ • (MySQLなので)Master, Slaveを束ねるのも兼ねている public

    interface ITypedConnection : IDisposable { DbConnection Slave { get; } DbConnection Master { get; } } public BattleEntity SelectById(BattleConnection battle, int id) { return battle.Master.Query<BattleEntity>("select * from battle where id = @id", new { id }); }
  10. 22 Redisとは • オンメモリで動作するデータストア • 単純なKey-Valueのデータ型のほかに、リスト・ハッシュ・ ソート済みセット・セットといったデータ構造を扱える • RDBの不得意な部分を補える •

    単体での高パフォーマンス・分散可能なのでキャッシュ用途に • SortedSetによるリアルタイムランキングなど • 詳しくはBuild Insiderの特集で記事を書いたのでそちらを • 高パフォーマンスなKey-Valueストア「Redis」活用術 - C#の Redisライブラリ「BookSleeve」の利用法 • http://www.buildinsider.net/small/rediscshap/01
  11. 23 シリアライズ • Redisでキャッシュする際のオブジェクトのシリアライズ形 式はprotobuf-netを採用 • Protocol Buffersはサイズ・速度ともに優秀 • 速度はフォーマットと実装で決まる

    • なのでC++やRubyでの性能比較は .NETにもあてはまるとは限らない • .NET実装のprotobuf-netは 実績もあり安定感ある BinaryFormatter Protobuf-net DataContract JSON.NET MsgPack-CLI
  12. 24 セッションストア • アプリケーションサーバーが複数台となるため、インメモリ なデフォルトのセッションは使えない • セッションストアとしてRedisを採用 • ただしASP.NETのセッションプロバイダとしては実装して いない

    • Protobuf-netによるジェネリックなデシリアライズが必要なため • やろうと思えば出来ないこともないですけど…… • 実装にかなり手間がかかる • よって、簡易的な俺々Redisセッションストアを作成
  13. 25 パイプライン • Redisの特徴としてパイプラインのサポートがある • 例えば三回データを取得するとき • コマンド通信(GET)->結果受信(RES), • コマンド通信(GET)->結果受信(RES),

    • コマンド通信(GET)->結果受信(RES) • パイプラインだと • コマンド通信(GET,GET,GET)->結果受信(RES,RES,RES) • 送受信の通信コストが一度だけで済む
  14. 26 BookSleeve • C#製のRedisライブラリ • https://code.google.com/p/booksleeve/ • 特徴は全てが非同期、全てがパイプライン • 全リクエストがコネクションを共有する

    • あらゆるリクエストのコマンドが自動的にパイプライン化されて非 同期通信するので、同時アクセスがあればあるほど効率的 • 扱いやすいよう上層のライブラリを作成・利用 • BookSleeveは全てがbyte[]なので、シリアライズしたりなど • https://github.com/neuecc/CloudStructures
  15. 31 Lazy Revisited • 昔ながらのLazyなスタイル • プロパティに初回アクセスあった時に生成 • Pros •

    使うのが簡単 • Cons • それが遅延なのか分からない • 何気なく呼んだらDBアクセスが!とか MyClass myProperty; public MyClass MyProperty { get { if (myProperty == null) { myProperty = new MyClass(); } return myProperty; } }
  16. 32 Lazy Revisited • Lazy<T>なスタイル • Pros • Lazyなのが明示的 •

    Cons • 使うのが面倒(毎回.Value…) public Lazy<MyClass> MyProperty { get; private set; } public Toaru() { MyProperty = new Lazy<MyClass>(() => new MyClass()); }
  17. 33 AsyncLazy • AwaitableなLazy • オリジナルはMSのPfxチームから • http://blogs.msdn.com/b/pfxteam/archive/2011/01/15/101 16210.aspx •

    ちょっとだけカスタマイズして使っています var person = new Person(); var name = await person.Name; // awaitで初期化・取得できる // 複数同時初期化が可能 await AsyncLazy.WhenAll(person1.Name, person2.Name, person3.Name);
  18. 34 AsyncLazy + Redis/DB public AsyncLazy<string> Name { get; set;

    } public AsyncLazy<int> Age { get; set; } public Person() { Name = new AsyncLazy<string>(() => Redis.GetString("Name" + id)); Age = new AsyncLazy<int>(() => { using(var dbConn = …) { return dbConn.Query<int>(“select age from . where id = @id”); } } } // RedisがパイプラインでNameを同時初期化 await AsyncLazy.WhenAll(person1.Name, person2.Name, person3.Name); // DBがマルチスレッドでAgeを同時初期化 await AsyncLazy.WhenAll(person1.Age, person2.Age, person3.Age);
  19. 35 AsyncLazy + Redis/DB public AsyncLazy<string> Name { get; set;

    } public Person() { Name = new AsyncLazy<string>(() => { var name = Redis.GetString("Name" + id)); if(name == null) { using(var conn = new Connection()) { name = conn.Query<string>(); } } return name; }); } // データがあればRedisがパイプラインで、なければDBがマルチスレッドでNameを同時初期化 await AsyncLazy.WhenAll(person1.Name, person2.Name, person3.Name);
  20. 37 非同期でのはまりどころ • TransactionScope内でawaitできない • 別スレッドになるので、実行時例外となる • 手動でBeginTransactionして回避 • デッドロック

    • .Result/Waitで取るとデッドロックする場合がある • 全てasyncで通せばデッドロックしないけれど…… • TransactionScope使いたいなら、その中で同期的に待つしかない • フィルターが非同期未対応なので、フィルター内で書く場合は待つしかない • 気をつけてデッドロックしないように記述する
  21. 38 HttpContext went away • HttpContext.Currentは基本取れる、と思っていた。 • 割と消える、消えるときは消える • await

    hoge.ConfigureAwait(false); の下では消える • .ConfigureAwait(false)しなければいい、とは言いますがデッド ロック避けのために必要な場合もあったり • HttpContext.Currentが存在することを前提にできない • ライブラリの挙動には要注意(中で使ってるかもしれないので) • これからのWeb開発では存在しない場合もあることが前提 • とはいえ当然避けられないので、色々回り道を模索しよう
  22. 41 スレッドセーフコネクション • (My)SqlConnectionはスレッドセーフではない • Parallelの中で開く or 外側でThreadLocalに包んでスレッ ドセーフ扱いにする •

    詳しくはWebで • 並列実行とSqlConnection • http://neue.cc/2013/03/09_400.html using (var connection = DisposableThreadLocal.Create(() => { var conn = new { Parallel.For(1, 1000, x => { var _ = connection.Value.Query<DateTime }
  23. 43 Async? • ExecuteReaderAsync • 実はC#+MySQLでは意味がない • MySQLライブラリがDelegate.BeginInvokeにExecuteReaderを 包んでるだけ…… •

    お使いのDriverが正しく対応しているかどうか、確認を。 • ただたんにTaskに包んだだけ、Delegate.BeginInvokeに包んだだ け、そんな可能性は十分にあります • そうでなくても楽さ(スレッドとかちょっと割とかなりいっ ぱい立ち上がる程度)を鑑みて全然アリ
  24. 44 APIアクセス • 今時だと使うのはHttpClient一択 • HttpClient詳解 http://www.slideshare.net/neuecc/httpclient • バッチからなど、大量に叩く必要がある時は? •

    Parallel.ForEachで叩きまくる • 3時間かかってた処理がたった5分に! • 非同期に統一してTask.WhenAllだと量を適度に絞るのがメンドウクサイ、 どうせスレッド余裕なわけだしリソース消費も許せるので制御はおまかせ • 但しThreadPoolが増えるの遅い • ThreadPoolが増えるタイミングは即時じゃない • IO待ちだと分かりきってるのでThreadPool.SetMinThreadで最 初から増やしてしまうのが効果的
  25. 47 ログ出し • ロガーはNLogを利用 • 画面下部にもログ書き出し • HttpContext単位で保持する カスタムのロガーを作成 •

    (GitのRevisionなども見えるように) • Redis発行はキーと レスポンスタイムを全部ログ取り
  26. 48 数字は常に見えるところに • 何がどの程度速いのか、遅いのか常に意識できるように • 肌感覚を養う • 開発環境も本番と同様のネットワーク構成にする • ネットワークによってはRedisがDBより遅いとか出てしまう

    • 例えばRedisが10msでDBが1msになるとか • そうするとRedisにキャッシュしないほうが速いじゃん!とかなる • 意味ない • この辺の構築を行いやすいのがクラウドは良い
  27. 49 • PHP, Ruby, .NET, Java, Pythonに対応したパフォーマンス 監視サービス • インストールも超簡単(インストーラ叩くだけ)

    • 閾値(エラーレートやレスポンスタイムの低下)などを設定 してiPhoneアプリからのPush通知
  28. 51

  29. 53 まとめ • シンプルに、シンプルに、シンプルに • DB-Redisのみの構成、Micro-ORM、単純なのはいいこと • 外に任せられるものは積極的に出して活用する • AWS,

    NewRelic, SumoLogic,etc. • 自前で組むよりも遥かに簡単で、遥かに高性能 • なお、リポジトリ管理はGitHubのBusinessプランを利用している • 環境は常に最新に • 言語は、環境は進化を続けている、全力で受け入れよう • C# 5.0はasyncを中心に非常に強力