1 准備工作
1.1 在k8s部署istio
Istio在k8s集群內的部署很簡單,非生產要求的部署,可以直接在https://github.com/istio/istio/releases 下載最新的發布包,壓縮包里有供簡單部署的yaml文件:
$ for i in install/kubernetes/helm/istio-init/files/crd*yaml; do kubectl apply -f $i; done
$ kubectl apply -f install/kubernetes/istio-demo.yaml
注意,上面兩條指令會把主要的組件都給裝上,鏡像下載比較費力,內存消耗也比較大。沒個十幾G內存的同學請慎重。
部署完成后會新增一個命名空間istio-system:
$ kubectl get pods -n istio-system
NAME READY STATUS RESTARTS AGE
grafana-f8467cc6-rbjlg 1/1 Running 0 1m
istio-citadel-78df5b548f-g5cpw 1/1 Running 0 1m
istio-cleanup-secrets-release-1.1-20190308-09-16-8s2mp 0/1 Completed 0 2m
istio-egressgateway-78569df5c4-zwtb5 1/1 Running 0 1m
istio-galley-74d5f764fc-q7nrk 1/1 Running 0 1m
istio-grafana-post-install-release-1.1-20190308-09-16-2p7m5 0/1 Completed 0 2m
istio-ingressgateway-7ddcfd665c-dmtqz 1/1 Running 0 1m
istio-pilot-f479bbf5c-qwr28 2/2 Running 0 1m
istio-policy-6fccc5c868-xhblv 2/2 Running 2 1m
istio-security-post-install-release-1.1-20190308-09-16-bmfs4 0/1 Completed 0 2m
istio-sidecar-injector-78499d85b8-x44m6 1/1 Running 0 1m
istio-telemetry-78b96c6cb6-ldm9q 2/2 Running 2 1m
istio-tracing-69b5f778b7-s2zvw 1/1 Running 0 1m
kiali-99f7467dc-6rvwp 1/1 Running 0 1m
prometheus-67cdb66cbb-9w2hm 1/1 Running 0 1m
1.2 istio自動注入
在介紹istio原理時有提到,istio會在每一個被管理的pod里注入一個sidecar容器envoy。那么istio是如何完成注入的呢,主要有2種方式:
- 手動注入:如下命令,在kubectl前先執行istioctl手動注入
$ istioctl kube-inject -f <your-app-spec>.yaml | kubectl apply -f -
- 自動注入:運用k8s的admission-control完成自動注入
• kube-apiserver配置文件的admission-control參數,增加MutatingAdmissionWebhook 以及 ValidatingAdmissionWebhook 兩項
• 給命名空間打上自動注入的標簽(下方我們允許istio在default空間自動注入,同時禁用了istio-system空間的自動注入):
$ kubectl label namespace default istio-injection=enabled
$ kubectl get namespace -L istio-injection
NAME STATUS AGE ISTIO-INJECTION
default Active 4d21h enabled
ingress-nginx Active 4d17h
istio-system Active 4d17h disabled
kube-node-lease Active 4d21h
kube-public Active 4d21h
kube-system Active 4d21h
后面的測試,我們以第2種方式自動注入envoy。
注:通過在deployment文件的annotations參數里加上sidecar.istio.io/inject: "false",可以覆蓋命名空間的標簽,禁止istio對本pod的自動注入。
1.3 應用部署要求
Istio原生與k8s能夠無縫對接, istio的存在與否對應用程序本身來說,也是透明的。所以應用的部署,主要涉及的是yaml文件的修改完善:
- Deployment文件的要求:
• 應帶有app和version標簽,這個主要是用來識別不同版本的pod。
• Deployment應明確列出端口列表,istio會忽略未列出的端口。
• 下方為data-product的示例,注釋部分為需要注意的地方:
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
# 建議pod名稱 = 應用名 + 版本號
name: data-product-v4.0
namespace: default
spec:
template:
metadata:
annotations:
sidecar.istio.io/inject: "true"
namespace: default
labels:
# 標簽1:應用名稱
app: data-product
# 標簽2:版本號
version: v4.0
spec:
containers:
- name: data-product
ports:
# 需要Istio管理的端口號列表
- containerPort: 50051
……
- Service文件的要求:
• 需要指定端口的協議類型,否則默認按TCP協議處理:
---
apiVersion: v1
kind: Service
metadata:
name: data-product
namespace: default
annotations:
prometheus.io/scrape: 'true'
prometheus.io/path: /metrics
prometheus.io/port: '8081'
labels:
kubernetes.io/cluster-service: "true"
kubernetes.io/name: "data-product"
spec:
ports:
- name: data-product
port: 50051
# 指定端口的協議類型為grpc
name: grpc
targetPort: 50051
selector:
app: data-product
2 負載均衡
$ kubectl get po -o wide
NAME READY STATUS
business-product-6b954db744-cgn8k 2/2 Running
business-product-6b954db744-dgm4x 2/2 Running
business-product-6b954db744-n5c8p 2/2 Running
business-product-v4.0-747ccffbb4-2p2z8 2/2 Running
business-product-v4.0-747ccffbb4-4bh6z 2/2 Running
business-product-v4.0-747ccffbb4-8kgqs 2/2 Running
business-product-v4.0-747ccffbb4-hz6lb 2/2 Running
business-product-v4.0-747ccffbb4-kgs6p 2/2 Running
business-product-v4.0-747ccffbb4-rmmwg 2/2 Running
business-product-v4.0-747ccffbb4-sspgr 2/2 Running
business-product-v4.0-747ccffbb4-sz7ll 2/2 Running
business-product-v4.0-747ccffbb4-vnd58 2/2 Running
data-product-5bc9f7bb9c-dfchd 2/2 Running
data-product-5bc9f7bb9c-tv8cj 2/2 Running
data-product-5bc9f7bb9c-xzz7s 2/2 Running
這里我們啟用了9個business-product的pod,和3個data-product的pod。上表有一個細節,每個pod里有2個容器,除了我們的應用程序容器外,還有一個就是enovy。
當pod被注入了enovy,並且是按1.3節的要求將port暴露給了istio,則Istio會自動接管pod的流量,實現負載均衡。
Business-product作為client與data-product server之間通過grpc通訊,這里我簡單修改了下business-product和data-product的邏輯,當收到請求時data-product會將自身和client的ip作為響應,而business-product則會在這個響應的基礎上再加上自己的ip。所以我們對business-product發起連續的請求,就能得到下方的一組ip對(其中每一行的第1個ip是client的ip,第2個是server檢測到的client ip,第3個是server的ip):
"ip": "172.30.27.8/24 - 127.0.0.1:43094 - 172.30.86.3/24",
"ip": "172.30.95.17/24 - 127.0.0.1:43120 - 172.30.86.3/24",
"ip": "172.30.27.9/24 - 127.0.0.1:43094 - 172.30.86.3/24",
"ip": "172.30.86.9/24 - 127.0.0.1:41662 - 172.30.27.3/24",
"ip": "172.30.95.15/24 - 127.0.0.1:43094 - 172.30.86.3/24",
"ip": "172.30.86.7/24 - 127.0.0.1:41712 - 172.30.27.3/24",
"ip": "172.30.27.11/24 - 127.0.0.1:43872 - 172.30.95.7/24",
"ip": "172.30.86.8/24 - 127.0.0.1:41712 - 172.30.27.3/24",
"ip": "172.30.95.14/24 - 127.0.0.1:43944 - 172.30.95.7/24",
"ip": "172.30.27.8/24 - 127.0.0.1:43944 - 172.30.95.7/24",
"ip": "172.30.95.17/24 - 127.0.0.1:43872 - 172.30.95.7/24",
"ip": "172.30.27.9/24 - 127.0.0.1:43944 - 172.30.95.7/24",
"ip": "172.30.86.9/24 - 127.0.0.1:43094 - 172.30.86.3/24",
"ip": "172.30.95.15/24 - 127.0.0.1:43872 - 172.30.95.7/24",
"ip": "172.30.86.7/24 - 127.0.0.1:43120 - 172.30.86.3/24",
"ip": "172.30.27.11/24 - 127.0.0.1:41662 - 172.30.27.3/24",
"ip": "172.30.86.8/24 - 127.0.0.1:43094 - 172.30.86.3/24",
"ip": "172.30.95.14/24 - 127.0.0.1:41712 - 172.30.27.3/24",
"ip": "172.30.27.8/24 - 127.0.0.1:41712 - 172.30.27.3/24",
"ip": "172.30.95.17/24 - 127.0.0.1:41712 - 172.30.27.3/24",
"ip": "172.30.27.9/24 - 127.0.0.1:41712 - 172.30.27.3/24",
"ip": "172.30.86.9/24 - 127.0.0.1:43872 - 172.30.95.7/24",
"ip": "172.30.95.15/24 - 127.0.0.1:41662 - 172.30.27.3/24",
"ip": "172.30.86.7/24 - 127.0.0.1:43944 - 172.30.95.7/24",
"ip": "172.30.27.11/24 - 127.0.0.1:43094 - 172.30.86.3/24",
"ip": "172.30.86.8/24 - 127.0.0.1:43872 - 172.30.95.7/24",
"ip": "172.30.95.14/24 - 127.0.0.1:43120 - 172.30.86.3/24",
我們連續發起了27次請求,不出所料,client的9個Pod分別被分配到了3次請求,接下來我們看看server是否負載上了:
• 27次請求中,3個server的pod均被分配了9次,負載是均衡的。
• 進一步分析,同一個client,每次被分配到的server是不同的,這是因為envoy代理在中間攔了一手,這正是負載的實現原理。
• 再進一步分析,server檢測到的客戶端ip,是127.0.0.1的本地ip,這個連接其實是面向本pod的envoy的。
所以我們可以認為envoy為我們在網格內建立起了一個服務之間的連接池,從而實現了負載均衡。
最后,我們再回過頭來對比針對grpc長連接的情況,k8s的負載和istio的負載的差異:
• K8s的負載是在連接發起時實現的,比如我們的例子里有9個客戶端,則調用了9次負載算法,次數太少,負載未必是均衡的。
• 基於連接的負載,一旦pod異常退出,連接斷開,重新建立起的連接有更大的概率被集中到個別沒斷開的pod上,這會加劇負載的不均衡性。
• Istio維護的負載是在每次通訊的時候完成的動態調度,不受連接數的限制,在大量通訊的情況下,istio能夠保證負載的均衡。
3 流量遷移:金絲雀發布
上一章envoy已經接管了我們的流量,接下來就可以讓envoy為我們做更細致的流量管理的動作了。本章我們會通過簡單的配置來實現金絲雀發布。
假設我們已經發布了data-product的v3.0版本,現在我們有一個v4.0的版本需要更新上去。版本發布過程中我們期望先將10%的流量導入到v4.0版本,剩余90%的流量還是由v3.0版本來處理。待v4.0版本運行穩定無異常后,我們再將100%的流量遷移到v4.0版本。
3.1 發布應用
回顧1.3章節,我們在發布data-product時,需要deployment的名稱區分版本(想想為啥?)。為實現兩個版本的平滑過渡、金絲雀發布,我們需要同時發布兩個版本的deployment:
$ kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
business-product 3/3 3 3 5h13m
data-product 3/3 3 3 4h30m
data-product-v4.0 3/3 3 3 4h16m
上表data-product是v3.0版本的(這里我命名不規范),data-product-v4.0是v4.0版本的,各有3個pod在運行。
3.2 創建目標規則:DestinationRule
目標規則(DestinationRule)是用來定義流量應該由哪些pod來接收的。下方的yaml文件我們創建了一個DestinationRule,標識發往data-product的流量,由data-product的v3.0和v4.0兩個版本來接收。這個yaml文件的關鍵是subsets字段的兩個labels,這對應着1.3和3.1節的應用版本。
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: data-product
spec:
host: data-product
subsets:
- name: v3
labels:
version: v3.0
- name: v4
labels:
version: v4.0
3.3 創建虛擬服務:VirtualService
Service和virtualservice的目標是一致的,都是將流量導向后端的pod。區別是由istio引入的virtualservice可以完成更細致的流量划分。
下方的yaml文件,我們將10%的流量導向了v4版本,90%的流量導向v3版本。這個文件的關鍵是subset字段,這對應於DestinationRule的subsets;以及weight權重,按百分比划分。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: data-product
spec:
hosts:
- data-product
http:
- route:
- destination:
host: data-product
subset: v3
weight: 90
- destination:
host: data-product
subset: v4
weight: 10
3.4 驗證和結語
限於篇幅,驗證數據就不貼了。平均每10次請求,會有1次導向v4,另外9次還在v3。
不難理解,運行一段時間覺得v4版本ok了,我們可以將權重配比調整到50%,75%,直到100%的流量都導向v4。之后v3版本的deployment就可以刪了。