Help us understand the problem. What is going on with this article?

Kubernetes Network Deep Dive (LoadBalancer)

はじめに

Kubernetes では、コンテナ(Pod) を外部からアクセスさせるために、Service LoadBalancer を使って外部公開が出来ます。LoadBalancer を作ったときに、どういうことが Kubernetes クラスタ上で起こり、どのように通信出来るのでしょうか。
マネージドサービスを使っていでも、何かの拍子に通信障害が発生することがあります。障害を解析するためには、正しい通信経路を把握することが重要です。

通信経路を調査した内容を備忘録として残します。Kubernetes 環境は、Oracle Cloud のマネージドサービスである OKE を使って調査していますが、他の環境でも基本的には同じような挙動になると思います。

事前準備

OKE クラスタを作成して、Deployment と LoadBalancer を作成します。作業用ディレクトリを作成します。

mkdir ~/kubedemo

Deployment のマニフェストファイルを作成します。Docker Image は、自分の Docker Hub にアップロードしている Webサーバー用のイメージです。

cat <<'EOF' > ~/kubedemo/001-deployment-cowweb.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cowweb
spec:
  replicas: 2
  selector:
    matchLabels:
      app: cowweb
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  template:
    metadata:
      labels:
        app: cowweb
    spec:
      containers:
      - name: cowweb
        image: sugimount/cowweb:v1.0
        ports:
        - name: api
          containerPort: 8080
        readinessProbe:
          httpGet:
            path: /cowsay/ping
            port: api
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /cowsay/ping
            port: api
          initialDelaySeconds: 15
          periodSeconds: 20
        imagePullPolicy: Always
EOF

apply します。

kubectl apply -f ~/kubedemo/001-deployment-cowweb.yaml

Deployment の状況を確認します。2個の Pod が正常稼働しています。

> kubectl get deployments -o wide
NAME     READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS   IMAGES                  SELECTOR
cowweb   2/2     2            2           35m   cowweb       sugimount/cowweb:v1.0   app=cowweb

Pod でも状態を確認します。10.0.10.310.0.10.4 の Node で Pod がそれぞれ稼働しています。

> kubectl get pods -o wide
NAME                      READY   STATUS    RESTARTS   AGE   IP           NODE        NOMINATED NODE   READINESS GATES
cowweb-754cf5977b-2f9fx   1/1     Running   0          25m   10.244.2.4   10.0.10.3   <none>           <none>
cowweb-754cf5977b-bbl7n   1/1     Running   0          25m   10.244.0.5   10.0.10.4   <none>           <none>

Service LoadBalancer 用のマニフェストファイルを作ります。

cat <<'EOF' > ~/kubedemo/002-service-cowweb.yaml
apiVersion: v1
kind: Service
metadata:
  name: cowweb
spec:
  ports:
  - name: http
    port: 80
    targetPort: 8080
  selector:
    app: cowweb
  type: LoadBalancer
EOF

apply します。

kubectl apply -f  ~/kubedemo/002-service-cowweb.yaml

こんな感じになっています。

> kubectl get service -o wide
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP       PORT(S)        AGE   SELECTOR
cowweb       LoadBalancer   10.96.134.200   168.138.217.204   80:32417/TCP   25m   app=cowweb

ちなみに、HTTP でリクエストすると、このように文字を返してくれます。リクエストを処理してくれたコンテナのホスト名を教えてくれます。

> curl "http://168.138.217.204/cowsay/say?say=HOSTNAME"
 _________________________
< cowweb-754cf5977b-bbl7n >
 -------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ネットワーク構成図

事前準備後の状態を、図に表しました。この構成図をベースに、LoadBalancer のアクセス経路を確認していきます。青い四角枠で、Node01 Node02 Node03 と書かれているのは、Kubernetesクラスタの Node です。Oracle Cloud 上の用語で言うと、Compute Instance です。Node の中に、Linux Bridge などあまり聞きなれない要素がありますが、これは後程説明します。

Deployment を作成したときに、コンテナ数を2と指定したので、3Node の中でコンテナがばらけて配置されています。コンテナが稼働している Node と稼働していない Node が居るなかで、どのようなアクセス経路になるかも見ていきます。

1589762814176.png

通信経路 LoadBalancer → Node

まずはインターネットに一番近いロードバランサーから確認していきましょう。Oracle Cloud 上では、Service LoadBalancer に対応したロードバランサーが確認できます。

1589756101282.png

ロードバランサーに紐づく Backend Set を確認します。10.0.10.2 10.0.10.3 10.0.10.4 の3 Node が、Port 32417 でぶら下がっています。なお、この Port は、Service LoadBalancer を作るたびに番号が変わります。Port の番号と Service LoadBalancer は1対1で紐づけされています。

