Kubernetes ログを読んで理解する kube-proxy の仕組み

kube-proxy は各ノードで動作するネットワークプロキシ。Service の仕組みの一部(ClusterIP や NodePort など)を実現し、クラスタ内部または外部から Pod にアクセスできるように制御する。
参考 Kubernetes Components
参考 Kubernetes Service

f:id:kimulla:20200309082105p:plain

Service は 特定ラベルの Pod に対する仮想的なエンドポイント。Kubernetes では Pod がどのノードに配置されるかが不定(オートヒーリングやオートスケールのため)のため、サービスディスカバリの仕組みが必要になる。

f:id:kimulla:20200309082919g:plain

Service のサービスディスカバリ機能は DNS ではなくてプロキシ方式になっている。Service に割り当てられた ClusterIP にアクセスすると、各 Pod にアクセスが分散される。そのため、Pod の IP アドレスを直接意識する必要がない。なぜ DNS ではなくプロキシ方式なのかについては、リファレンスで詳しく説明されている。(なお DNS 機能はある。これは Service の ClusterIP が再作成されて変更された場合に備えた機能だと理解している)

DNSの実装がレコードのTTLをうまく扱わず、期限が切れた後も名前解決の結果をキャッシュするという長い歴史がある。
いくつかのアプリケーションではDNSルックアップを1度だけ行い、その結果を無期限にキャッシュする。
アプリケーションとライブラリーが適切なDNS名の再解決を行ったとしても、DNSレコード上の0もしくは低い値のTTLがDNSに負荷をかけることがあり、管理が難しい。
引用元: Kubernetes Service

kube-proxy の実装には iptables や ipvs や userspace がある。デフォルトは iptables だが、カーネルや iptables のバージョンによっては userspace が選択される。

今回は、kube-proxy(実装はデフォルトの iptables) がどのように動作しているのか、をログから探っていく。

内部の動作を知る

検証バージョン

kubernetes 1.17 で検証。CRI に containerd、 CNI に calico を利用。

]# kubectl version
Client Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.3", GitCommit:"06ad960bfd03b39c8310aaf92d1e7c12ce618213", GitTreeState:"clean", BuildDate:"2020-02-11T18:14:22Z", GoVersion:"go1.13.6", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.3", GitCommit:"06ad960bfd03b39c8310aaf92d1e7c12ce618213", GitTreeState:"clean", BuildDate:"2020-02-11T18:07:13Z", GoVersion:"go1.13.6", Compiler:"gc", Platform:"linux/amd64"}

ログを設定する

kubernetes の各コンポーネントのログは klog という仕組みを利用している。klog は実行時の引数 -v= でログレベルを指定できる。
参考 Logging Conventions

そのため、まずは kube-proxy の loglevel を上げる。

]# kubectl -n kube-system edit daemonsets.apps kube-proxy

spec:
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      k8s-app: kube-proxy
  template:
    metadata:
      creationTimestamp: null
      labels:
        k8s-app: kube-proxy
    spec:
      containers:
      - command:
        - /usr/local/bin/kube-proxy
        - --config=/var/lib/kube-proxy/config.conf
        - --hostname-override=$(NODE_NAME)
        - --v=7 // ここを追加する

controller と scheduler の設定を変える

controller と scheduler で leader-elect が有効の場合に、 kube-proxy のログに Update が頻発して見づらい。そのため今回はオフにする。
参考 Leader election causes a lot of endpoint updates
参考 kube-proxy should ignore "spam" updates to endpoints

kubeadm でインストールした場合、 /etc/kubernetes/manifest にカスタマイズ用のファイルが用意される。この設定を以下のように変更する。

/etc/kubernetes/manifests/kube-scheduler.yaml

...
spec:
  containers:
  - command:
    - kube-scheduler
    - --authentication-kubeconfig=/etc/kubernetes/scheduler.conf
    - --authorization-kubeconfig=/etc/kubernetes/scheduler.conf
    - --bind-address=127.0.0.1
    - --kubeconfig=/etc/kubernetes/scheduler.conf
    - --leader-elect=false // これ
...

/etc/kubernetes/manifests/kube-controller-manager.yaml

...
spec:
  containers:
  - command:
    - kube-controller-manager
    - --allocate-node-cidrs=true
    - --authentication-kubeconfig=/etc/kubernetes/controller-manager.conf
    - --authorization-kubeconfig=/etc/kubernetes/controller-manager.conf
    - --bind-address=127.0.0.1
    - --client-ca-file=/etc/kubernetes/pki/ca.crt
    - --cluster-cidr=10.200.0.0/16
    - --cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt
    - --cluster-signing-key-file=/etc/kubernetes/pki/ca.key
    - --controllers=*,bootstrapsigner,tokencleaner
    - --kubeconfig=/etc/kubernetes/controller-manager.conf
    - --leader-elect=false // これ
