EKS Load Balancing 最佳實務:Service、NLB、ALB 與 IP Targets

EKS Load Balancing 最佳實務:Service、NLB、ALB 與 IP Targets

在 Amazon EKS 上做 Load Balancing,看起來很簡單;但只要開始追一個 request 到底經過哪些節點、最後打到哪個 Pod,事情就會變複雜。

Kubernetes LoadBalancer Service 可以建立 AWS Load Balancer,kube-proxy 可能又在節點上重新轉送一次,Ingress controller 也可能再加一層 routing。每一層本身都合理,但疊在一起之後,就可能造成額外 hop、流量分配不平均,以及更難排查的網路問題。

這篇把我過去三篇 EKS Load Balancing 系列整理成單篇,也補上目前在 EKS 上比較建議的做法:

  • 使用 AWS Load Balancer Controller 或 EKS Auto Mode 來整合 AWS 原生 Load Balancer。
  • 當 Load Balancer 應該直接打到 Pod 時,優先使用 IP targets。
  • 只有在清楚需要其行為時,才使用 instance targets 或 NGINX Ingress。
  • 除非應用程式真的需要 session affinity,否則避免啟用 sticky behavior。
  • 讓 Pods 平均分散在 Availability Zones,讓 Load Balancer 有健康且均衡的 targets。

為什麼預設 Service 路徑會增加複雜度

Kubernetes Service 提供 Pod 前面的穩定網路入口。當 Service 對外暴露時,Kubernetes 還需要決定外部流量要如何抵達被 Service selector 選到的 Pods。這裡最重要的欄位是 spec.externalTrafficPolicy

Kubernetes 支援兩種設定:

  • Cluster:流量可以被轉送到叢集中任一節點上的 Pod。
  • Local:流量只會送到收到 request 的那台節點上的本機 Pod。

Local 不代表 Kubernetes 完全停止 Service 層級的 load balancing。如果同一台節點上有多個符合條件的 Pods,kube-proxy 仍然可以在這些本機 endpoints 之間分配流量。關鍵差異在於,流量不會再被轉送到其他節點上的 Pods。

externalTrafficPolicy overview

預設情況下,kube-proxy 會透過 iptables 或 IPVS 在節點上實作 Service load balancing。以 iptables mode 來說,節點上會產生類似下面這種依照機率分配的規則:

-A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.20000000019 -j KUBE-SEP-AAAAAA
-A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.25000000000 -j KUBE-SEP-BBBBBB
-A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-CCCCCC
-A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-DDDDDD
-A KUBE-SVC-XXXXX -j KUBE-SEP-EEEEEE

Kubernetes Service iptables overview

這代表 AWS Load Balancer 可能先把流量送到某個節點,接著 Kubernetes 又在該節點上把 request 重新轉送到某個 Pod。這個第二段 hop,常常就是排查問題時最容易被忽略的地方。

以下是很常見的 Service:

apiVersion: v1
kind: Service
metadata:
  name: nginx-svc
  labels:
    app: nginx
spec:
  type: LoadBalancer
  ports:
  - port: 80
    protocol: TCP
  selector:
    app: nginx

如果使用 instance targets,Load Balancer 註冊的是節點。流量可能走這兩種路徑:

client -> Load Balancer -> Node-1 (NodePort) -> Pod-1 on Node-1
client -> Load Balancer -> Node-1 (NodePort) -> Pod-3 on Node-2

Instance target traffic flow

第二種路徑可以運作,但它多了一段 node-to-node forwarding。如果 Node-1 在轉送流量到 Pod-3 的過程中發生網路問題或被移除,即使 Pod-3 所在節點仍然健康,已建立的連線也可能中斷。

externalTrafficPolicy: Local 可以避免跨節點轉送,但會換來另一個取捨:沒有本機 backend Pod 的節點,應該會在 Load Balancer health check 中失敗,因為該節點沒有本機 endpoint 可以服務 request。

externalTrafficPolicy Local health check behavior

這是預期行為。真正的問題是流量分配會高度依賴 Pod 被排程到哪裡。如果只有一台節點有本機 Pods,健康流量就會集中到那台節點。

為什麼流量會不平均

流量不平均通常發生在這種情境:AWS Load Balancer 對註冊的節點分配得很平均,但節點背後的 Pods 並沒有平均收到流量。

在一個測試中,我讓四個 backend Pods 回傳自己的 Pod IP,方便統計每次 request 是由哪個 Pod 回應:

$ kubectl get pod -o wide
NAME                                READY   STATUS    IP
nginx-deployment-594764c789-5s668   1/1     Running   192.168.42.171
nginx-deployment-594764c789-9k949   1/1     Running   192.168.39.194
nginx-deployment-594764c789-b292m   1/1     Running   192.168.29.24
nginx-deployment-594764c789-s226c   1/1     Running   192.168.15.158