ロードバランサーのバランシングポリシーは、Round Robinとなっていて、HTTP リクエストのたびにアクセスする Node が順番に負荷分散されます。なお、このときには、どのコンテナがどの Node で稼働しているかは全く意識されていません。コンテナが動いているか否かに関わらず、全ての Node にリクエストされます。後述の iptables がバランシングしているため、通信したい Pod にたどり着きます。詳細はまた後程説明します。

1589702286765.png

先ほどの構成図で見ると、こんな感じになっています。Pod (コンテナ) が動いているか否かに関わらず、全ての Node にアクセスされます。

1589762836373.png

ens3 のInterface と IPアドレスを確認します。各Nodeで実行します。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-2 ~]# ip -d a show dev ens3
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 02:00:17:00:c8:78 brd ff:ff:ff:ff:ff:ff promiscuity 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.0.10.2/24 brd 10.0.10.255 scope global dynamic ens3
       valid_lft 77291sec preferred_lft 77291sec
[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-0 ~]# ip -d a show dev ens3
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 02:00:17:00:e9:3c brd ff:ff:ff:ff:ff:ff promiscuity 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.0.10.3/24 brd 10.0.10.255 scope global dynamic ens3
       valid_lft 73320sec preferred_lft 73320sec
[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# ip -d a show dev ens3
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 02:00:17:00:32:ee brd ff:ff:ff:ff:ff:ff promiscuity 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.0.10.4/24 brd 10.0.10.255 scope global dynamic ens3
       valid_lft 70534sec preferred_lft 70534sec

LoadBalancer で紐づけされている Port 32417 のリッスン状態と、リッスンしているプロセスを確認します。ss コマンドの結果を、32417 で grep します。全 Node で LISTEN 状態となっています。また、プロセスは kube-proxy が関係していることがわかります。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-2 ~]# ss -antup | grep -i 32417
tcp  LISTEN     0      16384                    *:32417                  *:*     users:(("kube-proxy",pid=14986,fd=10))
[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-0 ~]# ss -antup | grep -i 32417
tcp  LISTEN     0      16384                    *:32417                  *:*     users:(("kube-proxy",pid=14944,fd=11))
[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# ss -antup | grep -i 32417
tcp  LISTEN     0      16384                    *:32417                  *:*     users:(("kube-proxy",pid=14946,fd=11))

PID が分かったので、ps コマンドでも確認します

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-2 ~]# ps aux | grep 14986
root     14986  0.1  0.2 138996 37228 ?        Ssl  May16   1:43 /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/config.conf --hostname-override=10.0.10.2
[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-0 ~]# ps aux | grep 14944
root     14944  0.1  0.2 138996 35832 ?        Ssl  May16   1:37 /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/config.conf --hostname-override=10.0.10.3
[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# ps aux | grep 14946
root     14946  0.1  0.2 138996 37536 ?        Ssl  May16   1:48 /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/config.conf --hostname-override=10.0.10.4

kube-proxy の実態は何かというと、Kubernetes クラスタの DaemonSet です。DaemonSet を使うことで、全ての Node で kube-proxy コンテナを稼働しています。

> kubectl get daemonsets kube-proxy
NAME         DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR                 AGE
kube-proxy   3         3         3       3            3           beta.kubernetes.io/os=linux   25h

ちなみに、DaemonSet の詳細パラメータはこのようになっています。

> kubectl get daemonsets kube-proxy -o yaml
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  creationTimestamp: "2020-05-16T22:12:50Z"
  generation: 1
  labels:
    k8s-app: kube-proxy
  name: kube-proxy
  namespace: kube-system
  resourceVersion: "1521"
  selfLink: /apis/extensions/v1beta1/namespaces/kube-system/daemonsets/kube-proxy
  uid: 52ed0e7a-1e08-4065-aff8-78f99fd02074
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)
        env:
        - name: NODE_NAME
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: spec.nodeName
        image: ap-tokyo-1.ocir.io/axoxdievda5j/oke-public-kube-proxy@sha256:1485cddb8619868188c89deff29bd7c8b38e29ec0fa8a7eb5de166be088d1dfb
        imagePullPolicy: IfNotPresent
        name: kube-proxy
        resources: {}
        securityContext:
          privileged: true
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        volumeMounts:
        - mountPath: /var/lib/kube-proxy
          name: kube-proxy
        - mountPath: /run/xtables.lock
          name: xtables-lock
        - mountPath: /lib/modules
          name: lib-modules
          readOnly: true
      dnsPolicy: ClusterFirst
      hostNetwork: true
      nodeSelector:
        beta.kubernetes.io/os: linux
      priorityClassName: system-node-critical
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      serviceAccount: kube-proxy
      serviceAccountName: kube-proxy
      terminationGracePeriodSeconds: 30
      tolerations:
      - key: CriticalAddonsOnly
        operator: Exists
      - operator: Exists
      volumes:
      - configMap:
          defaultMode: 420
          name: kube-proxy
        name: kube-proxy
      - hostPath:
          path: /run/xtables.lock
          type: FileOrCreate
        name: xtables-lock
      - hostPath:
          path: /lib/modules
          type: ""
        name: lib-modules
  templateGeneration: 1
  updateStrategy:
    rollingUpdate:
      maxUnavailable: 1
    type: RollingUpdate