...

サービスを作成する

適当なアプリケーション(app)を立ち上げて ClusterIP を作成する。

]# kubectl create deployment --image=nginx app
deployment.apps/app created
]# kubectl scale deployment app --replicas=2
deployment.apps/app scaled

]# kubectl expose deployment app  --port=80

このとき、 Service と Endpoints が生成される。

]# kubectl get service
NAME         TYPE        CLUSTER-IP          EXTERNAL-IP   PORT(S)   AGE
app          ClusterIP   10.107.132.100:80   <none>        80/TCP    2s
kubernetes   ClusterIP   10.96.0.1           <none>        443/TCP   2d4h

]# kubectl get endpoints
NAME         ENDPOINTS                         AGE
app          10.200.43.11:80 10.200.43.12:80   6s
kubernetes   10.240.0.10:6443                  2d4h

この一連の流れがどのように行われるのかを kube-proxy のログから探っていく。

// worker ノードの kube-proxy のログを出力する
]# kubectl logs -f -n kube-system kube-proxy-hsd5t
...

ログを見る

ハンドラの実行

api-server の変更を kube-proxy が検知したらハンドラがスタートする。今回は Service と Endpoint を作成する。

I0308 01:03:38.016271       1 config.go:332] Calling handler.OnServiceAdd
I0308 01:03:38.016382       1 service.go:363] Adding new service port "default/app:" at 10.107.132.100:80/TCP
...
I0308 01:03:38.040561       1 config.go:150] Calling handler.OnEndpointsAdd
I0308 01:03:38.040770       1 endpoints.go:376] Setting endpoints for "default/app:" to [10.200.43.11:80 10.200.43.12:80]

Kubernetes は宣言的な構成管理をしている。各コンポーネントが api-server にアクセスしてどうあるべきかを把握し、各コンポーネントがあるべき姿になるように自立的に処理をする。

https://d33wubrfki0l68.cloudfront.net/7016517375d10c702489167e704dcb99e570df85/7bb53/images/docs/components-of-kubernetes.png
引用元: Kubernetes Components

Kubernetes の変更を検知して動作する処理は様々なコンポーネントが実装する。そのため、 client-go というモジュールになっていて、kube-proxy もこれを利用している。

client-go

Kubernetes の API にアクセスするためのモジュール。
参考 Github client-go

複雑そうなのでまだ細かくは見れてないが、次のような感じ。

  • Kubernetes の API にアクセスして更新情報を取得する(List&Watch)
  • 更新情報をもとにイベントハンドラを実行する(Resource Event Handler)
  • 更新情報をメモリ上に保管し、必要に応じて取得する(Thread safe store)


引用元 Github client-go

kube-proxy は次のようにしてイベントハンドラを登録していた。イベント発生ごとにハンドラが実行される。

pkg/proxy/config/config.go

// NewServiceConfig creates a new ServiceConfig.
func NewServiceConfig(serviceInformer coreinformers.ServiceInformer, resyncPeriod time.Duration) *ServiceConfig {
        result := &ServiceConfig{
                listerSynced: serviceInformer.Informer().HasSynced,
        }

        serviceInformer.Informer().AddEventHandlerWithResyncPeriod(
                cache.ResourceEventHandlerFuncs{
                        AddFunc:    result.handleAddService,
                        UpdateFunc: result.handleUpdateService,
                        DeleteFunc: result.handleDeleteService,
                },
                resyncPeriod,
        )

        return result
}

func (c *ServiceConfig) handleAddService(obj interface{}) {
        service, ok := obj.(*v1.Service)
        if !ok {
                utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", obj))
                return
        }
        for i := range c.eventHandlers {
                klog.V(4).Info("Calling handler.OnServiceAdd")
                c.eventHandlers[i].OnServiceAdd(service)
        }
}
Watch API

client-go では Watch API で各オブジェクトの更新情報を取っている(client-go listwatch.go)。ポーリングすると負荷がすごいんじゃないかと思ったけど SSE なのでコストの問題はそこまで考えなくてよさそう。なお、これはただの HTTP アクセスなので、その様子を curl でも確認できる。
参考 Efficient detection of changes
参考 Kubernetes コンセプト