Backend Pod response test

送出 79 次 HTTP requests 後,結果並不平均:

  • 192.168.42.171:12 次
  • 192.168.39.194:33 次
  • 192.168.29.24:23 次
  • 192.168.15.158:10 次

這不代表 Elastic Load Balancing 壞掉。它代表 Load Balancer 正在把流量分配到它註冊的 targets,而 targets 後面又有 Kubernetes Service networking 再做一次 routing。當某個節點收到 request 後,最後會選到哪個 Pod,是 Kubernetes Service networking 決定的,不是 AWS Load Balancer 決定的。

規模越大,這個問題越難處理。更多 Services 和 endpoints 代表更多節點層級的 forwarding state、更多 conntrack 壓力,以及更難在事故中釐清的封包路徑。

使用 IP Targets 直接把流量送到 Pods

在使用 Amazon VPC CNI plugin 的 Amazon EKS 中,跑在 EC2 nodes 上的 Pods 會取得 VPC 可路由的 secondary private IP。這讓 ALB 或 NLB target group 可以直接註冊 Pod IP。

這個選項適用於 Application Load Balancer 與 Network Load Balancer 的 target groups。Classic Load Balancer 不支援 IP targets。Instance targets 在某些 node-level 設計中仍然合理,但當應用程式 workload 是由 Kubernetes Pods 承載時,IP targets 通常更符合 EKS 的網路模型。

比較乾淨的路徑會變成:

client -> NLB or ALB -> Pod IP

IP target traffic flow

如果用 AWS Load Balancer Controller 管理 Network Load Balancer,可以使用像這樣的 Service:

apiVersion: v1
kind: Service
metadata:
  name: my-service
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "external"
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
  selector:
    app: my-app

如果是 Application Load Balancer,則可在 Ingress 上使用 IP target mode:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: game-2048
  name: ingress-2048
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
spec:
  ingressClassName: alb

使用 IP targets 時,Load Balancer 會直接把流量轉送到 Pod IP。在同樣類型的測試中,四個 NLB 後方的 Pods 收到的流量更平均:

  • 192.168.17.15:10 次
  • 192.168.27.143:12 次
  • 192.168.22.126:14 次
  • 192.168.14.48:13 次

這也是我在大多數 EKS workload 上,偏好使用 IP target mode 的主要原因。

IP Targets 仍可能看起來不平均的情況

IP target mode 移除了 Kubernetes Service 額外轉送那一層,但不代表所有情況下 request count 都會完全一致。以下狀況仍可能讓流量看起來不平均:

  • client cache 了過期的 Load Balancer DNS 結果;
  • 啟用了 sticky sessions 或 source-IP affinity;
  • targets 沒有平均分散在 Availability Zones;
  • workload 主要是長連線 TCP;
  • WebSocket 連線長時間保持開啟;
  • clients 重複使用少數幾條 persistent HTTP connections。

解法要看 workload。對短連線、stateless HTTP 流量來說,IP targets 加上均衡的 Pod placement 通常就能解決實務上的問題。對長連線服務來說,應該觀察 active connections 和 target utilization,而不只是 request count。

Amazon EKS 上的 Controller 選項

Kubernetes In-Tree Load Balancer Controller

早期在 AWS 上建立一個單純的 type: LoadBalancer Service,通常會由 Kubernetes in-tree AWS cloud provider code 處理。它很適合快速實驗,但功能跟 Kubernetes release cycle 綁在一起,也較難快速支援新的 AWS Load Balancer 功能。

對現代 EKS cluster 來說,如果需要 AWS Load Balancer 整合,優先考慮 AWS Load Balancer Controller 或 EKS Auto Mode。除非有明確相容性需求,否則舊的 in-tree 路徑應視為 legacy。

NGINX Ingress Controller

當你需要 NGINX 在 request path 中提供能力時,NGINX Ingress Controller 很有價值,例如進階 rewrite、controller-level 設定、自訂 routing,或是讓多個 Ingress objects 共用一層 NGINX。

在 AWS 上,常見做法是透過 LoadBalancer Service 暴露 NGINX,通常背後會建立 NLB:

apiVersion: v1
kind: Service
metadata:
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
    service.beta.kubernetes.io/aws-load-balancer-type: external
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: instance
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  externalTrafficPolicy: Local
  type: LoadBalancer

NGINX Ingress on EKS architecture