status:
  currentNumberScheduled: 3
  desiredNumberScheduled: 3
  numberAvailable: 3
  numberMisscheduled: 0
  numberReady: 3
  observedGeneration: 1
  updatedNumberScheduled: 3

通信経路 Node → Pod

DNAT

LoadBalancer から Node の Port 32417 に通信する経路がわかりました。次は、Node から 各Pod へのアクセス経路を見ていきます。Kubernetes では、kube-proxy というコンポーネントが、Node で受信したリクエストをどのように Pod にアクセスするかコントロールしています。kube-proxy mode は 3種類あります。

  • Userspace
  • Iptables
  • IPVS

OKE では、iptables が使われているので、これを解説していきます。Linux では、パケットを受信した際に iptables を使って様々な処理を行っています。アクセス経路を見ていくなかで一番需要なところは、iptables の NATテーブルです。受信したパケットの Destination(宛先) や Source(送信元) を DNAT/SNAT で変換をしています。

iptables を使っているLinuxでは、次のように iptables 上のチェインを処理しています。
参照元 : https://www.atmarkit.co.jp/ait/articles/1002/09/news119.html

1589760167245.png

では、1個の Node をピックアップして、アクセス経路を確認します。ロードバランサーから、各 Node へリクエストするパケットは次のようなイメージです。Source IP がロードバランサーとなっていて、宛先が Node の IPアドレスと、LoadBalancer に紐づく Port 32417 です。このパケットが iptables によって NAT される詳細を確認します。

1589760850172.png

iptables のルールを見ていく際に、同時に curl で定期的にリクエストを投げておくと、確認がしやすいのでおすすめです。watch コマンドで、0.1 秒おきに LoadBalancer にアクセスをするコマンドです。

watch -n 0.1 curl "http://168.138.217.204/cowsay/say?say=HOSTNAME"

パケットを受信したのちに、iptables のチェインに沿って、PREROUTING チェインが処理されます。PREROUTING チェインのルールを確認しましょう。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-2 ~]# iptables -t nat -vnL PREROUTING
Chain PREROUTING (policy ACCEPT 1 packets, 110 bytes)
 pkts bytes target     prot opt in     out     source               destination
39700 3029K KUBE-SERVICES  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */
26745 2251K DOCKER     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

PREROUTING チェイン中に含まれる、KUBE-SERVICES が次に評価されます。KUBE-SERVICES のルールを確認します。表示結果の左列 pkts は、このルールに該当したパケットの数を表示しています。watch コマンドで、0.1 秒おきに curl でアクセスしているので、この pkts 列が増えていることが確認できます。(何秒おきごとに0にリセットされます)

pkts 列が増えている箇所は、KUBE-NODEPORTS ルールのみです。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-2 ~]# iptables -t nat -vnL KUBE-SERVICES
Chain KUBE-SERVICES (2 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-MARK-MASQ  tcp  --  *      *      !10.244.0.0/16        10.96.134.200        /* default/cowweb:http cluster IP */ tcp dpt:80
    0     0 KUBE-SVC-B4JZRNA5R7ZOSIKX  tcp  --  *      *       0.0.0.0/0            10.96.134.200        /* default/cowweb:http cluster IP */ tcp dpt:80
    0     0 KUBE-FW-B4JZRNA5R7ZOSIKX  tcp  --  *      *       0.0.0.0/0            168.138.217.204      /* default/cowweb:http loadbalancer IP */ tcp dpt:80
    0     0 KUBE-MARK-MASQ  tcp  --  *      *      !10.244.0.0/16        10.96.0.1            /* default/kubernetes:https cluster IP */ tcp dpt:443
    0     0 KUBE-SVC-NPX46M4PTMTKRN6Y  tcp  --  *      *       0.0.0.0/0            10.96.0.1            /* default/kubernetes:https cluster IP */ tcp dpt:443
    0     0 KUBE-MARK-MASQ  udp  --  *      *      !10.244.0.0/16        10.96.5.5            /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
    0     0 KUBE-SVC-TCOU7JCQXEZGVUNU  udp  --  *      *       0.0.0.0/0            10.96.5.5            /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
    0     0 KUBE-MARK-MASQ  tcp  --  *      *      !10.244.0.0/16        10.96.5.5            /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
    0     0 KUBE-SVC-ERIFXISQEP7F7OF4  tcp  --  *      *       0.0.0.0/0            10.96.5.5            /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
    0     0 KUBE-MARK-MASQ  tcp  --  *      *      !10.244.0.0/16        10.96.5.5            /* kube-system/kube-dns:metrics cluster IP */ tcp dpt:9153
    0     0 KUBE-SVC-JD5MR3NA4I4DYORP  tcp  --  *      *       0.0.0.0/0            10.96.5.5            /* kube-system/kube-dns:metrics cluster IP */ tcp dpt:9153
  149 12540 KUBE-NODEPORTS  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL

次に、KUBE-NODEPORTS を確認します。KUBE-MARK-MASQ と KUBE-SVC-B4JZRNA5R7ZOSIKX の2つのチェインが見えます。両方ともパケットの評価対象ですが、 KUBE-MARK-MASQ の方はあまり関係ないので、スキップします。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-2 ~]# iptables -t nat -vnL KUBE-NODEPORTS
Chain KUBE-NODEPORTS (1 references)
 pkts bytes target     prot opt in     out     source               destination
   80  4800 KUBE-MARK-MASQ  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/cowweb:http */ tcp dpt:32417
   80  4800 KUBE-SVC-B4JZRNA5R7ZOSIKX  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/cowweb:http */ tcp dpt:32417

