Kubernetes において、kube-proxy がどのような動きをしているのか知ることは非常に重要である。ソースコード自体は読みやすい。
日付 | 内容 |
---|---|
2023-01-07 | v1.26.0 の内容で公開 |
ドキュメントに書かれているとおり、kube-proxy は DaemonSet でデプロイされており、各 Node のネットワークプロキシとして動作する。Service の一部を実装しており、iptables や ipvs を使用してトラフィックを制御している。userspace も使えたが、v1.26 で削除された。
早速ソースコードを読み始めるが、これは kube-proxy の初期化やサーバーの起動を行っている。
ここで重要なのは、NewProxyServer で iptables と ipvs の proxier の初期化を行っている点である。メインの処理としては、proxier で行っているといっても過言ではない。ここでは、iptalbes の実装を見ていくことにする。
ここで、healthz・メトリクス・proxier の起動、healthz の設定とかをしている。
デュアルスタックに対応しているかどうかで NewProxier と NewDualStackProxier のどちらかが呼ばれる。関数自体は分かれているものの、NewDualStackProxier では ipv4 と ipv6 の分 NewProxier を呼び出しているだけなので、とりあえずは NewProxier の実装を見ていく。
ここでは、NodePort を localhost で通信できるようにするための route_localnet、ブリッジしたパケットをフィルタリングするための bridge-nf-call-iptables が有効化されているかどうか確認している。ちなみに、v1.26 からは --iptables-localhost-nodeports
を指定することで localhost への通信を無効化できるようになった。
あとはマスカレード、LoadBalancer のヘルスチェック用のサーバーの設定もしている。
ここはなんかしている。重要なのは proxier.syncRunner.Loop で、NewProxier で下記のように初期化されている。2 番目の引数に渡されている syncProxyRules が実行される関数で、メインの処理である。
// We pass syncPeriod to ipt.Monitor, which will call us only if it needs to.
// We need to pass *some* maxInterval to NewBoundedFrequencyRunner anyway though.
// time.Hour is arbitrary.
proxier.syncRunner = async.NewBoundedFrequencyRunner("sync-runner", proxier.syncProxyRules, minSyncPeriod, time.Hour, burstSyncs)
ここはメインの処理で iptables のルールを生成している。800 行ぐらいしかないが、行数以上にでかい。
ここで MARK がついている場合に SNAT をしている。
-A KUBE-POSTROUTING -m mark ! --mark 0x00004000/0x00004000 -j RETURN
-A KUBE-POSTROUTING -j MARK --xor-mark 0x00004000
-A KUBE-POSTROUTING -m comment --comment \"kubernetes service traffic requiring SNAT\" -j MASQUERADE --random-fully
MARK のルールはここで生成されている。
-A KUBE-MARK-MASQ -j MARK --or-mark 0x00004000
そして、どういう場合に MARK がつくかというと、外部からのアクセスやクラスタの IP 外からのアクセス、ループバックの時とかにつくルールが生成される。
ここ で Service から Pod への DNAT を行っている。下記のようなルールが生成されて、各 Pod へリクエストが送られることがわかる。
-A KUBE-SEP-FTG4NWKEUMM5OZ72 -m comment --comment kube-system/kube-dns:dns -s 192.168.0.5 -j KUBE-MARK-MASQ
-A KUBE-SEP-FTG4NWKEUMM5OZ72 -m comment --comment kube-system/kube-dns:dns -m udp -p udp -j DNAT --to-destination 192.168.0.5:53
-A KUBE-SEP-GHNVWKROTCCBFQKZ -m comment --comment kube-system/kube-dns:dns -s 192.168.1.2 -j KUBE-MARK-MASQ
-A KUBE-SEP-GHNVWKROTCCBFQKZ -m comment --comment kube-system/kube-dns:dns -m udp -p udp -j DNAT --to-destination 192.168.1.2:53
-A KUBE-SEP-QTGKLRHEXOFUNUWC -m comment --comment kube-system/kube-dns:dns -s 192.168.1.3 -j KUBE-MARK-MASQ
-A KUBE-SEP-QTGKLRHEXOFUNUWC -m comment --comment kube-system/kube-dns:dns -m udp -p udp -j DNAT --to-destination 192.168.1.3:53
さて、実際に DNAT していることを tcpdump を使って確認していく。
node: pod => clusterIP
17:22:36.615574 IP (tos 0x0, ttl 64, id 51177, offset 0, flags [none], proto UDP (17), length 80)
192.168.1.3.40884 > 10.96.0.10.domain: [bad udp cksum 0xcc62 -> 0x2bdb!] 51013+ [1au] A? example.com. ar: . OPT UDPsize=1232 (52)
node: pod => coredns pod(DNAT)
17:22:36.615608 IP (tos 0x0, ttl 63, id 51177, offset 0, flags [none], proto UDP (17), length 80)
192.168.1.3.40884 > 192.168.0.5.domain: [bad udp cksum 0x82a6 -> 0x7597!] 51013+ [1au] A? example.com. ar: . OPT UDPsize=1232 (52)
controlplane:
17:22:36.604418 IP (tos 0x0, ttl 63, id 51177, offset 0, flags [none], proto UDP (17), length 80)
192.168.1.3.40884 > 192.168.0.5.domain: [udp sum ok] 51013+ [1au] A? example.com. ar: . OPT UDPsize=1232 (52)
controlplane: b
17:22:36.604441 IP (tos 0x0, ttl 62, id 51177, offset 0, flags [none], proto UDP (17), length 80)
192.168.1.3.40884 > 192.168.0.5.domain: [udp sum ok] 51013+ [1au] A? example.com. ar: . OPT UDPsize=1232 (52)
controlplane: c
17:22:36.604696 IP (tos 0x0, ttl 64, id 65186, offset 0, flags [DF], proto UDP (17), length 107)
192.168.0.5.domain > 192.168.1.3.40884: [bad udp cksum 0x82c1 -> 0x5737!] 51013* q: A? example.com. 1/0/1 example.com. [18s] A 93.184.216.34 ar: . OPT UDPsize=1232 (79)
controlplane: d
17:22:36.604706 IP (tos 0x0, ttl 63, id 65186, offset 0, flags [DF], proto UDP (17), length 107)
192.168.0.5.domain > 192.168.1.3.40884: [bad udp cksum 0x82c1 -> 0x5737!] 51013* q: A? example.com. 1/0/1 example.com. [18s] A 93.184.216.34 ar: . OPT UDPsize=1232 (79)
node:
17:22:36.616404 IP (tos 0x0, ttl 63, id 65186, offset 0, flags [DF], proto UDP (17), length 107)
192.168.0.5.domain > 192.168.1.3.40884: [udp sum ok] 51013* q: A? example.com. 1/0/1 example.com. [18s] A 93.184.216.34 ar: . OPT UDPsize=1232 (79)
17:22:36.616415 IP (tos 0x0, ttl 62, id 65186, offset 0, flags [DF], proto UDP (17), length 107)
10.96.0.10.domain > 192.168.1.3.40884: [udp sum ok] 51013* q: A? example.com. 1/0/1 example.com. [18s] A 93.184.216.34 ar: . OPT UDPsize=1232 (79)
ここでは、Service の ClusterIP へのリクエストがきた際に、Pod へランダムに振り分けるためのルールを生成している。また、Service の sessionAffinity を指定した際のルールもここで生成されている。
CoreDNS の replicas を 3 にした場合、下記のようなルールが生成されるのだが、ここで面白いのは --probability
に指定されている数値である。
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment \"kube-system/kube-dns:dns -> 192.168.0.5:53\" -m statistic --mode random --probability 0.3333333333 -j KUBE-SEP-FTG4NWKEUMM5OZ72
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment \"kube-system/kube-dns:dns -> 192.168.1.2:53\" -m statistic --mode random --probability 0.5000000000 -j KUBE-SEP-GHNVWKROTCCBFQKZ
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment \"kube-system/kube-dns:dns -> 192.168.1.3:53\" -j KUBE-SEP-QTGKLRHEXOFUNUWC
iptables は上から順に評価されるので、下記のように振り分けられることになる。上から順に 1/N で割り振ることで、均等に振り分けることができるのである。
sessionAffinity に ClientIP を指定した場合は、前述したルールの前に下記のルールが生成される。ここでは、タイムアウトとしてデフォルトの 10,800 秒(3 時間)が設定されており、最後のリクエストからタイムアウトの秒数が経過していなければ、同じ Pod へリクエストが流れることがわかる。
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment \"kube-system/kube-dns:dns -> 192.168.0.5:53\" -m recent --name KUBE-SEP-FTG4NWKEUMM5OZ72 --rcheck --seconds 10800 --reap -j KUBE-SEP-FTG4NWKEUMM5OZ72
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment \"kube-system/kube-dns:dns -> 192.168.1.2:53\" -m recent --name KUBE-SEP-GHNVWKROTCCBFQKZ --rcheck --seconds 10800 --reap -j KUBE-SEP-GHNVWKROTCCBFQKZ
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment \"kube-system/kube-dns:dns -> 192.168.1.3:53\" -m recent --name KUBE-SEP-QTGKLRHEXOFUNUWC --rcheck --seconds 10800 --reap -j KUBE-SEP-QTGKLRHEXOFUNUWC
LoadBalancer のヘルスチェック用のサーバーは、NewServiceHealthServer により作成されている。
そもそも LoadBalancer のヘルスチェック用のサーバーとは何かというと、type が LoadBalancer かつ externalTrafficPolicy が Local の場合に必要なもので、特定の Node に Endpoints が存在しない時にヘルスチェックを失敗させることで、その Node へトラフィックを流さないようにするものである。
healthCheckNodePort を設定することでポートを指定でき、指定しなかった場合は自動で設定される。externalTrafficPolicy が Local ではない時に設定しようとすると、下記のようにエラーになる。
The Service "test-service" is invalid: spec.healthCheckNodePort: Invalid value: 32234: may only be set when `type` is 'LoadBalancer' and `externalTrafficPolicy` is 'Local'
これをどう使うかというと、下記のように Node に対してリクエストを送るとヘルスチェックができる。エンドポイントが存在すれば 200、存在しなければ 503 が返ってくるのでこれでその Node へトラフィックを流してもいいかどうかわかる。
$ k get node node01 -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
node01 Ready <none> 16d v1.26.0 172.30.2.2 <none> Ubuntu 20.04.5 LTS 5.4.0-131-generic containerd://1.6.12
$ k get service test-service -o json | jq .spec.healthCheckNodePort
32234
$ curl 172.30.2.2:32234 -v
* Trying 172.30.2.2:32234...
* TCP_NODELAY set
* Connected to 172.30.2.2 (172.30.2.2) port 32234 (#0)
> GET / HTTP/1.1
> Host: 172.30.2.2:32234
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 503 Service Unavailable
< Content-Type: application/json
< X-Content-Type-Options: nosniff
< Date: Sun, 08 Jan 2023 12:11:40 GMT
< Content-Length: 93
<
{
"service": {
"namespace": "default",
"name": "test-service"
},
"localEndpoints": 0
* Connection #0 to host 172.30.2.2 left intact
}