15
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

netstatコマンドを高速化する

Last updated at Posted at 2018-02-11

はじめに

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を利用した情報の取得を行います。

15
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?