KUBE-SVC-B4JZRNA5R7ZOSIKX のルールです。ここが重要なルールになっています。このパケットをどこの Pod で処理するべきか確率で指定しています。具体的には、statistic mode random probability 0.50000000000という箇所です。

50%の確率で、KUBE-SEP-7PX4KXTRE542OGJM が選ばれます。残りの 50% の確率で、KUBE-SEP-EIDCCEGKQHFCYGJ6 が選ばれます。Deployment の コンテナ数を2としているので、50% となっていますが、コンテナ数 3以上だとこの確率が多少偏りながらバラケます。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-2 ~]# iptables -t nat -vnL KUBE-SVC-B4JZRNA5R7ZOSIKX
Chain KUBE-SVC-B4JZRNA5R7ZOSIKX (3 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-SEP-7PX4KXTRE542OGJM  all  --  *      *       0.0.0.0/0            0.0.0.0/0            statistic mode random probability 0.50000000000
    0     0 KUBE-SEP-EIDCCEGKQHFCYGJ6  all  --  *      *       0.0.0.0/0            0.0.0.0/0

コンテナ数を3にした時はこんな具合になります。
50%, 約33%, 約17% の確率でPodが選択されます。多少の偏りがあります。

[root@oke-crdazdbgvsw-n3geolfmm3d-s6bnuvahoda-0 ~]# iptables -t nat -vnL KUBE-SVC-B4JZRNA5R7ZOSIKX
Chain KUBE-SVC-B4JZRNA5R7ZOSIKX (3 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-SEP-AAEK6W7Y7Q76HCIL  all  --  *      *       0.0.0.0/0            0.0.0.0/0            statistic mode random probability 0.33332999982
    0     0 KUBE-SEP-XPMSAFEAWZ5BZEDD  all  --  *      *       0.0.0.0/0            0.0.0.0/0            statistic mode random probability 0.50000000000
    0     0 KUBE-SEP-HDPRQTFRK74GLPJO  all  --  *      *       0.0.0.0/0            0.0.0.0/0

それぞれ KUBE-SEP-7PX4KXTRE542OGJM と、KUBE-SEP-EIDCCEGKQHFCYGJ6 を見ていきます。Pod が持つ IPアドレスとPort へ、パケットの あて先(Destination) を DNAT しています。今後のアクセス経路の確認は、10.244.0.5:8080 の宛先のパケットを追っていきます。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-2 ~]# iptables -t nat -vnL KUBE-SEP-7PX4KXTRE542OGJM
Chain KUBE-SEP-7PX4KXTRE542OGJM (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-MARK-MASQ  all  --  *      *       10.244.0.5           0.0.0.0/0
   48  2880 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp to:10.244.0.5:8080
[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-2 ~]# iptables -t nat -vnL KUBE-SEP-EIDCCEGKQHFCYGJ6
Chain KUBE-SEP-EIDCCEGKQHFCYGJ6 (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-MARK-MASQ  all  --  *      *       10.244.2.4           0.0.0.0/0
   26  1560 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp to:10.244.2.4:8080

DNAT された状況を構成図で整理すると、次のようになります。

1589763469912.png

Routing

PREROUTING チェインが終わったあとに、評価されるのはパケットのルーティングです。

1589760167245.png

Node のルーティングテーブルを確認します。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-2 ~]# ip r
default via 10.0.10.1 dev ens3
10.0.10.0/24 dev ens3 proto kernel scope link src 10.0.10.2
10.244.0.0/24 via 10.244.0.0 dev flannel.1 onlink
10.244.1.0/24 dev cni0 proto kernel scope link src 10.244.1.1
10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink
169.254.0.0/16 dev ens3 proto static scope link
169.254.0.0/16 dev ens3 scope link metric 1002
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown

あて先パケット 10.244.0.5 は、次の行に該当します。10.244.0.0/24 に該当するパケットは、flannel.1 デバイスから、10.244.0.0 宛に転送すればよい事がわかります

10.244.0.0/24 via 10.244.0.0 dev flannel.1 onlink

ARP テーブルを確認します。10.244.0.0 宛の Mac アドレスは、86:07:e9:e7:d6:c6 と学習しています。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-2 ~]# ip n
169.254.169.254 dev ens3 lladdr 00:00:17:40:db:86 REACHABLE
10.244.1.2 dev cni0 lladdr a2:91:2a:09:29:14 REACHABLE
10.0.10.4 dev ens3 lladdr 02:00:17:00:32:ee STALE
10.244.0.0 dev flannel.1 lladdr 86:07:e9:e7:d6:c6 PERMANENT
10.0.10.3 dev ens3 lladdr 02:00:17:00:e9:3c STALE
10.244.2.0 dev flannel.1 lladdr 5a:47:be:59:26:8d PERMANENT
10.0.10.1 dev ens3 lladdr 00:00:17:40:db:86 REACHABLE

ARP で Mac アドレスが分かっているので、実際にパケットを送付しようとしますが、flannel.1 は、VXLAN のデバイスです。ipコマンドに、detail option をつけて実行すると、vxlan と表示されていることで判断できます。flannel.1 というデバイスから送付されるパケットは、VXLAN でカプセル化されます。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-2 ~]# ip -d a show dev flannel.1
4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8950 qdisc noqueue state UNKNOWN group default
    link/ether 72:19:02:f2:49:47 brd ff:ff:ff:ff:ff:ff promiscuity 0
    vxlan id 1 local 10.0.10.2 dev ens3 srcport 0 0 dstport 14789 nolearning ttl inherit ageing 300 udpcsum noudp6zerocsumtx noudp6zerocsumrx numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.244.1.0/32 scope global flannel.1
       valid_lft forever preferred_lft forever

