トラブルシューティング

ペパボ トラブルシュート伝 - TCP: out of memory -- consider tuning tcp_mem の dmesg から辿る 詳解 Linux net.ipv4.tcp_mem

トラブルシューティング

セキュリティ対策室の伊藤洋也です @hiboma

過去の障害対応中に遭遇した TCP: out of memory -- consider tuning tcp_mem という dmesg を端緒として、Linux カーネルが どのようにTCPのメモリを管理するのかを調べました。

本エントリのテーマ

  • TCP: out of memory -- consider tuning tcp_mem の dmesg
  • memory pressure モード
  • sysctl net.ipv4.tcp_mem の詳細

を扱います。

誰のために向けて書いた技術文章なのか?

  • Linux の TCP メモリ管理を調べたい人
  • TCP out of memory あるいは memory pressure モードを調べたい人
  • TCP out of memory, memory pressure モードの監視・メトリクス採取したい人
  • net.ipv4.tcp_mem を 深〜く 知りたい人

に向けて知見を提供できればと思い、エントリを公開しています。

そもそも、なぜ、このログを調べるに至ったのか?

過去に起きた障害が発端となります。

次の図のようにモデル化される production 構成で、静的コンテンツの配信が遅延する (数十秒単位) 事象に遭遇しました。

production構成のモデル

💡エントリの内容に沿って説明を容易にするために 簡素 にしたモデル図です

障害対応の概要

当時、障害対応を進めていた同僚が TCP: out of memory -- consider tuning tcp_mem ( 以降は TCP oom と略記します) のログを dmesg に出しているホストを発見しました。該当のホストは、haproxy を HTTP ロードバランサー として稼働させていました。 haproxy の upstream のホストでは、nginx が静的コンテンツを扱う構成となっていました。

TCP oom のログをもとに、同僚 ( @pyama86 ) が解決策を調査し sysctl net.ipv4.tcp_mem をチューニングすることで遅延をおさめ障害を復旧しました。

事後の調査

後の調査で haproxy を動かしていた VM (OpenStack Octavia サービスの Amphora 内で稼働) の搭載 RAM が 2GB と小さく、起動時に自動で決定される net.ipv4.tcp_mem が低く抑えられていたことで TCP oom を誘発したという結論に至りました。

しかし、静的コンテンツの配信の遅延が どういった原理で起きたのかまでは説明できませんでした。

そういった背景があり

  • TCP oom はどのように再現できるのか?
  • TCP oom はどのような条件で発生するのか?
  • TCP oom が発生すると何が起きるのか?
  • sysctl net.ipv4.tcp_mem のチューニングはどのようにすべきか?
  • 監視やメトリクスはどんなデータを参照すべきか?

の調査に入りました。

自分で全部調べよう

さて、いざ調べ出してみると オンラインあるいは書籍でも これらについて説明した文章は少なく、間違った記述も散見されました。それならば いっそ自分でソースを追って調べ確認を取った方が早かろう! … というモチベーションに駆動されてまとめたのが 本エントリの内容になります。

1. TCP oom の再現、実験、観察

まずは Vagrant 上に TCP oom を再現する環境を作り、実験・観察をします。

1-1 実験環境

実験環境に Vagrant を用います。ディストリビューションは Ubuntu Bionic を用いています。カーネルは 5.0 系で行いました。

vagrant@proxy000:~$ uname -a
Linux proxy000 5.0.0-37-generic #40~18.04.1-Ubuntu SMP Thu Nov 14 12:06:39 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

1-2 Vagrant を用意する

Vagrant で 3台の VM をたてて、 TCP oom を再現する構成を作りました。 Vagrantfile は下記に公開してあります。

GitHub hiboma/tcp_mem

下記が簡易の構成図です

Vagrant のモデルズ

注釈

モデルは 3ホスト構成になっていますが、1ホスト構成で client, server となるプロセスを起動して TCP oom を再現することも可能です。

しかしながら、プロセスが 1ホストに集約されていると /proc/net/netstat 等のメトリクスや tcpdump, bpftrace 等のツールで観察する際に、どのプロセスがどのようにシステムに作用しているかの切り分けが難しくなるため、複数のホストを分離した構成を実験環境としました。

