LinuxのTCP受信バッファ溢れと輻輳制御の話

年明け早々に新鮮なネタがあがったため、暖かいうちに書いておこうと思います。

今回はサービスのフロント側でトラフィックを受けているnginxサーバのTCP受信バッファが溢れた問題です。
後述しますが厄介なところは、ユーザがOSから得られるすべてのカウンターの値を見ても一切ドロップが発生していることが把握できないことです。(多分)

以前よりサービス品質を保つために自社サブネットを指定してtcpdumpをとり、外部から見た通信に異常がないか定期的に確認していました。
その作業をVCの銭亀おじさんたちに仕事の腰をへし折られてからしばらく忘れて怠っていました。
その間いい加減な仕事をしていたということですね。

年が明けてから思い出して久しぶりにdumpをとってみたところ、下記のようにものすごい数のTCPの再送や、重複ACKも確認できました。(popinが邪魔かも…)rmem1
これは明らかに経路のどこかでブラックホールが存在している症状です。
社内L8層とは関係なくトラフィックも地味に伸び続けていたため、どこかのリミットがはじけたと思いました。
かといってtcpdumpまではいかないものの、スイッチやサーバの各種エラーカウンターは休日も毎日確認していました。
なんでこんなひどい状態になるまで検出することができなかったのか、ちょっと考えましたが答えはcapファイルを見てちょっとまじめに考えたらすぐに予想がつきました。

再送多発のトリガーは下記の画像です。
TCPウィンドウサイズの変更をサーバ側がクライアントにプッシュしています。
増える分にはスループットが向上するので構いませんが、減少している場合はTCPの受信バッファに空きがなく、お腹いっぱいなのでちょっとまって状態という解釈ができます。rmem2
では実際に流れを追って見てみます。

まずサーバはクライアントからSYNを受信し、SYN+ACKを返す段階ではウィンドウサイズを14480に設定しています。
rmem3
次にデータのやりとりを始めた段階ですぐにTCP受信バッファの空きがないため、TCP Window Updateでウィンドウサイズを114に設定しています。
rmem4
通常であればこの流れで輻輳制御が作用し、ネットワークの輻輳が回避できます。
ここまでひどいパケットドロップも発生しないはずです。
しかしここで問題になるのが、例によって広告配信という特殊な環境と、Linux TCPスタックの実装との相性の悪さです。

TCPはそもそもの設計思想がスループットを優先する仕組みになっています。
スループットが優先されるシステムやサービスでは、ウィンドウサイズの操作による輻輳制御はとても有効に作用します。
しかし一つ一つの通信が小さく、短い時間で終了し、手数も馬鹿みたいに多い広告配信においては、ウィンドウサイズによる輻輳制御はまったく有効に機能しません。

クライアントからの再送も重なり、結果的にnginxサーバが慢性的なバッファフルによる輻輳状態に陥り、パケットをぼろぼろ落とすという現象が発生していたようです。

またカーネルソースを読んだところ、バッファフルの時は goto drop; とシンプルに一行書いてあるだけでした。
再送があるしウィンドウサイズで輻輳制御もしてるし、バッファフルになったらふわっと捨てればいいやという感じなんですかね。
せめてなにかのカウンターインクリメントしてほしい…。

と経緯は書いてみると長いものの、対策はLinuxネットワークチューニングのテンプレートによく書いてある内容です。
調査過程で気づいた通信中にふわっといなくなるモバイルネットワーク対策で送信側もついでに増やすことにしました。

net.core.wmem_max = 12582912
net.core.rmem_max = 12582912
net.ipv4.tcp_rmem = 10240 12582912 12582912
net.ipv4.tcp_wmem = 10240 12582912 12582912

どうせフルフルで使うので通常値 = 最大値の設定で運用しています。
前にドキュメント読んだ時「動的だから通常ここは設定変えなくてもおk」みたいなこと書いてあったんですけどね。
自社サービスを外から見てみるという事が改めて重要なことだと認識しました。

