LinuxソースコードからTIME_WAITの期間を分析する

LinuxソースコードからTIME_WAITの期間を分析する

1. はじめに

Linux での TIME_WAIT 状態のソケットの持続時間は約 60 秒だと私は常に思っていました。実際に、TIME_WAIT が 100 秒を超えるオンライン ソケットが存在します。これは、最近発生した複雑なバグの分析を伴うためです。そこで著者は Linux のソースコードを調べて調べました。

2. まずLinux環境を紹介しましょう

TIME_WAIT パラメータは通常、5 重再利用に関連しています。ここで、著者は、他の問題との混乱を避けるために、まずマシンのカーネルパラメータ設定を示します。

猫 /proc/sys/net/ipv4/tcp_tw_reuse 0

猫 /proc/sys/net/ipv4/tcp_tw_recycle 0

cat /proc/sys/net/ipv4/tcp_timestamps 1

ご覧のとおり、tcp_tw_recycle を 0 に設定することで、NAT 下で tcp_tw_recycle と tcp_timestamps を同時に有効にすることによって発生する問題を回避できます。

3. TIME_WAIT状態遷移図

ソケットの TIME_WAIT 状態について話すときは、TCP 状態遷移図を示す必要があります。

持続時間は図に示すように 2MSL です。ただし、この図では 2MSL の長さは示されていませんが、著者は Linux ソース コードで次のマクロ定義を発見しました。

#define TCP_TIMEWAIT_LEN (60*HZ) /* TIME-WAIT を破棄するまでの待機時間
				  * 状態、約60秒 */

英語の文言が文字通り意味する通り、TIME_WAIT 状態は 60 秒後に破棄されるので、2MSL は 60 秒でなければなりませんか?

4. 継続時間は本当に TCP_TIMEWAIT_LEN で定義されているとおりですか?

著者は、60 秒の TIME_WAIT 状態のソケットはカーネルによってリサイクルできると常に信じてきました。筆者自身もポート番号を telnet し、人為的に TIME_WAIT を作成し、時間を計るという実験を行ったところ、約 60 秒でリサイクルできました。

しかし、問題を追跡してみると、TIME_WAIT が最大 111 秒続く場合があることがわかりました。そうでなければ、この現象をまったく説明できません。このため、著者は自身の結論を覆し、TIME_WAIT 状態処理のカーネル ソース コードを再読せざるを得なくなりました。もちろん、この調査内容はブログにも書かれて共有される予定ですので、お楽しみに。

5. TIME_WAITタイマーのソースコード

TIME_WAIT をいつリサイクルできるかについて話すときは、期限切れの TIME_WAIT ソケットを破棄するために特に使用される TIME_WAIT タイマーについて話す必要があります。各ソケットが TIME_WAIT に入ると、必然的に次のコード ブランチを経由することになります。

tcp_v4_rcv

|->tcp_timewait_state_process

/* time_wait 状態のソケットをタイムホイールにリンクします

|->inet_twsk_スケジュール

カーネルは tcp_tw_recycle を有効にしていないため、最終的な呼び出しは次のようになります。

/* TCP_TIMEWAIT_LEN 60 * HZ ここに */
inet_twsk_schedule(tw、&tcp_death_row、TCP_TIMEWAIT_LEN、
					 デフォルト値: TCP_TIMEWAIT_LEN

さて、このコア機能を押してみましょう。

5.1、inet_twsk_schedule

ソースコードを読む前に、全体的な処理フローを見てみましょう。 Linux カーネルは、次の図に示すように、タイム ホイールを使用して期限切れの TIME_WAIT ソケットを処理します。

カーネルは 60 秒の時間を 8 つのスロット (INET_TWDR_RECYCLE_SLOTS) に分割し、各スロットは time_wait 状態のソケットの 7.5 (60/8) の範囲を処理します。

void inet_twsk_schedule(構造体 inet_timewait_sock *tw、構造体 inet_timewait_death_row *twdr、const int timeo、const int timewait_len)
{
	......
	// タイムホイールのスロットを計算する
	スロット = (timeo + (1 << INET_TWDR_RECYCLE_TICK) - 1) >> INET_TWDR_RECYCLE_TICK;
	......
	// スロータイムホイールのロジック。TCP\_TW\_RECYCLE が有効になっていないため、timeo は常に 60*HZ (60 秒) になります。
	// すべては slow_timer ロジックに従います if (slot >= INET_TWDR_RECYCLE_SLOTS) {
		/* スロータイマーをスケジュールする */
		(timeo >= timewait_len)の場合{
			スロット = INET_TWDR_TWKILL_SLOTS - 1;
		} それ以外 {
			スロット = DIV_ROUND_UP(timeo, twdr->period);
			(スロット >= INET_TWDR_TWKILL_SLOTS) の場合
				スロット = INET_TWDR_TWKILL_SLOTS - 1;
		}
		tw->tw_ttd = jiffies + timeo;
		// twdr->slotは現在処理中のスロットです
		// TIME_WAIT_LENでは、このロジックは通常7です
		スロット = (twdr->スロット + スロット) & (INET_TWDR_TWKILL_SLOTS - 1);
		list = &twdr->cells[スロット];
	} それ以外{
		// 短いタイマーを実行します。スペースの制約により、ここでは詳細には触れません......
	}
	......
	/* twdr->周期 60/8=7.5 */
	(twdr->tw_count++ == 0)の場合
		mod_timer(&twdr->tw_timer, jiffies + twdr->period);
	spin_unlock(&twdr->death_lock);
}

