LVS/DSR + iptablesで切断したコネクションが消えなくてハマった

LVS+DSRでMySQLサーバの参照を分散してRailsから接続してみたところ、
デプロイごとにMySQLのコネクションが増えていくという事象が発生してハマりました。
Railsの持続的なコネクションだとそもそもLVSで分散できねーだろ、ってツッコミは今回は無しでお願いします・・・

結論としてはLVS が FIN を落とすに書いてるとおり、LVSサーバのiptables設定がダメだったのですが、いちおう経緯を書いておきます。

iptables無しの状態

通常、MySQLに外部から接続すると

$ mysql -utest -ptest -h192.168.1.111 tmp_app_development

MySQLサーバ上にESTABLISHEDなコネクションが出来て、

[root@mysql01 ~]# netstat -an | grep 3306
tcp        0      0 0.0.0.0:3306                0.0.0.0:*                   LISTEN      
tcp        0      0 192.168.1.111:3306          192.168.1.61:49788          ESTABLISHED 

MySQLのプロセスリストが1つ増えます。

mysql> show processlist;
+----+------+--------------------+---------------------+---------+------+-------+------------------+
| Id | User | Host               | db                  | Command | Time | State | Info             |
+----+------+--------------------+---------------------+---------+------+-------+------------------+
| 34 | test | 192.168.1.61:49805 | tmp_app_development | Sleep   |   28 |       | NULL             |
| 37 | root | localhost          | NULL                | Query   |    0 | init  | show processlist |
+----+------+--------------------+---------------------+---------+------+-------+------------------+
2 rows in set (0.00 sec)

そして、切断するとどっちも消える。

[root@mysql01 ~]# netstat -an | grep 3306
tcp        0      0 0.0.0.0:3306                0.0.0.0:*                   LISTEN  
mysql> show processlist;
+------+------+--------------------+------+---------+------+-------+------------------+
| Id   | User | Host               | db   | Command | Time | State | Info             |
+------+------+--------------------+------+---------+------+-------+------------------+
| 2648 | test | 192.168.1.61:51120 | NULL | Query   |    0 | init  | show processlist |
+------+------+--------------------+------+---------+------+-------+------------------+
1 row in set (0.00 sec)

はずなんですが、、、

今回起こった事象

LVSサーバにiptablesを適用することになったので、こんな感じでMySQLの接続を許可する設定をいれました。

[root@lvs01 ~]# iptables -A INPUT -m state --state NEW -m tcp -p tcp --dport 3306 -j ACCEPT

そうするとデプロイするたびにMySQLサーバの接続数が増えていく。
APPサーバ側は異常はないんだけど、MySQLサーバで旧プロセスのコネクションが残ったままになってるっぽい。

簡単に確認するためにdatabase.ymlの接続先をLVSのVIPに切り替えて接続、切断してみる。

[mikeda@test01 tmp_app]$ ./bin/rails c
irb(main):001:0> User.first
irb(main):002:0> exit

切断しても、コネクションがESTABLISHEDのままで残っている・・・

[root@mysql01 ~]# netstat -an | grep 3306
tcp        0      0 0.0.0.0:3306                0.0.0.0:*                   LISTEN      
tcp        0      0 192.168.1.111:3306          192.168.1.61:49805          ESTABLISHED

MySQLのプロセスも消えずにどんどん溜まっていく・・・

mysql> show processlist;
+----+------+--------------------+------+---------+------+-------+------------------+
| Id | User | Host               | db   | Command | Time | State | Info             |
+----+------+--------------------+------+---------+------+-------+------------------+
| 34 | test | 192.168.1.61:49805 | tmp  | Sleep   |   28 |       | NULL             |
| 37 | root | localhost          | NULL | Query   |    0 | init  | show processlist |
+----+------+--------------------+------+---------+------+-------+------------------+
2 rows in set (0.00 sec)

でもMySQLのwait_timeoutは変えたくない・・・

というわけで調査と対応

LVSサーバ、MySQLサーバで切断時のtcpdumpを取ってみると、

