はじめに

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以下の容量でしかデータを読み出せない
  • 大量のネットワークセッションを扱う環境では/proc/net/tcpの容量は数MiByteになりうるが、readシステムコールが4096Byte以下の容量でしかデータを読み出せず、データをすべて読み込む為には数百回の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を利用した情報の取得を行います。 

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.