気ままにインフラエンジニア

渋谷で働くインフラエンジニアの備忘録。 TwitterID: @nakashii_

kernel: TCP: time wait bucket table overflow の解消とTIME_WAITを減らすチューニング

整理がてら。
httpdが動いているあるホスト上で、 /var/log/messages に以下のようなメッセージが出ていた。

kernel: TCP: time wait bucket table overflow
kernel: printk: 50078 messages suppressed.

"netstat -tna |grep TIME_WAIT"すると、10万以上の数でTIME_WAITが出ている。これを解消するまでの流れ。

そもそもこの値は、カーネルパラメータの
net.ipv4.tcp_max_tw_buckets
で設定されている。デフォルトは16384。
(↑の通り、エラーが出た環境では既にかなり大きな値が設定されていたのだが。)

/proc/sys/net/ipv4/tcp_max_tw_buckets
    システムが同時に保持する time-wait ソケットの最大数。この数を越えると、time-wait ソケットは直ちに破棄され、警告が表示されます。この制限は単純な DoS 攻撃を防ぐためにあります。わざと制限を小さくしてはいけません。ネットワークの状況によって必要な場合は、 (できればメモリを追加してから) デフォルトより増やすのはかまいません。
http://archive.linux.or.jp/JF/JFdocs/Adv-Routing-HOWTO/lartc.kernel.obscure.html

単純に値を増やして対応しても良いのだが、ホストと動いているサービスの状況によって対応策を使い分けたほうが良いと思う。

  • net.ipv4.tcp_max_tw_buckets を増やす

内部NWで閉じて利用しているか、DoS攻撃を防ぐための機能が上位のFirewall等で確実に機能している場合はこれでもよさそう。

  • TIME_WAITを減らすようにkernelパラメータをチューニングする

おおまかなパターンとしては以下の二つがあり、今回は後者を採用した。

1. TIME_WAITが自然解消されるまでの時間を減らす
これはかなり難解で、LinuxではTIME_WAITの保持時間を減らすにはカーネルをリビルドするしかない。
http://d.hatena.ne.jp/ono51/20111012/p1
この件を調べている途中、tcp_fin_timeoutを減らしたらOKという記事をたくさん見かけたが、どうも一緒に指定しているtcp_tw_recycleの作用が出ているだけに思える。
tcp_fin_timeout自体は、FIN-WAIT2からTIME_WAITに状態変化する時間のパラメータであり、TIME_WAIT状態が継続する時間とは関係ない。

2. TIME_WAITになったポートを再利用する
サーバ側で net.ipv4.tcp_tw_recycle を有効にする。
これで10万以上あったTIME_WAITが一気に100以下のレベルに。これは、閉じた環境で100ホスト程度がアクセスしてきているという環境だったから。

ただし、サーバ側で net.ipv4.tcp_tw_recycle が有効で、クライアント側でTCPのタイムスタンプオプションが有効(Linuxの場合net.ipv4.tcp_timestamps = 1)だと、NAT/LBを超えたときにSYNを落としてしまい、接続障害になる。
ユーザー向けに使っているとSB携帯などで障害が発生してしまうようなので、使わないほうがいいかも。

net.ipv4.tcp_tw_reuseを有効にしてもほとんど効果がなかった。これはおそらく、クライアント側も同じソケットをTIME_WAITしているため、クライアント側からの接続は新規セッションが使われるからだと思う。(ということは、クライアントとサーバー両方でONにしないとダメなんじゃないか?という疑問が。誰か検証してくれないかなー。)

  • アプリケーション側でコネクションの総数を減らす工夫をする

HTTPなので、keepaliveを有効にすることもできる。今回はHTTPを呼び出している側のシステムがコネクションプール的な概念を持たないので断念。


net.ipv4.tcp_tw_recycleを使う時の注意点はこのへんが詳しい。先達に感謝。
どさにっき
2007-05-21 - LowPriority

logrotateで簡単に世代バックアップ(couchdb)

couchDBのデータはreplicationをするか、database fileを適当にバックアップしておいてねという仕様のよう。*1

rsyncやcpのshellを書いてもよいのですが、バックアップの世代管理を手軽に行いたいときはlogrotateを愛用してます。

