前言

上一篇介紹了 Pod、Deployment 與 Service 的基本角色,但「有一個 Service」和「封包真的走得通」是兩件不同的事。本篇不再把網路元件當成名詞表,而是從一個 Pod 內的 localhost 開始,逐層追蹤封包如何走到另一個 Pod。

你會看到 network namespace、veth pair、Node routing、CNI、EndpointSlice、Service VIP 與 CoreDNS 各自負責哪一段。不同叢集可能使用 overlay、雲端 VPC 原生路由、eBPF 或其他資料平面,但 Kubernetes 對使用者提供的抽象仍然可以用同一套問題來理解:誰在查名字?誰在選後端?誰在搬封包?誰在阻擋封包?

本篇完成後,你應該能回答:

  • 同一個 Pod 裡的兩個 container,為什麼可以用 localhost 互相呼叫?
  • Pod A 直接連 Pod B 的 Pod IP,和連 Service 的 ClusterIP 有什麼不同?
  • 同節點與跨節點的封包,通常會經過哪些網路介面與路由?
  • CoreDNS 為什麼只參與 DNS lookup,而不是 HTTP/TCP 的代理?
  • EndpointSlice、kube-proxy 或 CNI 的 eBPF dataplane,如何共同讓 Service VIP 找到後端?
  • 加上 NetworkPolicy 後,為什麼 DNS 可能突然失效?

先分清楚四種網路路徑

Kubernetes 官方網路模型其實把問題拆成四類。若沒有先區分它們,很容易把「Pod 有 IP」、「Service 有 DNS」和「外部流量能進來」誤認成同一件事。

路徑 目的地 位址是否穩定 中間主要處理者 適合用來理解什麼
同一 Pod 內的 container-to-container 另一個 container 的 localhost:port 或 Pod IP Pod 網路位址相同 同一個 network namespace 的 Linux stack sidecar、代理、主程式之間的本機通訊
Pod IP 直連 PodIP:port 通常不穩定,Pod 重建可能換 IP CNI、Node routing、底層網路 Pod-to-Pod 封包如何在同節點或跨節點傳遞
Service VIP ClusterIP:port,通常先透過 DNS 找到 Service VIP 通常穩定,後端 Pod 可變 CoreDNS 做 lookup;Service dataplane 選 endpoint 用穩定名稱與虛擬 IP 存取一組後端
外部入口 NodePort、外部 Load Balancer、Gateway 或 Ingress 等 取決於叢集與雲端整合 叢集外部設備加上叢集內資料平面 外部使用者如何進入叢集

可以先用下面這張圖建立邊界。圖中的箭頭不是表示每個叢集一定有相同的實作,而是表示每種路徑的抽象責任:

flowchart TB
    subgraph POD[同一個 Pod]
        C1[Container A]
        C2[Container B]
        C1 -->|localhost:8080| C2
    end

    subgraph CLUSTER[叢集內]
        PA[Pod A]
        PB[Pod B]
        VIP[Service VIP]
        PA -->|Pod IP 直連| PB
        PA -->|ClusterIP:port| VIP
        VIP -->|選擇 ready endpoint| PB
    end

    USER[叢集外 client] -->|外部入口| ENTRY[LB / NodePort / Gateway / Ingress]
    ENTRY --> VIP

四個常見誤解

  1. DNS 名稱不是封包路徑:DNS 查詢只負責把名稱解析成 IP。解析完成後,應用程式會自行對該 IP 建立 TCP、UDP 或其他連線。
  2. Service 不是一個一定存在的伺服器程序:ClusterIP 通常是虛擬 IP,可能由 node 上的 iptables、nftables、IPVS 或 eBPF 規則處理。
  3. Pod IP 不是服務發現介面:它適合診斷與觀察,不適合由應用程式長期寫死。
  4. 外部入口不是本篇的重點:L4/L7 Load Balancing、TLS termination、Ingress、Gateway、來源 IP 與外部流量策略,會在後續 Part 8 集中說明。

Pod 為什麼可以使用 localhost?

Network namespace 是隔離網路的邊界