VXLAN でカプセル化をする際には、宛先の MacアドレスとIPアドレスを把握する必要があります。Linux Kernal 内部の「FDB(Forwarding Database)」に記録されています。あて先 10.244.0.5 のパケットは FDB の情報を使って、flannel.1 で VXLANカプセル化したのちに、Mac アドレス 86:07:e9:e7:d6:c6 ・IPアドレス 10.0.10.4 へ送付されることがわかります。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-2 ~]# bridge fdb show dev flannel.1
86:07:e9:e7:d6:c6 dst 10.0.10.4 self permanent
5a:47:be:59:26:8d dst 10.0.10.3 self permanent

POSTROUTING

ルーティングの評価が終わったので、次は POSTROUTING チェインです。

1589760167245.png

POSTROUTING のルールを確認します。 KUBE-POSTROUTING がパケットの評価対象です。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-2 ~]# iptables -t nat -vnL POSTROUTING
Chain POSTROUTING (policy ACCEPT 1 packets, 60 bytes)
 pkts bytes target     prot opt in     out     source               destination
88909 6574K KUBE-POSTROUTING  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes postrouting rules */
19557 1173K RETURN     all  --  *      *       10.244.0.0/16        10.244.0.0/16
    4   265 MASQUERADE  all  --  *      *       10.244.0.0/16       !224.0.0.0/4
    0     0 RETURN     all  --  *      *      !10.244.0.0/16        10.244.1.0/24
    0     0 MASQUERADE  all  --  *      *      !10.244.0.0/16        10.244.0.0/16

KUBE-POSTROUTING を確認します。target が MASQUERADE なので、パケットの Source が SNAT されます。具体的には、flannel.1 の IP アドレス 10.244.1.0 に SNAT されます。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-2 ~]# iptables -t nat -vnL KUBE-POSTROUTING
Chain KUBE-POSTROUTING (1 references)
 pkts bytes target     prot opt in     out     source               destination
   24  2040 MASQUERADE  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service traffic requiring SNAT */ mark match 0x4000/0x4000

構成図で表すとこんな感じです。

1589766270868.png

VXLAN通信

iptables で宛先の Pod を決めたあとに、ことなる Node へ通信する際には、flannel での VXLAN でパケットを送付します。

1589766738051.png

Routing

Node03 で VXLAN パケットを紐解くと、次のようなパケットとなっています。

1589766965230.png

Node03 側のルーティングテーブルに従って、パケットが処理されます。あて先が10.244.0.5 なので、cni0 インターフェースに転送されます。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# ip r
default via 10.0.10.1 dev ens3
10.0.10.0/24 dev ens3 proto kernel scope link src 10.0.10.4
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1
10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink
10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink
169.254.0.0/16 dev ens3 proto static scope link
169.254.0.0/16 dev ens3 scope link metric 1002
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown

