Linux
Network
snmp

本当に恐ろしいsnmpd

昨今の Linux での仮想マシン、コンテナの使われ方をみるに、一つのマシンの中で netdev がたくさん存在するケースは普通になってきたと思う。そんな状況で見かけた恐ろしい現象。

snmpd を起動させるだけで CPU 100% で回り始める。

再現方法は後述するけれども、調べてみると、同様の現象に見舞われている人たちがみつかる。中でも詳しいのはこの記事 で、巨大な BGP routing table を持つシステムの例と、数千の VLAN を持つ例も同様なんだそうな。原因は定期的なキャッシュ更新で、ipaddress, netdev の数に対して、爆発的に増大するアルゴリズムが使われているため。私も調べてみた限りでは、手っ取り早く設定ファイルで抑制する方法は存在しない。

  • 単純に時代の変化で、net-snmp snmpd が作られたころと今とで想定数が異なるんだろうな、というのは理解できなくもない。
  • 一部は net-snmp のコードによるもので、sorted array に多数の要素が格納されることによって引き起こされているところもある。パッチを書いてみた(後述)。
  • 少し前(といっても Ubuntu 16 でも)の Linux kernel 内部の dev_get_by_name が線形サーチなので、組み合わせると O(n^2) になっている。

いくつか試験をしてみると、効果絶大なのは例えば Ubuntu bionic(現行 LTS)のように新しい Linux kernel に乗り換えること。パッチは kernel を乗り換えてしまえば効果は限定的になる(kernelの遅い部分の呼び出し回数を減らすものなので)。

それはそれとして、netdev 一つ一つにつき都度 kernel に問い合わせるのは、いくら何でも効率が悪い。統計情報のテーブルごと取得してから、並び替えるという手順にしないと厳しい。snmpd の構造を変えたい気持ちになってくる。

実はあえて触れないでいた、最も効果的な対策は snmpd 以外の監視方法に切り替えること。

*1) 一定時間ごとにキャッシュを収集するルーチン _cache_load が呼び出されて、ネットワーク情報もキャッシュされる。時間間隔は C の定数で宣言されている。この機能をオフにする設定は存在しない。_cache_load を呼び出すコードは mib2c で MIB から自動生成されたコードになる。

IP アドレスがたくさんある場合

veth0 に適当に IPv4 private address をたくさんつける。下の例では 10000 個つけている。

sudo ip link add veth0 type veth peer name veth1
for i in {1..50}; do for j in {1..200}; do sudo ip addr add 192.168.$i.$j dev veth0; done; done

すると、みるみる snmpd の負荷があがる。

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
10385 snmp      20   0   77644  25204   4060 R 100.0  0.4   7:26.73 snmpd

元に戻すのは簡単で、veth0 を消してしまえば一気に全部解除できる。

sudo ip link del veth0

netdev がたくさんある場合

増やす手順

for i in {1..500}; do sudo ip link add veth${i}a type veth peer name veth${i}b; done

元に戻す手順

for i in {1..500}; do sudo ip link del veth${i}a; done

net-snmp patch

いくらなんでも、要素取得のたびにソートするのはやりすぎ。実際に入れ替えしてなくても検査が走り、kernelへの問い合わせが起こっている。

diff --git a/snmplib/container_binary_array.c b/snmplib/container_binary_array.c
index 46b954b77..c9ea86e5a 100644
--- a/snmplib/container_binary_array.c
+++ b/snmplib/container_binary_array.c
@@ -258,12 +258,6 @@ netsnmp_binary_array_get(netsnmp_container *c, const void *key, int exact)
     if (!t->count)
         return NULL;

-    /*
-     * if the table is dirty, sort it.
-     */
-    if (t->dirty)
-        Sort_Array(c);
-
     /*
      * if there is a key, search. Otherwise default is 0;
      */
@@ -625,6 +619,13 @@ _ba_find(netsnmp_container *container, const void *data)
 static void *
 _ba_find_next(netsnmp_container *container, const void *data)
 {
+    /*
+     * if the table is dirty, sort it.
+     */
+    binary_array_table *t = (binary_array_table*)container->container_data;
+    if (t->dirty)
+        Sort_Array(container);
+
     return netsnmp_binary_array_get(container, data, 0);
 }