Linux 的 network namespace(網路命名空間,簡稱 netns)會隔離一組網路資源,例如:

  • 網路介面,例如 lo、eth0
  • IP 位址與 ARP/neighbor table
  • routing table
  • iptables/nftables 等部分網路狀態
  • socket 與 port 的視野

Kubernetes 的 Pod 通常會先建立一個 Pod sandbox,再讓同一 Pod 的 containers 加入這個 network namespace。不同 container 仍然有各自的檔案系統、程序與環境變數,但它們共享網路介面、IP 與 port 空間。

因此,同一個 Pod 裡的 container 可以這樣互相呼叫:

1
2
3
Container A  -- http://127.0.0.1:8080 -->  Container B
\
`-- 同一個 network namespace

這也代表兩個 container 不能同時在同一個 Pod 裡監聽相同的 IP:port。例如主程式已經使用 0.0.0.0:8080,sidecar 就不能再綁定同一個 port;但它可以使用另一個 port,例如 127.0.0.1:15000。

localhost 不是「同一台 Node」

localhost 只表示「目前這個 process 所在的 network namespace」。以下三個情境完全不同:

1
2
3
同一 Pod 的 container B       -> 127.0.0.1:8080  可達
同一 Node 的另一個 Pod -> 127.0.0.1:8080 不會到達 Pod B
另一個 Node 的 Pod -> 127.0.0.1:8080 不會到達 Pod B

如果服務只綁定 127.0.0.1,它只會接受同一 network namespace 內的連線。要讓其他 Pod 透過 Pod IP 連線,服務通常要監聽 0.0.0.0 或 Pod eth0 的位址;Kubernetes 的 containerPort 欄位本身不會替程式打開 port,也不會改變程式的 bind address。

hostNetwork: true 是一個需要特別留意的例外:這類 Pod 會使用 Node 的 network namespace,因此它的 localhost 指向 Node,而不是獨立的 Pod 網路。這也會帶來 port 衝突與隔離性降低等取捨,不應和一般 Pod 混為一談。

veth pair:把 Pod 接到 Node

在常見的 Linux CNI 實作中,Pod netns 內的 eth0 會和 Node netns 裡的一個虛擬介面組成 veth pair。一端放在 Pod,另一端留在 Node;兩端像一條虛擬網路線,寫入其中一端的封包會從另一端出現。

flowchart LR
    subgraph PNS[Pod network namespace]
        LO[lo<br/>127.0.0.1]
        PE[eth0<br/>Pod IP]
    end

    subgraph NNS[Node network namespace]
        VE[vethXXXX]
        ROUTE[bridge / Linux route / eBPF]
        NIC[Node NIC]
    end

    PE <-->|veth pair| VE
    VE --> ROUTE
    ROUTE --> NIC
    LO -.->|只留在同一 netns| PE

這是很常見的資料路徑,但不是 Kubernetes 的固定規格。某些 CNI 會使用 Linux bridge,某些會直接設定路由或雲端 ENI,某些會在 socket 或封包 hook 上使用 eBPF。學習重點是:Pod 的介面在自己的 netns,Node 必須有某種機制把它接到其他 Pod 與外部網路。

CNI、Node routing 與同節點/跨節點差異

CNI 到底負責什麼?

CNI(Container Network Interface)是 container runtime 與網路插件之間的介面與規格,不是一個單獨的「Kubernetes 網路」。當 Pod sandbox 建立或刪除時,runtime 會依設定呼叫 CNI plugin;實際插件可能由多個元件組成。

一個 Pod 啟動時,CNI 常見會處理以下工作:

工作 可能的結果
IPAM 從 Pod CIDR、VPC、ENI 或其他位址池分配 Pod IP
建立介面 建立 veth、bridge、ENI 或其他網路裝置
加入 network namespace 把 Pod 端介面命名為 eth0,設定 IP 與 link state
設定路由 指定 default route、Pod CIDR 路由或下一跳
讓跨節點可達 使用 Node route、overlay、雲端路由或其他 dataplane
實作政策與可觀測性 視插件能力加入 NetworkPolicy、流量追蹤與 metrics

Pod IP 由網路插件的位址管理機制提供;Service 的 ClusterIP 則是 Kubernetes API/控制面分配的另一個位址範圍。 兩者不要混為同一個 IP pool。

同一節點:封包通常不必離開 Node

假設 Pod A 與 Pod B 在同一個 Node,抽象路徑可以畫成:

flowchart LR
    A[Pod A netns<br/>eth0: Pod IP A]
    AV[veth A]
    R[Node bridge / route / eBPF]
    BV[veth B]
    B[Pod B netns<br/>eth0: Pod IP B]

    A --> AV --> R --> BV --> B

封包通常會從 Pod A 的 eth0 進入 Node,再由同一個 Node 上的 bridge、routing table 或 eBPF dataplane 送到 Pod B 的介面。它不需要經過實體網卡,也不需要因為「是兩個 Pod」就經過 CoreDNS 或 Service。

要注意的是,Kubernetes 網路模型期待 Pod-to-Pod 通訊不需要 NAT;但「不需要 NAT」不等於每個 CNI 都有相同的 Linux 實作。你仍然可能看到 bridge、路由、封裝或 eBPF 等不同組合。

跨節點:Node 之間要知道 Pod 網段怎麼走

如果 Pod A 在 Node 1、Pod B 在 Node 2,封包需要先離開 Node 1,再到 Node 2,最後進入 Pod B 的 netns:

flowchart LR
    A[Pod A netns] --> N1[Node 1 route / dataplane]
    N1 -->|underlay、overlay 或雲端網路| N2[Node 2 route / dataplane]
    N2 --> B[Pod B netns]

跨節點能否成功,取決於 CNI 如何把 Pod 網段告訴 Node 或底層網路,以及防火牆、MTU、封裝與回程路由是否一致。這也是「同一 Node 的 Pod 可以互通,但換到另一個 Node 就 timeout」時,應該檢查 CNI 與 Node routing,而不是先猜應用程式壞掉的原因。

Overlay、VPC 原生路由與 eBPF 是選項,不是答案

不同叢集可以用不同的 dataplane。常見取捨如下:

方式 封包如何跨節點 常見優點 常見代價或注意事項
Overlay 把 Pod 封包包在 Node-to-Node 的外層封包中,例如 VXLAN 或 Geneve 對底層網路要求較少,Pod 網段彈性較高 有封裝成本、MTU 需要正確設定,故障排查多一層
VPC/原生路由 讓雲端路由表、ENI 或 Node route 直接知道 Pod 網段 可減少封裝,和雲端網路整合較自然 需要管理路由與 IP 配額,跨環境可攜性較低
eBPF dataplane 在 kernel hook 或 socket 層執行 forwarding、policy、Service translation 等程式 可整合可觀測性與快速路徑,部分情況可取代 kube-proxy 需要相容的 kernel、CNI 與運維工具,行為不能直接套用到其他 CNI

這些選項不是互斥的。例如使用 eBPF 的 CNI 仍可能搭配 overlay,也可能使用雲端原生路由。排錯時請先確認實際使用的 CNI、版本、kube-proxy 模式與網路拓撲:

1
2
3
kubectl get pods -n kube-system -o wide
kubectl get nodes -o wide
kubectl get pods -n kube-system --show-labels

在有權限的 Node 上,才進一步查看 ip addr、ip route、iptables、nft、ipvsadm 或 CNI 自己的 CLI。不要因為某篇文章的圖畫了 veth,就假設你的雲端叢集一定存在同名的介面或規則。

Service VIP 到底怎麼找到 Pod?

Pod IP 適合用來觀察封包,但應用程式通常需要一個不會隨 Pod 重建而改變的入口。Service 提供的就是這個抽象:一個穩定的虛擬 IP、port 與一組後端 endpoint。

先看控制面:Service、selector 與 EndpointSlice

假設有一個 Service:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: api
namespace: demo
spec:
selector:
app: api
ports:
- name: http
port: 80
targetPort: 8080

這裡有三個容易混淆的 port:

  • port: 80:client 看到的 Service port。
  • targetPort: 8080:後端 Pod 中程式實際監聽的 port。
  • Pod 的 containerPort:描述性欄位,不會自動讓程式監聽該 port。

控制面會根據 Service selector 找出符合 app: api 的 Pod,然後由 EndpointSlice controller 建立或更新 EndpointSlice。EndpointSlice 會記錄 endpoint IP、port、所在 Node 與 ready、serving、terminating 等狀態;不是只要 Pod 有 IP 就一定會被導流。

可以把這段控制流程想成:

flowchart LR
    S[Service selector<br/>app: api]
    C[EndpointSlice controller]
    E[EndpointSlice<br/>Pod IP + port + conditions]
    D[Service dataplane<br/>kube-proxy 或 CNI]
    V[ClusterIP:80]
    P1[Ready Pod 1:8080]
    P2[Ready Pod 2:8080]

    S --> C --> E --> D
    D --> V
    D --> P1
    D --> P2

EndpointSlice 是後續資料平面知道「有哪些後端」的重要來源。若 selector 寫錯、Pod 沒有 Ready、port 對不上,Service 可能存在、DNS 也能解析,但仍然沒有可用流量。

再看資料面:ClusterIP 是虛擬入口

client 執行 curl http://api.demo.svc.cluster.local/ 時,可以拆成以下步驟:

sequenceDiagram
    participant A as Client Pod
    participant DNS as CoreDNS
    participant DP as Service dataplane
    participant B as Ready backend Pod

    A->>DNS: DNS lookup api.demo.svc.cluster.local
    DNS-->>A: 回傳 Service ClusterIP
    A->>DP: 連線到 ClusterIP:80
    DP->>DP: 依 Service 與 EndpointSlice 選擇 endpoint
    DP->>B: 轉送到 PodIP:8080
    B-->>A: 回應

這裡最重要的分界是:CoreDNS 回答名稱查詢後,通常不會留在 HTTP/TCP 封包的路徑上。 後續封包由 Node 或 CNI 的 Service dataplane 處理。

Service dataplane 不一定是同一種實作

許多叢集使用 Node 上的 kube-proxy,也有叢集使用 CNI 提供的 kube-proxy replacement。常見選項如下:

dataplane 大致作法 不應該做的假設
kube-proxy 的 iptables mode 建立 netfilter 規則,把 Service VIP/port 導向某個 endpoint,常見會做 DNAT 不要把每次請求都想成固定的 Round Robin
kube-proxy 的 IPVS mode 使用 Linux IPVS 與相關規則處理虛擬服務與後端 是否可用、是否被棄用或預設啟用,取決於 Kubernetes 版本與叢集設定
kube-proxy 的 nftables mode 使用 nftables 規則實作 Service 虛擬 IP 不要假設所有 Linux Node 都還是 iptables
CNI eBPF dataplane 由 CNI 在 kernel 層執行 Service translation、負載分配與 policy 不要假設一定存在 kube-proxy 的規則或程序

Service 的後端選擇還可能受到 sessionAffinity、traffic policy、拓撲偏好與連線本身的 keep-alive 影響。Kubernetes 沒有保證你用幾次 curl 就會看見整齊的 Round Robin 序列;如果要了解實際分布,應檢查 Service 設定、EndpointSlice、dataplane 與應用程式連線行為。

ClusterIP、Pod IP 與 Headless Service 的比較

連線目標 是否經過一般 Service VIP client 看到什麼 典型用途
Pod IP 否 單一 Pod IP 除錯、低階網路測試
一般 Service 是 穩定的 ClusterIP 對一組可替換後端提供穩定入口
Headless Service(clusterIP: None) 沒有 VIP proxy DNS 可能回傳多個 endpoint Pod IP StatefulSet、由 client 自己選擇後端

Headless Service 仍然會使用叢集 DNS 來提供服務發現,但它和一般 Service 的 VIP 與負載處理路徑不同。完整的 DNS record、SRV、StatefulSet hostname 與 Service Discovery 會在後續 Part 7 深入。

CoreDNS:只負責查名字,不代理你的 HTTP

CoreDNS 是 Kubernetes 叢集常見的 DNS server。Pod 啟動時,kubelet 會依 Pod 的 DNS policy 寫入 /etc/resolv.conf;一般 ClusterFirst 設定會讓叢集網域先送到叢集 DNS,再視設定轉送外部網域查詢。

在一個叫 net-lab 的 namespace 裡,Service echo 的完整名稱通常是:

1
echo.net-lab.svc.cluster.local

其中 cluster.local 是常見的 cluster domain,但不是永遠固定,實際值以叢集設定為準。一般 Service 的 DNS lookup 通常回傳 ClusterIP;Headless Service 則可能直接回傳多個後端 Pod IP。

可以在 Pod 裡觀察基本設定:

1
2
3
4
5
6
cat /etc/resolv.conf

# 常見但依叢集而異的內容
# nameserver <cluster-dns-service-ip>
# search net-lab.svc.cluster.local svc.cluster.local cluster.local
# options ndots:5

接著應用程式可能做以下事情:

1
2
3
4
1. 對 CoreDNS 的 UDP/TCP 53 port 做 DNS query
2. 收到 echo.net-lab.svc.cluster.local = ClusterIP
3. 對 ClusterIP:80 建立 TCP 連線
4. 由 Service dataplane 把連線送往 ready endpoint

第 1 步會經過 CoreDNS;第 3、4 步不是 CoreDNS 在代理。這個界線在排錯時很有用:DNS 能解析,只代表名字查得到,不代表後續 port、路由、policy 或後端程式一定可達。

Service Discovery 的 DNS 搜尋路徑、ndots、Headless Service、SRV record、DNS cache 與跨 namespace 查詢,將在後續 Part 7 整理成獨立篇章;本篇只保留理解封包路徑所需的最小背景。

NetworkPolicy:誰可以進來,誰可以出去?

NetworkPolicy 是對 Pod 流量的 L4 層級規則,主要描述 TCP、UDP(以及視 CNI 支援程度而定的 SCTP)來源、目的地與 port。它不是 Kubernetes API Server 自己執行的防火牆;只有選用的網路插件支援 policy enforcement 時,建立 policy 才會真的影響封包。

Ingress 與 egress 的方向

方向是以「被 policy 選中的 Pod」為中心:

  • Ingress:誰可以連進被選中的 Pod,以及可以連哪些 port。
  • Egress:被選中的 Pod 可以連到誰,以及可以連哪些 port。
  • podSelector:選擇 Pod;在 NetworkPolicy 本身的 namespace 中,沒有另外指定 namespace 時,代表同一個 namespace 的 Pod。
  • namespaceSelector:選擇 namespace;若只寫它,通常表示該 namespace 內符合條件的所有 Pod。
  • 同一個 from 或 to 項目中同時寫 namespaceSelector 與 podSelector:兩者是 AND,表示指定 namespace 裡再指定 label 的 Pod。
  • from/to 陣列中分成兩個項目:兩個項目是 OR。

多個 NetworkPolicy 選中同一批 Pod 時,允許規則會合併;不是後建立的 policy 自動覆蓋先前的 policy。實務上常用「先 default deny,再逐條 allow」的方式建立白名單。

先拒絕 API 的所有入站,再開放指定來源

下面的兩個 policy 都放在 net-lab namespace,目標 Pod 是 label app: api。第一個 policy 選中 net-lab 裡所有 Pod 並拒絕 ingress;第二個 policy 再放行 app: client,以及 gateway-system namespace 中 label 為 app: gateway 的 Pod。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: net-lab
spec:
podSelector: {}
policyTypes:
- Ingress

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-client-and-gateway-to-api
namespace: net-lab
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
# podSelector 只匹配同一個 net-lab namespace
- podSelector:
matchLabels:
app: client
# 這一個項目內的 namespaceSelector + podSelector 是 AND
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: gateway-system
podSelector:
matchLabels:
app: gateway
ports:
- protocol: TCP
port: 8080

這個例子沒有聲稱「所有 gateway namespace 的 Pod 都能連線」:只有同時符合 namespace label 與 Pod label 的來源才符合第二個項目。也沒有把 web、api 混在 selector 和註解裡,讀者可以直接對照 policy 的目標與來源。

default deny egress 最容易漏掉 DNS

如果對 net-lab 的所有 Pod 套用 default deny egress,Pod 連 CoreDNS 的 UDP/TCP 53 也會被拒絕。結果常見為:直接使用 IP 的測試還可能成功,但使用 Service 名稱的應用程式看起來像「Service 壞了」。

下面是允許 net-lab Pod 存取 CoreDNS 的示例。k8s-app: kube-dns 是許多叢集使用的 label,但託管服務或自訂部署可能不同,套用前應先用 kubectl get pods -n kube-system --show-labels 確認:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: net-lab
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53

只允許 DNS 不代表應用程式流量也被允許;在同一套 default deny egress 下,仍需要另外允許 client 到 API 的 egress port,以及 API Pod 的 ingress。DNS 可能先用 UDP,再因回應太大或其他狀況使用 TCP,因此實務上通常同時放行 UDP 與 TCP 53。

NetworkPolicy 對 ICMP、ARP 等非 TCP/UDP/SCTP 流量的行為可能因 CNI 而異。不要用 ping 一個失敗就直接判定 TCP 一定不通,應使用和實際服務相同的協定與 port 測試。

可執行的 kind 多節點實驗:把封包路徑看出來

下面的實驗使用 kind 建立一個 control-plane 加兩個 worker 的本地叢集,並使用 nicolaka/netshoot 作為除錯 Pod。實驗目標不是模擬所有雲端 CNI,而是讓你實際看到:

  • Pod netns 內的 /etc/resolv.conf、介面與路由
  • 同一 Pod 內的 localhost
  • 跨 Node 的 Pod IP 直連
  • Service DNS 查詢、ClusterIP 與 EndpointSlice 的差異

1. 建立多節點 kind 叢集

需要先安裝 Docker(或相容的 container runtime)、kind 與 kubectl。kind 會使用自己的預設網路插件;本實驗觀察 Pod 網路與 Service 即可,不把它當成 NetworkPolicy enforcement 的驗證環境。

1
2
3
4
5
6
7
8
kind create cluster --name k8s-net-lab --config=- <<'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
EOF

這份設定會產生:

1
2
3
k8s-net-lab-control-plane
k8s-net-lab-worker
k8s-net-lab-worker2

為了刻意讓 client 與 server 分散到兩個 worker,替 Node 加上實驗用 label:

1
2
3
kubectl label node k8s-net-lab-worker lab-role=client --overwrite
kubectl label node k8s-net-lab-worker2 lab-role=server --overwrite
kubectl get nodes --show-labels

2. 部署 server、Service、netshoot 與共享網路的 Pod

echo Deployment 被固定到 server Node;netshoot 被固定到另一個 client Node。因此從 netshoot 連 echo Pod IP 時,可以觀察跨節點路徑。實際 Pod IP、Service IP 與網路介面名稱會由叢集動態分配,不要照抄下方的數字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Namespace
metadata:
name: net-lab

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo
namespace: net-lab
spec:
replicas: 1
selector:
matchLabels:
app: echo
template:
metadata:
labels:
app: echo
spec:
nodeSelector:
lab-role: server
containers:
- name: web
image: nginx:alpine
ports:
- name: http
containerPort: 80

---
apiVersion: v1
kind: Service
metadata:
name: echo
namespace: net-lab
spec:
selector:
app: echo
ports:
- name: http
port: 80
targetPort: 80

---
apiVersion: v1
kind: Pod
metadata:
name: netshoot
namespace: net-lab
labels:
app: client
spec:
nodeSelector:
lab-role: client
containers:
- name: netshoot
image: nicolaka/netshoot:latest
command: ["/bin/sh", "-c"]
args:
- sleep 3600

---
apiVersion: v1
kind: Pod
metadata:
name: shared-net
namespace: net-lab
spec:
containers:
- name: web
image: nginx:alpine
ports:
- containerPort: 80
- name: debug
image: nicolaka/netshoot:latest
command: ["/bin/sh", "-c"]
args:
- sleep 3600
EOF

等待資源就緒並確認 placement:

1
2
3
4
5
kubectl wait --for=condition=available deployment/echo -n net-lab --timeout=120s
kubectl get pods -n net-lab -o wide
kubectl get svc -n net-lab echo -o wide
kubectl get endpointslice -n net-lab \
-l kubernetes.io/service-name=echo -o wide

預期可以看到:

  • echo Pod 在 k8s-net-lab-worker2。
  • netshoot Pod 在 k8s-net-lab-worker。
  • echo Service 有一個 ClusterIP。
  • EndpointSlice 的 endpoint address 是 echo Pod 的 Pod IP,且 ready condition 為 true。

如果 Pod 是 Pending,先執行 kubectl describe pod -n net-lab <pod-name>;最常見的原因是 Node label 拼錯或對應的 worker 不存在。

3. 從 netshoot 觀察自己的 network namespace

1
2
3
kubectl exec -n net-lab netshoot -- cat /etc/resolv.conf
kubectl exec -n net-lab netshoot -- ip addr
kubectl exec -n net-lab netshoot -- ip route

把三個輸出連起來看:

  • eth0 上的位址是這個 Pod 的 IP。
  • lo 是 Pod 自己的 loopback,127.0.0.1 不會指向其他 Pod。
  • default route 的 gateway 與 Pod CIDR route 由 CNI 設定,實際數值依 kind 與 CNI 而異。
  • /etc/resolv.conf 的 nameserver 是叢集 DNS 入口;它不是 echo Service 的 ClusterIP。

4. 證明同一 Pod 內的 localhost

shared-net 裡的 web container 監聽 port 80,debug container 使用同一個 Pod network namespace。因此 debug 可以透過 127.0.0.1:80 呼叫 web:

1
2
3
4
kubectl exec -n net-lab shared-net -c debug -- \
curl -sS -I http://127.0.0.1:80
kubectl exec -n net-lab shared-net -c debug -- ip addr
kubectl get pod -n net-lab shared-net -o wide

這個 localhost 指向 shared-net 這個 Pod 的 network namespace;它不是 netshoot Pod,也不是所在 Node 的其他 Pod。

5. 分別測試 Pod IP 與 Service

先從 API 取得動態位址,再從 netshoot Pod 發出請求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POD_IP=$(kubectl get pod -n net-lab -l app=echo \
-o jsonpath='{.items[0].status.podIP}')
SVC_IP=$(kubectl get svc -n net-lab echo \
-o jsonpath='{.spec.clusterIP}')

echo "echo Pod IP: ${POD_IP}"
echo "echo Service ClusterIP: ${SVC_IP}"

# 直連 Pod IP:不經過這個 Service 的 ClusterIP 選擇
kubectl exec -n net-lab netshoot -- \
curl --connect-timeout 3 -sS -I "http://${POD_IP}:80/"

# 連 Service VIP:由 Service dataplane 導向 EndpointSlice 中的 ready endpoint
kubectl exec -n net-lab netshoot -- \
curl --connect-timeout 3 -sS -I "http://${SVC_IP}:80/"

# 連 Service 名稱:先經 CoreDNS,再連到同一個 Service VIP
kubectl exec -n net-lab netshoot -- \
dig +short echo.net-lab.svc.cluster.local
kubectl exec -n net-lab netshoot -- \
curl --connect-timeout 3 -sS -I http://echo.net-lab.svc.cluster.local/

對這三個測試應該這樣解讀:

  1. Pod IP 測試成功,表示目前 Pod IP、port、跨 Node 路徑與 nginx 至少可用。
  2. ClusterIP 測試成功,額外表示 Service selector、EndpointSlice 與 Service dataplane 能把 VIP 導到 endpoint。
  3. DNS 名稱測試成功,額外表示 Pod 可以連到 CoreDNS 並取得正確的 Service record;CoreDNS 回答後不會代理後面的 HTTP response。

若要觀察多個 endpoint 的 Service 選擇,可以把 echo Deployment 擴成多個 replicas,再查看 EndpointSlice;不要用少量短連線結果宣稱固定的 Round Robin:

1
2
3
4
kubectl scale deployment/echo -n net-lab --replicas=2
kubectl get pods -n net-lab -l app=echo -o wide
kubectl get endpointslice -n net-lab \
-l kubernetes.io/service-name=echo -o yaml

6. 清理實驗環境

確認不再需要實驗叢集後:

1
kind delete cluster --name k8s-net-lab

用分層方式排查網路問題

不要一開始就把所有問題都叫做「Kubernetes 網路壞了」。先問哪一條路徑失敗,再逐層縮小範圍:

現象 優先懷疑的層 建議檢查
同 Pod 的 localhost 成功,但 Pod IP 失敗 程式 bind address、Pod port、policy 程式是否只監聽 127.0.0.1、ss -lntp、NetworkPolicy
Pod IP 成功,但 Service VIP 失敗 selector、EndpointSlice、Service port、dataplane kubectl get svc、kubectl get endpointslice、targetPort、kube-proxy/CNI
Service IP 成功,但 Service DNS 失敗 /etc/resolv.conf、CoreDNS、DNS egress dig、CoreDNS Pod/Service、UDP/TCP 53 policy
同 Node 成功,跨 Node 失敗 CNI、Node route、overlay/VPC、MTU、Node firewall kubectl get pod -o wide、ip route、CNI logs、封裝與 MTU
DNS 能解析,但 HTTP timeout DNS 之外的資料面 Endpoint readiness、port、policy、return route、應用程式 logs
Service 存在但 EndpointSlice 沒 endpoint 控制面條件尚未滿足 selector label、Pod Ready condition、namespace、port

一個實用的排查順序是:

1
2
3
4
5
6
1. 先在來源 Pod 看 /etc/resolv.conf、ip addr、ip route
2. 用 Pod IP 測試,確認基本 Pod-to-Pod 路徑
3. 查看 Service 與 EndpointSlice,確認後端清單
4. 用 ClusterIP 測試,隔離 Service dataplane 問題
5. 最後用 DNS 名稱測試,確認 CoreDNS 與 DNS policy
6. 若只在跨 Node 失敗,再比較 Node、CNI、MTU 與回程路由

這個順序的價值在於把「名稱解析」、「VIP 選擇」、「Pod 路由」和「應用程式監聽」分開測。每一層都成功,才代表整條路徑成功。

本篇刻意不深入的主題

為了把 Pod-to-Pod 與 Service VIP 講完整,本篇刻意縮短以下內容:

  • L4/L7 Load Balancing:TCP/UDP 與 HTTP/gRPC 在 NodePort、LoadBalancer、Ingress、Gateway 的差異。
  • 外部入口:雲端外部 LB 如何把流量送到 Node,以及 externalTrafficPolicy、source IP、TLS termination。
  • 完整 Service Discovery:CoreDNS plugin、搜尋網域、ndots、SRV、Headless Service、StatefulSet hostname 與 DNS cache。
  • 生產級 NetworkPolicy:不同 CNI 的 enforcement、觀測工具、policy rollout 與跨 namespace 的治理。

這些內容會分別放到後續 Part 7(Service Discovery)與 Part 8(L4/L7 與外部流量),避免在一篇文章裡把每個名詞都講半段。

本篇重點回顧

  1. 同一 Pod 的 containers 共享 network namespace,所以可以透過 localhost 溝通,但也共享 IP 與 port 空間。
  2. Pod-to-Pod 直連使用 Pod IP;同節點通常經過 Node 內的介面與路由,跨節點則需要 CNI 與底層網路提供可達性。
  3. CNI 是網路插件介面,不代表所有叢集都使用相同的 bridge、overlay、VPC route 或 eBPF 實作。
  4. Service 的控制面透過 selector 維護 EndpointSlice;資料面再由 kube-proxy 或 CNI dataplane 將 ClusterIP 導向 ready endpoint。
  5. CoreDNS 處理 DNS lookup,不會代理後續的 HTTP/TCP 流量。
  6. NetworkPolicy 以 ingress/egress 與 Pod/namespace selector 表達限制;default deny egress 時,別忘了允許 DNS 的 UDP/TCP 53。
  7. 看到 DNS 成功不代表服務可達;Pod IP、ClusterIP 與 Service DNS 應該分層測試。

系列文章導覽

參考資源