ストイックに生きたい
kube-proxy は何をしてるのか
kube-proxy の実装を読む
このエントリーをはてなブックマークに追加

前置き

Kubernetes において、kube-proxy がどのような動きをしているのか知ることは非常に重要である。ソースコード自体は読みやすい。

日付内容
2023-01-07v1.26.0 の内容で公開

kube-proxy とは

ドキュメントに書かれているとおり、kube-proxy は DaemonSet でデプロイされており、各 Node のネットワークプロキシとして動作する。Service の一部を実装しており、iptables や ipvs を使用してトラフィックを制御している。userspace も使えたが、v1.26 で削除された。

cmd/kube-proxy/app/server.go

早速ソースコードを読み始めるが、これは kube-proxy の初期化やサーバーの起動を行っている。

ここで重要なのは、NewProxyServer で iptables と ipvs の proxier の初期化を行っている点である。メインの処理としては、proxier で行っているといっても過言ではない。ここでは、iptalbes の実装を見ていくことにする。

func (s *ProxyServer) Run() error

ここで、healthz・メトリクス・proxier の起動、healthz の設定とかをしている。

pkg/proxy/iptables/proxier.go

デュアルスタックに対応しているかどうかで NewProxierNewDualStackProxier のどちらかが呼ばれる。関数自体は分かれているものの、NewDualStackProxier では ipv4 と ipv6 の分 NewProxier を呼び出しているだけなので、とりあえずは NewProxier の実装を見ていく。

NewProxier

ここでは、NodePort を localhost で通信できるようにするための route_localnet、ブリッジしたパケットをフィルタリングするための bridge-nf-call-iptables が有効化されているかどうか確認している。ちなみに、v1.26 からは --iptables-localhost-nodeports を指定することで localhost への通信を無効化できるようになった。
あとはマスカレード、LoadBalancer のヘルスチェック用のサーバーの設定もしている。

func (proxier *Proxier) SyncLoop()

ここはなんかしている。重要なのは 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)

syncProxyRules

ここはメインの処理で iptables のルールを生成している。800 行ぐらいしかないが、行数以上にでかい。

SNAT

ここで 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 外からのアクセスループバックの時とかにつくルールが生成される。

DNAT

ここ で 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)

writeServiceToEndpointRules

ここでは、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 で割り振ることで、均等に振り分けることができるのである。

  1. 全てのリクエストの 1/3 を KUBE-SEP-FTG4NWKEUMM5OZ72 へ流す
  2. 1 の残りのリクエスト(2/3)の 1/2 を KUBE-SEP-GHNVWKROTCCBFQKZ へ流す
  3. 2 の残りのリクエスト(1/3)を KUBE-SEP-QTGKLRHEXOFUNUWC へ流す

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

pkg/proxy/healthcheck/service_health.go

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
}
© Ryota Sakamoto, 2023