ソース コードからわかるように、渡したタイムアウトは TCP_TIMEWAIT_LEN です。したがって、TIME_WAIT 状態に入ったばかりのソケットは、処理のために、現在の処理スロット (+7) から最も遠いスロットにリンクされることになります。次の図に示すように:

カーネルが TIME_WAIT を生成し続けると、低速タイマー ホイール全体は次の図のようになります。

すべてのスロットは TIME_WAIT 状態のソケットで埋められます。

5.2. 特定のクリーンアップ機能

inet_twsk_schedule が呼び出されるたびに渡される処理関数は次のとおりです。

/*パラメータ内のtcp_death_rowはタイムホイール処理関数を運ぶ構造体です*/
inet_twsk_schedule(tw、&tcp_death_row、TCP_TIMEWAIT_LEN、TCP_TIMEWAIT_LEN) のスケジュール
/* 特定の処理構造 */
構造体inet_timewait_death_rowtcp_death_row = {
	......
	/* slow_timer タイムホイール処理関数*/
	.tw_timer = TIMER_INITIALIZER(inet_twdr_hangman, 0,
					    (符号なしlong)&tcp_death_row)、
	/* slow_timer タイムホイール補助処理関数*/
	.twkill_work = __WORK_INITIALIZER(tcp_death_row.twkill_work,
					     inet_twdr_twkill_work)、
	/* 短時間ホイール処理関数*/
	.twcal_timer = TIMER_INITIALIZER(inet_twdr_twcal_tick, 0,
					    (符号なしlong)&tcp_death_row)、				
};

主に TCP_TIMEWAIT_LEN (60 秒) に設定された処理時間に関心があるため、slow_timer タイム ホイール処理関数、つまり inet_twdr_hangman を直接調べます。この関数は比較的短いです:

void inet_twdr_hangman(符号なしロングデータ)
{
	構造体 inet_timewait_death_row *twdr;
	符号なし整数 need_timer;

	twdr = (構造体inet_timewait_death_row *)データ;
	spin_lock(&twdr->death_lock);

	(twdr->tw_count == 0)の場合
		外出する;

	必要タイマー = 0;
	// このスロットで処理されるtime_waitソケットの数が100に達し、まだ処理されていない場合 if (inet_twdr_do_twkill_work(twdr, twdr->slot)) {
		twdr->thread_slots |= (1 << twdr->slot);
		// 残りのタスクを処理のためにワークキューに送信します。schedule_work(&twdr->twkill_work);
		必要タイマー = 1;
	} それ以外 {
		/* スロット全体を消去しましたが、何か残っていますか? */
		// 処理を続行するかどうかを決定する if (twdr->tw_count)
			必要タイマー = 1;
		// 現在のスロットが処理された場合、次のスロットにジャンプします
		twdr->スロット = ((twdr->スロット + 1) & (INET_TWDR_TWKILL_SLOTS - 1));
	}
	// さらに処理が必要な場合は、7.5秒後にこの関数を再度実行します。if (need_timer)
		mod_timer(&twdr->tw_timer, jiffies + twdr->period);
外:
	spin_unlock(&twdr->death_lock);
}

シンプルですが、この機能には多くの詳細があります。最初の詳細は inet_twdr_do_twkill_work にあります。このスロットに time_waits が多すぎて現在のプロセスがブロックされるのを防ぐために、100 個の time_wait ソケットを処理した後に戻ります。このスロットの残りの time_wait は、カーネルの work_queue メカニズムによって処理されます。

注目に値する。 slow_timer タイムホイールでは正確な時間が決定されないため、すべてが直接削除されます。したがって、特定のスロット、たとえば 52.5 ~ 60 秒のスロットの順番になると、52.5 ~ 60 秒のすべての time_wait が直接クリアされます。これは、time_wait が 60 秒に達していない場合でも当てはまります。小さなタイムホイール(tw_cal)が正確に時間を決定します。スペースの制約上、ここでは詳しく説明しません。

