云原生環境網絡方案2 --- Service和Service Mesh
在之前的文章中,我們描述了容器環境的底層網絡實現原理以及一些常見的k8s網絡插件。這些組件利用Linux系統的內核網絡協議棧完成容器與容器之間網絡互連的工作。到了這一步,我們僅僅解決了容器與容器之間互連,在微服務架構中,網絡層面的互連僅僅是基礎,我們還需要從網絡層面解決:服務發現,負載均衡,訪問控制,服務監控,端到端加密等問題。
本文的目的在于描述基于Service的k8s網絡的原理,首先,我們需要了解以下概念:
- Pod & Deployment & EndPoints
- Service
- ClusterIP
- Headless
- NodePort
- LoadBalancer
- ExternalName
- Ingress
- ServiceMesh
Pod
Pod是K8S的基本調度單位,我們可以理解其為一組共享命名空間的容器,這一組容器共享PID,IPC,網絡,存儲,UTC命名空間,相互之間可以通過localhost訪問,訪問相同的文件等。每個Pod中的容器會共用一個IP地址,該IP地址由K8S底層的網絡方案決定如何分配。
一般來說,Pod不會被單獨使用,一般會通過Deployment對象進行編排,以實現:
- 定制Pod的版本,副本數
- 通過k8s狀態控制器恢復失敗的Pod
- 通過控制器完成指定的策略控制,如滾動更新,重新生成,回滾等
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
spec:
selector:
matchLabels:
run: my-nginx
replicas: 2
template:
metadata:
labels:
run: my-nginx
spec:
containers:
- name: my-nginx
image: nginx
ports:
- containerPort: 80
上面是官網上的一個deployment的例子,其中可以看到,其設置了一個名為my-nginx的deployment,副本數為2,其模板是一個名為my-nginx的pod。
這里其實隱含了一個EndPoint對象,在k8s中我們一般不會直接與EndPoint對象打交道。EndPoint代表的是一組可供訪問的Pod對象,在創建deployment的過程中,后臺會自動創建EndPoint對象。kube-proxy會監控Service和Pod的變化,創建相關的iptables規則從網絡層對數據包以路由的方式做負載均衡。
Service
Pod是可變且易變,如果像傳統的開發中使用IP地址來標識是不靠譜的,需要一個穩定的訪問地址來訪問Pod的實例,在K8S中使用Service對象來實現這一點。服務與服務之間的訪問,會通過服務名進行。通過配置Service對象,K8S會在內部DNS系統(默認coreDNS)中添加相關的PTR與反向PTR。在集群內部任何地方,可以通過服務名來訪問相關的服務。
apiVersion: v1
kind: Service
metadata:
name: my-nginx
labels:
run: my-nginx
spec:
ports:
- port: 80
protocol: TCP
selector:
run: my-nginx
以上的yaml文件創建了一個名為my-nginx的service,關聯到之前創建的名為my-nginx的pod組。
$ kubectl get svc my-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-nginx ClusterIP 10.0.162.149 <none> 80/TCP 21s
$ kubectl describe svc my-nginx
Name: my-nginx
Namespace: default
Labels: run=my-nginx
Annotations: <none>
Selector: run=my-nginx
Type: ClusterIP
IP: 10.0.162.149
Port: <unset> 80/TCP
Endpoints: 10.244.2.5:80,10.244.3.4:80
Session Affinity: None
Events: <none>
以上例子創建了一個ClusterIP類型的Service,這是Service的默認類型,ClusterIP是一個虛擬IP,kube-proxy在系統層面以iptables 路由規則或者LVS的形式,將訪問發送給后端的pod。在K8S環境中,也是可以顯示指定不生產ClusterIP,這種情況被稱為Headless Service。Service共有有5種類型:
- Headless
- ClusterIP
- NodePort
- LoadBalance
- ExternalService
ClusterIP的原理上面已經大致提過。Headless Service就是上面提到的不設置ClusterIP的情況。在配置ClusterIP的情況下,CoreDNS中的記錄的Service名指向的是ClusterIP,而在不設置ClusterIP的情況下,CoreDNS中的Service名會直接指向多個后臺Pod的地址。
其中,NodePort是在將某Service暴露到集群外部的情況下使用。在設置Service類型為NodePort的情況下,會在每個Node上設置NAT規則暴露服務。如下圖所示:
集群的80端口被映射為ServiceA,那么集群的兩個對外IP:222.111.98.2和222.111.98.3的80端口都可以開啟,可以通過這兩個IP:port在集群外訪問ServiceA。
apiVersion: v1
kind: Service
metadata:
name: ServiceA
spec:
ports:
- port: 3000
protocol: TCP
targetPort: 443
nodePort: 80
selector:
run: PodA
type: NodePort
同時,節點也有內部節點,內部的Pod可以從內部節點的80端口來訪問ServiceA。
LoadBalancer指的是外部的Loadbalancer,即云平臺提供者的集群外部負載均衡服務。除了NodePort所做的事情之外,LoadBalancer對象會將集群所有對外接口的IP地址提供給外部Loadbalancer服務。
還有一個是ExternalName,主要用來讓內部POD訪問外部服務。
apiVersion: v1
kind: Service
metadata:
name: External-ElasticSearch
namespace: prod
spec:
type: ExternalName
externalName: my.elasticsearch.org:9200
上面是官方文檔中的一個例子,該例子里面,將一個外部的服務 my.database.example.com映射為一個內部的服務名my-service,內部POD可以用my-service來訪問該外部服務。
通過External-Service,我們可以把一些有狀態的資源如各類數據庫進行外置,集群內部的各Pod可以通過其進行訪問。
Ingress
以上NodePort以及LoadBalancer實現的是從外部對機器內部進行L4網絡訪問。在公有云環境中,LoadBalancer需要配合云服務提供商的負載均衡器使用,每個暴露出去的服務需要搭配一個負載均衡器,代價比較昂貴。那有沒有比較便宜一些的解決方案呢?答案是Ingress對象。在集群內部搭建Ingress服務,提供反向代理能力,實現負載均衡能力,一方面對基于http的流量可以進行更細粒度的控制,此外,不需要額外的LoadBalancer設備的費用。Ingress對象需要配合Ingress controller來進行使用。
從使用方面來講,Ingress對象是集群級別的metadata,其定義了路由規則,證書等Ingress Controller所需的參數。Ingress Controller是實際執行這些參數的實例。從面向對象的角度來說,Ingress是接口,Ingress Controller是其對應的實現。接口只有一個,實現可以有很多。實際上來講,Ingress Controller的實現有很多,比如Nginx Ingress,Traefik,envoy等。集群內可能存在多個Ingress對象和Ingress Controller,Ingress對象可以指定使用某個Ingress Controller。
從功能上來看,Ingress一般會具有以下能力:對外提供訪問入口,TLS卸載,http路由,負載均衡,身份認證,限流等,這些基本上都是針對http協議提供的。這些能力會通過Ingress Controller提供。
Ingress Controller需要部署為Service或者DaemonSet,一般來說有三種部署方式:
- 以Deployment方式部署為LoadBalancer類型的Service,一般在公有云場景下使用,相應云服務商會提供外置的LoadBalancer將流量分發到Ingress Controller的地址上。
- 以Deployment方式部署為NodePort類型的Service,這種情況下會在所有節點上打開相應端口,這種情況,外部訪問會比較麻煩,一般來說需要外置一個Ngnix或者其他類型的負載均衡器來分發請求,這樣其實就變得和情況1類似,但是可能會更麻煩。
- 以DaemonSet的形式部署在指定的幾個對外節點上。在部署過程中,可以給邊緣節點打上相應的tag,在配置是使用NodeSelector來指定邊緣節點,在每個邊緣節點上部署Ingress Controller。外界可以通過這些邊緣節點上的Ingress Controller來訪問集群內部的服務。
總體而言,Ingress的作用是對外部訪問進行細粒度的網絡控制。目前市面上有幾種產品形態,如API Gateway以及ServiceMesh Gateway都可以在這個生態位工作。
ServiceMesh
在原生k8s環境中,kube-proxy組件會通過watch-list接口觀察Service和EndPoints的變化,動態調整iptables/LVS規則,實現端到端的請求路由。在ServiceMesh環境中,會在每個POD中注入一個Sidecar容器來實現流量代理能力。在istio中,往往會使用強大的envoy來完成這件事情。所有的這一切都是通過修改底層基礎設施來完成的,上層應用不感知。
上圖是istio mesh的架構圖,在基于istio的service mesh中,底層的Service和EndPoints還是基于k8s的原生機制,但是編排能力由istio進行了接管,原生的基于kube-proxy的網絡層編排,變成了有istio控制面進行編排,各個Pod中的SideCar代理同步配置。POD與POD之間的流量由SideCar代理進行了劫持,變成了代理與代理之間端到端的流量通訊。
基于這樣的機制,一方面出入的流量都會經過envoy代理,對比僅僅由內核iptables和LVS進行轉發,envoy代理有更多機會和更強大的能力對流量進行控制。代理與代理之間的通信可以進行加密,解決了以往站內通信都是明文的問題。
同樣,我們可以看到,由于出入都需要代理,天生比僅通過內核轉發多出了兩個用戶態的環節,在性能上必然有一定的衰減。
借用上面一張圖,我們可以看到,進入POD的請求,首先是在內核態的PREROUTING鏈上進行判斷,符合條件的流量會被導入到ISTIO_INBOUND鏈,然后被重定向到用戶態的Envoy SideCar進行處理,處理完成之后,被重新注回到內核,再被重新定向到用戶態的真實的APP中進行業務上的處理。其返回數據,同樣先進入內核然后被劫持到用戶態SideCar中處理,最后又被重新發回內核態發生出去。
在兩個被SideCar代理托管的服務之間通信,需要被SideCar代理處理4次,數據包有8次內核態與用戶態之間的切換,其帶來的延時可想而知。安全從來都不是沒有代價的,需要從性能,部署復雜度等方面進行取舍。
小結
本文介紹了k8s環境下,基于服務的網絡原理。并且比較了k8s原生的和基于服務網格的服務架構。兩者對比如下,供參考:
k8s原生 | Istio Mesh | |
---|---|---|
服務間路由編排 | kube-proxy | lstio控制面 |
負載均衡 | iptables/lvs | envoy-sidecar |
端到端加密 | App自己實現 | mTLS in envoy-sidecar |
證書管理 | 第三方 | istio citadel |
從外到內訪問 | API-Gateway,Ingress,NodePort,LoadBalancer | Istio-gateway |
監控及可視化 | 第三方工具及相關應用打點 | 通過Sidecar無縫進行 |
ServiceMesh在給我們帶來很多便利的同時,也帶來了性能上的降低,以及ServiceMesh本身的復雜性。在選型過程中,需要注意結合自身情況作出選擇。