Service の静的ノードポート用のレンジを分割する ServiceNodePortStaticSubrange
という機能が、Kubernetes v1.28 からデフォルトで有効になりました。これは、Service の動的なノードポートの割当と、静的なノードポートの衝突リスクを減らすための機能です。この記事では Kubernetes v1.28.1 時点で、この機能について調べた内容を記載しています。
まとめ
kube-apiserver の --service-node-port-range
で指定される全体のポートレンジが 2 分割され、ポートの値が小さい方は静的割り当て用の優先レンジとなりました。静的なノードポートの指定する場合は、この優先レンジを使うと衝突のリスクを減らせます。
例えばポートレンジのデフォルト値 30000-32767
の場合は、30000-30085
が静的割当の優先レンジとなっています。静的なノードポートを使う場合は、ポートレンジの小さい値(この例だと 30000
)から使っていくと良いでしょう。
背景
Service のノードポートは、動的・静的どちらの割り当ても可能です。動的・静的どちらの割り当ても、API サーバーが管理するノードポートのレンジから選択されます。しかし動的なノードポートの割当はこのレンジ内からランダムに選択されるため、静的なノードポートと衝突する可能性がありました。
例えば以下のように Service に type: NodePort
を指定し .spec.ports[].nodePort
を指定しない場合は、動的にノードポートが割り当てられます。
apiVersion: v1
kind: Service
metadata:
name: nodeport-dynamic
spec:
type: NodePort
selector:
run: nginx
ports:
- port: 80
今回はノードポートに 30296
が割り当てられました。
$ kubectl get svc nodeport-dynamic
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nodeport-dynamic NodePort 10.97.31.191 <none> 80:30296/TCP 69s
^^^^^
一方、.spec.ports[].nodePort
を指定することで静的にノードポートを指定できます。ここで指定したポートが既に動的なポートで使われていた場合に衝突が起こります。
apiVersion: v1
kind: Service
metadata:
name: nodeport-static
spec:
type: NodePort
selector:
run: nginx
ports:
- port: 80
# 静的にポートを指定できる
nodePort: 30296
この例のように、静的なノードポートの指定が衝突した場合、以下のようなエラーで作成に失敗します。今回の機能が実装されるまでは、どのポートを指定しても常にこのリスクがありました。
The Service "nodeport-static" is invalid: spec.ports[0].nodePort: Invalid value: 30296: provided port is already allocated
この機能の以前に、Service の IP アドレスの衝突を避ける ServiceIPStaticSubrange
という機能が Kubernetes v1.26 からデフォルトで有効となっています。今回の ServiceNodePortStaticSubrange
という機能はそれのノードポート版と言えます。
分割されるポートのレンジ
この衝突の問題を解決するのが、ServiceNodePortStaticSubrange
という機能です。
ノードポートで利用できる動的・静的を含めた全体のレンジは kube-apiserver の --service-node-port-range
で指定します。デフォルト値は 30000 〜 32767 となっています。この機能では、この全体のレンジを2つに分割し、ポートの値が小さい方を静的割り当て用の優先レンジ、値が大きい方を動的割り当て用の優先レンジとして分割して管理します。
例えばデフォルト値の 30000 〜 32767 の場合は以下のように分割されます。
ポート範囲 | 個数 | 用途 |
---|---|---|
30000-30085 | 86 | 静的割り当て優先レンジ |
30086-32767 | 2682 | 動的割り当て優先レンジ |
このレンジはあくまで優先して使われるレンジであり、動的割り当て用レンジが枯渇した場合、静的割り当て用レンジからも動的に割り当てられる点にご注意ください。
静的割り当て用レンジのサイズ
静的割り当て用の優先レンジは、--service-node-port-range
のポートの個数(式のnodeport-size
)に応じて 0〜128 個の範囲で分割されます。この静的用レンジの大きさは、下記の計算式求められます。ただしポートの個数が 16 以下の場合は 0 となります。
min(max(16, nodeport-size / 32), 128)
以下はポートレンジごとの例です、カッコ内はポートの個数を示しています。
--service-node-port-range | 静的割り当て優先レンジ | 動的割り当て優先レンジ |
---|---|---|
30000-30015 (16) | 無し | 30000-30015 |
30000-30127 (128) | 30000-30015 (16) | 30016-30127 (112) |
30000-32767 (2768) | 30000-30085 (86) | 30086-32767 (2682) |
30000-34095 (4906) | 30000-30127 (128) | 30128-34095 (3968) |
この計算式と例は Kubernetes 公式ブログの記事 Kubernetes 1.27: Avoid Collisions Assigning Ports to NodePort Services で紹介されています。詳細はそちらをご覧ください。
デモ
Kubernetes v1.28.1 の環境(minikube)で、動的用レンジの個数分 NodePort を使った Service を作り、ポートの割当を確認しました。以下の gif は割当の状況を早送りしたものです。静的用のレンジ (30000-30085
) には割当が行われていないことが確認できます。.
は空き、x
は割り当て済みを表しています。
以下はデモ全体の動画です。動的用のレンジを使い切ると静的用のレンジが使われます。この様子はデモの 2:09 から確認できます。
なお、全体のレンジを使い切ると、下記のように allocate a nodePort: range is full
というエラーが返るようになります。
Error from server (InternalError): error when creating "svc.yaml": Internal error occurred: failed to
allocate a nodePort: range is full
参考: デモのスクリプト
Service の作成につかったスクリプトは以下です。高速化のため 100 個単位でまとめて送信しています。
metadata.generateName
を使うと、オブジェクトの名前生成を Kubernetes に任せることができます。
#!/bin/bash
n=${1:-0}
svc=$(mktemp)
cat >"$svc" <<EOF
apiVersion: v1
kind: Service
metadata:
generateName: nodeport-
spec:
type: NodePort
selector:
run: nginx
ports:
- port: 80
---
EOF
for ((i = 0; i < n / 100; i++)); do
for ((j = 0; j < 100; j++)); do
cat "$svc"
done | kubectl create -f -
done
for ((i = 0; i < (n % 100); i++)); do
kubectl create -f "$svc"
done
ノードポートの割当表示は以下のように kubectl で取得したノードポート一覧を ruby のスクリプトで整形し、それを watch で 1 秒おきに表示しています。
watch --color -n 1 "kubectl get svc -o json | jq '.items[].spec.ports[] | select(.nodePort != null) | .nodePort'| ./plot.rb"
#!/usr/bin/env ruby
require 'set'
allocated = Set.new
while line = gets
allocated.add(line.chomp)
end
# header
print ' | '
10.times do |n|
printf '%-10d', n * 10
end
puts
print '------+ '
puts '-' * 100
(30_000..32_767).each do |n|
print "#{n} | " if (n % 100).zero?
if allocated.include?(n.to_s)
print "\e[1m\e[31mx\e[0m"
else
print "\e[32m.\e[0m"
end
puts if ((n + 1) % 100).zero?
end
puts