注: tcp_tw_recycle がオンになっている場合は、小さなタイムホイール (tw_cal) が使用されます。

5.3. まず仮定を立てる

タイムホイールのデータは 1 スロット間隔、つまり (60/8=7.5) 内で処理できると想定します。システムには tcp_tw_max_buckets 設定があるため、設定が適切であれば、この仮定は依然として比較的信頼できます。

注: 60/8 はなぜ 7 ではなく小数点まで正確である必要があるのでしょうか?

実際の計算は60*HZで行われるため、

HZ が 1024 の場合、周期は 7680 になり、精度は ms レベルであることを意味します。

したがって、この記事の計算は小数点まで正確である必要があります。

5.4. スロット内のTIME_WAIT <= 100の場合

スロットの TIME_WAIT <= 100 の場合、当然、処理関数は work_queue を有効にしません。同時に、次の期間に次のスロットを処理できるように、スロット+1 が追加されます。次の図に示すように:

5.5. スロット内でTIME_WAIT>100の場合

スロットの TIME_WAIT が 100 より大きい場合、カーネルは残りのタスクを処理のために work_queue に渡します。同時に、スロットは変更されません!つまり、次の期間(7.5 秒後)が到来すると、同じスロットが処理されます。私たちの仮定によれば、この時点でスロットは処理されているため、スロットは 7.5 秒で前進します。つまり、スロット 0 が先頭にあると仮定すると、実際にスロット 1 を処理するのに 15 秒かかります。

各スロットの TIME_WAIT が 100 より大きいと仮定すると、各スロットの処理には 15 秒かかります。

この状況に対して、著者はそれをシミュレートするプログラムを作成しました。