LVS側ではえんえんとFINパケットが届いているんだけど、

[root@lvs01 ~]# tcpdump -n port 3306
...
05:21:47.240436 IP 192.168.1.61.49822 > 192.168.1.111.mysql: Flags [F.], seq 390, ack 1871, win 618, options [nop,nop,TS val 8292014 ecr 8285790], length 0
05:21:47.441272 IP 192.168.1.61.49822 > 192.168.1.111.mysql: Flags [F.], seq 390, ack 1871, win 618, options [nop,nop,TS val 8292215 ecr 8285790], length 0
05:21:47.843234 IP 192.168.1.61.49822 > 192.168.1.111.mysql: Flags [F.], seq 390, ack 1871, win 618, options [nop,nop,TS val 8292617 ecr 

MySQL側には何も来ない。

[root@mysql01 ~]# tcpdump -n port 3306
...

というわけでLVS/DSR + iptablesでググってみるとこういう記事を見つけた。

LVS が FIN を落とす

LVS の DR 構成は、帰りのパケットが LVS を通らないために netfilter(iptables) がコネクションの確立を認識せず、FIN を通さないということが判りました。netfilter で TCP の state を見ないようにすることで、無事解決です。

なるほど。
とりあえず書かれているようにLVSサーバのiptables設定を変更してみると、

[root@lvs01 ~]# iptables -D INPUT -m state --state NEW -m tcp -p tcp --dport 3306 -j ACCEPT
[root@lvs01 ~]# iptables -A INPUT -m tcp -p tcp --dport 3306 -j ACCEPT

解決!!!

LVSサーバではクライアントからのFINと(MySQLサーバからのFINに対する)ackのみが確認されて、

[root@lvs01 ~]# tcpdump -n port 3306
...
05:42:10.944673 IP 192.168.1.61.49851 > 192.168.1.111.mysql: Flags [F.], seq 390, ack 1871, win 618, options [nop,nop,TS val 9515718 ecr 9509710], length 0
05:42:10.945068 IP 192.168.1.61.49851 > 192.168.1.111.mysql: Flags [.], ack 1872, win 618, options [nop,nop,TS val 9515718 ecr 9513806], length 0

MySQLサーバではFIN、FIN/ack、ackの全てが確認できました。

[root@mysql01 ~]# tcpdump -n port 3306
...
05:42:10.594478 IP 192.168.1.61.49851 > 192.168.1.111.mysql: Flags [F.], seq 391, ack 1871, win 618, options [nop,nop,TS val 9515718 ecr 9509710], length 0
05:42:10.594571 IP 192.168.1.111.mysql > 192.168.1.61.49851: Flags [F.], seq 1871, ack 392, win 243, options [nop,nop,TS val 9513806 ecr 9515718], length 0
05:42:10.594852 IP 192.168.1.61.49851 > 192.168.1.111.mysql: Flags [.], ack 1872, win 618, options [nop,nop,TS val 9515718 ecr 9513806], length 0

DSRの挙動を考えると、なるほどなという感じですね。

ついでに

Railsではなくmysqlコマンドで接続、切断すると、

$ mysql -utest -ptest -h192.168.1.111
mysql> exit
Bye
$ 

コネクションはESTABLISHEDではなくFIN_WAIT2になりました。

[root@mysql01 ~]# netstat -an | grep 3306
tcp        0      0 0.0.0.0:3306                0.0.0.0:*                   LISTEN      
tcp        0      0 192.168.1.111:3306          192.168.1.61:49793          FIN_WAIT2

これはtcp_fin_timeoutの設定値に従って60秒で消えます。

[root@mysql01 ~]# sysctl -a | grep tcp_fin_timeout
net.ipv4.tcp_fin_timeout = 60

ちゃんと調べられてないけど、この違いはなんなんだろう。

まとめ

LVS/DSR + iptablesは初めてで、ちょっとハマりました。
もしかしたら常識なのかもですが。

LVS/DSRはハマりどころ多いですねぇ。