cni0 のインターフェースは、ip コマンドで見ると、bridge という文字が見えます。これは Linux Bridge ということを示しています。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# ip -d a show dev cni0
5: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8950 qdisc noqueue state UP group default qlen 1000
    link/ether 16:9d:af:e1:de:84 brd ff:ff:ff:ff:ff:ff promiscuity 0
    bridge forward_delay 1500 hello_time 200 max_age 2000 ageing_time 30000 stp_state 0 priority 32768 vlan_filtering 0 vlan_protocol 802.1Q bridge_id 8000.16:9d:af:e1:de:84 designated_root 8000.16:9d:af:e1:de:84 root_port 0 root_path_cost 0 topology_change 0 topology_change_detected 0 hello_timer    0.00 tcn_timer    0.00 topology_change_timer    0.00 gc_timer  130.78 vlan_default_pvid 1 vlan_stats_enabled 0 group_fwd_mask 0 group_address 01:80:c2:00:00:00 mcast_snooping 1 mcast_router 1 mcast_query_use_ifaddr 0 mcast_querier 0 mcast_hash_elasticity 4 mcast_hash_max 512 mcast_last_member_count 2 mcast_startup_query_count 2 mcast_last_member_interval 100 mcast_membership_interval 26000 mcast_querier_interval 25500 mcast_query_interval 12500 mcast_query_response_interval 1000 mcast_startup_query_interval 3125 mcast_stats_enabled 0 mcast_igmp_version 2 mcast_mld_version 1 nf_call_iptables 0 nf_call_ip6tables 0 nf_call_arptables 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.244.0.1/24 scope global cni0
       valid_lft forever preferred_lft forever

Linux Bridge とは、Linux Kernel で処理される仮想的な L2SW です。brctl コマンドで表示が出来ます。これを見ると、cni0 という名前の Linux Bridge に、3つの Interface veth12732afb, veth719c12d5, veth8340ad20が接続されています。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# brctl show
bridge name     bridge id               STP enabled     interfaces
cni0            8000.169dafe1de84       no              veth12732afb
                                                        veth719c12d5
                                                        veth8340ad20
docker0         8000.0242ea7bfe39       no

veth というのは、Linux Kernal 内で実現される、仮想的なLANケーブルです。Node03 のホスト上で、ip コマンドで確認できます。veth8340ad20@if3 という表示は、1本の仮想的なLANケーブルのうち、1つの端子という概念を示しています。逆側の端子は、Pod(コンテナ)内で使われています。

6: veth8340ad20@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8950 qdisc noqueue master cni0 state UP group default
    link/ether 5a:b3:b9:f3:cd:9d brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 1
    veth
    bridge_slave state forwarding priority 32 cost 2 hairpin on guard off root_block off fastleave off learning on flood on port_id 0x8001 port_no 0x1 designated_port 32769 designated_cost 0 designated_bridge 8000.16:9d:af:e1:de:84 designated_root 8000.16:9d:af:e1:de:84 hold_timer    0.00 message_age_timer    0.00 forward_delay_timer    0.00 topology_change_ack 0 config_pending 0 proxy_arp off proxy_arp_wifi off mcast_router 1 mcast_fast_leave off mcast_flood on vlan_tunnel off numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
7: veth12732afb@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8950 qdisc noqueue master cni0 state UP group default
    link/ether 2a:0b:ba:83:de:72 brd ff:ff:ff:ff:ff:ff link-netnsid 1 promiscuity 1
    veth
    bridge_slave state forwarding priority 32 cost 2 hairpin on guard off root_block off fastleave off learning on flood on port_id 0x8002 port_no 0x2 designated_port 32770 designated_cost 0 designated_bridge 8000.16:9d:af:e1:de:84 designated_root 8000.16:9d:af:e1:de:84 hold_timer    0.00 message_age_timer    0.00 forward_delay_timer    0.00 topology_change_ack 0 config_pending 0 proxy_arp off proxy_arp_wifi off mcast_router 1 mcast_fast_leave off mcast_flood on vlan_tunnel off numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
9: veth719c12d5@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8950 qdisc noqueue master cni0 state UP group default
    link/ether 02:14:2a:75:f4:1e brd ff:ff:ff:ff:ff:ff link-netnsid 2 promiscuity 1
    veth
    bridge_slave state forwarding priority 32 cost 2 hairpin on guard off root_block off fastleave off learning on flood on port_id 0x8003 port_no 0x3 designated_port 32771 designated_cost 0 designated_bridge 8000.16:9d:af:e1:de:84 designated_root 8000.16:9d:af:e1:de:84 hold_timer    0.00 message_age_timer    0.00 forward_delay_timer    0.00 topology_change_ack 0 config_pending 0 proxy_arp off proxy_arp_wifi off mcast_router 1 mcast_fast_leave off mcast_flood on vlan_tunnel off numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

ちょっとわかりにくいので、構成図的にはこのあたりのことを指しています。

1589768055541.png