また、同僚に説明するにあたって、リアルな構成に即したモデルの方が馴染みがよいだろうという内的な事情もあります。

1-3 実験の手順

以下の手順を踏み、構成図でいう proxy で TCP oom を再現します。

1. proxy の net.ipv4.tcp_mem を極端に小さい数値にする

デフォルト設定でワークロードをかけて再現するのは調整が面倒なので、意図的に net.ipv4.tcp_mem を引き下げて TCP クォータが逼迫した状態を作り出します。

sysctl net.ipv4.tcp_mem='100 200 300'

くらいまで下げましょう (もっと下げても構いません)

2. server に 1MB の静的ファイルを置く

構成図の proxy で TCP oom を出すにあたって、以下の条件を再現する必要があります。

  • proxy が memory pressure モード (後述します) であること
  • proxy で動く haproxy の TCP ソケットで、送信バッファに一定量のデータを溜めたものが close されること

小さなファイルでは送信バッファが埋まらず TCP oom を再現できないので、1MB のファイルを用います。

3. client から、Apache Bench (ab) で 静的ファイルを GET する リクエストを出す

構成図の client から以下のようなワークロードをかけます。

ab -c 5 -n 100 192.168.100.100:8888/1mb.txt

1-4 実験・観察

1, 2, 3 の手順を踏むと、 proxy の dmesg に TCP oom のログが出るのを観測できるでしょう

vagrant@proxy000:~$ tail /var/log/syslog
Dec 17 07:27:02 proxy000 kernel: [ 9774.254080] TCP: out of memory -- consider tuning tcp_mem
Dec 17 07:27:02 proxy000 kernel: [ 9774.399771] TCP: out of memory -- consider tuning tcp_mem
Dec 17 07:27:02 proxy000 kernel: [ 9774.401927] TCP: out of memory -- consider tuning tcp_mem
Dec 17 07:27:02 proxy000 kernel: [ 9774.900174] TCP: out of memory -- consider tuning tcp_mem
Dec 17 07:27:03 proxy000 kernel: [ 9775.152904] TCP: out of memory -- consider tuning tcp_mem

再現が取れたところで /proc/ 以下のメトリクスを観察したり、Linux カーネルのドキュメントやソースを追いかけたり、TCP プロトコルの教科書も合わせて読みつつ、理解を深めていきましょう。

2. TCP oom はどんな条件で発生しますか?

TCP oom は以下の条件で発生します。

  • Linux カーネルが、TCP ソケットに送信・受信バッファに割り当てたメモリ(ページ数) が 閾値 を超えている
  • TCPソケットの送信バッファに一定量のデータを残したまま close される

また、TCP oom が発生する状態で Linux カーネルは memory pressure モード に遷移しており、TCP ソケットの送信・受信バッファに割り当てるメモリを制限しています。そのため TCP での送受信のパフォーマンスにペナルティが課されます。

閾値 と書きましたが、一体 どの数値が何を超えたことを指すのでしょう? 詳細を次章以降に記述していきます。

3. sysctl net.ipv4.tcp_mem とは何か?

TCP oom と memory pressure モードを理解する際に重要なのが sysctl net.ipv4.tcp_mem です. man tcp 7 の説明を引用します.

       tcp_mem (since Linux 2.4)
              This is a vector of 3 integers: [low, pressure, high].  These
              bounds, measured in units of the system page size, are used by
              TCP to track its memory usage.  The defaults are calculated at
              boot time from the amount of available memory.  (TCP can only
              use low memory for this, which is limited to around 900
              megabytes on 32-bit systems.  64-bit systems do not suffer
              this limitation.)

              low       TCP doesn't regulate its memory allocation when the
                        number of pages it has allocated globally is below
                        this number.

              pressure  When the amount of memory allocated by TCP exceeds
                        this number of pages, TCP moderates its memory
                        consumption.  This memory pressure state is exited
                        once the number of pages allocated falls below the
                        low mark.

              high      The maximum number of pages, globally, that TCP will
                        allocate.  This value overrides any other limits
                        imposed by the kernel.

ref http://man7.org/linux/man-pages/man7/tcp.7.html

linuxjm.osdn.jp に翻訳された man も引用します。