今回はchefに使われているcouchdbをバックアップしていますが、プロダクトに組み込みの各種DB・ファイル等(pg_dumpだったりその他の普通のtarballだったり)いろいろ使い道はあるかと。

/etc/logrotate.d/couchdb に追記

# 日次バックアップ
/var/backups/couchdb_backup_daily.tar.gz {
  missingok
  nocreate
  daily
  rotate 14
  sharedscripts
  postrotate
    /bin/tar -C /var/lib/ -czf /var/backups/couchdb_backup_daily.tar.gz couchdb
  endscript
}
# 月次バックアップ
/var/backups/couchdb_backup_monthly.tar.gz {
  missingok
  nocreate
  monthly
  rotate 3
  sharedscripts
  postrotate
    /bin/tar -C /var/lib/ -czf /var/backups/couchdb_backup_monthly.tar.gz couchdb
  endscript
}

頻度(rotate)は適当に変えて下さい。

管理するデータ量が多い場合は、postrotateの圧縮後に、couchDBのデータ容量を削減しておきます。(↓chefの場合)

/usr/bin/curl -H "Content-Type: application/json" -X POST http://localhost:5984/chef/_compact
# 設定のテスト
logrotate -d /etc/logrotate.d/couchdb
# 手動で実行してみて、rotateされることを確認
logrotate -f /etc/logrotate.d/couchdb


logrotate.dの設定自体にはdailyより細かい指定はできないが、-fオプション付きをcronで回すことで、さらに頻繁にバックアップすることも。
(ログならともかくDBのデータでそこまでするのであれば別の方法がよさそうですが)


shを書いてcronを動作確認してという手間が無いのでlogrotateは便利なんですが、バックアップしてることが他人に伝わりにくいのが難点といえば難点でしょうか…

*1: http://wiki.apache.org/couchdb/How_to_make_filesystem_backups

RPMビルド時のデフォルトファイル名を変更する

自前でrpmをビルドした場合、出来上がるrpmのファイル名はspecファイルによって決まる。
specに指定がない場合、rpmbuildのデフォルトのパラメータが利用され、大体の場合
xxxxx-1.0.1-1.x86_64.rpm
のようなファイルが出来上がる。いくつかのOSが混在している環境でrpmをビルドすると、ファイル名が重複してしまいややこしい。
specを編集したりする方法もあるが、specファイル入のアーカイブからspecだけ取り出して編集するのは面倒であるし、できたファイルをいちいちrenameもしたくないので、rpmbuildの設定ファイルを変更する。

/usr/lib/rpm/macrosを編集し、

%_build_name_fmt        %%{ARCH}/%%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm

から

%_build_name_fmt        %%{ARCH}/%%{NAME}-%%{VERSION}-%%{RELEASE}%{dist}.%%{ARCH}.rpm

にするだけ。


以下仕組み。

rpmbuildの設定は以下のコマンドで参照する。

rpmbuild --showrc |less

rpmbuildの参照する設定ファイルは大体の場合以下のファイル。

/usr/lib/rpm/macros
/usr/lib/rpm/redhat/macros
/etc/rpm/macros.*
~/.rpmmacros

ファイル名に関連するのは_rpmfilenameで、さらに_build_name_fmtを参照しているようだ。
とりあえずCentOSの6と5を使い分けられればよいので…と思っていたら、いいパラメータがあったので使わせてもらう。

-14: dist       .el6

el6.x86_64とかよく見る。
今回はビルドサーバーで作業する全員に反映したいので、~/.rpmmacrosでなく/usr/lib/rpm/macrosに設定した。これで、
xxxxx-1.0.1-1.el6.x86_64.rpm
というファイル名で出来上がってくることになる。

CentOS6.xでは、/etc/rpm/macros.distに上記のdistが定義されていたが、CentOS5.xではshowrcを見た感じ同じようなファイルはなかったので作成した。

cat <<'EOF' >/etc/rpm/macros.dist
# dist macros.
%rhel 5
%centos 5
%centos_ver 5
%dist .el5
%el5 1
EOF

rpmbuild --showrc |grep dist
-14: dist       .el5

反映されている。あとは、CentOS6と同じように%distを%_build_name_fmtに入れ込めばOK。


参考
Rpmbuild - setting name of created .rpm - Stack Overflow

iptablesで帯域制限しながらネットワーク負荷をかける

