事象
インスタンスを public subnet から private subnet に移して Cloud NAT 越しに外部サイトにアクセスするように構成変更したところ、外部接続でタイムアウトが頻発するようになった。
原因
Cloud NAT を通すと、同じ向き先のホストとポート番号の組に対して、デフォルトでは VM あたり 64 ポートしか使えないのが原因

これは public ip を持っている VM インスタンスならば、デフォルトで
$ cat /proc/sys/net/ipv4/ip_local_port_range
32768 61000
つまり28232
ポートほど使えていたのが、64
ポートしか使えなくなることを意味するので、外部アクセスがそれなりにあるシステムですぐに問題になる。
Cloud NAT の問題であることを特定する方法
Cloud NAT の stackdriver logging を有効にしてログを取得し、DROPPED
なログがあれば、Cloud NAT の問題。
resource.type="nat_gateway"
jsonPayload.allocation_status="DROPPED"
実際に使っているコネクション数を数える
TIME_WAITなソケットもカウント対象になるので実際に開けるコネクション数はもっと少ない。
ポート数の制限は単一 destination ホスト:ポートに対してなので、そこは絞る。
netstat で開いているエフェメラルポートの数を調べるならこう。
$ netstat | grep xxxxxx:https
tcp 0 0 download:35532 xxxxxx:https SYN_WAIT
tcp 0 0 download:35532 xxxxxx:https TIME_WAIT
tcp 0 0 download:36110 xxxxxx:https TIME_WAIT
tcp 0 0 download:37406 xxxxxx:https TIME_WAIT
tcp 0 0 download:36518 xxxxxx:https TIME_WAIT
tcp 0 0 download:35898 xxxxxx:https TIME_WAIT
....
$ netstat | grep xxxxxx:https | wc-l
328
さらに、GKE (k8s)を使っているような場合、同じVMで動いている全Podで開いている数を数えなければならないので注意。
対処方法
(1) Minimum ports per VM instance の数を 64 から増やしつつ、Cloud NATのグローバルIPを増やす
デフォルトで 64 ポートしか開けないなら増やせば良いのだが、そうするとNAT経由でアクセスできるVM数が減ってしまう。
思い切って 32000
ぐらいに増やしてしまいたい所ではあるが、それをやると厳しいので netstat
で開いているソケット数を数えて、ギリギリな線に落ち着かせる必要がある。
または、ポートレンジを増やした分だけ、グローバルIP数も増やせば、扱えるVMの数を減らさずに調整できる。
どのぐらいのポート数、グローバルIP数を設定するのかは職人芸による調整が必要。
ref. マネージドNATサービス GCP Cloud NAT がリリースされました#ポート数と接続数の制限
Cloud NATに割り当てるIPアドレス一つにつき、TCP、UDPともに 65,536 個のポートがあります。そのうち、1024 までのウェルノウンポートは使用できないため、64,512 個のポートが使えることになります。
デフォルトでは、一つのVMにつき64ポート(TCP 64個、UDP 64個)を取得します。したがって、一つのNAT IPアドレスにつき、最大で1,008個のVMインスタンスまでNAT経由で使用可能になります。
(2) TIME_WAIT ソケットの数を減らす(再利用する)
ベンチマーククライアントを動かす時によくやるカーネルパラメータチューニングであるが、TIME_WAITソケットが滞留しているせいで必要になるポート数が増えるので、同じ destination の host:port の TIME_WAIT ソケットが再利用されるように設定する。
これは直接 Cloud NAT 上のポートの数を減らしているわけではないが、間接的に Cloud NAT で開いているポートの数を減らすことに寄与するのか、私の環境では一定の効果があるように見えた。
ref. ぜんぶTIME_WAITのせいだ! ref. net.ipv4.tcp_tw_recycle は廃止されました
net.ipv4.tcp_tw_reuse = 1
おまけ: TIME_WAIT ソケットの解放時間を調節する TCP_TIMEWAIT_LEN
はカーネルをコンパイルしないと設定不可能なのと、そもそも tcp_tw_reuse により1秒で再利用されるので設定不要なはず ref. Linuxカーネルの「TCP_TIMEWAIT_LEN」変更は無意味?
(3) public ip を付与して Cloud NAT を使わない
いっそのこと Cloud NAT を使わなければ 64 ポートの制限が外せる。
上述したようにデフォルトのカーネルパラメータでも 28232
ポート使えるようになる。
その他: 帯域幅
ref. マネージドNATサービス GCP Cloud NAT がリリースされました#帯域幅
Cloud NAT経由になると、グローバルIPアドレスを付与したVMと同じ程度の帯域幅になるとのことです。
複数VMで帯域幅を共有するので、速度低下が懸念される。
避けたい場合は public ip を付与して Cloud NAT を使わないようにする。