tcp_mem (Linux 2.4 以降)
    これは 3 つの整数 [low, pressure, high] からなるベクトル値である。 これらは TCP がメモリー使用量
    を追跡するために用いられる (使用量はシステムのページサイズ単位で計測される)。 デフォルトはブート時に
    利用できるメモリーの量から計算される。 (実際には、TCP は low memory のみを使用する。値は 32ビット
    システムでは約 900 メガバイトに制限される。 64 ビットシステムではこの制限はない。)

    low
        TCP は、グローバルにアロケートしたページがこの数値以下の場合は、 メモリーアロケーションを調整しない。

    pressure
        TCP がアロケートしたメモリーがこの数値分のページ数を越えると、 TCP はメモリー消費を抑えるようになる。
        アロケートしたページ数が low 以下になると、このメモリー圧迫状態から脱する。

    high
        TCP がグローバルに割り当てるページ数の最大値。 この値はカーネルによって課されるあらゆる制限よりも優先される。

man は概要を記述するのにとどまっており、少し説明が物足りないですね 🙂 以降の章で、net.ipv4.tcp_mem の詳細に踏み込み説明していきます。

3-1 net.ipv4.tcp_mem は 閾値

net.ipv4.tcp_mem は TCPメモリ管理の「閾値」として作用します。

$ sysctl net.ipv4.tcp_mem
net.ipv4.tcp_mem = 5256 7010    10512

数値の単位は ページ (x86-64アーキテクチャで 1ページ は 4096 byte) です。

net.ipv4.tcp_mem の3つの数値には名前がついています。man 7 tcpLinux カーネルのドキュメント を引用してまとめます。

名称 数値 リミットの種類
max sysctl net.ipv4.tcp_mem[2] ハードリミット
pressure sysctl net.ipv4.tcp_mem[1] ソフトリミット
min / low sysctl net.ipv4.tcp_mem[0]  

net.ipv4.tcp_mem のモデル図

limit のモデル図

Linux カーネル は TCP の送信・受信処理で、 TCP メモリクォータ (tcp_memory_allocated) と net.ipv4.tcp_mem の min, pressure, max を閾値として比較して、システム全体の TCP ソケットのメモリ管理を動的にコントロールします。それぞれのリミットを超えた時の詳細を次章以降で説明します。

⚠️ハードリミットの呼称について

Linux カーネルのソース ( net/core/sock.c ) を読むと net.ipv4.tcp_mem[2] (sk_prot_mem_limits) に hard limit と呼んでいるコメントが付いています。本文での ハードリミット の呼称はこれに準じます。

