セキュリティ対策室の伊藤洋也です @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 構成で、静的コンテンツの配信が遅延する (数十秒単位) 事象に遭遇しました。
💡エントリの内容に沿って説明を容易にするために 簡素 にしたモデル図です
障害対応の概要
当時、障害対応を進めていた同僚が 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 は下記に公開してあります。
下記が簡易の構成図です
注釈
モデルは 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 tcp や Linux カーネルのドキュメント を引用してまとめます。
名称 | 数値 | リミットの種類 |
---|---|---|
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 のモデル図
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 メモリ管理をモデル化したものです。
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] を下回っている状態です。
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
送信処理で memory pressure モードが影響する箇所です。
sk_wmem_schedule, sk_stream_alloc_skb
送信処理に関係する箇所です。
sk_rmem_schedule
受信処理に関係する箇所です。
tcp_check_oom
TCP oom に関係する関数です。
5. TCP oom と memory pressure モードに関連する /proc ファイル
TCP oom や memroy pressure が発生した際のトラブルシューティングや、監視やメトリクス採取をする際に参照すべき /proc
ファイルを説明します。
/proc/net/sockstat の TCP mem
/proc/net/sockstat
の mem
欄は、「ネットワークプロトコルごとで割り当てたメモリ(単位はページ。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/sotkstat
の TCP
の mem
は TCP ( IPv4 のみ. IPv6 は含まない) で使っているページ数を示しています。
TCP mem
の数値は、例えば、実験環境で Apache Bench - ab
や curl
等のコマンドで HTTP リクエストを出すと増減するのを観測できます。( TCP を扱うコマンドなら何でも構いません)
カーネルのソースで /proc/net/sockstat
の mem
は、struct proto
の memory_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/protocols
の memory
欄も ネットワークプロトコルごとで割り当てたメモリ(ページ) を示しています。
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/sockstat
の mem
と /proc/net/protocols
の memory
は同一の値を指しています。
/proc/net/protocols
のソースを見てみると struct proto
の memory_allocated
を参照しており、 /proc/net/sockstat
の mem
と同一であることを確認できます。
いずれ数値もポインタでグローバル変数 tcp_memory_allocated を参照しています。
atomic_long_t tcp_memory_allocated; /* Current allocated memory. */ 👈
EXPORT_SYMBOL(tcp_memory_allocated);
/proc/net/protocols の press
/proc/net/protocols
の press
欄は、該当のプロトコルが 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 の press
が yes
になっています。これは TCP が memory pressure モードであることを示しています。
/proc/net/netstat: TCPMemoryPressures
memory pressure モードに入る際 に /proc/net/netstat
の TCPMemoryPressures が +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/sockstat
の TCPMemoryPressuresChrono に 「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/netstat
の TCPAbortOnMemory が +1 加算されます。
以下の netstat -s
は TCPAbortOnMemory を示しています。 (ソース)
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 -s
は PruneCalled を示しています. (ソース)
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 -s
は TCPRcvQDrop を示しています. (ソース)
vagrant@proxy000:~$ netstat -s | grep TCPRcvQDrop
TCPRcvQDrop: 328
/proc/net/netstat: TCPAbortFailed
TCP oom が発生すると 該当のソケットで RST を送りコネクションを切断します。この際に ソケットバッファ(skb)の割り当てに失敗する と、/proc/net/netstat
の TCPAbortFailed が +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.tx の memory 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_rmem と net.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 と比較して動作します。
図は Systems Performance: Enterprise and the Cloud Figure - Brendan Gregg の 7.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技術入門 ――進化を続ける基本プロトコル (WEB+DB PRESS plusシリーズ) 安永 遼真
- 詳解 Linuxカーネル 第3版
- 詳解TCP/IP〈Vol.1〉プロトコル
- Professional Linux Kernel Architecture
- TCP/IP Architecture, Design, and Implementation in Linux
- Linuxカーネル2.6解読室
『TCP/IP Architecture, Design, and Implementation in Linux』は 2.4 系のソースで Linux の TCP/IP を説明した書籍ですが、ソースを追う際の資料として頻繁に参照しました。
技術ブログ
- LinuxサーバーのTCPネットワークのパフォーマンスを決定するカーネルパラメータ – 1編 - TOAST Meetup 編集部
- gihyo.jp 基本から学ぶ TCPと輻輳制御 ……押さえておきたい輻輳制御アルゴリズム - 中山悠
- The "Out of socket memory" error - Tsuna's blog
- Kernel socket structure and TCP_DIAG - StackExchange
- Linux TCP/IP parameter note - @timwata
- UNIXソケットバッファサイズ - Linuxの備忘録とか・・・(目次へ
- TenForward - Linux 3.3 の新機能 Per-cgroup TCP buffer limits (2)
その他
- https://github.com/torvalds/linux/blob/master/Documentation/networking/ip-sysctl.txt
- http://man7.org/linux/man-pages/man8/ss.8.html
- https://linuxjm.osdn.jp/html/LDP_man-pages/man7/socket.7.html
- https://linuxjm.osdn.jp/html/LDP_man-pages/man7/tcp.7.html
- RFC2018 TCP Selective Acknowledgment Options
- http://vger.kernel.org/~davem/skb_sk.html
- http://vger.kernel.org/~davem/tcp_output.html
- bpftrace Reference Guide - GitHub iovisor/bpftrace
- net: Disambiguate kernel message - GitHub torvalds/linux