這是合理架構,但要清楚它做了什麼:AWS Load Balancer 先把流量送到 NGINX,NGINX 再把流量 route 到 Services 和 Pods。當你需要 NGINX 行為時,這層很有用;如果 ALB 或 NLB 已經能滿足 routing 需求,這層就不一定必要。

當你想要一個集中式 routing layer 服務多個 namespaces、需要 NGINX 特有的 request handling,或希望多個 Ingress objects 共用同一個 NLB-backed entry point 時,NGINX Ingress 仍然很適合。重點是要把這層 proxy 視為刻意設計,而不是無意識的預設選項。

AWS Load Balancer Controller

AWS Load Balancer Controller 會透過 AWS APIs,從 Kubernetes objects 管理 ALB、NLB、target groups、listeners、rules 等資源。它從過去的 ALB Ingress Controller 演進而來,目前同時支援 ALB 和 NLB 使用情境。

不同於 NGINX Ingress,AWS Load Balancer Controller 本身不是 data path 上的 gateway。它會根據 Kubernetes objects 去 reconcile AWS resources,之後由 Load Balancer 將流量送到已註冊的 targets。搭配 IP target mode 時,這些 targets 可以直接是 Pods。

當你想同時保留 Kubernetes manifests 的操作方式,又想使用 AWS 原生 Load Balancer 功能時,這通常是最好的預設選項:

  • ALB:適合 HTTP/HTTPS routing、host/path rules、TLS termination 與 Web application traffic。
  • NLB:適合 TCP/UDP、固定 IP 需求、高吞吐量,或需要保留 source IP 的情境。
  • IP target mode:適合讓 Load Balancer 直接打到 Pods。
  • TargetGroupBinding:適合把 Kubernetes workloads 接到既有 target groups。
  • IngressGroup:適合讓多個 Ingress resources 共用同一個 ALB。

EKS Auto Mode

EKS Auto Mode 可以自動為 LoadBalancer Service 建立與設定 NLB,降低安裝與維運 controller 的工作量。若你的 cluster 使用 Auto Mode,請先確認支援的 annotations 與行為,再直接套用舊版 AWS Load Balancer Controller 範例。

整體設計原則仍然一樣:明確決定流量應該打到 nodes 還是 Pod IPs。

實務建議

可行時,優先讓應用程式 Pods 使用 IP targets。在 EKS 搭配 VPC CNI 時,Pod IP 在 VPC 內可路由,因此 ALB 與 NLB 通常可以直接把流量送到 Pods,移除節點層級 Service forwarding 的額外 hop。

只有在你明確需要 node-level targeting 時,才使用 instance targets。這可能適合基礎設施元件、DaemonSet 類型流量,或無法直接使用 Pod targets 的情境。

當你需要 NGINX 出現在 data path 中時,才使用 NGINX Ingress。不要只是因為「Kubernetes 應該要有 Ingress controller」就加一層 NGINX。在 AWS 上,透過 AWS Load Balancer Controller 使用 ALB Ingress,常常就能在不多跑一層 reverse proxy 的情況下完成 HTTP routing。

除非應用程式需要,否則避免 sticky sessions。Stickiness 有其用途,但它會直接影響流量平均分配。對 ALB 和 NLB 來說,先檢查 target group attributes,再判斷是否是 Kubernetes 或 ELB 演算法問題。

讓 Pods 分散在 Availability Zones。可以使用 topology spread constraints、Pod anti-affinity 或排程規則,確保每個啟用的 Load Balancer zone 都有健康 backend capacity。

Health check 要刻意設計。externalTrafficPolicy: Local、instance targets 和 node-local endpoints 可能讓部分節點 health check 失敗,這不一定是故障,但必須符合你的可用性模型。

結論

EKS Load Balancing 最常見的誤解,是以為流量只經過一個 Load Balancer。實際上,許多 clusters 會先由 AWS 做一次 load balancing,接著又由 Kubernetes 在節點上做一次 routing。第二層 routing 有它的用途,但也會增加 hop、隱藏最後的 Pod selection,並讓流量分配看起來更難理解。

對大多數 AWS-native EKS workloads 來說,較乾淨的設計是使用 AWS Load Balancer Controller 或 EKS Auto Mode,搭配 IP targets、均衡的 Pod placement,以及避免不必要的 stickiness。只有在 NGINX Ingress 或 instance targets 的行為正是你需要的設計時,才把它們放進路徑中。

參考資料

Eason Cao
Eason Cao Eason is an engineer working at FANNG and living in Europe. He was accredited as AWS Professional Solution Architect, AWS Professional DevOps Engineer and CNCF Certified Kubernetes Administrator. He started his Kubernetes journey in 2017 and enjoys solving real-world business problems.
comments powered by Disqus