netperfはネットワークの速度を測定するのにいいツールだけど、拠点間なりサービスなりが入っているネットワークで気軽に使って通信が過大になってしまっては怒られると思う。
ので、送信側のiptablesで帯域を制限しながら負荷をかける。*1

今回はiptablesとnetperfを使う場合。

送信側のnetperfのオプション

netperf -H [hostname|ip] -f M -p 12865 -- -P 12866

iptablesのルールに確実に引っ掛けるため、ポートを明示して利用する。

送信側のサーバーに帯域制限のiptablesを設定する

-A OUTPUT -j ACCEPT -p tcp --dport 12866 -m hashlimit --hashlimit 1000/sec --hashlimit-mode dstip --hashlimit-burst 2000 --hashlimit-name netperf-limit
-A OUTPUT -j REJECT -p tcp --dport 12866

...
-A OUTPUT   -m state --state RELATED,ESTABLISHED -j ACCEPT 

hashlimitはOUTPUTにも指定できる。
あと、IptablesのOUTPUTルールをACCEPTでなくDROPで運用している場合は、今回のルールはESTABLISHEDの上に指定したほうが多分良いと思う。
今回は特定のポートを明示して拒否しているので、REJECTしても大丈夫。

秒間1000パケットの指定だと、1000(p/sec) * 1500(MTU,Bytes) = 1.5MBps ぐらいの速度が出るはず。結果を見てみる。

# netperf -H [hostname|ip] -f M -p 12865 -- -P 12866
Recv   Send    Send                          
Socket Socket  Message  Elapsed              
Size   Size    Size     Time     Throughput  
bytes  bytes   bytes    secs.    MBytes/sec  

 87380  16384  16384    10.57       1.62 

だいたいあってる。

参考までに、受信側のサーバーでの結果。

# dstat -Tclnd
--epoch--- ----total-cpu-usage---- ---load-avg--- -net/total- -dsk/total-
  epoch   |usr sys idl wai hiq siq| 1m   5m  15m | recv  send| read  writ
1331206136|  0   0 100   0   0   0|   0    0    0| 779B  713B|   0     0 
1331206137|  0   0 100   0   0   0|   0    0    0|3105k   75k|   0  4096B
1331206138|  0   0 100   0   0   0|   0    0    0|1201k   27k|   0     0 
1331206139|  0   0 100   0   0   0|   0    0    0|1563k   35k|   0     0 
1331206140|  0   0 100   0   0   0|   0    0    0|1384k   32k|   0     0 
1331206141|  0   0 100   0   0   0|   0    0    0|1618k   36k|   0    56k
1331206142|  0   0 100   0   0   0|   0    0    0|1565k   35k|   0     0 
1331206143|  0   0 100   0   0   0|   0    0    0|1262k   28k|   0     0 
1331206144|  0   0 100   0   0   0|   0    0    0|1557k   35k|   0     0 
1331206145|  0   0 100   0   0   0|   0    0    0|1380k   30k|   0     0 
1331206146|  0   0 100   0   0   0|   0    0    0|1618k   36k|   0     0 
1331206147|  0   0 100   0   0   0|   0    0    0|1606k   36k|   0    76k
1331206148|  0   0 100   0   0   0|   0    0    0| 522k   12k|   0     0 
1331206149|  0   0 100   0   0   0|   0    0    0| 368B  326B|   0     0 

実はData Connectionのポートが別だということに気づくまで結構悩んでしまった…。

参考:10.3.8. Hashlimitマッチ

*1:もちろん、上流ではNW機器によって制限はかかっている。精神衛生上の問題

kernel-develが見つからずrpmbuildがエラーになる

IntelのNICドライバの最新版をせっせとRPM化していたら、rpmbuildでエラー。

Makefile:71: *** Kernel header files not in any of the expected locations.
Makefile:72: *** Install the appropriate kernel development package, e.g.
Makefile:73: *** kernel-devel, for building kernel modules and try again.

うぇい。kernel-develは入ってるぞ。別に入れ忘れてない。

と思ったら、バージョンが違っていた。
後から入れたkernel-develのバージョンがkernelと違っていたらしい。

kernel       2.6.32-220
kernel-devel 2.6.32-220.2.1

