Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Mono is Dead

Monoは死んだ

  • Be the first to comment

Mono is Dead

  1. 1. Mono is Dead ∼高速なC#サーバを目指して∼
  2. 2. もくじ • Introduction • C#で1万Clientを捌く • Mono is Dead
  3. 3. Introduction
  4. 4. やったこと • 1対1で対戦する • TCPの • ゲームサーバを • C#(Mono)で作った
  5. 5. 結果 • 1サーバあたり10,000クライアント程度 ならいける • 無事iOSとAndroidにリリースして、ちゃ んと動いてる
  6. 6. 長く苦しい戦いだった… • 苦しかったのは主にMonoのせい • 今日はMonoをdisります • が、その前に前提知識(async/await構 文)の共有をします
  7. 7. Q&A • なんでC#なの? • クライアントがC#(Unity)で書かれて いて、一部のロジックを共有したかっ た
  8. 8. Q&A • P2Pでやらないのはなぜ? • 改ざんに対する処置がゲームサーバ作 るよりめんどそう
  9. 9. Q&A • .NET Framework使わないの? • Windowsを運用するのはとてもつらい
  10. 10. Q&A • なんでTCPなの?UDPじゃだめなの? • UDPはつらい • パケット組み立てるのつらい • 再送信処理を実装するのつらい • 送信元を識別するのつらい • 切断検知つらい
  11. 11. C#で1万Clientを捌く
  12. 12. C#でTCP通信 • お題: 5バイトのデータを受け取り、そ のデータをそのまま返すサーバを作ろ う • つまり劣化版Echoサーバ
  13. 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. 14. ダメ • Readでbuf.Lengthだけ読める保証は無い • Readの戻り値が0の時は終端だったりエ ラーの場合なので例外を投げる • 例外の処理をしよう
  15. 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. 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. 17. 簡単 • 同期的に書くのはとても簡単 • 例外処理を含めても簡単
  18. 18. だがしかし
  19. 19. だがしかし CPUが全く働いてない
  20. 20. だがしかし • 呼び出したスレッドが止まる • 8スレッドで動かした場合、8クライアントが Readするだけで全部止まる • たかだかスレッド数までしかクライアントを 捌けない
  21. 21. スレッド増やせば? • メモリが足りない • スタック領域だけで最低256KBぐらい • コンテキストスイッチで時間が掛かる • なので1万クライアントをスレッドで捌くのは きつい • いわゆるC10K問題
  22. 22. つまり • 1万クライアントをスレッドで同期処 理するのは無理
  23. 23. そこで • 非同期処理 • C#には非同期処理用の関数がある • BeginRead, EndRead, BeginWrite, EndWrite • これを使えば解決できるはず
  24. 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. 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. 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. 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. 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. 29. 結論 • 非同期処理はつらい
  30. 30. そこで • async/await構文 • C# 5.0 で入った新しい非同期処理
  31. 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. 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. 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. 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. 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. 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. 37. async/awaitは良い • 同期版とほぼ同じように書ける • 例外処理も簡単に書ける
  38. 38. 実際の動き • スレッドの代わりにタスクと呼ばれる 単位で動作する • タスク自体はメモリをほぼ使わない • いわゆる軽量スレッド • awaitする度にタスクを処理するスレッ ドが変わる
  39. 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. 40. 実際の動き
  41. 41. 実際の動き タスク9個の場合
  42. 42. Q&A • ConfigureAwait(false)って何? • これが無いと、完了通知先が必ず呼 び出し元のスレッドになる • UI処理する場合は便利だけど、ス レッド待ちで遅くなるし、デッドロッ クが起きる可能性もある
  43. 43. async/awaitは良い • 同期版とほぼ同じように書ける • 例外処理も簡単に書ける • メモリをほとんど使わない (new!) • CPUを使いきれる (new!) • つまり1万クライアント捌ける
  44. 44. async/awaitまとめ • async/awaitを使って • 高速で • 書きやすい • C#サーバ • これは…いける!
  45. 45. async/awaitまとめ
  46. 46. Mono is Dead ∼本編始まるよ!∼
  47. 47. .NET is Alive • MicrosoftVisual C#を使って実装 • VC#を使っている時点ではほぼ問題な く実装できてた
  48. 48. Mono is Dead • VC#ではうまく動いていたexeをMonoで 実行すると…
  49. 49. Mono is Dead • VC#ではうまく動いていたexeをMonoで 実行すると… _人人人人人人_ > 突然の死 <  ̄Y^Y^Y^Y^Y ̄
  50. 50. LogicalSetData で死ぬ
  51. 51. LogicalSetData で死ぬ • System.Runtime.Remoting.Messaging.CallC ontext.LogicalSetData • TLS(Thread Local Storage)のタスク版み たいなやつ(正確には違うけど) • タスク単位のグローバル変数っぽいの を作りたい場合、これに頼るしか無い (はず)
  52. 52. LogicalSetData で死ぬ • VC#では問題なく動いていたのに、 Monoだとおかしな動作をする • Monoのソースコードを眺めてみると…
  53. 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. 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. 55. LogicalSetData で死ぬ • Issueにも報告さ れている • Mono 4.0以降で 直っていること が分かった
  56. 56. LogicalSetData で死ぬ • タスク単位のグローバル変数を使う場 合はMono 4.0以降が必須 • Mono 4.0は2015年5月4日にリリース • 公式パッケージに入ってない可能性が あるので気をつけよう
  57. 57. LogicalSetData で死ぬ • 結論: Mono 3.x は死ぬべき
  58. 58. キャンセルトークンで死ぬ
  59. 59. キャンセルトークンで死ぬ • ReadAsyncやWriteAsyncなどの非同期処 理を中断する機能 • 主にタイムアウトの為に使う
  60. 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. 61. キャンセルトークンで死ぬ • これでいけそうに見える • が、実際はI/O処理を中断できない • NetworkStreamがキャンセルトークンに 対応してない • え、タイムアウトどうやって実現する の?
  62. 62. キャンセルトークンで死ぬ • Stack Overflow曰く • 「タイムアウトになったら別タスクで Closeすりゃ中断できるよ」 • これなら…いける!
  63. 63. _人人人人人人_ > 突然の死 <  ̄Y^Y^Y^Y^Y ̄ キャンセルトークンで死ぬ
  64. 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. 65. キャンセルトークンで死ぬ • そもそもNetworkStreamは仕様的にはス レッドセーフではない • なので死ぬのは仕方がないという気も する • そしてタイムアウト実現は振り出しに 戻る
  66. 66. キャンセルトークンで死ぬ • 結局キューに詰めて一本化することで 何とかした
  67. 67. キャンセルトークンで死ぬ • ReadAsyncやCloseをリクエストキュー に詰めて、 • ワーカータスクがそれを処理し、 • 結果をリプライキューに詰めたのを、 • 呼び出し元がリプライキューの結果を 読む
  68. 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. 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. 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. 71. キャンセルトークンで死ぬ • キューから1個ずつ取ってきて処理す るので、同時にReadAsyncやCloseが呼ば れたりしない • responseQueueがキャンセルトークンに 対応してるので、無事タイムアウト処 理ができるようになった
  72. 72. キャンセルトークンで死ぬ • 結論: Monoは通信のタイムアウトすら簡 単に対応できない
  73. 73. 補足 • キャンセルトークンが使えないのは VC#でも同じ • ただし別タスクからCloseを呼び出しま くっても落ちなかった
  74. 74. Q&A • これってちゃんとClose呼ばれるの? • 呼ばれないこともある • でもソケットはSafeHandleなのでファ イナライザがうまいことやってくれる
  75. 75. BufferBlockで死ぬ
  76. 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. 77. BufferBlockで死ぬ • Queue<T>はスレッドセーフではない • ConcurrentQueue<T>はawaitで待てない • async/awaitに対応したキューが必要
  78. 78. BufferBlockで死ぬ • System.Threading.Tasks.Dataflow.BufferBlo ck が使えそう • BufferBlock.SendAsync(data, token)で送信 • BufferBlock.ReceiveAsync(token)で受信 • キャンセルトークンが使える!
  79. 79. _人人人人人人_ > 突然の死 <  ̄Y^Y^Y^Y^Y ̄ BufferBlockで死ぬ
  80. 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. 81. BufferBlockで死ぬ • 普通にバグ • ReceiveAsyncのタイムアウト時の処理で レースコンディション起こしてる
  82. 82. BufferBlockで死ぬ • 結局自作した • AsyncMutex • AsyncConditionVariable • AsyncQueue • 今のところ問題なく動いてる
  83. 83. BufferBlockで死ぬ • 結論: Monoはまともに使えないライブ ラリを提供している
  84. 84. SGenで死ぬ
  85. 85. SGenで死ぬ • Mono曰く • SGen is a new and powerful garbage collector.
  86. 86. SGenで死ぬ _人人人人人人_ > 突然の死 <  ̄Y^Y^Y^Y^Y ̄
  87. 87. SGenで死ぬ • しばらく動かしてると唐突に死ぬ • スタックトレースを見るとSGenの関数 内で死んでる • new and powerful ェ…
  88. 88. SGenで死ぬ • 古き良き Boehm GC を使うと死ななく なった • ただしメモリが4GBまでしか使えない • 複数プロセス起動することで対応
  89. 89. SGenで死ぬ • 結論: Monoはnew and powerful (笑) な GCを提供している
  90. 90. mmap(NONE)で死ぬ
  91. 91. mmap(NONE)で死ぬ _人人人人人人_ > 突然の死 <  ̄Y^Y^Y^Y^Y ̄
  92. 92. mmap(NONE)で死ぬ • しばらく動かしていると • mmap(...PROT_NONE...) failed • というエラーを出して死ぬ
  93. 93. mmap(NONE)で死ぬ • Issueにあるように、コンパイラのビル ド時に-DUSE_MMAPと-DUSE_MUNMAP を外す必要がある • Monoをソースからビルドし直し
  94. 94. mmap(NONE)で死ぬ • Monoはしばらく動かしてると大体死ぬ
  95. 95. メモリリークで死ぬ
  96. 96. メモリリークで死ぬ
  97. 97. メモリリークで死ぬ • 未解決問題 • 負荷が掛かっている場合だけメモリ使 用量が増え続ける • ゲームサーバのコードが悪いせいなの かどうか分からない
  98. 98. メモリリークで死ぬ • PHP製作者曰く: • 「僕なら、10リクエストごとにApache を再起動しますね。」 • ということで、一定量動かしたらプロ セスを再起動するようにした
  99. 99. メモリリークで死ぬ
  100. 100. メモリリークで死ぬ • 結論: Monoはメモリリークを起こしてい るかも、と疑心暗鬼になるだけの下地 がある
  101. 101. まとめ • Monoは人類には早かった • 次やるならクライアントをホストにす ると思う
  102. 102. まとめ • とはいえ、async/awaitのおかげでサーバ のコードは相当短くなった • 全部で5,000行程度 • ※クライアントとの共通コードは除く •言い換えれば5,000行程度でMonoが死にま くったという…
  103. 103. まとめ • Monoは(人間とプロセスが)死ぬ • 覚悟を持って使いましょう
  104. 104. おわり

×