オルタナティブ・ブログ > プログラマー社長のブログ >

プログラミングでメシが食えるか!?

多スレッドで実現していたプロキシ処理を多重化で実現

»

おそらく今年最後の技術ネタでしょう。

10月22日に「高負荷システムではログ出力も大変!」という記事を書いたところ、WIDEプロジェクトでご一緒した山本さんから「ネイティブスレッドを2万個上げるのようなシステムは、設計が間違っているように思います。」というコメントをいただきました。

まあ、まさにその通りなのですが、開発の過程にはさまざまな背景があるもので、今回の場合は、作って評価してみることを繰り返しながら機能も仕様も大幅に変わりながらまとまってきたようなこともあり、多数のセッションへの対応はスレッド数で対応していました。

今どきのハードウェア・OSはパワフルなので、下手にソフトウェアで並列処理を自前で実現するよりも、スレッドに任せてしまえ、という意見もあるのですが、ものには限度というものがあり、実際のところ、2万スレッドを超高速に動かすのは、今の環境でも結構苦労しますし、それ以上になるとリソースの問題からかなり困難になってきます。

メモリーをスレッド数分必要とすることは当然なのですが、さらに、ディスクリプタやTCPのバッファ、ディスクキャッシュなども多数必要で、それらがメモリー空間を取り合うことになり、高負荷をかけ続けるとOSごとぶっ飛びます。カーネルパラメーターのチューニングなどで何とか2万スレッドで安定稼働を続けられるようにしましたが、今のところ2万スレッドくらいが限界です。

とはいえ、なんとか2万スレッドで対処できる範囲では実用化できたこともあり、ようやく少し余裕ができたので、スレッドに頼らない構造へのチャレンジを地道にしてきました。やってみれば大したことではないのですが、いろいろ工夫しながらだいたい形になったところです。

今回のシステムでは、大きく分けて「コネクト処理」と「プロキシ処理(通信の中継)」を、呆れるほど大量に、しかも高速にこなす必要があり、従来は両方をスレッドに頼ってきました。基本的に1セッションに1スレッド使う感じです。この構造はプログラムの流れが追いやすく、機能追加・変更が容易なため、機能や仕様が流動的な間は構造自体が大きなメリットになりますが、より大規模・高性能に対応しようとすると限界にぶち当たります。

そこで、今度は1スレッドで多数のセッションを処理するように、epollを使って多重化して、少ないスレッド数で多くのセッションを処理できるようにしたわけです。この方式の注意点は、各処理を決してブロックさせてはならない点で、ノンブロッキングでの送受信が必要になります。

簡単にまとめるのが難しいのですが、コネクト処理はこんな感じに変更しました。

・従来
10000スレッド×2プロセス
排他しながら接続依頼を受けたらコネクト
ループ{
        排他取得成功:接続依頼受信:コネクト
                接続成功:プロキシ処理を実行
}

・改良
2スレッド(接続処理確認とタイムアウト処理)×2プロセス
コネクト処理をノンブロッキングで多重化
ループ{
        接続依頼受信:コネクト多重化処理に対象を追加
                接続成功:プロキシ処理にセッション追加
}

コネクト処理は2スレッドのペアをどんどん増やして確認もしたのですが、並列性を上げると接続失敗が増えたため、この数に落ち着きました。コネクトはカーネルとしても時間がかかる処理で、接続待ちを多数のスレッドで行ってもオーバーヘッドの方が大きいのかもしれません。

プロキシ処理は以下のような感じです。

・従来
10000スレッド×2プロセス
1スレッドで1プロキシセッション
ループ{
        受信レディ:プロキシ処理
                送受信はブロッキングモード
}

・改良
1024スレッド×2プロセス
1スレッドでNプロキシセッション
ループ{
        受信レディ:該当セッションのプロキシ処理
                送受信はノンブロッキングモード
                送信先が書き込みレディでない間は受信しない
}

こちらも、1024スレッドというのは、あるパターンで実験して一番良かった数で、実はその後いろいろと改良しているので、再び検証し直すと他の値が良いかもしれません。

プロキシ処理と一言で書いていますが、実はプロキシ処理自体もかなり複雑なことをしています。1セッション1スレッドで、試行錯誤しながら作っている間にどんどん機能が膨らみ、ある時点で関数分けをかなり苦労して行っておいたので、多重化方式に移行するのは実はそれほど手間がかかりませんでした。1つの大きなループの中で処理を増やして行くと、フラグなどの変数がどんどん増えていくもので、それらがループの至るところで判定されるような作りだと、あとで関数に分解するのがとても大変になります。

結局、改良版の方がソースも役割ごとに綺麗に分かれて、見やすくなった気もします。が、当初は仕様・機能が見渡せていなかったので、最初からこの構造で作れたかといわれると、難しかったでしょう。

まあ、なんだか抽象的で良くわからない内容になってしまいましたが、こうやって技術ネタも書いていると、詳しい方からコメントをいただけたりして、それを刺激に、面倒な改良もやる気がでるもので、私の場合、まさに「自分のためのブログ」なのですねぇ・・・。

Comment(5)