云原生環(huán)境網(wǎng)絡(luò)方案2 --- Service和Service Mesh
在之前的文章中,我們描述了容器環(huán)境的底層網(wǎng)絡(luò)實(shí)現(xiàn)原理以及一些常見的k8s網(wǎng)絡(luò)插件。這些組件利用Linux系統(tǒng)的內(nèi)核網(wǎng)絡(luò)協(xié)議棧完成容器與容器之間網(wǎng)絡(luò)互連的工作。到了這一步,我們僅僅解決了容器與容器之間互連,在微服務(wù)架構(gòu)中,網(wǎng)絡(luò)層面的互連僅僅是基礎(chǔ),我們還需要從網(wǎng)絡(luò)層面解決:服務(wù)發(fā)現(xiàn),負(fù)載均衡,訪問控制,服務(wù)監(jiān)控,端到端加密等問題。
本文的目的在于描述基于Service的k8s網(wǎng)絡(luò)的原理,首先,我們需要了解以下概念:
- Pod & Deployment & EndPoints
- Service
- ClusterIP
- Headless
- NodePort
- LoadBalancer
- ExternalName
- Ingress
- ServiceMesh
Pod
Pod是K8S的基本調(diào)度單位,我們可以理解其為一組共享命名空間的容器,這一組容器共享PID,IPC,網(wǎng)絡(luò),存儲(chǔ),UTC命名空間,相互之間可以通過localhost訪問,訪問相同的文件等。每個(gè)Pod中的容器會(huì)共用一個(gè)IP地址,該IP地址由K8S底層的網(wǎng)絡(luò)方案決定如何分配。
一般來說,Pod不會(huì)被單獨(dú)使用,一般會(huì)通過Deployment對(duì)象進(jìn)行編排,以實(shí)現(xiàn):
- 定制Pod的版本,副本數(shù)
- 通過k8s狀態(tài)控制器恢復(fù)失敗的Pod
- 通過控制器完成指定的策略控制,如滾動(dòng)更新,重新生成,回滾等
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
上面是官網(wǎng)上的一個(gè)deployment的例子,其中可以看到,其設(shè)置了一個(gè)名為my-nginx的deployment,副本數(shù)為2,其模板是一個(gè)名為my-nginx的pod。

這里其實(shí)隱含了一個(gè)EndPoint對(duì)象,在k8s中我們一般不會(huì)直接與EndPoint對(duì)象打交道。EndPoint代表的是一組可供訪問的Pod對(duì)象,在創(chuàng)建deployment的過程中,后臺(tái)會(huì)自動(dòng)創(chuàng)建EndPoint對(duì)象。kube-proxy會(huì)監(jiān)控Service和Pod的變化,創(chuàng)建相關(guān)的iptables規(guī)則從網(wǎng)絡(luò)層對(duì)數(shù)據(jù)包以路由的方式做負(fù)載均衡。
Service
Pod是可變且易變,如果像傳統(tǒng)的開發(fā)中使用IP地址來標(biāo)識(shí)是不靠譜的,需要一個(gè)穩(wěn)定的訪問地址來訪問Pod的實(shí)例,在K8S中使用Service對(duì)象來實(shí)現(xiàn)這一點(diǎn)。服務(wù)與服務(wù)之間的訪問,會(huì)通過服務(wù)名進(jìn)行。通過配置Service對(duì)象,K8S會(huì)在內(nèi)部DNS系統(tǒng)(默認(rèn)coreDNS)中添加相關(guān)的PTR與反向PTR。在集群內(nèi)部任何地方,可以通過服務(wù)名來訪問相關(guān)的服務(wù)。
apiVersion: v1
kind: Service
metadata:
name: my-nginx
labels:
run: my-nginx
spec:
ports:
- port: 80
protocol: TCP
selector:
run: my-nginx
以上的yaml文件創(chuàng)建了一個(gè)名為my-nginx的service,關(guān)聯(lián)到之前創(chuàng)建的名為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>
以上例子創(chuàng)建了一個(gè)ClusterIP類型的Service,這是Service的默認(rèn)類型,ClusterIP是一個(gè)虛擬IP,kube-proxy在系統(tǒng)層面以iptables 路由規(guī)則或者LVS的形式,將訪問發(fā)送給后端的pod。在K8S環(huán)境中,也是可以顯示指定不生產(chǎn)ClusterIP,這種情況被稱為Headless Service。Service共有有5種類型:
- Headless
- ClusterIP
- NodePort
- LoadBalance
- ExternalService
ClusterIP的原理上面已經(jīng)大致提過。Headless Service就是上面提到的不設(shè)置ClusterIP的情況。在配置ClusterIP的情況下,CoreDNS中的記錄的Service名指向的是ClusterIP,而在不設(shè)置ClusterIP的情況下,CoreDNS中的Service名會(huì)直接指向多個(gè)后臺(tái)Pod的地址。
其中,NodePort是在將某Service暴露到集群外部的情況下使用。在設(shè)置Service類型為NodePort的情況下,會(huì)在每個(gè)Node上設(shè)置NAT規(guī)則暴露服務(wù)。如下圖所示:

