Submit Search
Mono is Dead
•
57 likes
•
17,680 views
M
melpon
Follow
Monoは死んだ
Read less
Read more
1 of 104
Download now
Downloaded 27 times
More Related Content
Mono is Dead
1.
Mono is Dead ∼高速なC#サーバを目指して∼
2.
もくじ • Introduction • C#で1万Clientを捌く •
Mono is Dead
3.
Introduction
4.
やったこと • 1対1で対戦する • TCPの •
ゲームサーバを • C#(Mono)で作った
5.
結果 • 1サーバあたり10,000クライアント程度 ならいける • 無事iOSとAndroidにリリースして、ちゃ んと動いてる
6.
長く苦しい戦いだった… • 苦しかったのは主にMonoのせい • 今日はMonoをdisります •
が、その前に前提知識(async/await構 文)の共有をします
7.
Q&A • なんでC#なの? • クライアントがC#(Unity)で書かれて いて、一部のロジックを共有したかっ た
8.
Q&A • P2Pでやらないのはなぜ? • 改ざんに対する処置がゲームサーバ作 るよりめんどそう
9.
Q&A • .NET Framework使わないの? •
Windowsを運用するのはとてもつらい
10.
Q&A • なんでTCPなの?UDPじゃだめなの? • UDPはつらい •
パケット組み立てるのつらい • 再送信処理を実装するのつらい • 送信元を識別するのつらい • 切断検知つらい
11.
C#で1万Clientを捌く
12.
C#でTCP通信 • お題: 5バイトのデータを受け取り、そ のデータをそのまま返すサーバを作ろ う •
つまり劣化版Echoサーバ
13.
同期版 TcpClient client =
// Accept部分は省略 Stream stream = client.GetStream(); while (true) { var buf = new buf[5]; stream.Read(buf, 0, buf.Length); stream.Write(buf, 0, buf.Length); }
14.
ダメ • Readでbuf.Lengthだけ読める保証は無い • Readの戻り値が0の時は終端だったりエ ラーの場合なので例外を投げる •
例外の処理をしよう
15.
同期版(改) static void ReadFully(this
Stream stream, byte[] buf) { var readBytes = 0; while (readBytes != buf.Length) { var n = stream.Read(buf, readBytes, buf.Length - readBytes); if (n == 0) throw new Exception("hoge-"); readBytes += n; } }
16.
同期版(改) try { while (true)
{ var buf = new buf[5]; stream.ReadFully(buf); stream.Write(buf, 0, buf.Length); } } catch (Exception e) { // 例外出たらログを残して接続を閉じる logger.Error("error", e); client.Close(); }
17.
簡単 • 同期的に書くのはとても簡単 • 例外処理を含めても簡単
18.
だがしかし
19.
だがしかし CPUが全く働いてない
20.
だがしかし • 呼び出したスレッドが止まる • 8スレッドで動かした場合、8クライアントが Readするだけで全部止まる •
たかだかスレッド数までしかクライアントを 捌けない
21.
スレッド増やせば? • メモリが足りない • スタック領域だけで最低256KBぐらい •
コンテキストスイッチで時間が掛かる • なので1万クライアントをスレッドで捌くのは きつい • いわゆるC10K問題
22.
つまり • 1万クライアントをスレッドで同期処 理するのは無理
23.
そこで • 非同期処理 • C#には非同期処理用の関数がある •
BeginRead, EndRead, BeginWrite, EndWrite • これを使えば解決できるはず
24.
同期版(再掲) try { while (true)
{ var buf = new buf[5]; stream.ReadFully(buf); stream.Write(buf, 0, buf.Length); } } catch (Exception e) { // 例外出たらログを残して接続を閉じる logger.Error("error", e); client.Close(); }
25.
非同期版 var buf =
new byte[5]; Func<IAsyncResult> func; func = ar1 => { stream.EndReadFully(ar1); stream.BeginWrite(buf, 0, buf.Length, ar2 => { stream.EndWrite(ar2); // 再度BeginReadを始める(whileループ相当) stream.BeginReadFully(buf, 0, buf.Length, func, null); }; }; stream.BeginReadFully(buf, 0, buf.Length, func, null); 完全に別コード
26.
非同期版 var buf =
new byte[5]; Func<IAsyncResult> func; func = ar1 => { stream.EndReadFully(ar1); stream.BeginWrite(buf, 0, buf.Length, ar2 => { stream.EndWrite(ar2); // 再度BeginReadを始める(whileループ相当) stream.BeginReadFully(buf, 0, buf.Length, func, null); }; }; stream.BeginReadFully(buf, 0, buf.Length, func, null); どうやって実装するのか分かりませんでした
27.
非同期版 var buf =
new byte[5]; Func<IAsyncResult> func; func = ar1 => { stream.EndReadFully(ar1); // 例外処理どうしよう stream.BeginWrite(buf, 0, buf.Length, ar2 => { stream.EndWrite(ar2); // 例外処理どうしよう // 再度BeginReadを始める(whileループ相当) stream.BeginReadFully(buf, 0, buf.Length, func, null); }; }; stream.BeginReadFully(buf, 0, buf.Length, func, null); 例外処理つらい
28.
非同期版 var buf =
new byte[5]; Func<IAsyncResult> func; func = ar1 => { stream.EndReadFully(ar1); stream.BeginWrite(buf, 0, buf.Length, ar2 => { stream.EndWrite(ar2); // 再度BeginReadを始める(whileループ相当) stream.BeginReadFully(buf, 0, buf.Length, func, null); }; }; stream.BeginReadFully(buf, 0, buf.Length, func, null); whileループすら再帰とか…
29.
結論 • 非同期処理はつらい
30.
そこで • async/await構文 • C#
5.0 で入った新しい非同期処理
31.
同期版(再掲) static void ReadFully( this
Stream stream, byte[] buf) { var readBytes = 0; while (readBytes != buf.Length) { var n = stream.Read(buf, readBytes, buf.Length - readBytes); if (n == 0) throw new Exception("hoge-"); readBytes += n; } }
32.
async/await版 static async Task
ReadFullyAsync( this Stream stream, byte[] buf) { var readBytes = 0; while (readBytes != buf.Length) { var n = await stream.ReadAsync(buf, readBytes, buf.Length - readBytes) .ConfigureAwait(false); if (n == 0) throw new Exception("hoge-"); readBytes += n; }; }
33.
差分 static async Task
ReadFullyAsync( this Stream stream, byte[] buf) { var readBytes = 0; while (readBytes != buf.Length) { var n = await stream.ReadAsync(buf, readBytes, buf.Length - readBytes) .ConfigureAwait(false); if (n == 0) throw new Exception("hoge-"); readBytes += n; }; }
34.
同期版(再掲) try { while (true)
{ var buf = new buf[5]; stream.ReadFully(buf); stream.Write(buf, 0, buf.Length); } } catch (Exception e) { // 例外出たらログを残して接続を閉じる logger.Error("error", e); client.Close(); }
35.
async/await版 try { while (true)
{ var buf = new buf[5]; await stream.ReadFullyAsync(buf) .ConfigureAwait(false); await stream.WriteAsync(buf, 0, buf.Length) .ConfigureAwait(false); } } catch (Exception e) { // 例外出たらログを残して接続を閉じる logger.Error("error", e); client.Close(); }
36.
差分 try { while (true)
{ var buf = new buf[5]; await stream.ReadFullyAsync(buf) .ConfigureAwait(false); await stream.WriteAsync(buf, 0, buf.Length) .ConfigureAwait(false); } } catch (Exception e) { // 例外出たらログを残して接続を閉じる logger.Error("error", e); client.Close(); }
37.
async/awaitは良い • 同期版とほぼ同じように書ける • 例外処理も簡単に書ける
38.
実際の動き • スレッドの代わりにタスクと呼ばれる 単位で動作する • タスク自体はメモリをほぼ使わない •
いわゆる軽量スレッド • awaitする度にタスクを処理するスレッ ドが変わる
39.
実際の動き try { while (true)
{ var buf = new buf[5]; await stream.ReadFullyAsync(buf) .ConfigureAwait(false); await stream.WriteAsync(buf, 0, buf.Length) .ConfigureAwait(false); } } catch (Exception e) { // 例外出たらログを残して接続を閉じる logger.Error("error", e); client.Close(); } Thread1 Thread2
40.
実際の動き
41.
実際の動き タスク9個の場合
42.
Q&A • ConfigureAwait(false)って何? • これが無いと、完了通知先が必ず呼 び出し元のスレッドになる •
UI処理する場合は便利だけど、ス レッド待ちで遅くなるし、デッドロッ クが起きる可能性もある
43.
async/awaitは良い • 同期版とほぼ同じように書ける • 例外処理も簡単に書ける •
メモリをほとんど使わない (new!) • CPUを使いきれる (new!) • つまり1万クライアント捌ける
44.
async/awaitまとめ • async/awaitを使って • 高速で •
書きやすい • C#サーバ • これは…いける!
45.
async/awaitまとめ
46.
Mono is Dead ∼本編始まるよ!∼
47.
.NET is Alive •
MicrosoftVisual C#を使って実装 • VC#を使っている時点ではほぼ問題な く実装できてた
48.
Mono is Dead •
VC#ではうまく動いていたexeをMonoで 実行すると…
49.
Mono is Dead •
VC#ではうまく動いていたexeをMonoで 実行すると… _人人人人人人_ > 突然の死 <  ̄Y^Y^Y^Y^Y ̄
50.
LogicalSetData で死ぬ
51.
LogicalSetData で死ぬ • System.Runtime.Remoting.Messaging.CallC ontext.LogicalSetData •
TLS(Thread Local Storage)のタスク版み たいなやつ(正確には違うけど) • タスク単位のグローバル変数っぽいの を作りたい場合、これに頼るしか無い (はず)
52.
LogicalSetData で死ぬ • VC#では問題なく動いていたのに、 Monoだとおかしな動作をする •
Monoのソースコードを眺めてみると…
53.
LogicalSetData で死ぬ [ThreadStatic] static
Hashtable logicalDatastore; static void LogicalSetData(string name, object data) { var r = logicalDatastore; if (r == null) r = logicalDatastore = new Hashtable(); r[name] = data; } Mono-3.2.8のソースより
54.
LogicalSetData で死ぬ [ThreadStatic] static
Hashtable logicalDatastore; static void LogicalSetData(string name, object data) { var r = logicalDatastore; if (r == null) r = logicalDatastore = new Hashtable(); r[name] = data; } ただのTLS実装になっている そんな実装で大丈夫か? Mono-3.2.8のソースより
55.
LogicalSetData で死ぬ • Issueにも報告さ れている •
Mono 4.0以降で 直っていること が分かった
56.
LogicalSetData で死ぬ • タスク単位のグローバル変数を使う場 合はMono
4.0以降が必須 • Mono 4.0は2015年5月4日にリリース • 公式パッケージに入ってない可能性が あるので気をつけよう
57.
LogicalSetData で死ぬ • 結論:
Mono 3.x は死ぬべき
58.
キャンセルトークンで死ぬ
59.
キャンセルトークンで死ぬ • ReadAsyncやWriteAsyncなどの非同期処 理を中断する機能 • 主にタイムアウトの為に使う
60.
キャンセルトークンで死ぬ public virtual Task<int>
ReadAsync( byte[] buffer, int offset, int count, CancellationToken cancellationToken) // Task1 CancellationToken token = tokenSource.Token; await stream.ReadAsync(buf, 0, size, token); // Task2 // ReadAsyncの処理を中断させる tokenSource.Cancel();
61.
キャンセルトークンで死ぬ • これでいけそうに見える • が、実際はI/O処理を中断できない •
NetworkStreamがキャンセルトークンに 対応してない • え、タイムアウトどうやって実現する の?
62.
キャンセルトークンで死ぬ • Stack Overflow曰く •
「タイムアウトになったら別タスクで Closeすりゃ中断できるよ」 • これなら…いける!
63.
_人人人人人人_ > 突然の死 <  ̄Y^Y^Y^Y^Y ̄ キャンセルトークンで死ぬ
64.
キャンセルトークンで死ぬ static async Task
TestConnect() { try { var client = new TcpClient(); var task = Task.Run( () => client.ConnectAsync("localhost", 8080)); client.Close(); await task.ConfigureAwait(false); } catch (Exception) { } } これを10万回ぐらい呼び出すと大体死ぬ 全体コード
65.
キャンセルトークンで死ぬ • そもそもNetworkStreamは仕様的にはス レッドセーフではない • なので死ぬのは仕方がないという気も する •
そしてタイムアウト実現は振り出しに 戻る
66.
キャンセルトークンで死ぬ • 結局キューに詰めて一本化することで 何とかした
67.
キャンセルトークンで死ぬ • ReadAsyncやCloseをリクエストキュー に詰めて、 • ワーカータスクがそれを処理し、 •
結果をリプライキューに詰めたのを、 • 呼び出し元がリプライキューの結果を 読む
68.
キャンセルトークンで死ぬ public async Task<int>
ReadAsync( byte[] buf, int offset, int length, CancellationToken token) { // リクエストキューに詰めて await requestQueue.Enqueue(new Request() { Type = Operation.ReadAsync, Buf = buf, Offset = offset, Length = length, }).ConfigureAwait(false); // レスポンスキューに結果が返ってくるのを待つ var resp = await responseQueue.Dequeue(token) .ConfigureAwait(false); return resp.ReadResult; }
69.
キャンセルトークンで死ぬ public async Task<int>
ReadAsync( byte[] buf, int offset, int length, CancellationToken token) { // リクエストキューに詰めて await requestQueue.Enqueue(new Request() { Type = Operation.ReadAsync, Buf = buf, Offset = offset, Length = length, }).ConfigureAwait(false); // レスポンスキューに結果が返ってくるのを待つ var resp = await responseQueue.Dequeue(token) .ConfigureAwait(false); return resp.ReadResult; } responseQueueがCancellationTokenに対応してればいい
70.
キャンセルトークンで死ぬ async void Run()
{ while (true) { // リクエストを受け取り var op = await requestQueue.Dequeue().ConfigureAwait(false); // リクエスト毎の処理をして switch (op.Type) { case Operation.ReadAsync: var result = await client.ReadAsync( op.Buf, op.Offset, op.Length).ConfigureAwait(false); // レスポンスを返す await responseQueue.Enqueue(resp) .ConfigureAwait(false); break; case Operation.WriteAsync: ... } }
71.
キャンセルトークンで死ぬ • キューから1個ずつ取ってきて処理す るので、同時にReadAsyncやCloseが呼ば れたりしない • responseQueueがキャンセルトークンに 対応してるので、無事タイムアウト処 理ができるようになった
72.
キャンセルトークンで死ぬ • 結論: Monoは通信のタイムアウトすら簡 単に対応できない
73.
補足 • キャンセルトークンが使えないのは VC#でも同じ • ただし別タスクからCloseを呼び出しま くっても落ちなかった
74.
Q&A • これってちゃんとClose呼ばれるの? • 呼ばれないこともある •
でもソケットはSafeHandleなのでファ イナライザがうまいことやってくれる
75.
BufferBlockで死ぬ
76.
BufferBlockで死ぬ public async Task<int>
ReadAsync( byte[] buf, int offset, int length, CancellationToken token) { // リクエストキューに詰めて await requestQueue.Enqueue(new Request() { Type = Operation.ReadAsync, Buf = buf, Offset = offset, Length = length, }).ConfigureAwait(false); // レスポンスキューに結果が返ってくるのを待つ var resp = await responseQueue.Dequeue(token) .ConfigureAwait(false); return resp.ReadResult; } requestQueueとresponseQueueはどうやって作るの?
77.
BufferBlockで死ぬ • Queue<T>はスレッドセーフではない • ConcurrentQueue<T>はawaitで待てない •
async/awaitに対応したキューが必要
78.
BufferBlockで死ぬ • System.Threading.Tasks.Dataflow.BufferBlo ck が使えそう •
BufferBlock.SendAsync(data, token)で送信 • BufferBlock.ReceiveAsync(token)で受信 • キャンセルトークンが使える!
79.
_人人人人人人_ > 突然の死 <  ̄Y^Y^Y^Y^Y ̄ BufferBlockで死ぬ
80.
BufferBlockで死ぬ static async Task
SendTask(BufferBlock<string> bb) { for (int i = 0; i < 10000; i++) { await bb.SendAsync(i.ToString()).ConfigureAwait(false); await Task.Delay(1).ConfigureAwait(false); } } static async Task ReceiveTask(BufferBlock<string> bb) { for (int i = 0; i < 10000; i++) { try { await bb.ReceiveAsync(TimeSpan.FromMilliseconds(1)) .ConfigureAwait(false); } catch (Exception) { } } } SendTaskとReceiveTaskを同時に実行すると大体死ぬ 全体コード
81.
BufferBlockで死ぬ • 普通にバグ • ReceiveAsyncのタイムアウト時の処理で レースコンディション起こしてる
82.
BufferBlockで死ぬ • 結局自作した • AsyncMutex •
AsyncConditionVariable • AsyncQueue • 今のところ問題なく動いてる
83.
BufferBlockで死ぬ • 結論: Monoはまともに使えないライブ ラリを提供している
84.
SGenで死ぬ
85.
SGenで死ぬ • Mono曰く • SGen
is a new and powerful garbage collector.
86.
SGenで死ぬ _人人人人人人_ > 突然の死 <  ̄Y^Y^Y^Y^Y ̄
87.
SGenで死ぬ • しばらく動かしてると唐突に死ぬ • スタックトレースを見るとSGenの関数 内で死んでる •
new and powerful ェ…
88.
SGenで死ぬ • 古き良き Boehm
GC を使うと死ななく なった • ただしメモリが4GBまでしか使えない • 複数プロセス起動することで対応
89.
SGenで死ぬ • 結論: Monoはnew
and powerful (笑) な GCを提供している
90.
mmap(NONE)で死ぬ
91.
mmap(NONE)で死ぬ _人人人人人人_ > 突然の死 <  ̄Y^Y^Y^Y^Y ̄
92.
mmap(NONE)で死ぬ • しばらく動かしていると • mmap(...PROT_NONE...)
failed • というエラーを出して死ぬ
93.
mmap(NONE)で死ぬ • Issueにあるように、コンパイラのビル ド時に-DUSE_MMAPと-DUSE_MUNMAP を外す必要がある • Monoをソースからビルドし直し
94.
mmap(NONE)で死ぬ • Monoはしばらく動かしてると大体死ぬ
95.
メモリリークで死ぬ
96.
メモリリークで死ぬ
97.
メモリリークで死ぬ • 未解決問題 • 負荷が掛かっている場合だけメモリ使 用量が増え続ける •
ゲームサーバのコードが悪いせいなの かどうか分からない
98.
メモリリークで死ぬ • PHP製作者曰く: • 「僕なら、10リクエストごとにApache を再起動しますね。」 •
ということで、一定量動かしたらプロ セスを再起動するようにした
99.
メモリリークで死ぬ
100.
メモリリークで死ぬ • 結論: Monoはメモリリークを起こしてい るかも、と疑心暗鬼になるだけの下地 がある
101.
まとめ • Monoは人類には早かった • 次やるならクライアントをホストにす ると思う
102.
まとめ • とはいえ、async/awaitのおかげでサーバ のコードは相当短くなった • 全部で5,000行程度 •
※クライアントとの共通コードは除く •言い換えれば5,000行程度でMonoが死にま くったという…
103.
まとめ • Monoは(人間とプロセスが)死ぬ • 覚悟を持って使いましょう
104.
おわり
Download