はじめに
Linuxカーネルとnetstatコマンドをちょこっとだけ改造したらnetstatコマンドが早くなった、というお話です。
概要
- netstatコマンドは、TCPセッションについての情報をカーネルから取得する際には
/proc/net/tcp
という特殊なファイル1を参照している- IPv4では
/proc/net/tcp
、IPv6については/proc/net/tcp6
と別のファイルに分かれているが、都度併記するとややこしいので以降は/proc/net/tcp
と表記する
- IPv4では
- procfsは内部でseqファイルシステムという仕組みを利用している
- seqファイルシステムについてはこちらにまとめられている seqファイルシステムについて - Qiita
- seqファイルシステムを利用した特殊なファイルに対してreadシステムコールを実行した際、通常はカーネル側で確保された
PAGE_SIZE
(多くの環境では4096Byte)の値と同じ容量のバッファを経由して、readシステムコールに指定されたユーザー空間側のバッファ領域にデータをコピーする- つまり、ユーザー空間側のバッファ領域にいくら大きな容量を(readシステムコールの第2、第3引数に2)指定しても、4096Byte以下の容量でしかデータを読み出せない事を意味する
- 多量・高頻度のTCPコネクションを扱う環境では
/proc/net/tcp
の容量は数MiByteになりうるが、readシステムコールは4096Byte以下の容量でしかデータを読み出すことができず、/proc/net/tcp
の内容すべてを読み込む為には数百回のreadシステムコールを実行する必要がある-
/proc/net/tcp
ではTCPの1セッション=1行が150Byte、1万セッションを保持(TIME_WAIT含む)している環境であればファイルの容量は1.5MiByte前後となり、readシステムコールを300回以上実行することになる - 実際には行単位で区切りのいい容量に丸められて4096Byteよりも若干少ないデータ量しか読み出せないので、回数はさらに増える
-
- Linuxカーネル内の
/proc/net/tcp
の処理を行っている箇所とnetstatコマンドを修正して1回のreadシステムコールで読みだすデータ量を増やすと、readシステムコールの呼び出し回数が減少し、netstatコマンドが高速になった
環境
- 以降の操作やプログラムの作成、動作確認は本記事作成時点でのCentOS7の最新環境で行った
$ uname -r
3.10.0-693.17.1.el7.x86_64
$ rpm -q kernel
kernel-3.10.0-693.11.6.el7.x86_64
kernel-3.10.0-693.17.1.el7.x86_64
$ rpm -q net-tools
net-tools-2.0-0.22.20131004git.el7.x86_64
$ cat /etc/redhat-release
CentOS Linux release 7.4.1708 (Core)
$ LANG=C lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 2
On-line CPU(s) list: 0,1
Thread(s) per core: 2
Core(s) per socket: 1
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 63
Model name: Intel(R) Xeon(R) CPU E5-2673 v3 @ 2.40GHz
Stepping: 2
CPU MHz: 2394.456
BogoMIPS: 4788.91
Virtualization: VT-x
Hypervisor vendor: Microsoft
Virtualization type: full
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 30720K
NUMA node0 CPU(s): 0,1
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology eagerfpu pni pclmulqdq vmx ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm tpr_shadow vnmi ept vpid fsgsbase bmi1 avx2 smep bmi2 erms xsaveopt
$ free -m
total used free shared buff/cache available
Mem: 7966 416 6876 8 673 7205
Swap: 0 0 0
手順
準備
- RPMパッケージのビルドに必要な諸々をインストール・設定しておく
sudo useradd mockbuild
sudo yum -y install rpm-build
sudo yum -y groups install "Development Tools"
Linuxカーネルをどうにかする
カーネルのSRPMを取得してインストール
- ビルド時に必要になるパッケージもインストールする
yumdownloader --source kernel
rpm -ivh kernel-3.10.0-693.17.1.el7.centos.plus.src.rpm
sudo yum -y install m4 gcc xmlto asciidoc hmaccalc python-devel newt-devel 'perl(ExtUtils::Embed)' pesign elfutils-devel zlib-devel binutils-devel bison audit-libs-devel java-devel openssl-devel numactl-devel pciutils-devel ncurses-devel
パッチを用意&SPECファイルを修正
-
~/rpmbuild/SOURCES/proc-net-tcp.patch
として下記の内容でパッチを作成
--- a/net/ipv4/tcp_ipv4.c 2018-01-14 15:02:54.000000000 +0000
+++ b/net/ipv4/tcp_ipv4.c 2018-02-10 06:08:39.043951339 +0000
@@ -2382,10 +2382,21 @@
return 0;
}
+static ssize_t tcp_seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
+{
+ struct seq_file *m = file->private_data;
+ if (!m->buf) {
+ m->buf = kmalloc( m->size = 16 * PAGE_SIZE, GFP_KERNEL);
+ if (!m->buf)
+ return -ENOMEM;
+ }
+ return seq_read(file, buf, size, ppos);
+}
+
static const struct file_operations tcp_afinfo_seq_fops = {
.owner = THIS_MODULE,
.open = tcp_seq_open,
- .read = seq_read,
+ .read = tcp_seq_read,
.llseek = seq_lseek,
.release = seq_release_net
};
--- a/net/ipv6/tcp_ipv6.c 2018-01-14 15:02:54.000000000 +0000
+++ b/net/ipv6/tcp_ipv6.c 2018-02-10 06:10:39.910935928 +0000
@@ -1812,10 +1812,21 @@
return 0;
}
+static ssize_t tcp_seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
+{
+ struct seq_file *m = file->private_data;
+ if (!m->buf) {
+ m->buf = kmalloc( m->size = 16 * PAGE_SIZE, GFP_KERNEL);
+ if (!m->buf)
+ return -ENOMEM;
+ }
+ return seq_read(file, buf, size, ppos);
+}
+
static const struct file_operations tcp6_afinfo_seq_fops = {
.owner = THIS_MODULE,
.open = tcp_seq_open,
- .read = seq_read,
+ .read = tcp_seq_read,
.llseek = seq_lseek,
.release = seq_release_net
};
- ビルド時に上記のパッチを適用するようにSPECファイルを修正
$ diff ~/rpmbuild/SPECS/kernel.spec.org ~/rpmbuild/SPECS/kernel.spec
8c8
< %define buildid .centos.plus
---
> %define buildid .procnettcp
463a464,465
> Patch50000: proc-net-tcp.patch
>
832a835,836
>
> ApplyPatch proc-net-tcp.patch
Linuxカーネルのパッケージをビルド
- 今回使用した環境での所要時間は1.5時間強
rpmbuild -ba ~/rpmbuild/SPECS/kernel.spec
find ~/rpmbuild/ -name "*.rpm"
結果
- ビルドして得られたRPMパッケージをインストールして再起動すると、
/proc/net/tcp
に対するreadが64KiByteずつ行えるようになった
catコマンドでの確認
-
/proc/net/tcp
の内容を読み出すcatコマンドについて、straceコマンドでreadシステムコールの様子を観察する
$ uname -r
3.10.0-693.17.1.el7.procnettcp.x86_64
$ LANG=C strace -e open,read cat /proc/net/tcp|wc -l
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\35\2\0\0\0\0\0"..., 832) = 832
open("/proc/net/tcp", O_RDONLY) = 3
read(3, " sl local_address rem_address "..., 65536) = 11700
read(3, "", 65536) = 0
78
+++ exited with 0 +++
- 改造前のLinuxカーネルの環境下で同じコマンドを実行した結果が下記
- readシステムコールの第3引数として同じ64KiByteが指定されているにもかかわらず、4096Byteよりも少ないデータしか読み出せていないことがわかる
$ uname -r
3.10.0-693.17.1.el7.x86_64
$ LANG=C strace -e open,read cat /proc/net/tcp|wc -l
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\35\2\0\0\0\0\0"..., 832) = 832
open("/proc/net/tcp", O_RDONLY) = 3
read(3, " sl local_address rem_address "..., 65536) = 4050
read(3, " 26: 0502010A:861A 10813FA8:005"..., 65536) = 1650
read(3, "", 65536) = 0
38
+++ exited with 0 +++
netstatコマンドでの確認
- 改造後のカーネルの環境でnetstatコマンドについて、straceコマンドでreadシステムコールの様子を観察する
- readするときに4096Byteしかバッファを渡してないのであまり影響がない
$ uname -r
3.10.0-693.17.1.el7.procnettcp.x86_64
$ LANG=C strace -e open,read netstat -atn |wc -l
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300j\0\0\0\0\0\0"..., 832) = 832
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\35\2\0\0\0\0\0"..., 832) = 832
open("/lib64/libpcre.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360\25\0\0\0\0\0\0"..., 832) = 832
open("/lib64/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`\16\0\0\0\0\0\0"..., 832) = 832
open("/lib64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0m\0\0\0\0\0\0"..., 832) = 832
open("/proc/net/tcp", O_RDONLY) = 3
read(3, " sl local_address rem_address "..., 4096) = 4096
read(3, "00000000 03:00001608 00000000 "..., 4096) = 4096
read(3, " 3 ffff880287e55d00 "..., 4096) = 4096
read(3, " \n", 4096) = 12
read(3, "", 4096) = 0
open("/proc/net/tcp6", O_RDONLY) = 3
read(3, " sl local_address "..., 4096) = 853
read(3, "", 4096) = 0
+++ exited with 0 +++
87
- 改造前のカーネルの環境でnetstatコマンドについて、straceコマンドでreadシステムコールの様子が下記
- readシステムコールで指定したバッファサイス4096Byteに対して、4050Byteしか読み出せていない事がわかる
$ uname -r
3.10.0-693.17.1.el7.x86_64
$ LANG=C strace -e open,read netstat -atn |wc -l
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300j\0\0\0\0\0\0"..., 832) = 832
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\35\2\0\0\0\0\0"..., 832) = 832
open("/lib64/libpcre.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360\25\0\0\0\0\0\0"..., 832) = 832
open("/lib64/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`\16\0\0\0\0\0\0"..., 832) = 832
open("/lib64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0m\0\0\0\0\0\0"..., 832) = 832
open("/proc/net/tcp", O_RDONLY) = 3
read(3, " sl local_address rem_address "..., 4096) = 4050
read(3, " 26: 0502010A:870C 10813FA8:005"..., 4096) = 4050
read(3, " 53: 0502010A:E488 19E57328:01B"..., 4096) = 900
read(3, "", 4096) = 0
open("/proc/net/tcp6", O_RDONLY) = 3
read(3, " sl local_address "..., 4096) = 676
read(3, "", 4096) = 0
+++ exited with 0 +++
64
netstatをどうにかする
net-toolsのSRPMを取得してインストール
yumdownloader --source net-tools
rpm -ivh net-tools-2.0-0.22.20131004git.el7.src.rpm
sudo yum -y install libselinux-devel
パッチを用意&SPECファイルを修正
-
~/rpmbuild/SOURCES/net-tools-netstat-proc-net-tcp-buffer-size.patch
として下記の内容でパッチを作成- ページサイズの16倍の容量でバッファ領域を確保するように変更している
--- a/lib/proc.c 2013-09-30 08:07:32.000000000 +0000
+++ b/lib/proc.c 2018-02-11 04:48:40.959339035 +0000
@@ -88,9 +88,9 @@
if (!buffer) {
pagesz = getpagesize();
- buffer = malloc(pagesz);
+ buffer = malloc(16*pagesz);
}
- setvbuf(fd, buffer, _IOFBF, pagesz);
+ setvbuf(fd, buffer, _IOFBF, 16*pagesz);
return fd;
}
- ビルド時に上記のパッチを適用するようにSPECファイルを修正
$ diff ~/rpmbuild/SPECS/net-tools.spec.org ~/rpmbuild/SPECS/net-tools.spec
67a68,69
> Patch100: net-tools-netstat-proc-net-tcp-buffer-size.patch
>
104a107,108
>
> %patch100 -p1 -b .proc-net-tcp
net-toolsのパッケージをビルド
- 改造前のnetstatコマンドとの比較のため、ビルドして得られたRPMパッケージはインストールしない
rpmbuild -bb ~/rpmbuild/SPECS/net-tools.spec
ls -l ~/rpmbuild/BUILD/net-tools/netstat
結果
- 改造前のnetstatコマンドについて、straceコマンドでreadシステムコールの様子を観察
-
/proc/net/tcp
をopenした後のreadは4096Byteずつデータが読みだされている
-
$ LANG=C strace -e open,read netstat -atn |wc -l
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300j\0\0\0\0\0\0"..., 832) = 832
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\35\2\0\0\0\0\0"..., 832) = 832
open("/lib64/libpcre.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360\25\0\0\0\0\0\0"..., 832) = 832
open("/lib64/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`\16\0\0\0\0\0\0"..., 832) = 832
open("/lib64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0m\0\0\0\0\0\0"..., 832) = 832
open("/proc/net/tcp", O_RDONLY) = 3
read(3, " sl local_address rem_address "..., 4096) = 4096
read(3, "00000000 03:0000022B 00000000 "..., 4096) = 4096
read(3, " 3 ffff8802c43a7d00 "..., 4096) = 3508
read(3, "", 4096) = 0
open("/proc/net/tcp6", O_RDONLY) = 3
read(3, " sl local_address "..., 4096) = 853
read(3, "", 4096) = 0
+++ exited with 0 +++
83
- 改造後のnetstatコマンドについて、straceコマンドでreadシステムコールの様子を観察
- readシステムコールの第3引数として65536=64KiByteが設定され、4096Byteより大きい容量を一度のreadシステムコールで読み出せている
$ LANG=C strace -e open,read ~/rpmbuild/BUILD/net-tools-2.0/netstat -atn |wc -l
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300j\0\0\0\0\0\0"..., 832) = 832
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\35\2\0\0\0\0\0"..., 832) = 832
open("/lib64/libpcre.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360\25\0\0\0\0\0\0"..., 832) = 832
open("/lib64/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`\16\0\0\0\0\0\0"..., 832) = 832
open("/lib64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0m\0\0\0\0\0\0"..., 832) = 832
open("/proc/net/tcp", O_RDONLY) = 3
read(3, " sl local_address rem_address "..., 65536) = 11700
read(3, "", 65536) = 0
open("/proc/net/tcp6", O_RDONLY) = 3
read(3, " sl local_address "..., 65536) = 853
read(3, "", 65536) = 0
+++ exited with 0 +++
83
TCPセッションが多い状態での動作の比較
- TCPセッション数を増やすために、ApacheとApacheに付属するベンチマークツールのabコマンドを使用する
- Apacheをインストール・起動し、abコマンドを繰り返し実行するワンライナーを実行しながら改造前後のnetstatコマンドの動作を比較する
sudo yum -y install httpd
sudo systemctl start httpd.service
while : ; do ab -n 100000 -c 1000 127.0.0.1/test; done
- timeコマンドでの計測では、TCPセッション数が16000強の状態では従来のnetstatコマンドで300ミリ秒以上かかっていたのが、改造後のnetstatコマンドでは100ミリ秒台と、半分以下の時間で済むようになった
- 特にsystem timeが顕著に減少している
$ time netstat -atn |wc -l
16528
real 0m0.355s
user 0m0.110s
sys 0m0.254s
$ time ~/rpmbuild/BUILD/net-tools-2.0/netstat -atn |wc -l
16202
real 0m0.133s
user 0m0.115s
sys 0m0.027s
最後に
- ちなみに、netstatコマンドと同様の目的で利用される事の多いssコマンドは通常3
/proc/net/tcp
の参照とは異なる仕組み(特殊なソケットを利用する"netlink")でTCPセッション数を取得しており、上記の改造後のnetstatコマンドよりも高速に処理ができる
$ time ss -atn |wc -l
15922
real 0m0.046s
user 0m0.033s
sys 0m0.021s
netstatコマンドというかnet-toolsがobsoleteと見なされつつある状況も踏まえれば、素直にssコマンドを利用するのが良いでしょう。