集群的80端口被映射為ServiceA,那么集群的兩個(gè)對(duì)外IP:222.111.98.2和222.111.98.3的80端口都可以開啟,可以通過這兩個(gè)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
同時(shí),節(jié)點(diǎn)也有內(nèi)部節(jié)點(diǎn),內(nèi)部的Pod可以從內(nèi)部節(jié)點(diǎn)的80端口來訪問ServiceA。
LoadBalancer指的是外部的Loadbalancer,即云平臺(tái)提供者的集群外部負(fù)載均衡服務(wù)。除了NodePort所做的事情之外,LoadBalancer對(duì)象會(huì)將集群所有對(duì)外接口的IP地址提供給外部Loadbalancer服務(wù)。

還有一個(gè)是ExternalName,主要用來讓內(nèi)部POD訪問外部服務(wù)。
apiVersion: v1
kind: Service
metadata:
name: External-ElasticSearch
namespace: prod
spec:
type: ExternalName
externalName: my.elasticsearch.org:9200
上面是官方文檔中的一個(gè)例子,該例子里面,將一個(gè)外部的服務(wù) my.database.example.com映射為一個(gè)內(nèi)部的服務(wù)名my-service,內(nèi)部POD可以用my-service來訪問該外部服務(wù)。

通過External-Service,我們可以把一些有狀態(tài)的資源如各類數(shù)據(jù)庫進(jìn)行外置,集群內(nèi)部的各Pod可以通過其進(jìn)行訪問。
Ingress
以上NodePort以及LoadBalancer實(shí)現(xiàn)的是從外部對(duì)機(jī)器內(nèi)部進(jìn)行L4網(wǎng)絡(luò)訪問。在公有云環(huán)境中,LoadBalancer需要配合云服務(wù)提供商的負(fù)載均衡器使用,每個(gè)暴露出去的服務(wù)需要搭配一個(gè)負(fù)載均衡器,代價(jià)比較昂貴。那有沒有比較便宜一些的解決方案呢?答案是Ingress對(duì)象。在集群內(nèi)部搭建Ingress服務(wù),提供反向代理能力,實(shí)現(xiàn)負(fù)載均衡能力,一方面對(duì)基于http的流量可以進(jìn)行更細(xì)粒度的控制,此外,不需要額外的LoadBalancer設(shè)備的費(fèi)用。Ingress對(duì)象需要配合Ingress controller來進(jìn)行使用。
從使用方面來講,Ingress對(duì)象是集群級(jí)別的metadata,其定義了路由規(guī)則,證書等Ingress Controller所需的參數(shù)。Ingress Controller是實(shí)際執(zhí)行這些參數(shù)的實(shí)例。從面向?qū)ο蟮慕嵌葋碚f,Ingress是接口,Ingress Controller是其對(duì)應(yīng)的實(shí)現(xiàn)。接口只有一個(gè),實(shí)現(xiàn)可以有很多。實(shí)際上來講,Ingress Controller的實(shí)現(xiàn)有很多,比如Nginx Ingress,Traefik,envoy等。集群內(nèi)可能存在多個(gè)Ingress對(duì)象和Ingress Controller,Ingress對(duì)象可以指定使用某個(gè)Ingress Controller。
從功能上來看,Ingress一般會(huì)具有以下能力:對(duì)外提供訪問入口,TLS卸載,http路由,負(fù)載均衡,身份認(rèn)證,限流等,這些基本上都是針對(duì)http協(xié)議提供的。這些能力會(huì)通過Ingress Controller提供。
Ingress Controller需要部署為Service或者DaemonSet,一般來說有三種部署方式:
- 以Deployment方式部署為LoadBalancer類型的Service,一般在公有云場(chǎng)景下使用,相應(yīng)云服務(wù)商會(huì)提供外置的LoadBalancer將流量分發(fā)到Ingress Controller的地址上。
- 以Deployment方式部署為NodePort類型的Service,這種情況下會(huì)在所有節(jié)點(diǎn)上打開相應(yīng)端口,這種情況,外部訪問會(huì)比較麻煩,一般來說需要外置一個(gè)Ngnix或者其他類型的負(fù)載均衡器來分發(fā)請(qǐng)求,這樣其實(shí)就變得和情況1類似,但是可能會(huì)更麻煩。
- 以DaemonSet的形式部署在指定的幾個(gè)對(duì)外節(jié)點(diǎn)上。在部署過程中,可以給邊緣節(jié)點(diǎn)打上相應(yīng)的tag,在配置是使用NodeSelector來指定邊緣節(jié)點(diǎn),在每個(gè)邊緣節(jié)點(diǎn)上部署Ingress Controller。外界可以通過這些邊緣節(jié)點(diǎn)上的Ingress Controller來訪問集群內(nèi)部的服務(wù)。
總體而言,Ingress的作用是對(duì)外部訪問進(jìn)行細(xì)粒度的網(wǎng)絡(luò)控制。目前市面上有幾種產(chǎn)品形態(tài),如API Gateway以及ServiceMesh Gateway都可以在這個(gè)生態(tài)位工作。
ServiceMesh
在原生k8s環(huán)境中,kube-proxy組件會(huì)通過watch-list接口觀察Service和EndPoints的變化,動(dòng)態(tài)調(diào)整iptables/LVS規(guī)則,實(shí)現(xiàn)端到端的請(qǐng)求路由。在ServiceMesh環(huán)境中,會(huì)在每個(gè)POD中注入一個(gè)Sidecar容器來實(shí)現(xiàn)流量代理能力。在istio中,往往會(huì)使用強(qiáng)大的envoy來完成這件事情。所有的這一切都是通過修改底層基礎(chǔ)設(shè)施來完成的,上層應(yīng)用不感知。