int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind) 
{
...

	/* Over hard limit. */
	if (allocated > sk_prot_mem_limits(sk, 2))
		goto suppress_allocation;

...

⚠️ソフトリミットの呼称について

net.ipv4.tcp_mem[1] に対して ソフトリミット として名称を記載するソースやコメントはありません。ハードリミットと対比して説明を容易にするために筆者 @hiboma が採用した呼称です。ご了承ください。

4. Linux カーネルの TCP メモリ管理を俯瞰する

TCP oom を理解するにあたって、Linux カーネルのメモリ管理に踏み込んでいきましょう。次の図は、Linux カーネルの TCP メモリ管理をモデル化したものです。

TCPメモリ管理のモデル

Linux カーネルは、TCP ソケットでデータを送信する・データを受信する際にデータをバッファリングします。バッファはカーネル内に確保します。バッファ用のメモリは、メモリ管理サブシステムから割り当てます。

また、カーネルは、システム内の全ての TCP ソケットを対象としてどれだけのバッファのメモリ (ページ) を割り当てたかを、TCP メモリプール を通して追跡します。 TCP ソケットは TCP メモリプールからクォータを確保し、クォータの範囲内で送信バッファ・受信バッファのサイズを拡大・縮小して扱います。

⚠️ TCP メモリプール・ソケットメモリープルという呼称について

TCP メモリプール、ソケットメモリープルという呼称は TCP/IP Architecture, Design, and Implementation in Linux に準じています。

4-1 TCP メモリプールとソフトリミット・ハードリミットの関係

TCP メモリープルで追跡されるソケットのメモリ使用量である TCP メモリクォータ (カーネル内部では tcp_memory_allocated というグローバル変数) は、TCP レイヤーの送信処理・受信処理の各所で ソフトリミットハードリミット と比較され、いずれかのリミットを超えていた場合にメモリ割り当てに制限がかかります。

名称 数値 リミットの種類
max sysctl net.ipv4.tcp_mem[2] ハードリミット
pressure sysctl net.ipv4.tcp_mem[1] ソフトリミット
min / low sysctl net.ipv4.tcp_mem[0]  

min (low): tcp_memory_allocated < net.ipv4.tcp_mem[0]

min (low) は、 TCP メモリクォータ (tcp_memory_allocated) が net.ipv4.tcp_mem[0] を下回っている状態です。

low

min (low) は、memory pressure モードを脱けるかどうかの閾値として用いられます。

pressure: tcpmemoryallocated < net.ipv4.tcp_mem[1]

pressure は TCP メモリクォータ (tcp_memory_allocated) が net.ipv4.tcp_mem[1] を超えた状態です。ソフトリミットのモデル図になります。

ソフトリミットのモデル図

pressure の説明にman 7 tcp を引用します.

          pressure  When the amount of memory allocated by TCP exceeds
                   this number of pages, TCP moderates its memory
                   consumption.  This memory pressure state is exited
                   once the number of pages allocated falls below the
                   low mark.

加えて Documentation/networking/ip-sysctl.txt を引用します.

pressure: when amount of memory allocated by TCP exceeds this number
of pages, TCP moderates its memory consumption and enters memory
pressure mode, which is exited when memory consumption falls
under "min".

tcp_memory_allocated が pressure = net.ipv4.tcp_mem[0] を超えると、Linux カーネルは memory pressure モード に入り 送信・受信に制限がかかり、TCP のメモリ割り当てを控えようとします。詳細は 4-2 で後述します。

max: tcpmemoryallocated < net.ipv4.tcp_mem[0]

max: tcp_memory_allocated > net.ipv4.tcp_mem[2] は、 TCP メモリクォータ (tcp_memory_allocated) が net.ipv4.tcp_mem[2] を値を超えた状態です。ハードリミットのモデル図になります。

ハードリミットのモデル図

high の説明にman 7 tcp を引用します.

          high      The maximum number of pages, globally, that TCP will
                   allocate.  This value overrides any other limits
                   imposed by the kernel.

加えて Documentation/networking/ip-sysctl.txt を引用します。

max: number of pages allowed for queueing by all TCP sockets.

max: tcp_memory_allocated > net.ipv4.tcp_mem[2] は、 TCP で確保できる最大のメモリ(ページ数) の閾値を超えた状態であり、TCP の送信受信で制限が入る、あるいは、エラーが起こります。TCP oom が発生するのも、この状態です。

詳細は 4-2 で後述します。

4-2 ソフトリミット・ハードリミットを超えると何が制限されるのか

モデル図

4-1 の 3つのリミットと、TCP oom、memory presssure モードの関係をまとめたモデル図です。

TCP メモリクォータがソフトリミット を超え、 memory pressure モード に入ると以下の処理を行います。

  • TCP ソケットの送信・受信バッファのサイズを制限する 1 2 3
  • 受信ウィンドウサイズを縮小する (ウィンドウをクローズする)
  • 受信キュー入ったセグメントから重複したシーケンス番号をもつセグメントをマージして、空きメモリの確保を試みる ( collapse 処理 ) 1
  • シーケンス番号順に受信できなかったセグメンを保持する Ouf of Order キューに入った SACK 済みセグメントを破棄して、空きメモリを確保する ( prune 処理 ) 1
  • 受信スロースタートの閾値を制限する 1 2

さらに、TCP メモリクォータが ハードリミット を超えると、以下の処理も行います。

  • セグメントの受信処理で、新規に受信したセグメントを破棄する (パケットドロップ) 1
  • memory pressure モードを継続し、送信バッファ・受信バッファのメモリ割り当てを控える
  • セグメントの送信処理で、メモリを確保できるまでプロセスをブロックして待機させる 1 2
  • connect(2) を ENOBUFS で失敗させる 1
  • セグメントの受信処理で、 Ouf of Order キューのセグメントを破棄して、空きを確保しようとする ( SACK renege ) 1
  • TCP の タイマー処理をやり直す 1
  • 送信バッファに一定量のデータを持ったままソケットを close すると TCP oom を起こす ( RST を送信し TCP oom の dmesg ログを出す )1

カーネルは、ソフトリミットを超えただけでは送信・受信処理をエラーにはせず制限や回収処理を行います。 ハードリミット を超えると、送信・受信でのエラー発生や TCP コネクション断を起こしてでもメモリを制限・回収します。

TCP oom は、TCP メモリクォータがハードリミットを超えた際に起こりうるエラーの 一事象 です。 TCP oom が出た時点では、送信・受信処理において、上記の問題も併発していることに注意しましょう。

いずれの制限・メモリ回収処理も、一時的に TCP の送信・受信のパフォーマンスを犠牲にして、メモリが不足した状態から回復を試みようとするカーネルの挙動とみなせます。

📙ソース

ここまでに記した内容は、実際に書籍やソースを読み調べたものですが、本ブログにソースの説明まで掲載すると長大になるので、一部だけ触れます。本エントリで省いたソースの詳細は、以下のリポジトリに掲載しています。補助資料としてご覧ください。(書き切ってない箇所が多々あります。すいません。)

GitHub hiboma/hiboma TCP oom.md

ソフトリミット・ハードリミットの比較は __sk_mem_raise_allocated で処理されます。__sk_mem_raise_allocated を起点にして、周辺の関数を辿ると、TCP oom ならびに memory pressure モードがどんな影響を及ぼすかを調べることできます。ポイントとなる関数と呼び出しの図を載せておきます。

tcp_under_memory_pressure

tcp_under_memory_pressure

送信処理で memory pressure モードが影響する箇所です。


sk_wmem_schedule, sk_stream_alloc_skb

sk_wmem_schedule

送信処理に関係する箇所です。


sk_rmem_schedule

sk_rmem_schedule

受信処理に関係する箇所です。


tcp_check_oom

tcp_check_oom.png

TCP oom に関係する関数です。

5. TCP oom と memory pressure モードに関連する /proc ファイル

TCP oom や memroy pressure が発生した際のトラブルシューティングや、監視やメトリクス採取をする際に参照すべき /proc ファイルを説明します。

/proc/net/sockstat の TCP mem

/proc/net/sockstatmem 欄は、「ネットワークプロトコルごとで割り当てたメモリ(単位はページ。x86_64 アーキテクチャでは 1ページ は 4096byte) 」を示しています。

vagrant@proxy000:~$ cat /proc/net/sockstat
sockets: used 143
TCP: inuse 7 orphan 0 tw 0 alloc 9 mem 1 👈
UDP: inuse 4 mem 1
UDPLITE: inuse 0
RAW: inuse 0
FRAG: inuse 0 memory 0

従って /proc/net/sotkstatTCPmem は TCP ( IPv4 のみ. IPv6 は含まない) で使っているページ数を示しています。

TCP mem の数値は、例えば、実験環境で Apache Bench - abcurl 等のコマンドで HTTP リクエストを出すと増減するのを観測できます。( TCP を扱うコマンドなら何でも構いません)

実験のGIFアニメーション

カーネルのソースで /proc/net/sockstatmem は、struct protomemory_allocated メンバを参照しているのを確認できます。struct proto はトランスポートレイヤのプロトコルを表現する構造体です。

/* Networking protocol blocks we attach to sockets.
 * socket layer -> transport layer interface
 */
struct proto {

... (略) 

	atomic_long_t		*memory_allocated;	/* Current allocated memory. */ 👈

/proc/net/protocols の memory

/proc/net/protocolsmemory 欄も ネットワークプロトコルごとで割り当てたメモリ(ページ) を示しています。

vagrant@proxy000:~$ cat /proc/net/protocols
protocol  size sockets  memory press maxhdr  slab module     cl co di ac io in de sh ss gs se re sp bi br ha uh gp em
PACKET    1408      2      -1   NI       0   no   kernel      n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n
PINGv6    1136      0      -1   NI       0   yes  kernel      y  y  y  n  n  y  n  n  y  y  y  y  n  y  y  y  y  y  n
RAWv6     1136      2      -1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  y  y  y  y  n  n
UDPLITEv6 1280      0       1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  n  n  y  y  y  n
UDPv6     1280      2       1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  n  n  y  y  y  n
TCPv6     2304      2       1   no     304   yes  kernel      y  y  y  y  y  y  y  y  y  y  y  y  y  n  y  y  y  y  y
XDP        960      0      -1   NI       0   no   kernel      n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n
UNIX      1024    105      -1   NI       0   yes  kernel      n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n
UDP-Lite  1088      0       1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  y  n  n  y  y  y  n
PING       928      0      -1   NI       0   yes  kernel      y  y  y  n  n  y  n  n  y  y  y  y  n  y  y  y  y  y  n
RAW        936      0      -1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  y  y  y  y  n  n
UDP       1088      4       1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  y  n  n  y  y  y  n
TCP       2144      6       1👈 no     304   yes  kernel      y  y  y  y  y  y  y  y  y  y  y  y  y  n  y  y  y  y  y
NETLINK   1064     15      -1   NI       0   no   kernel      n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n

実は /proc/net/sockstatmem と  /proc/net/protocolsmemory は同一の値を指しています。

/proc/net/protocols のソースを見てみると struct protomemory_allocated を参照しており/proc/net/sockstatmem と同一であることを確認できます。

いずれ数値もポインタでグローバル変数 tcp_memory_allocated を参照しています。

atomic_long_t tcp_memory_allocated;	/* Current allocated memory. */ 👈
EXPORT_SYMBOL(tcp_memory_allocated); 

/proc/net/protocols の press

/proc/net/protocolspress 欄は、該当のプロトコルが memory pressure モードかどうかを示します。

vagrant@proxy000:~$ cat /proc/net/protocols
protocol  size sockets  memory press maxhdr  slab module     cl co di ac io in de sh ss gs se re sp bi br ha uh gp em
PACKET    1408      2      -1   NI       0   no   kernel      n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n
PINGv6    1136      0      -1   NI       0   yes  kernel      y  y  y  n  n  y  n  n  y  y  y  y  n  y  y  y  y  y  n
RAWv6     1136      2      -1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  y  y  y  y  n  n
UDPLITEv6 1280      0       1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  n  n  y  y  y  n
UDPv6     1280      2       1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  n  n  y  y  y  n
TCPv6     2304      2      36   yes    304   yes  kernel      y  y  y  y  y  y  y  y  y  y  y  y  y  n  y  y  y  y  y
XDP        960      0      -1   NI       0   no   kernel      n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n
UNIX      1024    111      -1   NI       0   yes  kernel      n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n
UDP-Lite  1088      0       1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  y  n  n  y  y  y  n
PING       928      0      -1   NI       0   yes  kernel      y  y  y  n  n  y  n  n  y  y  y  y  n  y  y  y  y  y  n
RAW        936      0      -1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  y  y  y  y  n  n
UDP       1088      4       1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  y  n  n  y  y  y  n
TCP       2144     28      36   yes👈  304   yes  kernel      y  y  y  y  y  y  y  y  y  y  y  y  y  n  y  y  y  y  y
NETLINK   1064     15      -1   NI       0   no   kernel      n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n

上記では TCP の pressyes になっています。これは TCP が memory pressure モードであることを示しています。

/proc/net/netstat: TCPMemoryPressures

memory pressure モードに入る際/proc/net/netstatTCPMemoryPressures が +1 加算されます

以下の netstat -s は TCPMemoryPressures を示しています。(ソース)

vagrant@proxy000:~$ netstat -s | grep 'ran low'
    TCP ran low on memory 80 times

TCPMemoryPressures はかなり古いバージョンで実装されたようです。正確なコミットまでは調べていませんが、 2.6.18 で存在を確認しています。

/proc/net/netstat: TCPMemoryPressuresChrono

memory pressure モードを抜ける際/proc/net/sockstatTCPMemoryPressuresChrono「memory pressure モードであった時間 (単位は ms) 」 が加算されます.

先の TCPMemoryPressures と合わせて、TCPMemoryPressuresChrono がどのようなタイミングで計上されるか、どのような数値を計上しているかについて模式図を作りました。

TCPMemoryPressuresChrono は v4.13-rc1 で登場します。

/proc/net/netstat: OfoPruned

Ofo は Ouf of Order の略称で、TCP の Ouf of Order キューを指します。Ouf of Order キュー はシーケンス番号順に受信できなかったセグメントを保持するキューです。

memory pressure モードで Ouf of Order キューのセグメントを破棄すると OfOPruned が +1 加算されます。

以下の netstat -s は OfOPruned を示しています. (ソース)

vagrant@proxy000:~$ netstat -s | grep 'dropped from out-of-order'
    58 packets dropped from out-of-order queue because of socket buffer overrun

/proc/net/netstat: TCPAbortOnMemory

TCP oom が発生すると /proc/net/netstatTCPAbortOnMemory が +1 加算されます。

以下の netstat -sTCPAbortOnMemory を示しています。 (ソース)

vagrant@proxy000:~$ netstat -s | grep pressure
    116 connections aborted due to memory pressure

/proc/net/netstat: PruneCalled, RcvPruned

ハードリミットを超えていて、 受信キューの collapse 処理と Ouf Of Order キューのセグメントを破棄(DROP) を行ってもなお、空きメモリが確保できない場合は TCPRcvPrune が +1 加算されます。

以下の netstat -sPruneCalled を示しています. (ソース)

vagrant@proxy000:~$ netstat -s | grep pruned
    515 packets pruned from receive queue because of socket buffer overrun

coallapse は、受信キューや Ouf of Order キューに入ったセグメントをマージして空きメモリを確保する処理です。

/proc/net/netstat: TCPRcvQDrop

ハードリミットを超えていて、受信したセグメント(パケット) をドロップした際に TCPRcvDrop が +1 加算されます。

以下の netstat -sTCPRcvQDrop を示しています. (ソース)

vagrant@proxy000:~$ netstat -s | grep TCPRcvQDrop
    TCPRcvQDrop: 328

/proc/net/netstat: TCPAbortFailed

TCP oom が発生すると 該当のソケットで RST を送りコネクションを切断します。この際に ソケットバッファ(skb)の割り当てに失敗する と、/proc/net/netstatTCPAbortFailed が +1 加算されます.

ここでのソケットバッファの割り当て失敗は、TCP クォータの制限による失敗ではなく、Slab アロケータによる割り当て失敗です。TCPAbortFailed は 、Slab アロケータが逼迫した状況を指すメトリクスになるでしょう。

6. その他

各章の補遺を記します。

⚠️ TCP oom ≠ Out of Memoy Killler

TCP: out of memory -- consider tuning tcp_mem は、 カーネルが空きメモリが逼迫した際にプロセスを SIGKILL してメモリを回収する仕組みの OOM Killer = Out of Memory Killer とは関係ありません。

⚠️ TCP の memory pressure モード ≠ メモリ管理サブシステムの memory pressure

本エントリで表記する memory pressure モードkernel のドキュメント networking/ip-sysctl.txmemory pressure mode に準じます。メモリ管理サブシステムや cgroup の説明で用いられてる memory pressure とは関係ありません。

📝 net.ipv4.tcp_mem のデフォルト値はブート時に自動で計算されます

net.ipv4.tcp_mem のデフォルト値は搭載している RAM に準じてブート時に自動で計算・設定されます. 計算のソースは以下の通りです

static void __init tcp_init_mem(void)
{
	unsigned long limit = nr_free_buffer_pages() / 16;

	limit = max(limit, 128UL);
	sysctl_tcp_mem[0] = limit / 4 * 3;		/* 4.68 % */
	sysctl_tcp_mem[1] = limit;			/* 6.25 % */
	sysctl_tcp_mem[2] = sysctl_tcp_mem[0] * 2;	/* 9.37 % */
}

本エントリの冒頭に示した障害では、haproxy を動かしていた VM の RAM が 2GB と少なかったため、ブート時に自動で計算された net.ipv4.tcp_mem のデフォルト値が低くなり、memory pressure ならびに TCP oom を起こし、パフォーマンスダウンを招いたと分析しています。

📝 sysctl net.ipv4.tcp_mem はオンラインで変更できる

net.ipv4.tcp_mem の閾値は、カーネル全体でグローバル ( 全 namespace の 全 TCP ソケット) に作用します。sysctl コマンドで net.ipv4.tcp_mem の値を変更すると、直ちにシステムの全てのソケットに対して「閾値」の変更が反映されます。

変更に際して、TCP ソケットのファイルデスクリプタを掴んでいるプロセス再起動や、TCP ソケットの再作成/再接続といった処理をせずとも、直ちに反映されます。また、sysctl を設定したタイミングでカーネル内部でメモリを割り当てする・予約する (あるいは解放する) という設定ではありません。

本エントリの冒頭に示した障害でも、sysctl の変更によって直ちに復旧を果たすことができました。

⚠️ net.ipv4.tcp_rmem と net.ipv4.tcp_wmem は?

よく似た名前の sysctl 設定に net.ipv4.tcp_rmemnet.ipv4.tcp_wmem がありますが、別の設定です.

  • TCP の 1 ソケット ごとに適用される 送信・受信バッファサイズの最大サイズ を設定する sysctl です
  • 単位は バイト です ( ややこしい! )
  • net.ipv4.tcp_mem はシステムグローバルな設定ですが、net.ipv4.tcp_rmem , net.ipv4.tcp_wmem は net namespace ごとでも設定可能 です。

これらの設定を一緒くたに扱ってしまっている説明を見かけますが、異なるものです。ご注意ください。

📝 net.ipv4.tcp_mem と cgroup の話

過去、cgroup ごとに tcp_mem を設定できたようですが 実装の不備があったようで このコミット で 削除されています。詳しくは TenForward - Linux 3.3 の新機能 Per-cgroup TCP buffer limits (2) をご参照ください。

📝 ソフトリミットとハードリミットのデザイン

「ハードリミット」という単語を出したところで、 Linux (UNIX) のソフトリミット と ハードリミットのデザインについて触れたいと思います。

ソフトリミット・ハードリミットの、一例として、Linux では、setrlimit(2) システムコールで RLIMIT_CPU を指定して、プロセスの CPU 時間に制限を課すことができます。 ( シェルの ulimit, PAM の pam_limit の名前を上げた方が思い出せる方が多そうです)

setrlimit(2) + RLIMIT_CPU で制限を課したプロセスの CPU 時間が、ソフトリミット に達すると、カーネルは プロセスに SIGXCPU をシグナル送信します。ただし、SIGXCPU はシグナルハンドラで無視できます。さらに、CPU 時間が ハードリミット に達すると、カーネルはプロセスに SIGKILL をシグナル送信します。SIGKILL はシグナルハンドラをセットしても無視できず、プロセスは必ず終了します。

ディスククォータにもソフトリミットとハードリミットが存在します。

ディスククォータを用いると、ユーザごとにディスク使用量と inode 数を制限できます。いずれかの使用量が ソフトリミット を超えると、ユーザに警告が送られますが、新規のファイル作成や、既存のファイルサイズの拡大は依然として可能です。さらに、 ハードリミット を超えると、新たにディスクに書き込みができなくなります。

これらに倣って、TCP メモリクォータのハードリミットのデザインは、ソフトリミットに対してより強制的に、あるいは、エラーを誘発してでも制限をするものと捉えるとよいかと思います。

📝 kswapd の閾値モデルとの類似

kswapd の閾値のモデルは TCP のソフトリミット・ハードリミットと似たモデルであるように思います。kswapd は、メモリ使用量を WaterMark と比較して動作します。

kswapd の閾値モデル

図は Systems Performance: Enterprise and the Cloud Figure - Brendan Gregg7.8 kswapd wake-ups and modes を模写・引用しました。

7.まとめ

障害を発端として TCP: out of memory -- consider tuning tcp_mem を調べたことから以下を記述しました。

  • TCP oom を実験環境で再現する
  • sysctl net.ipv4.tcp_mem の説明
  • Linux カーネルの TCP メモリ管理を俯瞰
  • TCP oom と memory pressure に関連する /proc ファイル

正直な話、実践の場で TCP oom や memory pressure モード、net.ipv4.tcp_mem の詳細まで把握しておく必要は無いでしょうが、ここにある文章がトラブルシューティング等の場面で役立ってくれると幸いです。

リンク・リファレンス

書籍

『TCP/IP Architecture, Design, and Implementation in Linux』は 2.4 系のソースで Linux の TCP/IP を説明した書籍ですが、ソースを追う際の資料として頻繁に参照しました。

技術ブログ

その他

補助資料