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