上圖是istio mesh的架構(gòu)圖,在基于istio的service mesh中,底層的Service和EndPoints還是基于k8s的原生機(jī)制,但是編排能力由istio進(jìn)行了接管,原生的基于kube-proxy的網(wǎng)絡(luò)層編排,變成了有istio控制面進(jìn)行編排,各個(gè)Pod中的SideCar代理同步配置。POD與POD之間的流量由SideCar代理進(jìn)行了劫持,變成了代理與代理之間端到端的流量通訊。
基于這樣的機(jī)制,一方面出入的流量都會(huì)經(jīng)過envoy代理,對(duì)比僅僅由內(nèi)核iptables和LVS進(jìn)行轉(zhuǎn)發(fā),envoy代理有更多機(jī)會(huì)和更強(qiáng)大的能力對(duì)流量進(jìn)行控制。代理與代理之間的通信可以進(jìn)行加密,解決了以往站內(nèi)通信都是明文的問題。
同樣,我們可以看到,由于出入都需要代理,天生比僅通過內(nèi)核轉(zhuǎn)發(fā)多出了兩個(gè)用戶態(tài)的環(huán)節(jié),在性能上必然有一定的衰減。

借用上面一張圖,我們可以看到,進(jìn)入POD的請(qǐng)求,首先是在內(nèi)核態(tài)的PREROUTING鏈上進(jìn)行判斷,符合條件的流量會(huì)被導(dǎo)入到ISTIO_INBOUND鏈,然后被重定向到用戶態(tài)的Envoy SideCar進(jìn)行處理,處理完成之后,被重新注回到內(nèi)核,再被重新定向到用戶態(tài)的真實(shí)的APP中進(jìn)行業(yè)務(wù)上的處理。其返回?cái)?shù)據(jù),同樣先進(jìn)入內(nèi)核然后被劫持到用戶態(tài)SideCar中處理,最后又被重新發(fā)回內(nèi)核態(tài)發(fā)生出去。
在兩個(gè)被SideCar代理托管的服務(wù)之間通信,需要被SideCar代理處理4次,數(shù)據(jù)包有8次內(nèi)核態(tài)與用戶態(tài)之間的切換,其帶來的延時(shí)可想而知。安全從來都不是沒有代價(jià)的,需要從性能,部署復(fù)雜度等方面進(jìn)行取舍。
小結(jié)
本文介紹了k8s環(huán)境下,基于服務(wù)的網(wǎng)絡(luò)原理。并且比較了k8s原生的和基于服務(wù)網(wǎng)格的服務(wù)架構(gòu)。兩者對(duì)比如下,供參考:
| k8s原生 | Istio Mesh | |
|---|---|---|
| 服務(wù)間路由編排 | kube-proxy | lstio控制面 |
| 負(fù)載均衡 | iptables/lvs | envoy-sidecar |
| 端到端加密 | App自己實(shí)現(xiàn) | mTLS in envoy-sidecar |
| 證書管理 | 第三方 | istio citadel |
| 從外到內(nèi)訪問 | API-Gateway,Ingress,NodePort,LoadBalancer | Istio-gateway |
| 監(jiān)控及可視化 | 第三方工具及相關(guān)應(yīng)用打點(diǎn) | 通過Sidecar無縫進(jìn)行 |
ServiceMesh在給我們帶來很多便利的同時(shí),也帶來了性能上的降低,以及ServiceMesh本身的復(fù)雜性。在選型過程中,需要注意結(jié)合自身情況作出選擇。