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

43,474 views

Published on

Build Insider Offline #1

Published in: Technology
0 Comments
78 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
43,474
On SlideShare
0
From Embeds
0
Number of Embeds
19,591
Actions
Shares
0
Downloads
0
Comments
0
Likes
78
Embeds 0
No embeds

No notes for slide

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

  1. 1. .NET最先端技術によるハイパフォーマンスウェブアプリケーション株式会社グラニ取締役CTO河合 宜文 - @neuecchttp://neue.cc/
  2. 2. 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/
  3. 3. 3グラニについて• 株式会社グラニ• http://grani.jp/• ソーシャルゲーム開発• 去年9月に設立→今年1月に「神獄のヴァルハラゲート」リリース• GREE FP版ランキング1位• 会員数60万人突破• CM放送
  4. 4. 4ソーシャルゲームの規模感• 普通のウェブアプリケーション• ただしユーザーの1クリックの度にDB更新が入るなど負荷が高い• 1ユーザーのPV数、滞在時間も通常のウェブに比べて長い• ピーク時5000リクエスト/sec以上• デイリーで1億リクエスト以上• 非常に高負荷のかかるウェブアプリケーション• 神獄のヴァルハラゲートはPHPで動いてる(所謂LAMP)• え?• え?え?
  5. 5. 5PHP→C#• なぜPHP?• 諸事情あって• 現在C#に全面移行作業中• この発表までには間に合いませんでした!• なので実例、ではないですが、まあPHPで実績ありますので……• リリース後にはReal Worldな実例としてまたどこかで
  6. 6. 6なぜC#?• パフォーマンス上の問題• 圧倒的な皮下脂肪率• 台数増えすぎによる影響• 但しCakePHPというクソ重いフレームワークのせいもあり• 開発効率の問題• コンパイルエラーで発見できるものが見落とされる• 何でもハッシュに詰めるしかないのでIntelliSenseが利かなすぎる• 貧弱なコレクション処理(LINQがない!)• その他その他、あげればキリがない
  7. 7. 7 7Infrastructure
  8. 8. 8AWSを利用した現在(PHP)の構成
  9. 9. 9AWSを利用した現在(PHP)の構成200台辛い…
  10. 10. 10C#に変わった時の構成はたして何台に削減できているでしょうか!Memcachedはさようなら
  11. 11. 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的な使い方
  12. 12. 12 12Database
  13. 13. 13なぜリレーショナルデータベースを使うのか• NoSQLでいい?• Azure Table, Riak, Dynamoなど無限にスケールするし?• 機能面では満たせるかもしれないが、依然として選べない• 少なくともヴァルハラゲートの規模で、何とかなっている• 水平分割が始まったらさすがに苦しいのですが、まだ垂直だけで済んでる• 利点• ちょっとSQL叩いてのカジュアルな解析• データの弄りやすさ• 周辺ツールの充実具合• を、補足できるだけの仕掛けがない限りはRDBを選択する
  14. 14. 14DB側の性能問題対策のための垂直分割• 一台では負荷に耐えられないので機能単位での垂直分割• ユーザー情報/ギルド情報/バトル情報、みたいな分け方• 現在6分割• テーブルが物理的別DBに分かれるため外部キーが張れない• よって一切、外部キーは使っていない• クエリに若干の制限(DBを超えたジョインが不可能)• 水平分割は無限にスケールするが最終手段として極力避ける• 記述可能なクエリにかなり制限がかかる• アプリケーション側での分割制御の手間がかかる• アドホックなクエリでの集計が不可能になる
  15. 15. 15MySQLのツール• HeidiSQL• クエリ書いたりテーブル定義したりエクスポートしたり• MySQL Workbenchよりも使いやすい• JetProfiler• リアルタイムなプロファイラ• Linux, Mac, Windows
  16. 16. 16C#からのDB取り扱い事情• EntityFrameworkなどの重量級O/Rマッパは不採用• DBの垂直分割により外部キーによるリレーションが存在しないので、ORMのクエリ生成が生かせない• マスタを積極的にキャッシュするなど、インメモリ結合が中心となるため、ORMのジョインの抽象化が全く活かせない• そのため単純なクエリが多いため、素のSQLでもあまり苦はない• デザイナなどORMのメンテナンスがコスト高• 遅い
  17. 17. 17Micro-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 });
  18. 18. 18性能比較• HandCodedはADO.NETのDataReaderを回してデータを読み取る• DapperはHandCodedとほぼ変わらない• ※EFは古いバージョンのため最新版ではもう少し性能改善されています55 56120900HANDCODED DAPPER EF(COMPILED) ENTITYFRAMEWORK
  19. 19. 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 });}
  20. 20. 20キャッシュの単位永久に保存する領域 – データベースなど期間保存 – Memcached/RedisなどExpire付きリクエスト単位 - HttpContext.Itemsアプリケーション単位 – Static変数
  21. 21. 21 21Redis
  22. 22. 22Redisとは• オンメモリで動作するデータストア• 単純なKey-Valueのデータ型のほかに、リスト・ハッシュ・ソート済みセット・セットといったデータ構造を扱える• RDBの不得意な部分を補える• 単体での高パフォーマンス・分散可能なのでキャッシュ用途に• SortedSetによるリアルタイムランキングなど• 詳しくはBuild Insiderの特集で記事を書いたのでそちらを• 高パフォーマンスなKey-Valueストア「Redis」活用術 - C#のRedisライブラリ「BookSleeve」の利用法• http://www.buildinsider.net/small/rediscshap/01
  23. 23. 23シリアライズ• Redisでキャッシュする際のオブジェクトのシリアライズ形式はprotobuf-netを採用• Protocol Buffersはサイズ・速度ともに優秀• 速度はフォーマットと実装で決まる• なのでC++やRubyでの性能比較は.NETにもあてはまるとは限らない• .NET実装のprotobuf-netは実績もあり安定感あるBinaryFormatter Protobuf-netDataContract JSON.NET MsgPack-CLI
  24. 24. 24セッションストア• アプリケーションサーバーが複数台となるため、インメモリなデフォルトのセッションは使えない• セッションストアとしてRedisを採用• ただしASP.NETのセッションプロバイダとしては実装していない• Protobuf-netによるジェネリックなデシリアライズが必要なため• やろうと思えば出来ないこともないですけど……• 実装にかなり手間がかかる• よって、簡易的な俺々Redisセッションストアを作成
  25. 25. 25パイプライン• Redisの特徴としてパイプラインのサポートがある• 例えば三回データを取得するとき• コマンド通信(GET)->結果受信(RES),• コマンド通信(GET)->結果受信(RES),• コマンド通信(GET)->結果受信(RES)• パイプラインだと• コマンド通信(GET,GET,GET)->結果受信(RES,RES,RES)• 送受信の通信コストが一度だけで済む
  26. 26. 26BookSleeve• C#製のRedisライブラリ• https://code.google.com/p/booksleeve/• 特徴は全てが非同期、全てがパイプライン• 全リクエストがコネクションを共有する• あらゆるリクエストのコマンドが自動的にパイプライン化されて非同期通信するので、同時アクセスがあればあるほど効率的• 扱いやすいよう上層のライブラリを作成・利用• BookSleeveは全てがbyte[]なので、シリアライズしたりなど• https://github.com/neuecc/CloudStructures
  27. 27. 27 27Asynchronous
  28. 28. 28同期的シチュエーション• GetAでRedisやDBアクセスなどがあり10msかかるとする• 三回アクセスするので、30msかかってるvar a = GetA(); // 10msvar b = GetB(); // 10msvar c = GetC(); // 10ms// +30ms
  29. 29. 29非同期的シチュエーション• GetAAsyncなどで非同期でアクセスがある• 結果として10msかかるのはかわらない• 三回アクセスするので、30msかかってるvar a = await GetAAsync(); // 10msvar b = await GetBAsync(); // 10msvar c = await GetCAsync(); // 10ms// +30ms
  30. 30. 30非同期的シチュエーション2• Task.WhenAllで待つことで、非同期が同時に走ってる• 結果として1回分のアクセスである10msで済む• BookSleeveはこのような待ち方が容易なのが強いawait Task.WhenAll(GetAAsync(), GetBAsync(), GetCAsync()); // +10ms// +10ms
  31. 31. 31Lazy Revisited• 昔ながらのLazyなスタイル• プロパティに初回アクセスあった時に生成• Pros• 使うのが簡単• Cons• それが遅延なのか分からない• 何気なく呼んだらDBアクセスが!とかMyClass myProperty;public MyClass MyProperty{get{if (myProperty == null){myProperty = new MyClass();}return myProperty;}}
  32. 32. 32Lazy Revisited• Lazy<T>なスタイル• Pros• Lazyなのが明示的• Cons• 使うのが面倒(毎回.Value…)public Lazy<MyClass> MyProperty { get; private set; }public Toaru(){MyProperty = new Lazy<MyClass>(() => new MyClass());}
  33. 33. 33AsyncLazy• AwaitableなLazy• オリジナルはMSのPfxチームから• http://blogs.msdn.com/b/pfxteam/archive/2011/01/15/10116210.aspx• ちょっとだけカスタマイズして使っていますvar person = new Person();var name = await person.Name; // awaitで初期化・取得できる// 複数同時初期化が可能await AsyncLazy.WhenAll(person1.Name, person2.Name, person3.Name);
  34. 34. 34AsyncLazy + Redis/DBpublic 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);
  35. 35. 35AsyncLazy + Redis/DBpublic 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);
  36. 36. 36AsyncLazy• Pros• BookSleeveの自動パイプラインと合わせて、作りこんだモデルクラスであっても、MGET的な効率的な取得ができる• DBのマルチスレッドによる同時初期化が自然に記述できる• Cons• awaitまみれで面倒くさい• ううむ……
  37. 37. 37非同期でのはまりどころ• TransactionScope内でawaitできない• 別スレッドになるので、実行時例外となる• 手動でBeginTransactionして回避• デッドロック• .Result/Waitで取るとデッドロックする場合がある• 全てasyncで通せばデッドロックしないけれど……• TransactionScope使いたいなら、その中で同期的に待つしかない• フィルターが非同期未対応なので、フィルター内で書く場合は待つしかない• 気をつけてデッドロックしないように記述する
  38. 38. 38HttpContext went away• HttpContext.Currentは基本取れる、と思っていた。• 割と消える、消えるときは消える• await hoge.ConfigureAwait(false); の下では消える• .ConfigureAwait(false)しなければいい、とは言いますがデッドロック避けのために必要な場合もあったり• HttpContext.Currentが存在することを前提にできない• ライブラリの挙動には要注意(中で使ってるかもしれないので)• これからのWeb開発では存在しない場合もあることが前提• とはいえ当然避けられないので、色々回り道を模索しよう
  39. 39. 39 39Parallel
  40. 40. 40Parallel.ForEach• WebアプリではTask+WhenAll中心なので出番なし• バッチ処理でのDBへの大量インサート/アップデートに利用• シングルスレッドで1時間→パラレルで5分• 劇的!• しかもforeachをParallel.ForEachに書き換えるだけ!• (インサートはバルクインサートと併用)
  41. 41. 41スレッドセーフコネクション• (My)SqlConnectionはスレッドセーフではない• Parallelの中で開く or 外側でThreadLocalに包んでスレッドセーフ扱いにする• 詳しくはWebで• 並列実行とSqlConnection• http://neue.cc/2013/03/09_400.htmlusing (var connection = DisposableThreadLocal.Create(() => { var conn = new{Parallel.For(1, 1000, x => { var _ = connection.Value.Query<DateTime}
  42. 42. 42注意点• 数百万件の処理程度でもMaxで100並列は十分超えるので(I/O待ちしている間に新しいTaskが自動的に立ち上がっていく)、コネクションプールの上限設定には気をつけること• デフォルトは100なので、大きめに見積もったほうがいいです• 怖ければ、開きっ放しじゃなく素直にOpen/Closeしましょう• 大雑把かつ富豪といえば富豪• 現代のリソースはあまってる場合はあまってる• どこが富豪にしてよくて、どこがケチらなきゃいけないかの見極め
  43. 43. 43Async?• ExecuteReaderAsync• 実はC#+MySQLでは意味がない• MySQLライブラリがDelegate.BeginInvokeにExecuteReaderを包んでるだけ……• お使いのDriverが正しく対応しているかどうか、確認を。• ただたんにTaskに包んだだけ、Delegate.BeginInvokeに包んだだけ、そんな可能性は十分にあります• そうでなくても楽さ(スレッドとかちょっと割とかなりいっぱい立ち上がる程度)を鑑みて全然アリ
  44. 44. 44APIアクセス• 今時だと使うのはHttpClient一択• HttpClient詳解 http://www.slideshare.net/neuecc/httpclient• バッチからなど、大量に叩く必要がある時は?• Parallel.ForEachで叩きまくる• 3時間かかってた処理がたった5分に!• 非同期に統一してTask.WhenAllだと量を適度に絞るのがメンドウクサイ、どうせスレッド余裕なわけだしリソース消費も許せるので制御はおまかせ• 但しThreadPoolが増えるの遅い• ThreadPoolが増えるタイミングは即時じゃない• IO待ちだと分かりきってるのでThreadPool.SetMinThreadで最初から増やしてしまうのが効果的
  45. 45. 45 45Monitoring
  46. 46. 46MiniProfiler• .NET/Ruby用のシンプルな画面統合プロファイラ• PM > Install-Package MiniProfiler• レスポンスタイムとDBのプロファイリングが行える
  47. 47. 47ログ出し• ロガーはNLogを利用• 画面下部にもログ書き出し• HttpContext単位で保持するカスタムのロガーを作成• (GitのRevisionなども見えるように)• Redis発行はキーとレスポンスタイムを全部ログ取り
  48. 48. 48数字は常に見えるところに• 何がどの程度速いのか、遅いのか常に意識できるように• 肌感覚を養う• 開発環境も本番と同様のネットワーク構成にする• ネットワークによってはRedisがDBより遅いとか出てしまう• 例えばRedisが10msでDBが1msになるとか• そうするとRedisにキャッシュしないほうが速いじゃん!とかなる• 意味ない• この辺の構築を行いやすいのがクラウドは良い
  49. 49. 49• PHP, Ruby, .NET, Java, Pythonに対応したパフォーマンス監視サービス• インストールも超簡単(インストーラ叩くだけ)• 閾値(エラーレートやレスポンスタイムの低下)などを設定してiPhoneアプリからのPush通知
  50. 50. 50• スローリクエスト時の完全なスタックトレースが見れる• 未処理例外も非常に見やすく• グラフ化、詳細画面• 件数ソート・フィルタ• SQLのスロークエリや割合なども
  51. 51. 51
  52. 52. 52 52Conclusion
  53. 53. 53まとめ• シンプルに、シンプルに、シンプルに• DB-Redisのみの構成、Micro-ORM、単純なのはいいこと• 外に任せられるものは積極的に出して活用する• AWS, NewRelic, SumoLogic,etc.• 自前で組むよりも遥かに簡単で、遥かに高性能• なお、リポジトリ管理はGitHubのBusinessプランを利用している• 環境は常に最新に• 言語は、環境は進化を続けている、全力で受け入れよう• C# 5.0はasyncを中心に非常に強力
  54. 54. 54 54We’re Hiringhttp://grani.jp/recruit.html

×