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

More Related Content

Mono is Dead