地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【Linux】TCPの再送制御あたりのソースを眺める

概要

ブラウザからWebサーバを経由しAPサーバにアクセスする場合の通信に関する説明をざっくりまとめてみました。再送制御あたりがメインです。

記事を書こうと思った背景

AWSの資格学習をしていてRDSあたりのフェールオーバ ーあたりで「DNSベースでやるのはいいけどJVMやらサードパーティで提供されてるコネクションプールなんかで良くないこと起きそう?」って思ったのが記事書こうと思った動機です。

LinuxTCPの再送制御あたりの知識って断片的な知識だけで実はきちんと理解してないことに気づいたのでAWSの資格勉強の息抜きに調べてみた。*.hをメインに眺めるだけで突っ込んだTCPあたりの実装はそんなに読まない方針でいく。

そもそもTCPの再送制御ってなんだって話は以下が詳しかった。

TCP/IP - 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の再送タイムアウトを制御したい - Qiita

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側でも対処が必要

参考リンク

(おまけ) Linuxにおける選択的応答SACKの脆弱性

tcp_sack

RFC2018 による選択的確認応答 (SACK) を有効にする

SACKとは受信者が順序がバラバラのセグメントを受信したことを送信者に通知することを可能にする仕組み。「私はシーケンス番号10まですべてを受け取り、次に11を期待しますが、20-30も受け取りました」みたいなことを可能にする。この機能を使った脆弱性

ブログ: TCP SACK Panicについて知っておくべきこと