Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
64
Help us understand the problem. What is going on with this article?
@0xfffffff7

TCPサーバーの非同期処理はけっきょくどの手法がいいのか?

More than 3 years have passed since last update.

昔Unix系システムではselect()でIO処理を非同期にして、ファイルディスクリプタの状態に変化があった場合のみ処理を行うことができた。これはメモリやサーバーリソースが極端に少なかった時代に有効な手法だった(らしい)。90年代後半、00年代前半にはマルチスレッドが主流になり、サーバーサイドではAcceptして受け入れたクライアントのソケットをスレッドに渡して送受信を行うことでコネクションの多重化を行った。(もちろん目的に応じて非同期も活躍していた)

00年代後半からC10K問題が騒がれ、node.jsやnginxが登場した。大量のスレッドを生成してリソースやメモリを消費する方法よりも、再びソケットをノンブロッキングにしてディスクリプタの状態を監視する非同期処理が注目されるようになる。そして最近ではjavascript promise、RX、Reactive ExtensionなどクライアントサイドのUIに近い場所で非同期処理を効率的に記述できるライブラリが登場している。

ただし、サーバーサイドで非同期処理を考えた場合に、シングルスレッドで非同期IOを処理するだけではマルチコア、マルチCPUをあまり活かせない。なので、Accept()で受けた後にソケットのレディ状態を非同期で待ち、ソケットがレディ状態になったらスレッドで実際の送受信を行う手法もある。非同期処理自体もselect()、poll、epollなど複数の手法がある。また、処理の多重化にはスレッドを使う方法とプロセスをforkする方法がある。さらにあらかじめスレッドやプロセスを生成しておいて、それらにデータを受け渡す手法もある。

手法 メリット デメリット
select メモリやリソース消費が抑えられる 扱えるディスクリプタに制限がある
一つのソケットの送受信処理の間に他のソケットを扱えない
poll メモリやリソース消費が抑えられる
ディスクリプタ制限なし
一つのソケットの送受信処理の間に他のソケットを扱えない
EPOLL pollの高速化版
ディスクリプタ制限なし
一つのソケットの送受信処理の間に他のソケットを扱えない
fork 処理待ちソケットがあまり発生しない プロセス生成コストが大きい
thread 処理待ちソケットがあまり発生しない スレッドの生成コストとコネクション分のスタック領域を消費する
pre-fork 処理待ちソケットがあまり発生しない
プロセス生成コストが起動時しかない
データの受け渡しとロック処理、プロセスの管理の手間
pre-thread 処理待ちソケットがあまり発生しない
スレッド生成コストが起動時しかない
データの受け渡しとロック処理、スレッドの管理の手間
EPOLL+thread メモリとリソース消費を抑えつつ、待ち時間を最大限に低くできる スレッド生成コスト。データの受け渡しとスレッド管理の手間
EPOLL+pre thread メモリとリソース消費を抑えつつ、待ち時間を最大限に低くできる データの受け渡しとスレッド管理の手間がより複雑だが、うまく実装すれば完璧なパフォーマンスを出すはず

この辺りは『Linuxネットワークプログラミングバイブル』が詳しい。

個人的には「EPOLL+thread」が良いと思うが、上記の本に載っているベンチマークによると、接続時間が最も速いのは「EPOLL+thread」だが、送受信が最も速かったのは「fork」らしい。接続時間と合わせた総合的な順でいえば「pre-thread」が一番高速という結果になっていた。送受信に限って言えば、「pre-〜」も含めてforkやスレッドなど特別なことをするよりも普通に「poll」や「EPOLL」を使用した方が速いという結果になっている(本当かな??)。ちなみに「EPOLL+thread」は総合4位で「select」や「poll」や「EPOLL」よりも遅いと書かれているが、ちょっとこの結果も信じられない。

ベンチマークの内容は50回の送受信で接続する処理をさらに50回繰り返すスレッドを1000スレッド起動して、サーバーはディアルコアで、スレッド化やfork化するのはsend()の部分のみという実験らしい。

けっきょくスレッドやforkは事前生成してもデータの受け渡しや共有データのロックなどでパフォーマンスが悪くなるのだろうか。ディアルコアという点が影響している可能性もけっこうあると思う(書籍にもサーバーサイドのリソース監視はしてない単純比較と書いてある)。

「EPOLL+pre-thread」が紹介されていないので、おそらくマルチコア環境でスレッド数を適切に抑えればこれが最も速いかもしれない。自分で書くときは「EPOLL+pre-thread」でスレッド数を抑えてやろうと考えている。

ちなみにAccept自体をスレッドで複数処理するという方法も書籍には紹介されていたが、けっきょくロックしてどれか一つが受け口になるため、この方法の是非は自分にはわからない。

64
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
64
Help us understand the problem. What is going on with this article?