コネクション確立の性能
まずはクライアントとのコネクションが確立するまでの時間を計測し、コネクション数に応じた数値の変化を観測します。ループバックアドレス(127.0.0.1)を通してコネクションを張った直後にpoll(2), epoll(7)などの多重I/O用関数を使用します。多重I/O用関数を使用する前で計測を開始、関数通過後accept(2)が終わって新たなディスクリプタが生成された直後までにかかった時間を計測します。その計測をコネクション数(=ディスクリプタ数)の増加との関わりで検証します。
クライアント用のプロセスと分けてしまうと、accept(2)用ソケットのbacklog(listen(2)の第2引数に設定する値)に達してしまい、コネクション待ちで余計なサスペンドが発生するので測定値にばらつきが出てしまいます。今回は多重I/O用関数を使用する直前にクライアントからのconnect(2)を行うことで、backlog溢れを回避しています。使用するプログラムは、ここではpoll(2)用しか表示していませんが、それ以外のプログラム(epoll_level_conn.c、epoll_edge_conn.c)はダウンロードして使用してください。
当検証では10,000個のディスクリプタを生成し計測しています。計測対象の多重I/O用関数は、poll(2)とepoll(2)のレベルトリガとエッジトリガとします。select(2)は仕様上の制約により、1,000程度のディスクリプタ管理までしかできませんので、当測定対象から除外しました。
また、ディスクリプタ数がこれだけ多い場合、素の設定ではディスクリプタを生成できない可能性があります。事前に使用可能ローカルポート数の幅を60,000以上に、ファイルオープン数を30,000程度に広げておいてください。
使用可能ローカルポート数の幅は、/proc/sys/net/ipv4/ip_local_port_rangeで設定されています。使用ファイルオープン数はulimitで設定できますが、システム上の最大数は/proc/sys/fs/file-maxにありますので、その値を参考にして設定します。実際の作業は下記を参考にしてください。
% cat /proc/sys/net/ipv4/ip_local_port_range 32768 61000 % echo "1024 65000" > /proc/sys/net/ipv4/ip_local_port_range % cat /proc/sys/net/ipv4/ip_local_port_range 1024 65000 % ulimit -n 1024 % cat /proc/sys/fs/file-max 199090 % ulimit -n `cat /proc/sys/fs/file-max` % ulimit -n 199090 %
以下がpoll(2)用の性能測定用プログラムになります。epoll(2)のレベルトリガ・エッジトリガの性能測定用プログラムとの処理の流れは同じですので、詳細はダウンロードして参照してください。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <sys/time.h>
#include <poll.h>
#define TV2SEC(tv) ((double)((tv).tv_sec) + (double)((tv).tv_usec / 1000000.0))
int create_sockaddr( in_port_t * port );
void client_connect( in_port_t port );
int wrap_accept( int sock );
int main( int argc, char ** argv ) {
if( argc < 2 || 0 == atoi( argv[1] )) {
fprintf( stderr, "usage : %s <conn>\n", argv[0] );
exit( 1 );
}
int maxconnect = atoi( argv[1] );
struct timeval tv1,tv2;
struct pollfd pfds[maxconnect + 1];
in_port_t port;
int fd, i;
int sock = create_sockaddr( &port );
memset( pfds, 0, sizeof( pfds ));
pfds[0].fd = sock;
pfds[0].events = POLLIN;
pfds[0].revents = 0;
int nfds = 1;
for( i = 0; i < maxconnect; i ++ ) {
client_connect( port );
gettimeofday( &tv1, 0 );
poll( pfds, maxconnect + 1, -1 );
fd = wrap_accept( sock );
gettimeofday( &tv2, 0 );
printf( "%5d\t%f\n", i, TV2SEC( tv2 ) - TV2SEC( tv1 ));
pfds[nfds].fd = fd;
pfds[nfds].events = POLLIN;
pfds[nfds].revents = 0;
nfds ++;
}
return 0;
}
int create_sockaddr( in_port_t * port ) {
struct sockaddr_in addr;
int sock = socket( AF_INET, SOCK_STREAM, 0 );
addr.sin_family = AF_INET;
addr.sin_port = htons( 0 );
addr.sin_addr.s_addr = htonl( INADDR_ANY );
bind( sock, ( struct sockaddr *)&addr, sizeof( addr ));
listen( sock, SOMAXCONN );
socklen_t socklen = sizeof( addr );
getsockname( sock, ( struct sockaddr *)&addr, &socklen);
*port = addr.sin_port;
return sock;
}
void client_connect( in_port_t port ) {
struct sockaddr_in addr;
int sock = socket( AF_INET, SOCK_STREAM, 0 );
addr.sin_family = AF_INET;
addr.sin_port = htons( ntohs( port ));
addr.sin_addr.s_addr = htonl( INADDR_LOOPBACK );
connect( sock, ( struct sockaddr *)&addr, sizeof( struct sockaddr_in ));
}
int wrap_accept( int sock ) {
struct sockaddr_in addr;
socklen_t len = sizeof( addr );
return accept( sock, ( struct sockaddr *)&addr, &len );
}
# ./poll_conn 10000 # ./epoll_level_conn 10000 # ./epoll_edge_conn 10000
計測結果は下記の通りです(以下のすべての図の横軸はコネクション数、縦軸の単位は秒です)。
pollはコネクション数に応じて緩やかな角度で増えており、epollはコネクションの数に応じたレスポンス値の変化がまったくなく、0付近で推移しています。これだけを見ると、pollは非常に遅く、epollの性能の良さを認識いただけるでしょう。