Pod(コンテナ)で使われている veth の逆側端子を確認します。Docker は、Host 側のネットワークとコンテナ側のネットワークを分離するために、Linux Kernel で実装されている Network Namespace を使っています。veth の逆側端子を確認するために、Network Namespace に潜り込みます。まずは、事前準備で作成した Deployment で動いている、Docker のプロセス ID を確認するために、Node03 上で Docker コンテナの一覧を確認します。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# docker ps
CONTAINER ID        IMAGE                                                                              COMMAND                  CREATED             STATUS              PORTS               NAMES
d6bbbbc5115f        sugimount/cowweb                                                                   "java -jar /home/app…"   19 hours ago        Up 19 hours                             k8s_cowweb_cowweb-754cf5977b-bbl7n_default_c00366f7-e669-4b32-b026-ec7275765caf_0
0d314798ec5b        ap-tokyo-1.ocir.io/odx-oke/oke-public/pause-amd64:3.1                              "/pause"                 19 hours ago        Up 19 hours                             k8s_POD_cowweb-754cf5977b-bbl7n_default_c00366f7-e669-4b32-b026-ec7275765caf_0
460ff8b9fc3d        ap-tokyo-1.ocir.io/axoxdievda5j/oke-public-coredns                                 "/coredns -conf /etc…"   28 hours ago        Up 28 hours                             k8s_coredns_coredns-67bb89d7c6-rmmc8_kube-system_5ce48083-0645-496c-bded-1cc887297106_0
5f69239ed698        ap-tokyo-1.ocir.io/axoxdievda5j/oke-public-cluster-proportional-autoscaler-amd64   "/cluster-proportion…"   28 hours ago        Up 28 hours                             k8s_autoscaler_kube-dns-autoscaler-655fffb489-54cr2_kube-system_63091718-2661-45f8-9c54-20e5b93440f9_0
c826513995e5        ap-tokyo-1.ocir.io/odx-oke/oke-public/pause-amd64:3.1                              "/pause"                 28 hours ago        Up 28 hours                             k8s_POD_kube-dns-autoscaler-655fffb489-54cr2_kube-system_63091718-2661-45f8-9c54-20e5b93440f9_8
c0b00a56ec0f        ap-tokyo-1.ocir.io/odx-oke/oke-public/pause-amd64:3.1                              "/pause"                 28 hours ago        Up 28 hours                             k8s_POD_coredns-67bb89d7c6-rmmc8_kube-system_5ce48083-0645-496c-bded-1cc887297106_8
c1d839b6853b        f0fad859c909                                                                       "/opt/bin/flanneld -…"   28 hours ago        Up 28 hours                             k8s_kube-flannel_kube-flannel-ds-pbvw2_kube-system_7a2cc3da-c0cc-4a6e-b9dc-e46cc912f462_1
1521999875c4        ap-tokyo-1.ocir.io/axoxdievda5j/oke-public-proxymux-cli                            "/bin/proxymux.sh --…"   28 hours ago        Up 28 hours                             k8s_proxymux-client_proxymux-client-lz874_kube-system_fa3dc817-677a-4dbc-8250-a934277b03d7_0
c6d8f857f746        ap-tokyo-1.ocir.io/odx-oke/oke-public/pause-amd64:3.1                              "/pause"                 28 hours ago        Up 28 hours                             k8s_POD_proxymux-client-lz874_kube-system_fa3dc817-677a-4dbc-8250-a934277b03d7_0
08d376bc8f01        ap-tokyo-1.ocir.io/axoxdievda5j/oke-public-kube-proxy                              "/usr/local/bin/kube…"   28 hours ago        Up 28 hours                             k8s_kube-proxy_kube-proxy-q688m_kube-system_ac3d6bdc-07eb-4851-a9d8-9a1d21036bf2_0
f483656c18dc        ap-tokyo-1.ocir.io/odx-oke/oke-public/pause-amd64:3.1                              "/pause"                 28 hours ago        Up 28 hours                             k8s_POD_kube-flannel-ds-pbvw2_kube-system_7a2cc3da-c0cc-4a6e-b9dc-e46cc912f462_0
7ec8b8c14cf1        ap-tokyo-1.ocir.io/odx-oke/oke-public/pause-amd64:3.1                              "/pause"                 28 hours ago        Up 28 hours                             k8s_POD_kube-proxy-q688m_kube-system_ac3d6bdc-07eb-4851-a9d8-9a1d21036bf2_0

この中から、Image の名前 sugimount/cowweb に対応する、CONTAINTER ID は、d6bbbbc5115f です。Docker の inspect コマンドで、pid を確認します。PID は、16119 です。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# docker inspect d6bbbbc5115f --format '{{.State.Pid}}'
16119

念のため、Node03 上で 16119 のプロセスを確認します。コンテナイメージ内の jar を実行しているため、正しいです。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# ps aux | grep 16119
opc      16119  0.2  5.6 5679088 860396 ?      Ssl  May17   2:29 java -jar /home/app/cowweb-1.0.jar

16119 プロセスが Network Namespace を使っているか確認します。次のディレクリでこのような実行結果となっていると、どうやら Network Namespace を使っているだろうな、ということが読み取れます。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# ls -la /proc/16119/ns/net
lrwxrwxrwx. 1 opc opc 0 May 18 02:19 /proc/16119/ns/net -> net:[4026532411]