kernel-develのインストールでで生成されるフォルダ
/usr/src/kernels/[kernel_ver]
と、
/lib/modules/[kernel_ver]
あたりの関係がおかしくなるようだ。
バージョンの違いを認識する前に

ln -s /usr/src/kernels/2.6.32-220.2.1.el6.x86_64 /usr/src/linux

とかやってみて、これだと成功するなぁ…と悩んでいるログがhistoryにある。

一度removeしてから、バージョン指定でインストールする。(上記の場合)

yum install kernel-devel-2.6.32-220.el6

無事成功。

non-root userでRPMパッケージを作成する (CentOS6.2)

RPMをビルドする必要があったのだが、いろいろなところで

You are strongly advised against package building as root

と推奨されているので、素直に従う。
※こことか HowTos/I need the Kernel Source - CentOS Wiki

まずディレクトリの作成

mkdir ~/rpm
mkdir ~/rpm/BUILD
mkdir ~/rpm/RPMS
mkdir ~/rpm/RPMS/`uname -p`
mkdir ~/rpm/RPMS/noarch
mkdir ~/rpm/SOURCES
mkdir ~/rpm/SPECS
mkdir ~/rpm/SRPMS
mkdir ~/rpm/tmp

rpmbuild用の設定ファイルを作成

cat <<EOF > ~/.rpmmacros
%_topdir $HOME/rpm
%_tmppath $HOME/rpm/tmp
EOF

あとは、拾ってきたSourceをSOURCESに配置して

rpmbuild -tb ~/rpm/SOURCES/xxxxxx.tar.gz

などでOK。

LDAP環境下の一般ユーザーで作ったけど、全然関係ないサーバーにもrpm持って行ってインストールできたので問題ないと思う。
CentOS5系でもたぶん同じ手順でいけるでしょう。
rpmbuildのオプションは別途。


参考
Building RPM packages as non-root user « #!/bin/blog

CentOS 6.2 上のKVMをリモートのvirt-managerから操作する

CentOS系ではyum install qemu-kvm libvirtしたあと、root以外でlibvirtを実行できるような設定になっていない。

・rootユーザーでSSHログインはしない
virt-managerはサーバー上でなく、手元からリモートで動かしたい

上記を満たすために、libvirtdの設定を変更する。

# vi /etc/libvirt/libvirtd.conf
unix_sock_group = "libvirt"
unix_sock_rw_perms = "0770"
groupadd libvirt
usermod -a -G libvirt [username]

これでlibvirtdを再起動すればOK…のはずだったのだが。
SSHの鍵認証をさせて、virt-managerで接続させても延々と"libvirtError: authentication failed"のような認証エラーが出る。

# vi /usr/share/polkit-1/actions/org.libvirt.unix.policy
<allow_any>auth_admin</allow_any> 
<allow_inactive>auth_admin</allow_inactive>

上記の修正で直ったという情報もあったけれど、どうもしっくりこないので調べた。(そもそもこれで認証が通ったら、unix_sock_rw_perms = "0770"の設定は何なんだ?)

結論としては、PolicyKitの認証が関係していた。
CentOSでは、libvirtd.confがほとんどの場合コメントアウトしてあり、以下の部分も同様。

# /etc/libvirt/libvirtd.conf
# auth_unix_rw = "none"

libvirtd.conf中のauth_unix_rwの説明に、以下のようにある。

# Set an authentication scheme for UNIX read-write sockets
# By default socket permissions only allow root. If PolicyKit
# support was compiled into libvirt, the default will be to
# use 'polkit' auth.

つまりCentOS6では、libvirtが最初からpolicykitを使うようにビルドされているということだ。

libvirtd.confを以下のように編集しなおして、無事操作できた。

# vi /etc/libvirt/libvirtd.conf
unix_sock_group = "libvirt"
unix_sock_rw_perms = "0770"
auth_unix_rw = "none"

ちなみに、Ubuntu系では最初から

auth_unix_ro = "none"
auth_unix_rw = "none"

と設定されている。

openSUSEのマニュアルにも、 第7章 接続と権限

推奨設定
SSH を利用したリモートからのトンネル
7.1.1.1項 「パーミッションとグループ所有者を利用した UNIX ソケット向けのアクセス制御」

想定済みというわけらしい…。うーんこのRHEL


参考にしたサイト