広告インフラまじ鬼畜。

netfilterのconntrack_maxとip_local_port_rangeの関係

いろいろ追い続けてきたnetfilterですが、Linux NAT Boxとしての物理的な限界値であるOSの使用できるポートの上限数にたどり着きました。

メモリをガン積みしていくらconntrack_maxの上限を増やしたところで、ip masquaradeに使うポートがなければ頭打ちです。

これについてはnetstat-natコマンドが見やすいです。
あれやこれやワンライナーでつなげて見ると、ASSUREDの状態のconntrackでも一つのポートをシェアしてる様子が伺えます。
これは手元のクライントマシンでnetfilterをロードして抽出した結果です。

tcp   192.168.2.111:62258               xx.xxx.xx.xxx:www             TIME_WAIT
tcp   192.168.2.111:62258               xx.xxx.xx.xxx:www             TIME_WAIT
tcp   192.168.2.111:62258               xx.xxx.xx.xxx:www             TIME_WAIT

つまりconntrack_countがローカルポートの使用数というわけではないことがわかります。

こんな感じでリアルで使っているポートをユニークで洗い出すと使用しているポートの数が分かります。

 sudo netstat-nat | awk '{print $2}' | cut -d ":" -f2 | sort -n | uniq | wc -l
25

いくらip_local_port_rangeを増やしたところでnetfilterのメモリ使用容量から逆算すると、せいぜい16Gもつんでいれば十分なようですね。
FWやsuricata等で鬼のようなトラッキングをしたいという時は別でしょうけど、GWだけでつかっている場合には16Gで十分おつりがきます。

FWアプライアンスがメモリをいっぱい積んでる理由も納得できますね。

netfiterのip_conntrackの上限について掘り下げてみた

conntrackの値を監視しようとしてスクリプトを作っている最中にこんなことに気づきました。

cat /proc/sys/net/ipv4/netfilter/ip_conntrack_count
53424

いくらなんでも多すぎじゃね、っていうか実際こんなにコネクションが残ってたらポートが枯渇するかどうかっていう話になってしまいます。

さっそく内容を確認してみました。conntrackコマンドでもprocfsでもどちらでも確認することができます。
5万行でてくるのでheadで一部だけ見ます。

$ sudo head -n 3 /proc/net/ip_conntrack
tcp      6 180817 ESTABLISHED src=10.x.x.xx dst=1.xx.x.xx sport=80 dport=52843 [UNREPLIED] src=1.xx.x.xx dst=xxx.xx.x.xxx sport=52843 dport=80 mark=0 use=2
tcp      6 88234 ESTABLISHED src=10.x.x.xx dst=1.xx.xxx.xxx sport=80 dport=64778 [UNREPLIED] src=1.x.xxx.xxx dst=xxx.xx.x.xxx sport=64778 dport=80 mark=0 use=2
tcp      6 329583 ESTABLISHED src=10.x.x.xx dst=1.x.x.x sport=80 dport=63624 [UNREPLIED] src=1.x.x.xx dst=xxx.xx.x.xxx sport=63624 dport=9 mark=0 use=2

3個目のカラムはタイムアウトです。ESTABLISHEDなTCPだとデフォルト値の5日間残ります。この時点で慌ててタイムアウトの値を小さくしたのですが、テーブルをFLUSHするのも怖いので、5日間かけて消えてなくなるのをじっくり待つことにしました。

しかし[UNREPLIED]ってなんぞやと思い調べてみたところ、公式に回答がのっていました。

http://www.frozentux.net/iptables-tutorial/iptables-tutorial.html#THECONNTRACKENTRIES

通信が双方から行われていないと[UNREPLIED]になるようです。TCPのセッションとしては残っていないけど、conntrackのテーブルに記憶されている状態なのかな。
[ASSURED]は破棄されないと書いてあるので、実際は[ASSURED]の数が上限値という感じでしょうか。こちらは全体で100ちょっとでした。