ip コマンドで Network Namespace を扱うために、上記で確認したものにたいして、シンボリックリンクを作成します。シンボリックリンクの名前は任意の名前です。

mkdir -p /var/run/netns/
ln -s /proc/16119/ns/net /var/run/netns/cowweb-netns

シンボリックリンクを作成したので、ip netns コマンドでネットワークネームスペースを扱うことが出来るようになりました。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# ip netns
cowweb-netns (id: 2)

ネットワークネームスペース内で、bash を起動します。これによって、該当のコンテナが使っているネットワークネームスペース内に潜り込んで、インターフェースやルーティングテーブルを確認できます。

ip netns exec cowweb-netns bash

ネットワークネームスペース内で、インターフェースを確認します。ホストの時に実行した結果と異なるものが表示されました。eth0@if9 と表示されているところが、Pod(コンテナ)側で使っている、veth の端子です。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# ip -d a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
3: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8950 qdisc noqueue state UP group default
    link/ether 7a:ea:83:2f:1b:ce brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 0
    veth numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.244.0.5/24 scope global eth0
       valid_lft forever preferred_lft forever

eth0@if9 で書かれている if9 は、Host側 Interface の 9番目に対応しています。具体的には次のインターフェースです。それぞれ、9 と 3 の数字が相互リンクされているので、veth でつながっているの両端子ということがわかります。

9: veth719c12d5@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8950 qdisc noqueue master cni0 state UP group default
    link/ether 02:14:2a:75:f4:1e brd ff:ff:ff:ff:ff:ff link-netnsid 2 promiscuity 1
    veth
    bridge_slave state forwarding priority 32 cost 2 hairpin on guard off root_block off fastleave off learning on flood on port_id 0x8003 port_no 0x3 designated_port 32771 designated_cost 0 designated_bridge 8000.16:9d:af:e1:de:84 designated_root 8000.16:9d:af:e1:de:84 hold_timer    0.00 message_age_timer    0.00 forward_delay_timer    0.00 topology_change_ack 0 config_pending 0 proxy_arp off proxy_arp_wifi off mcast_router 1 mcast_fast_leave off mcast_flood on vlan_tunnel off numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

ネットワークネームスペース内で、ルーティング情報も確認します。Default Route は、eth0 です。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# ip r
default via 10.244.0.1 dev eth0
10.244.0.0/24 dev eth0 proto kernel scope link src 10.244.0.5
10.244.0.0/16 via 10.244.0.1 dev eth0

eth0 の先には、veth を経由して、Host 側の Bridge cni0 があるので、Host を経由して外の世界へ通信出来ることがわかります。

1589769767124.png

ちなみに、ip netns identify コマンドを使うと、今の Shell が使っている Network Namespace の名前を確認できます。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# ip netns identify
cowweb-netns

Network Namespace から離脱するため、Bash を終了します。ip netns identify の結果が空白になりました。

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# exit
exit
[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]# ip netns identify

[root@oke-cywgzrvme2w-n2timtcmvqw-sv75u4uwhqq-1 ~]#

ちなみに、kubectl exec でコンテナ内の sh を起動しても、全く同じ結果が得られます。

> kubectl exec -it cowweb-754cf5977b-bbl7n sh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl kubectl exec [POD] -- [COMMAND] instead.
~ $

インターフェースの確認

~ $ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
3: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 8950 qdisc noqueue state UP
    link/ether 7a:ea:83:2f:1b:ce brd ff:ff:ff:ff:ff:ff
    inet 10.244.0.5/24 scope global eth0
       valid_lft forever preferred_lft forever

ルーティングテーブルの確認

~ $ ip r
default via 10.244.0.1 dev eth0
10.244.0.0/24 dev eth0  src 10.244.0.5
10.244.0.0/16 via 10.244.0.1 dev eth0

ということで話を戻すと、Host側の flannel.1 デバイスで 、VXLAN カプセル化を解いたパケットの通信経路の話です。Host 側のルーティングテーブルに従って、Linux Bridge の cni0 と、veth を経由して、Pod(コンテナ)へ通信する経路となります。ちょっと難しいですね。

1589770192787.png

まとめ

Kubernetes クラスタで、Service LoadBalancer を使ったときの通信経路を確認しました。一般的なIPアドレスやMacアドレスの話から、iptables, Network Namespace, Linux Bridge, veth など、あまり聞きなれない概念が出てきたと思います。

マネージドサービスを使っている場合は、これらのことをあまり意識する場面は無いと思います。ただ、実際に障害が発生した時には、どこまで通信できて、どこから通信出来ないのか、といった情報を基に障害切り分けをしていきます。そういった時に、ここの記事を思い出して活用してもらえると嬉しいです。

参考URL

http://enakai00.hatenablog.com/entry/2015/04/02/173739

sugimount
CloudNativeな色々をやっています / 投稿している内容は個人的な見解なので、所属組織とは関係ありません https://twitter.com/sugimount
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした