[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
SlideShare a Scribd company logo
.NET最先端技術による
ハイパフォーマンスウェブアプリケーション
株式会社グラニ
取締役CTO
河合 宜文 - @neuecc
http://neue.cc/
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がない!)
• その他その他、あげればキリがない
7 7
Infrastructure
8
AWSを利用した現在(PHP)の構成
9
AWSを利用した現在(PHP)の構成
200台辛い…
10
C#に変わった時の構成
はたして何台に削減で
きているでしょうか!
Memcachedはさようなら
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
Database
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変数
21 21
Redis
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-netDataContract 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
27 27
Asynchronous
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開発では存在しない場合もあることが前提
• とはいえ当然避けられないので、色々回り道を模索しよう
39 39
Parallel
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で最
初から増やしてしまうのが効果的
45 45
Monitoring
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通知
50
• スローリクエスト時の完全なスタックトレースが見れる
• 未処理例外も非常に見やすく
• グラフ化、詳細画面
• 件数ソート・フィルタ
• SQLのスロークエリや割合なども
51
52 52
Conclusion
53
まとめ
• シンプルに、シンプルに、シンプルに
• DB-Redisのみの構成、Micro-ORM、単純なのはいいこと
• 外に任せられるものは積極的に出して活用する
• AWS, NewRelic, SumoLogic,etc.
• 自前で組むよりも遥かに簡単で、遥かに高性能
• なお、リポジトリ管理はGitHubのBusinessプランを利用している
• 環境は常に最新に
• 言語は、環境は進化を続けている、全力で受け入れよう
• C# 5.0はasyncを中心に非常に強力
54 54
We’re Hiring
http://grani.jp/recruit.html
.NET最先端技術によるハイパフォーマンスウェブアプリケーション

More Related Content

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