はじめに
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.3
と 10.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 が居るなかで、どのようなアクセス経路になるかも見ていきます。
通信経路 LoadBalancer → Node
まずはインターネットに一番近いロードバランサーから確認していきましょう。Oracle Cloud 上では、Service LoadBalancer に対応したロードバランサーが確認できます。
ロードバランサーに紐づく 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 にたどり着きます。詳細はまた後程説明します。
先ほどの構成図で見ると、こんな感じになっています。Pod (コンテナ) が動いているか否かに関わらず、全ての Node にアクセスされます。
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
では、1個の Node をピックアップして、アクセス経路を確認します。ロードバランサーから、各 Node へリクエストするパケットは次のようなイメージです。Source IP がロードバランサーとなっていて、宛先が Node の IPアドレスと、LoadBalancer に紐づく Port 32417
です。このパケットが iptables によって NAT される詳細を確認します。
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 された状況を構成図で整理すると、次のようになります。
Routing
PREROUTING チェインが終わったあとに、評価されるのはパケットのルーティングです。
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 チェインです。
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
構成図で表すとこんな感じです。
VXLAN通信
iptables で宛先の Pod を決めたあとに、ことなる Node へ通信する際には、flannel での VXLAN でパケットを送付します。
Routing
Node03 で VXLAN パケットを紐解くと、次のようなパケットとなっています。
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
ちょっとわかりにくいので、構成図的にはこのあたりのことを指しています。
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 を経由して外の世界へ通信出来ることがわかります。
ちなみに、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(コンテナ)へ通信する経路となります。ちょっと難しいですね。
まとめ
Kubernetes クラスタで、Service LoadBalancer を使ったときの通信経路を確認しました。一般的なIPアドレスやMacアドレスの話から、iptables, Network Namespace, Linux Bridge, veth など、あまり聞きなれない概念が出てきたと思います。
マネージドサービスを使っている場合は、これらのことをあまり意識する場面は無いと思います。ただ、実際に障害が発生した時には、どこまで通信できて、どこから通信出来ないのか、といった情報を基に障害切り分けをしていきます。そういった時に、ここの記事を思い出して活用してもらえると嬉しいです。
参考URL