Edited at

netstatコマンドを高速化する

More than 1 year has passed since last update.


はじめに

Linuxカーネルとnetstatコマンドをちょこっとだけ改造したらnetstatコマンドが早くなった、というお話です。


概要


  • netstatコマンドは、TCPセッションについての情報をカーネルから取得する際には/proc/net/tcpという特殊なファイル1を参照している


    • IPv4では/proc/net/tcp、IPv6については/proc/net/tcp6と別のファイルに分かれているが、都度併記するとややこしいので以降は/proc/net/tcpと表記する



  • procfsは内部でseqファイルシステムという仕組みを利用している



  • 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コマンドを利用するのが良いでしょう。





  1. ここではprocfs自体については詳しく述べない。man procなどを適宜参照。 



  2. readシステムコールの引数等についてはman 2 readなどを適宜参照。 



  3. netlinkによる情報の取得に失敗した場合にはprocfsを利用した情報の取得を行います。