// Kubernetes API にアクセスできるようにする
]# kubectl proxy

// 別のターミナルでアクセスする
]# curl  http://127.0.0.1:8001/api/v1/namespaces/default/services?watch -v
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8001 (#0)
> GET /api/v1/namespaces/default/services?watch HTTP/1.1
> Host: 127.0.0.1:8001
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Date: Sun, 08 Mar 2020 06:10:33 GMT
< Transfer-Encoding: chunked
<

// kubectl expose deployment app --port=80 を実行する

{"type":"ADDED","object":{"kind":"Service","apiVersion":"v1","metadata":{"name":"app","namespace":"default","selfLink":"/api/v1/namespaces/default/services/app","uid":"632c167f-1bf9-4a22-9e27-588f3678caa4","resourceVersion":"206371","creationTimestamp":"2020-03-08T06:10:46Z","labels":{"app":"app"}},"spec":{"ports":[{"protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"app"},"clusterIP":"10.108.210.134","type":"ClusterIP","sessionAffinity":"None"},"status":{"loadBalancer":{}}}}

// kubectl delete service app を実行する

{"type":"DELETED","object":{"kind":"Service","apiVersion":"v1","metadata":{"name":"app","namespace":"default","selfLink":"/api/v1/namespaces/default/services/app","uid":"632c167f-1bf9-4a22-9e27-588f3678caa4","resourceVersion":"206375","creationTimestamp":"2020-03-08T06:10:46Z","labels":{"app":"app"}},"spec":{"ports":[{"protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"app"},"clusterIP":"10.108.210.134","type":"ClusterIP","sessionAffinity":"None"},"status":{"loadBalancer":{}}}}
^C

話が逸れたため、ログに戻る。

下準備する

kubernetes に必要なチェーン/ルールがなければ作成する。このとき、他のトランザクションから更新されないようにロック(-w 5 なので 5 秒間)をかける。

I0308 01:03:38.016416       1 proxier.go:774] Syncing iptables rules
I0308 01:03:38.016423       1 iptables.go:400] running iptables: iptables [-w 5 -N KUBE-EXTERNAL-SERVICES -t filter]
I0308 01:03:38.018364       1 iptables.go:400] running iptables: iptables [-w 5 -C INPUT -t filter -m conntrack --ctstate NEW -m comment --comment kubernetes externally-visible service portals -j KUBE-EXTERNAL-SERVICES]
I0308 01:03:38.020568       1 iptables.go:400] running iptables: iptables [-w 5 -N KUBE-SERVICES -t filter]
I0308 01:03:38.023594       1 iptables.go:400] running iptables: iptables [-w 5 -C FORWARD -t filter -m conntrack --ctstate NEW -m comment --comment kubernetes service portals -j KUBE-SERVICES]
I0308 01:03:38.026402       1 iptables.go:400] running iptables: iptables [-w 5 -N KUBE-SERVICES -t filter]
I0308 01:03:38.028986       1 iptables.go:400] running iptables: iptables [-w 5 -C OUTPUT -t filter -m conntrack --ctstate NEW -m comment --comment kubernetes service portals -j KUBE-SERVICES]
I0308 01:03:38.031520       1 iptables.go:400] running iptables: iptables [-w 5 -N KUBE-SERVICES -t filter]
I0308 01:03:38.033715       1 iptables.go:400] running iptables: iptables [-w 5 -C INPUT -t filter -m conntrack --ctstate NEW -m comment --comment kubernetes service portals -j KUBE-SERVICES]
I0308 01:03:38.036184       1 iptables.go:400] running iptables: iptables [-w 5 -N KUBE-FORWARD -t filter]
I0308 01:03:38.038395       1 iptables.go:400] running iptables: iptables [-w 5 -C FORWARD -t filter -m comment --comment kubernetes forwarding rules -j KUBE-FORWARD]
I0308 01:03:38.042250       1 iptables.go:400] running iptables: iptables [-w 5 -N KUBE-SERVICES -t nat]
I0308 01:03:38.044151       1 iptables.go:400] running iptables: iptables [-w 5 -C OUTPUT -t nat -m comment --comment kubernetes service portals -j KUBE-SERVICES]
I0308 01:03:38.046162       1 iptables.go:400] running iptables: iptables [-w 5 -N KUBE-SERVICES -t nat]
I0308 01:03:38.048091       1 iptables.go:400] running iptables: iptables [-w 5 -C PREROUTING -t nat -m comment --comment kubernetes service portals -j KUBE-SERVICES]
I0308 01:03:38.050060       1 iptables.go:400] running iptables: iptables [-w 5 -N KUBE-POSTROUTING -t nat]
I0308 01:03:38.051977       1 iptables.go:400] running iptables: iptables [-w 5 -C POSTROUTING -t nat -m comment --comment kubernetes postrouting rules -j KUBE-POSTROUTING]

ルールを更新する

ルールの更新はログからは分かりづらいので、あらかじめソースコード(pkg/proxy/iptables/proxier.go)をパラ読みした雰囲気を説明する。ポイントは次の通り。

  • 既存のルールをバッファに保存する(iptables-save)。
  • 新規に作成するルールをバッファに追加する。
  • バッファにため込んだルールを一括で適用する(iptables-restore)。

iptables-save/iptables-restore を利用するのは iptables コマンドによる追加が高コストだからだと思う。例えば iptables コマンドで 100 件のルールを追加する場合、1 件追加するごとに、カーネルに登録されている全ルールを取得して全体更新するハメになる。これをルールごとに100回繰り返す。一度に複数ルールの登録はできない。

そのほかに一連の更新をアトミックにする、という意図もあると思う。(proxies.go で排他制御の仕組みが入ってたので kube-proxy 上では同時に 2 つ実行されないと思うが、途中で失敗したら困るという意味)

iptables の問題点を解決するために nftables という新しい仕組みがある。これを kube-proxy でも使うという Issue が close されてるので、そのうち nftables が使えるかもしれない。
参考 Proposal: nftables backend for kube-proxy
参考 iptablesの後に来るものは何か?: nftables

ルールを更新する(1回目)

1 回目は Calling handler.OnServiceAdd に対応する更新。ポイントは次の通り。

  • Service に対するエンドポイントのルール(10.107.132.100/32 port 80)が追加される
  • ただし転送先(Pod のエンドポイント) がないので、 REJECT で設定される
I0308 01:03:38.053952       1 iptables.go:295] running iptables-save [-t filter]
I0308 01:03:38.058812       1 iptables.go:295] running iptables-save [-t nat]
I0308 01:03:38.063131       1 proxier.go:865] Using `--random-fully` in the MASQUERADE rule for iptables
I0308 01:03:38.064500       1 proxier.go:1504] Restoring iptables rules: *filter
:KUBE-SERVICES - [0:0]
:KUBE-EXTERNAL-SERVICES - [0:0]
:KUBE-FORWARD - [0:0]
-A KUBE-SERVICES -m comment --comment "default/app: has no endpoints" -m tcp -p tcp -d 10.107.132.100/32 --dport 80 -j REJECT
-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x00004000/0x00004000 -j ACCEPT
-A KUBE-FORWARD -s 10.200.0.0/16 -m comment --comment "kubernetes forwarding conntrack pod source rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack pod destination rule" -d 10.200.0.0/16 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
COMMIT
*nat
:KUBE-SERVICES - [0:0]
:KUBE-NODEPORTS - [0:0]
:KUBE-POSTROUTING - [0:0]
:KUBE-MARK-MASQ - [0:0]
:KUBE-SVC-TCOU7JCQXEZGVUNU - [0:0]
:KUBE-SEP-X2ADUEUQITSVT76E - [0:0]
:KUBE-SEP-YKQ3S3HS332IQ2RP - [0:0]
:KUBE-SVC-ERIFXISQEP7F7OF4 - [0:0]
:KUBE-SEP-K6THTRPC7QLNHCOY - [0:0]
:KUBE-SEP-KYJQOPQVNIZ5BQQ7 - [0:0]
:KUBE-SVC-JD5MR3NA4I4DYORP - [0:0]
:KUBE-SEP-65C5AM72V4635H3F - [0:0]
:KUBE-SEP-Q7VZTB2HQIWWSBOZ - [0:0]
:KUBE-SVC-NPX46M4PTMTKRN6Y - [0:0]
:KUBE-SEP-HFMBYHW5FO36NATD - [0:0]
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x00004000/0x00004000 -j MASQUERADE --random-fully
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SERVICES -m comment --comment "kube-system/kube-dns:dns cluster IP" -m udp -p udp -d 10.96.0.10/32 --dport 53 ! -s 10.200.0.0/16 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -m comment --comment "kube-system/kube-dns:dns cluster IP" -m udp -p udp -d 10.96.0.10/32 --dport 53 -j KUBE-SVC-TCOU7JCQXEZGVUNU
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m statistic --mode random --probability 0.5000000000 -j KUBE-SEP-X2ADUEUQITSVT76E
-A KUBE-SEP-X2ADUEUQITSVT76E -s 10.200.192.74/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-X2ADUEUQITSVT76E -m udp -p udp -j DNAT --to-destination 10.200.192.74:53
-A KUBE-SVC-TCOU7JCQXEZGVUNU -j KUBE-SEP-YKQ3S3HS332IQ2RP
-A KUBE-SEP-YKQ3S3HS332IQ2RP -s 10.200.192.75/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-YKQ3S3HS332IQ2RP -m udp -p udp -j DNAT --to-destination 10.200.192.75:53
-A KUBE-SERVICES -m comment --comment "kube-system/kube-dns:dns-tcp cluster IP" -m tcp -p tcp -d 10.96.0.10/32 --dport 53 ! -s 10.200.0.0/16 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -m comment --comment "kube-system/kube-dns:dns-tcp cluster IP" -m tcp -p tcp -d 10.96.0.10/32 --dport 53 -j KUBE-SVC-ERIFXISQEP7F7OF4
-A KUBE-SVC-ERIFXISQEP7F7OF4 -m statistic --mode random --probability 0.5000000000 -j KUBE-SEP-K6THTRPC7QLNHCOY
-A KUBE-SEP-K6THTRPC7QLNHCOY -s 10.200.192.74/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-K6THTRPC7QLNHCOY -m tcp -p tcp -j DNAT --to-destination 10.200.192.74:53
-A KUBE-SVC-ERIFXISQEP7F7OF4 -j KUBE-SEP-KYJQOPQVNIZ5BQQ7
-A KUBE-SEP-KYJQOPQVNIZ5BQQ7 -s 10.200.192.75/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-KYJQOPQVNIZ5BQQ7 -m tcp -p tcp -j DNAT --to-destination 10.200.192.75:53
-A KUBE-SERVICES -m comment --comment "kube-system/kube-dns:metrics cluster IP" -m tcp -p tcp -d 10.96.0.10/32 --dport 9153 ! -s 10.200.0.0/16 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -m comment --comment "kube-system/kube-dns:metrics cluster IP" -m tcp -p tcp -d 10.96.0.10/32 --dport 9153 -j KUBE-SVC-JD5MR3NA4I4DYORP
-A KUBE-SVC-JD5MR3NA4I4DYORP -m statistic --mode random --probability 0.5000000000 -j KUBE-SEP-65C5AM72V4635H3F
-A KUBE-SEP-65C5AM72V4635H3F -s 10.200.192.74/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-65C5AM72V4635H3F -m tcp -p tcp -j DNAT --to-destination 10.200.192.74:9153
-A KUBE-SVC-JD5MR3NA4I4DYORP -j KUBE-SEP-Q7VZTB2HQIWWSBOZ
-A KUBE-SEP-Q7VZTB2HQIWWSBOZ -s 10.200.192.75/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-Q7VZTB2HQIWWSBOZ -m tcp -p tcp -j DNAT --to-destination 10.200.192.75:9153
-A KUBE-SERVICES -m comment --comment "default/kubernetes:https cluster IP" -m tcp -p tcp -d 10.96.0.1/32 --dport 443 ! -s 10.200.0.0/16 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -m comment --comment "default/kubernetes:https cluster IP" -m tcp -p tcp -d 10.96.0.1/32 --dport 443 -j KUBE-SVC-NPX46M4PTMTKRN6Y
-A KUBE-SVC-NPX46M4PTMTKRN6Y -j KUBE-SEP-HFMBYHW5FO36NATD
-A KUBE-SEP-HFMBYHW5FO36NATD -s 10.240.0.10/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-HFMBYHW5FO36NATD -m tcp -p tcp -j DNAT --to-destination 10.240.0.10:6443
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
COMMIT
I0308 01:03:38.064999       1 iptables.go:360] running iptables-restore [-w 5 --noflush --counters]
I0308 01:03:38.070431       1 proxier.go:753] syncProxyRules took 54.056247ms
I0308 01:03:38.070653       1 bounded_frequency_runner.go:296] sync-runner: ran, next possible in 0s, periodic in 1h0m0s

ルールを更新する(2 回目)

2 回目は Calling handler.OnEndpointsAdd に対する更新。ポイントは次の通り。

  • Service から Pod への DNAT ルールが追加される
    • DNAT するだけなのでオーバーレイネットワーク(CNI Plugin など)が別途必要になる
  • Pod には、確率で均等にロードバランシングされる(--mode random --probability 0.5000000000)
  • 1 回目で REJECT した Service の IP アドレスへのルールは、 Pod の Endpoint にジャンプするようになっている
  • SNAT のためのマーカ用ルールが追加される
    • KUBE-MARK-MASQ
    • 次の POSTROUTING で SNAT される
 -A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x00004000/0x00004000 -j MASQUERADE --random-fully
 -A KUBE-MARK-MASQ -j MARK --set-xmark 0x00004000/0x00004000

1 回目との差分だけ載せる。

 :KUBE-SERVICES - [0:0]
 :KUBE-EXTERNAL-SERVICES - [0:0]
 :KUBE-FORWARD - [0:0]
--A KUBE-SERVICES -m comment --comment "default/app: has no endpoints" -m tcp -p tcp -d 10.107.132.100/32 --dport 80 -j REJECT
 -A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
 -A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x00004000/0x00004000 -j ACCEPT
 -A KUBE-FORWARD -s 10.200.0.0/16 -m comment --comment "kubernetes forwarding conntrack pod source rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
@@ -21,6 +20,9 @@
 :KUBE-SVC-JD5MR3NA4I4DYORP - [0:0]
 :KUBE-SEP-65C5AM72V4635H3F - [0:0]
 :KUBE-SEP-Q7VZTB2HQIWWSBOZ - [0:0]
+:KUBE-SVC-RTINPLO7IQRLY2BV - [0:0]
+:KUBE-SEP-QSBYLXACZFFAKEJ2 - [0:0]
+:KUBE-SEP-RDDL6UYTWGRKFDR2 - [0:0]
 :KUBE-SVC-NPX46M4PTMTKRN6Y - [0:0]
 :KUBE-SEP-HFMBYHW5FO36NATD - [0:0]
 -A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x00004000/0x00004000 -j MASQUERADE --random-fully
@@ -49,6 +51,14 @@
 -A KUBE-SVC-JD5MR3NA4I4DYORP -j KUBE-SEP-Q7VZTB2HQIWWSBOZ
 -A KUBE-SEP-Q7VZTB2HQIWWSBOZ -s 10.200.192.75/32 -j KUBE-MARK-MASQ
 -A KUBE-SEP-Q7VZTB2HQIWWSBOZ -m tcp -p tcp -j DNAT --to-destination 10.200.192.75:9153
+-A KUBE-SERVICES -m comment --comment "default/app: cluster IP" -m tcp -p tcp -d 10.107.132.100/32 --dport 80 ! -s 10.200.0.0/16 -j KUBE-MARK-MASQ
+-A KUBE-SERVICES -m comment --comment "default/app: cluster IP" -m tcp -p tcp -d 10.107.132.100/32 --dport 80 -j KUBE-SVC-RTINPLO7IQRLY2BV
+-A KUBE-SVC-RTINPLO7IQRLY2BV -m statistic --mode random --probability 0.5000000000 -j KUBE-SEP-QSBYLXACZFFAKEJ2
+-A KUBE-SEP-QSBYLXACZFFAKEJ2 -s 10.200.43.11/32 -j KUBE-MARK-MASQ
+-A KUBE-SEP-QSBYLXACZFFAKEJ2 -m tcp -p tcp -j DNAT --to-destination 10.200.43.11:80
+-A KUBE-SVC-RTINPLO7IQRLY2BV -j KUBE-SEP-RDDL6UYTWGRKFDR2
+-A KUBE-SEP-RDDL6UYTWGRKFDR2 -s 10.200.43.12/32 -j KUBE-MARK-MASQ
+-A KUBE-SEP-RDDL6UYTWGRKFDR2 -m tcp -p tcp -j DNAT --to-destination 10.200.43.12:80
 -A KUBE-SERVICES -m comment --comment "default/kubernetes:https cluster IP" -m tcp -p tcp -d 10.96.0.1/32 --dport 443 ! -s 10.200.0.0/16 -j KUBE-MARK-MASQ
 -A KUBE-SERVICES -m comment --comment "default/kubernetes:https cluster IP" -m tcp -p tcp -d 10.96.0.1/32 --dport 443 -j KUBE-SVC-NPX46M4PTMTKRN6Y
 -A KUBE-SVC-NPX46M4PTMTKRN6Y -j KUBE-SEP-HFMBYHW5FO36NATD

まとめ

kube-proxy は仮想的なエンドポイントを提供する。デフォルトの実装は、 iptables のルールで実現されている。既存の Linux の仕組みを上手く使ってる感じで賢い。