概要
ブラウザからWebサーバを経由しAPサーバにアクセスする場合の通信に関する説明をざっくりまとめてみました。再送制御あたりがメインです。
記事を書こうと思った背景
AWSの資格学習をしていてRDSあたりのフェールオーバ ーあたりで「DNSベースでやるのはいいけどJVMやらサードパーティで提供されてるコネクションプールなんかで良くないこと起きそう?」って思ったのが記事書こうと思った動機です。
LinuxのTCPの再送制御あたりの知識って断片的な知識だけで実はきちんと理解してないことに気づいたのでAWSの資格勉強の息抜きに調べてみた。*.hをメインに眺めるだけで突っ込んだTCPあたりの実装はそんなに読まない方針でいく。
そもそもTCPの再送制御ってなんだって話は以下が詳しかった。
実験環境/Linux tcpのソースバージョン
実験環境は以下
$ uname -a Linux choco001 3.10.0-1127.13.1.el7.x86_64 #1 SMP Tue Jun 23 15:46:38 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
実装自体は以下のあたりからメインで眺めていく。
linux/tcp.c at master · torvalds/linux · GitHub
ヘッダーは以下。
linux/tcp.h at master · torvalds/linux · GitHub
関連するカーネルパラメータ
以下に今回の再送制御関連のパラメータをあげます。基本的にこの辺のパラメータは以下の構造体を使って設定している。案の定巨大な構造体なので気になるところ以外はオールスルーで行きます。
linux/ipv4.h at master · torvalds/linux · GitHub
tcp_syn_retries
TCP 接続を行うために最初の SYN パケットの再送信を試みる回数
接続先が落ちてるケースでtcpのsynをリトライする回数。1回目のsynから2回目のsynを投げるまでの時間は1秒。tcp.hには以下のように定義されている。
#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ)) /* RFC6298 2.1 initial RTO value */ static inline u32 tcp_timeout_init(struct sock *sk) { int timeout; timeout = tcp_call_bpf(sk, BPF_SOCK_OPS_TIMEOUT_INIT, 0, NULL); if (timeout <= 0) timeout = TCP_TIMEOUT_INIT; return timeout; }
この値を変えたいケースはDNSベースでフェールオーバ をサポートしてるDBなんかで名前をアプリ側で引いた際に既に落ちてるノードのIPが帰ってきたらアプリにエラーが通知されるまでリトライ分の時間がかかってしまう。
(クライアントTCPがSYNセグメントに対する応答を受信しなかった場合ETIMEOUTが返されアプリ側で再送してるケースはここでは考慮しない。)
リトライ分の時間の計算はちょっと厄介で調べたけどいまいちピントは来なかった。リトライアルゴリズム自体はLinuxもエクスポネンシャルバックオフ(のようなアルゴリズム)を使って指数関数的にリトライ間隔時間を増やして実装している。
RFCあたりにもっと詳しく書いてるらしいが解読はできず。。。
RFC 6298 - Computing TCP's Retransmission Timer 日本語訳
ちなみにこの辺の値はカーネルにハードコードされているので自前で値を変える場合はビルドしてあげる必要がある。このレイヤで書き換えるぐらいならアプリ側でハンドリングすればいいだけの話の気もする。
#define TCP_RTO_MAX ((unsigned)(120*HZ)) #define TCP_RTO_MIN ((unsigned)(HZ/5))
接続先次第でこの辺を動的に変えてる記事があったので貼っておきます。システムコール自体をhookしてやってるようです。恐ろしい。。Webサーバのインターネット側とバックエンド側で、TCPの再送タイムアウトを変えたいような場合と言うのはなんとなく分かるけど自分たちで実装するとは。。。
tcp_retries1
TCP 接続要求に対する応答の再送を試みる回数を指定
tcp_retries2
TCP パケットの再送を試みる回数を指定
tcp_retries2 は確立された接続の再送信タイムアウトになります。タイムアウトを短くしないといけないような状況の時は再送回数を調整して短くすることでアプリケーションの無駄な待ち時間を減らすことができます。(ESTABLISHな状態で相手が死んだケースでのリトライ回数)
net.ipv4.tcp_retries2 = 15
デフォルト値は15となっていてこの辺はRFCにも推奨値なんかが乗ってたりするみたいです。ESTABLISHなコネクションで接続先がRSTを投げずに死んだ場合はこの値だけリトライします。
https://jprs.jp/tech/material/rfc/RFC1122-ja.txt
This value influences the timeout of an alive TCP connection, when RTO retransmissions remain unacknowledged. Given a value of N, a hypothetical TCP connection following exponential backoff with an initial RTO of TCP_RTO_MIN would retransmit N times before killing the connection at the (N+1)th RTO. The default value of 15 yields a hypothetical timeout of 924.6 seconds and is a lower bound for the effective timeout. TCP will effectively time out at the first RTO which exceeds the hypothetical timeout. RFC 1122 recommends at least 100 seconds for the timeout, which corresponds to a value of at least 8.
net.ipv4.tcp_keepalive_time
接続がアイドル状態になってから、keep-alive プローブを送信するまでの時間を秒単位で指定
まとめ
- DBがフェイルオーバする際にFINが送られるならアプリ側では(下手にDNSキャッシュを持ってなければ)特にやることはなさそう
- DBが不慮の停止でFINを送信できない場合は注意が必要
- (1) コネクションプールしてるならきちんと死活監視をする。アプリでやるにしろOSでやるにしろ間隔は短めにするのが良さそう
- (2) アプリ側のクラッシュも考慮するならDB側でも対処が必要
参考リンク
- TCP再送タイムアウト時間の規格と実装 - co1row’s diary
- TCPの再送時間について – くじらぴーまん
- LinuxのTCP SYNの再送間隔の初期値が3秒から1秒に変更されていた - 元RX-7乗りの適当な日々
- sockets - How to hack into the Unix Kernel to remove the Exponential Backoff from TCP? - Stack Overflow
- TCPとタイムアウトと私 - Cybozu Inside Out | サイボウズエンジニアのブログ
(おまけ) Linuxにおける選択的応答SACKの脆弱性
tcp_sack
RFC2018 による選択的確認応答 (SACK) を有効にする
SACKとは受信者が順序がバラバラのセグメントを受信したことを送信者に通知することを可能にする仕組み。「私はシーケンス番号10まですべてを受け取り、次に11を期待しますが、20-30も受け取りました」みたいなことを可能にする。この機能を使った脆弱性。