パブリッククラス TimeWaitSimulator {

    パブリック静的voidメイン(String[] args) {
        ダブルデルタ = (60) * 1.0 / 8;

        // 0 はパージの開始を意味し、1 はパージの完了を意味します // パージが完了すると、スロットは前進します int startPurge = 0;
        二重の合計 = 0;
        整数スロット = 0;
        (スロット<8)の間{
            (開始パージ == 0)の場合{
                合計 += デルタ;
                パージ開始 = 1;
                (スロット == 7)の場合{
                    // work_queue に入った後はすぐにクリーンアップされると想定されているため、 // したがって、スロットが 7 の場合、最後のパージ プロセスである 7.5 秒を待つ必要はありません。
                    System.out.println("スロット " + スロット + " が最後の " + 合計に到達しました);
                    壊す;
                }
            }
            (パージ開始 == 1)の場合{
                合計 += デルタ;
                パージ開始 = 0;
                System.out.println("スロット" + "次の時刻に" + 合計に移動")
                // クリーンアップ後、スロットは前方に移動します slot++;
            }
        }
    }
}

結果は以下の通りです。

スロットは時間 15.0 で次のスロットに移動する

スロットは時間 30.0 で次のスロットに移動する

スロットは45.0の時点で次のスロットに移動する

スロットは時間60.0で次のスロットに移動する

スロットは75.0の時点で次のスロットに移動する

スロットは90.0の時点で次のスロットに移動する

スロットは時間 105.0 に次のスロットに移動する

スロット7は最後の112.5に到達しました

つまり、処理が52.5〜60秒のタイムホイールに到達したときに、実際には外側では112.5秒が経過しており、処理が完全に遅れていることになります。ただし、TIME_WAIT 状態のソケット (inet_timewait_sock) はメモリをほとんど占有しないため、システムの使用可能なリソースに大きな影響を与えることはありません。しかし、これは NAT 環境では落とし穴となり、これは著者の記事で前述したバグでもあります。
上記の計算をグラフとタイムラインに従って描くと、次のようになります。

つまり、TIME_WAIT 状態のソケットが一定期間 (7.5 秒) 内に現在のスロットを処理できる場合、そのソケットは最大 112.5 秒間存在できます。

処理が 7.5 秒以内に完了しない場合は、応答時間ホイールは 1 周期以上回転し続ける必要があります。ただし、tcp_tw_max_buckets の制限により、このような厳しい条件を満たすことは不可能です。

5.6. PAWS (ラップされたシーケンスに対する保護) は TIME_WAIT を拡張します

実際のところ、上記の結論は十分に厳密ではありません。 TIME_WAIT 時間をさらに延長できます。このソースコードを見てください:

列挙型 tcp_tw_status
tcp_timewait_state_process(構造体inet_timewait_sock *tw、構造体sk_buff *skb、
			   定数構造体tcphdr *th)
{
	......
	もし(paws_reject)
		NET_INC_STATS_BH(twsk_net(tw)、LINUX_MIB_PAWSESTABREJECTED);
		
	もし (!th->rst) {
		/* この場合、TIMEWAIT タイマーをリセットする必要があります。
		 *
		 * ACKless SYNの場合は、古い重複である可能性があります
		 * ランダムなシーケンス番号 <rcv_nxt. を持つ新しい正常な SYN。
		 * 最後のケースではスケジュールを変更しないでください。
		 */
		/* ラップアラウンドチェックに失敗したパケットが到着した場合、または ack パケットが * タイマーを新しい 60 秒にリセットした場合 */
		(paws_reject || th->ack) の場合
			inet_twsk_schedule(tw、&tcp_death_row、TCP_TIMEWAIT_LEN、
					   TCP_TIMEWAIT_LEN を指定します。

		/* ACKを送信します。注意: バケットは置かないので、
		 * 発信者によって解放されます。
		 */
		/* 現在の時刻の待機状態で返されるべき ACK を相手側に送信する */
		TCP_TW_ACK を返します。
	}
	inet_twsk_put(tw);
	/* paws によって検証されたパケットは tcp_tw_success を返すので、time_wait 状態のソケット 5 連も 3 ウェイ ハンドシェイク後に正常に再利用できることに注意してください * /
	TCP_TW_SUCCESS を返します。
}

上記のロジックは次の図に示されています。

コードの最後にある TCP_TW_SUCCESS の戻りに注意してください。PAWS チェックに合格したパケットは TCP_TW_SUCCESS を返すため、TIME_WAIT 状態のソケット (5 つ組) も 3 ウェイ ハンドシェイク後に正常に再利用できます。

上記は、Linux ソースコードから TIME_WAIT の期間を分析する詳細な内容です。Linux ソースコードでの TIME_WAIT の期間の詳細については、123WORDPRESS.COM の他の関連記事に注目してください。

以下もご興味があるかもしれません:
  • Apacheのtime_wait接続が多すぎる問題の解決策
  • Linux サーバーでの過剰な TIME_WAIT の問題を軽減する方法について説明します。
  • Linuxで大量のTIME WAITを解決する方法の詳細な説明
  • time_wait がソケットを強制的に閉じる問題を解決する
  • サーバーに TIME_WAIT 状態が多すぎる場合のトラブルシューティング

<<:  一般的なMySQLストレージエンジンの長所と短所

>>:  HTMLページがincludeを使用してphpファイルをインポートした後に余分な空白行があります

推薦する

Ubuntu でディスク容量不足により MySQL が起動しない場合の解決策

序文最近、データベースのテーブルに 2 つのフィールドを追加しました。その後、ディスク容量不足のよう...

Dockerはmacvlanをベースにホスト間コンテナ通信を実装する

2 台のテスト マシンを見つけます。 [root@docker1 centos_zabbix]# d...

Vueはログインタイプの切り替えを実装します

この記事では、ログインタイプの切り替えを実装するためのVueの具体的なコードを例として紹介します。具...

Vue ログインページでクッキーを使用してパスワードを 7 日間記憶する方法

問題の説明プロジェクトのログインページでは、7日間パスワードを記憶する必要がある機能があります。この...

特殊効果メッセージボックスを実現するネイティブJS

この記事では、ネイティブ JS で実装された特殊効果メッセージ ボックスを紹介します。効果は次のとお...

Mac でソースコードから MySQL 5.7.17 をコンパイルしてインストールするチュートリアル

1. ダウンロードして解凍します: /Users/xiechunping/Softwares/mys...

CSSを使用して3Dフォトウォール効果を作成する

CSS を使用して 3D フォト ウォールを作成します。具体的なコードは次のとおりです。 <!...

Linuxでテキスト比較を実現するコツを教えます

序文コードを書く過程で、必然的にコードに何らかの変更を加えることになります。しかし、変更を加えるとき...

MySQL の一般的な問題とアプリケーション スキルの概要

序文MySQL の日常的な開発やメンテナンスでは、パスワードの紛失やテーブルの破損など、避けられない...

大規模な Vue.js プロジェクトの構築と維持のための 10 のベスト プラクティス

目次1. スロットを使用してコンポーネントを理解しやすくし、より強力にする2. Vuexストアを正し...

mysql-5.7.21-winx64 無料インストール版のインストール - Windows チュートリアル詳細説明

1 ダウンロードアドレスは https://dev.mysql.com/downloads/mysq...

HTML テーブル マウス ドラッグ ソート機能

効果画像: 1. ファイルをインポートする<script src="js/jquer...

マウスの尾行効果を実現する JavaScript

マウス効果では、setTimeout を使用して固定時間にノードを生成し、ノードを削除し、生成された...

jsは双方向データバインディング(アクセサ監視)を実現します

この記事の例では、双方向データバインディングを実現するためのjsの具体的なコードを参考までに共有して...

VueでJSXを使用する方法

JSXとは何かJSX は Javascript の構文拡張であり、JSX = Javascript ...