さらにこんなものをみつけました。

http://archive.linux.or.jp/JF/JFdocs/netfilter-faq-3.html#ss3.16

あなたは /proc/net/ip_conntrack を見て、UNREPLIED エントリに非常に大きな タイマ値(最高で5日)が割り当てられているのに気付き、どうして我々が (明らかにコネクションではない) UNREPLIED エントリに無駄に conntrack エントリを使おうとするのかと不思議に思われたのですね?
その答えは簡単です: UNREPLIED エントリは、テンポラリなエントリだからです。 つまり、コネクション追跡エントリが限界まで来たらすぐに、古い UNREPLIED エントリを削除します。言いかえると、conntrack を何も持たないよりは、 UNREPLIED エントリの中の有用かもしれない情報を、それが実際に必要 とするまで保持しておくのがよいだろうというわけです。

「何度もトラッキングしなおすよりメモリにおいといたほうがいいだろwww」

ってことですかね。
しかし、メモリにたくさんとっておいてスキャンするコストと、少ない件数をメモリに残しておいて新しくトラッキングさせるのは、どっちがコストが安いんでしょうね。
もう少し調べる必要がありそうです。

Ubuntuでmacvlan

Ubuntuでmacvlanを使うのはとても簡単。

sudo aptitude install uml-utilities
sudo ip link add dev mvleth0 link eth0 type macvlan
sudo ip link set mveth0 up
sudo ifconfig mveth0 10.1.100.1 netmask 255.255.0.0

だけです。そうするとこのようにインターフェイスが生えます。

eth0 Link encap:Ethernet HWaddr 52:54:00:6d:b1:a5 
 inet addr:10.1.3.65 Bcast:10.1.255.255 Mask:255.255.0.0
 inet6 addr: fe80::5054:ff:fe6d:b1a5/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
 RX packets:11285 errors:0 dropped:187 overruns:0 frame:0
 TX packets:694 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000 
 RX bytes:859612 (859.6 KB) TX bytes:118257 (118.2 KB)
 Interrupt:11 Base address:0x6000
mveth0 Link encap:Ethernet HWaddr 82:80:0b:36:24:c4 
 inet addr:10.1.100.10 Bcast:10.1.255.255 Mask:255.255.0.0
 inet6 addr: fe80::8080:bff:fe36:24c4/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
 RX packets:7659 errors:0 dropped:181 overruns:0 frame:0
 TX packets:9 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:0 
 RX bytes:665866 (665.8 KB) TX bytes:726 (726.0 B)

ただしvyattaと違って他からみたMACアドレスは実デバイスになってしまいます。なにかカーネルパラメータが不足しているかもしれません。

? (10.1.100.10) at 52:54:00:6d:b1:a5 [ether] on br0

pvcreateで “device-mapper: ioctl: error adding target to table

例によって中古ディスクの使いまわしで発生した問題です。
既存のパーティションを削除した後、新たにlvmパーティションを作成し、pvcreateでエラーがでました。

パーティション作り直しでもディスクのメタ情報は消えないようです。

# /sbin/dmsetup  status

pdc_bgbaijhjgc: 0 3906898080 linear ←これ
VolGroup00-LogVol00: 0 972357632 linear
# /sbin/dmsetup info pdc_bgbaijhjgc
Name:              pdc_bgbaijhjgc
State:             ACTIVE
Read Ahead:        256
Tables present:    LIVE
Open count:        0
Event number:      0
Major, minor:      253, 2
Number of targets: 1
UUID: DMRAID-pdc_bgbaijhjgc

Google先生仰せのままに解決しました。

# /sbin/dmsetup remove_all

# /sbin/dmsetup  status
VolGroup00-LogVol01: 0 4194304 linear 
VolGroup00-LogVol00: 0 972357632 linear 

# /usr/sbin/pvcreate /dev/sdc1 
Physical volume "/dev/sdc1" successfully created