全書目錄
本文目錄
第4章 服務彈性........................................................................................................ 1
4.1 負載均衡..................................................................................................... 1
4.2 超時............................................................................................................ 3
4.3 重試............................................................................................................ 5
4.4 斷路器......................................................................................................... 7
4.5 池彈出....................................................................................................... 13
4.6 組合:斷路器 + 池彈出 + 重試.................................................................. 16
第4章 服務彈性
請記住你的服務和應用會在不可靠網絡上進行通信。過去,程序員們經常嘗試使用一些框架,比如EJB、CORBA、RMI等,來將基於網絡的調用簡化為如同本地函數調用那樣。這沒法讓程序員們安心,因為在沒法確定應用能夠應對所有網絡故障的情況下,是沒法相信整個系統能完全應對故障的。因此,你永遠沒法假設,一個通過網絡被遠程調用的實體能永遠在期望的時間內返回所期望的內容(或者說,正如The Hitchhiker’s Guide to the Galaxy一書的作者Douglas Adams曾經說的,人們常常犯的一個錯誤就是低估了傻瓜的創造性)。你也不想單個服務的不正常行為成為影響整個系統的一個致命錯誤。
Istio帶來了實現應用彈性的很多能力,但正如前文所說,這些能力來自邊車代理。而且,本章會介紹的彈性功能其實不依賴於任何變成語言和運行時,不管你選擇何種庫或框架去實現你的服務:
-
客戶端側負載均衡:Istio增強了Kubernetes的負載均衡功能。
-
超時:等待返回N秒后就不再等待。
-
重試:如果一個pod返回503之類的錯誤,則嘗試其它pod。
-
簡單斷路器:為了不讓降級了的服務被請求淹沒,可開啟斷路器拒絕更多的請求。
-
池彈出(Pool ejection):將出錯了的pod從負載均衡池中移出。
接下來我們會通過示例程序介紹這些功能。本章我們依然會使用前幾章中用到的customer、preference和recommendation等服務。
4.1 負載均衡
一個增加吞吐和降低延遲的核心能力是負載均衡(load balancing)。一個常見實現是使用一個集中式負載均衡器,負責接收所有客戶端的連接,然后將請求分發給后端系統。這是一種非常好的實現,但是,負載均衡器可能成為整個系統的瓶頸或故障單點。負載均衡能力可以被轉移至客戶端,通過使用客戶端側負載均衡器。客戶端側負載均衡器可使用高級負載均衡算法來增加系統可用性、降低延遲以及增加吞吐能力。
Istio的Envoy代理和應用容器運行在同一個pod中,本身具有負載均衡能力,因此就可成為應用的客戶端側負載均衡器。它支持如下算法:
-
ROUND_ROBIN(輪詢):這種算法平均分配負載,輪流將負載轉發給負載均衡池中的后端。
-
RANDOM(隨機):這種算法隨機地將負載分配給池中的后端。
-
LEAST_CONN(最小連接數):這種算法隨機地從池中選擇兩個后端,然后將負載轉發給連接較少的那個。這是一種帶權重最小請求數算法的實現。
在前面關於路由的章節中,你用到了DestionationRule和VirtualService對象去控制流量如何被導向特定pod。本章中,我們會介紹利用DestionationRule去控制特定pod的通信行為。一開始,我們會討論如何利用Istio DestionationRule去配置負載均衡。
首先,請確保沒有DestionationRule實例存在。你可使用下面的命令去刪除DestionationRule和VirtualService對象:
oc delete virtualservice --all -n tutorial
oc delete destinationrule --all -n tutorial
然后,將recommendation服務的副本數擴大到3:
oc scale deployment recommendation-v2 --replicas=3 -n tutorial
過一會,所有pod都會達到正常狀態,能接受請求了。現在,利用之前用到的腳本去向系統發送請求:
#!/bin/bash
while true
do curl customer-tutorial.$(minishift ip).nip.io
sleep .1
done
通過輸出你能看到默認輪詢負載均衡效果:
customer => ... => recommendation v1 from '99634814': 1145
customer => ... => recommendation v2 from '6375428941': 1
customer => ... => recommendation v2 from '4876125439': 1
customer => ... => recommendation v2 from '2819441432': 181
customer => ... => recommendation v1 from '99634814': 1146
customer => ... => recommendation v2 from '6375428941': 2
customer => ... => recommendation v2 from '4876125439': 2
customer => ... => recommendation v2 from '2819441432': 182
customer => ... => recommendation v1 from '99634814': 1147
現在,將負載均衡算法改為RANDOM,利用下面的Istio DestionationRule定義:
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: recommendation
namespace: tutorial
spec:
host: recommendation
trafficPolicy:
loadBalancer:
simple: RANDOM
這個目標策略配置到recommendation服務的請求使用隨機負載均衡算法,發生在preference服務調用recommendation服務的時候,這時候Envoy代理就成了preference pod內的客戶端側負載均衡。
我們來創建這個對象:
oc -n tutorial create -f istiofiles/destination-rule-recommendation_lb_policy_app.yml
現在,在輸出中你將看到更加隨機的效果:
customer => ... => recommendation v2 from '2819441432': 183
customer => ... => recommendation v2 from '6375428941': 3
customer => ... => recommendation v2 from '2819441432': 184
customer => ... => recommendation v1 from '99634814': 1153
customer => ... => recommendation v1 from '99634814': 1154
customer => ... => recommendation v2 from '2819441432': 185
customer => ... => recommendation v2 from '6375428941': 4
customer => ... => recommendation v2 from '6375428941': 5
customer => ... => recommendation v2 from '2819441432': 186
customer => ... => recommendation v2 from '4876125439': 3
在繼續下面的步驟前,先做下清理和恢復工作:
oc -n tutorial delete -f \
istiofiles/destination-rule-recommendation_lb_policy_app.yml
oc scale deployment recommendation-v2 --replicas=1 -n tutorial
4.2 超時
超時(Timeout)是一種讓系統具有彈性和高可用性的重要手段。通過網絡調用服務可能會產生不可預料的結果,其中最惡劣的是延遲。延遲是因為目標服務故障了呢,還是只是慢了一些?它確實在運行着嗎?高延遲意味着這些可能都發生了。那你的服務該如何應對呢?只是徒勞等待?如果有客戶在等待這個請求,等待不是一個好辦法。因為等待也占用資源,可能導致其他系統也出現等待,導致一連串錯誤。你的網絡中可能隨時出現超時,你可以使用Istio服務網格去應對。
在Istio中,超時是指Envoy代理等待業務服務響應的時長。一旦超過這個時長沒有得到響應,那么Envoy代理將放棄繼續等待,從而保證不會無限期等待某個響應。對HTTP請求默認響應時長為15秒,也就是說如果超過15秒業務服務沒有反饋,則調用失敗。
回到recommendation服務的代碼,找到RecommendationVerticle.java類,注釋掉能產生延遲的代碼行:
public void start() throws Exception {
Router router = Router.router(vertx);
router.get("/").handler(this::timeout); // adds 3 secs
router.get("/").handler(this::logging);
router.get("/").handler(this::getRecommendations);
router.get("/misbehave").handler(this::misbehave);
router.get("/behave").handler(this::behave);
vertx.createHttpServer().requestHandler(router::accept)
.listen(LISTEN_ON);
}
保存代碼更改,編譯、打包和重新部署:
cd recommendation/java/vertx
mvn clean package
docker build -t example/recommendation:v2 .
oc delete pod -l app=recommendation,version=v2 -n tutorial
最后一步會導致v2版本pod被重建,使用剛剛構建的recommendation服務鏡像。現在,調用customer服務,你會看到服務延遲:
time curl customer-tutorial.$(minishift ip).nip.io
customer => preference => recommendation v2 from
'2819441432': 202
real 0m3.054s
user 0m0.003s
sys 0m0.003s
你可以多調用幾次。Recommendation服務的v1版本不會延遲,因為這部分代碼沒有被修改。
上面的例子中,雖然v2版本pod中的recommendation服務在3秒后才發回響應,但這時長沒有超過默認15秒,因此Envoy代理不會放棄等待,因此調用還是成功了。但是,在某些場景中,默認時長可能不太合適,因此需要調整。Istio的VirutalService類型允許你在單個服務層面動態調整超時時長。
下面的VirtualService對象定義中將調用recommendation服務的超時時長設置為1秒鍾:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
creationTimestamp: null
name: recommendation
namespace: tutorial
spec:
hosts:
- recommendation
http:
- route:
- destination:
host: recommendation
timeout: 1.000s
使用下面的命令去創建VirtualService對象:
oc -n tutorial create -f \
istiofiles/virtual-service-recommendation-timeout.yml
再給customer服務發出請求,你會看到請求要么成功(此時請求都路由到recommendation服務的v1版本),要么返回504錯誤(此時請求被路由到v2版本):
time curl customer-tutorial.$(minishift ip).nip.io
customer => 503 preference => 504 upstream request timeout
real 0m1.151s
user 0m0.003s
sys 0m0.003s
使用下面的命令去進行清理:
oc delete virtualservice recommendation -n tutorial
4.3 重試
因為網絡天生的不可靠性,以及服務pod也可能會臨時宕機,你可能會遇到間歇性錯誤,對於每周甚至每天多次部署的分布式微服務來說遇到這種錯誤的可能性會更大。使用Istio的重試功能,在真正處理錯誤之前,你可以進行多次重試。下面我們看如何利用Istio做到這一點。
首先你要做的是模擬間歇性網絡出錯。在recommendation服務例子中,有個特定的端點,它只是設置一個標志位;這個標志位將getRecommendations函數的返回值設置為503。要將這個misbehave標志位設置為true,進入v2 pod,然后執行下面的命令:
oc exec -it $(oc get pods|grep recommendation-v2 \
|awk '{ print $1 }'|head -1) -c recommendation /bin/bash
curl localhost:8080/misbehave
現在,當你請求customer服務時,你會看到一些503錯誤:
#!/bin/bash
while true
do
curl customer-tutorial.$(minishift ip).nip.io
sleep .1
done
customer => preference => recommendation v1 from '99634814': 200
customer => 503 preference => 503 misbehavior from '2819441432'
現在來看下VirtualService中的重試配置:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: recommendation
namespace: tutorial
spec:
hosts:
- recommendation
http:
- route:
- destination:
host: recommendation
retries:
attempts: 3
perTryTimeout: 2s
這個定義將訪問recommendation服務的重試次數設為3,每次超時時間設為2秒,因此累計超時時間為6秒,加上原調用所花的時間,就是整個調用所花的時間。
創建這個對象:
oc -n tutorial create -f \
istiofiles/virtual-service-recommendation-v2_retry.yml
現在再發送請求,你不會看到錯誤了。這意味着即使遇到了503錯誤,Istio會自動進行重試。
customer => preference => recommendation v1 from '99634814': 35
customer => preference => recommendation v1 from '99634814': 36
customer => preference => recommendation v1 from '99634814': 37
customer => preference => recommendation v1 from '99634814': 38
在開始下面的步驟前,先做一些清理工作:
oc delete destinationrules --all -n tutorial
oc delete virtualservices --all -n tutorial
重啟v2 pod,以將 mishehave 標志位恢復為false:
oc delete pod -l app=recommendation,version=v2
4.4 斷路器
現代家庭所使用的電力安全裝置斷路器(circuit breaker)會保護特定設備免於承受過載的電流。也許你曾經看到,當插入一個錄音機、吹風機甚至加熱器到一插座中后,斷路器會發生跳閘。電流過載會帶來危險,因為它會讓電線發熱,這可能會導致火災。斷路器會開啟並斷開線路,防止危險發生。
注意:軟件系統中的“斷路器”概念首次由Micheel Nygard在他的書 Releast It!中引入,現在這本書出到了第二版。
在2012年Netflix發布的Hystrix庫中的斷路器和隔艙(bulkhead)模式已被廣泛使用。Netflix的很多庫,比如Eureka(用於服務發現)、Ribbon(用於負載均衡)和Hystrix(斷路器和隔艙),在業內很快流行起來,並開始用於微服務和雲原生架構。Netflix OSS是在Kubernetes/OpenShift面世之前發布的,確實存在一些不足,比如它只支持Java,要求應用開發者將庫封裝到應用中等。圖4-1提供了一個時間線,介紹從何時起軟件業界開始把單體應用開發團隊和大規模的幾個月的瀑布開發模式開始拆分,到Netflix OSS的誕生,再到微服務屬於被提出。
圖4-1.微服務時間線
Istio將更多的彈性實現下沉到基礎架構中,這樣你可以將寶貴的時間和精力放到業務邏輯中去,從而創造業務差異性優勢。
Istio可在連接池層面實現斷路器功能。要進行測試驗證,首先要確保recommendation v2版本開啟了3秒的延遲。修改后的RecommendationVertical.java文件中如下面所示:
Router router = Router.router(vertx);
router.get("/").handler(this::logging);
router.get("/").handler(this::timeout); // adds 3 secs
router.get("/").handler(this::getRecommendations);
router.get("/misbehave").handler(this::misbehave);
router.get("/behave").handler(this::behave);
使用下面的Istio DestionationRule將流量導至v1和v2版本的recommendation服務:
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
creationTimestamp: null
name: recommendation
namespace: tutorial
spec:
host: recommendation
subsets:
- labels:
version: v1
name: version-v1
- labels:
version: v2
name: version-v2
使用如下命令創建DestionationRule對象:
oc -n tutorial create -f \
istiofiles/destination-rule-recommendation-v1-v2.yml
然后定義VirtualService對象:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
creationTimestamp: null
name: recommendation
namespace: tutorial
spec:
hosts:
- recommendation
http:
- route:
- destination:
host: recommendation
subset: version-v1
weight: 50
- destination:
host: recommendation
subset: version-v3
weight: 50
再創建VirtualService對象:
oc -n tutorial create -f \
istiofiles/virtual-service-recommendation-v1_and_v2_50_50.yml
在第一章中,我們推薦你安裝Siege命令行工具,它可用於通過命令行進行簡單的壓力測試。
我們使用20個客戶端,每個發送2個並發請求給customer服務,使用下面的命令:
siege -r 2 -c 20 -v customer-tutorial.$(minishift ip).nip.io
你會看到如下輸出:
所有請求都成功了,但花的時間比較長,因為v2 pod的響應比較慢。假設生產環境中3秒的延遲是因為一個實例或pod上有太多的請求造成的,你不想大量請求都放到隊列中,也不想那個實例或pod會越來越慢。此時,可以增加斷路器。
要為服務創建斷路器功能,創建如下DestionationRule定義:
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
creationTimestamp: null
name: recommendation
namespace: tutorial
spec:
host: recommendation
subsets:
- name: version-v1
labels:
version: v1
- name: version-v2
labels:
version: v2
trafficPolicy:
connectionPool:
http:
http1MaxPendingRequests: 1
maxRequestsPerConnection: 1
tcp:
maxConnections: 1
outlierDetection:
baseEjectionTime: 120.000s
consecutiveErrors: 1
interval: 1.000s
maxEjectionPercent: 100
接下來應用這個定義:
oc -n tutorial replace -f istiofiles/destination-rule-recommendation_cb_policy_version_v2.yml
因為前面的VirtualService將流量在v1和v2之間平分,因此,這個DestionationRule只會對一半流量起作用。這個定義限制連接數和等待請求數為1(其它設置會在第50頁上的Pool Injection章節進行介紹)。
現在,再次運行siege命令:
siege -r 2 -c 20 -v customer-tutorial.$(minishift ip).nip.io
你會看到幾乎所有請求都在1秒內就完成了,而且沒有任何報錯。你可以嘗試更多次,以確認這個結果是穩定的。斷路器會將任何等待請求或連接數超過閾值的連接斷開。斷路器的目標是快速斷開。
做下環境清理:
oc delete virtualservice recommendation -n tutorial
oc delete destinationrule recommendation -n tutorial
4.5 池彈出
我們所要討論的最后一個彈性能力,會確定不正常工作的集群節點,並在一段時間(冷卻期)內不向它導入任何流量(實際上是將它從負載均衡池中彈出)。因為Istio代理是基於Envoy的,而Envory將這種實現稱為異常檢測(outlier detection),我們會在Istio中使用同樣的術語。
想象一個場景,你的軟件開發團隊每周幾次在在工作日的中午將各組件部署到生產環境,此時可人為刪除故障pod以保證系統彈性。池彈出(Pool ejection)或異常檢測是一種很有用的彈性策略,當有一組pod服務於客戶端請求時。當請求被發往一個pod,而這個pod出錯了(比如返回50x錯誤)時,Istio會在一定時間內將該pod從池中彈出。在我們的例子中,冷卻期被設置為15秒。這種做法通過確保正常的pod參與請求處理,從而增加了系統總體可用性。
首先,你要確保已有了DestionationRule和VirtualService對象。我們將流量分為兩半:
oc -n tutorial create -f \
istiofiles/destination-rule-recommendation-v1-v2.yml
oc -n tutorial create -f \
istiofiles/virtual-service-recommendation-v1_and_v2_50_50.yml
然后,將recommendation服務的v2 pod的數目擴展到2,以在負載均衡池中有多個實例:
oc scale deployment recommendation-v2 --replicas=2 -n tutorial
等待所有pod都正常運行,然后給customer服務產生一些請求:
#!/bin/bash
while true
do curl customer-tutorial.$(minishift ip).nip.io
sleep .1
done
你會看到在recommendation服務的兩個版本之間做50/50的負載均衡。對於v2版本,你會看到一些請求被一個pod處理,另外的請求被另一個pod處理:
customer => ... => recommendation v1 from '99634814': 448
customer => ... => recommendation v2 from '3416541697': 27
customer => ... => recommendation v1 from '99634814': 449
customer => ... => recommendation v1 from '99634814': 450
customer => ... => recommendation v2 from '2819441432': 215
customer => ... => recommendation v1 from '99634814': 451
customer => ... => recommendation v2 from '3416541697': 28
customer => ... => recommendation v2 from '3416541697': 29
customer => ... => recommendation v2 from '2819441432': 216
要測試池彈出功能,你要讓其中一個pod出錯。找到v2 pod:
oc get pods -l app=recommendation,version=v2
recommendation-v2-2819441432 2/2 Running 0 1h
recommendation-v2-3416541697 2/2 Running 0 7m
進入一個pod, 執行命令,然后退出:
oc -n tutorial exec -it recommendation-v2-3416541697 -c recommendation \ /bin/bash
curl localhost:8080/misbehave
exit
這是一個特殊端點,它會讓該pod返回503錯誤:
#!/bin/bash
while true
do curl customer-tutorial.$(minishift ip).nip.io
sleep .1
done
在輸出中,你會看到每次recommendation-v2-3416541697收到請求后它都返回503錯誤:
customer => ... => recommendation v1 from '2039379827': 495
customer => ... => recommendation v2 from '2036617847': 248
customer => ... => recommendation v1 from '2039379827': 496
customer => ... => recommendation v1 from '2039379827': 497
customer => 503 preference => 503 misbehavior from '3416541697'
customer => ... => recommendation v2 from '2036617847': 249
customer => ... => recommendation v1 from '2039379827': 498
customer => 503 preference => 503 misbehavior from '3416541697'
好了,我們現在可以測試Istio的池彈出功能了。定義如下的DestionationRule對象:
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
creationTimestamp: null
name: recommendation
namespace: tutorial
spec:
host: recommendation
subsets:
- labels:
version: v1
name: version-v1
trafficPolicy:
connectionPool:
http: {}
tcp: {}
loadBalancer:
simple: RANDOM
outlierDetection:
baseEjectionTime: 15.000s
consecutiveErrors: 1
interval: 5.000s
maxEjectionPercent: 100
- labels:
version: v2
name: version-v2
trafficPolicy:
connectionPool:
http: {}
tcp: {}
loadBalancer:
simple: RANDOM
outlierDetection:
baseEjectionTime: 15.000s
consecutiveErrors: 1
interval: 5.000s
maxEjectionPercent: 100
其中,Istio被配置為每隔5秒鍾去檢查不正常pod,並在發生1次后就將它從負載均衡池中移出去,而且保持15秒鍾。
oc -n tutorial replace -f
istiofiles/destination-rule-recommendation_cb_policy_pool
_ejection.yml
給customer服務發送一些請求:
#!/bin/bash
while true
do curl customer-tutorial.$(minishift ip).nip.io
sleep .1
Done
你會發現一旦recommendation-v2-3416541697收到請求並返回503后,它都會被彈出負載均衡池不再接受請求,直到15秒鍾的冷卻期過期。
customer => ... => recommendation v1 from '2039379827': 509
customer => 503 preference => 503 misbehavior from '3416541697'
customer => ... => recommendation v1 from '2039379827': 510
customer => ... => recommendation v1 from '2039379827': 511
customer => ... => recommendation v1 from '2039379827': 512
customer => ... => recommendation v2 from '2036617847': 256
customer => ... => recommendation v2 from '2036617847': 257
customer => ... => recommendation v1 from '2039379827': 513
customer => ... => recommendation v2 from '2036617847': 258
customer => ... => recommendation v2 from '2036617847': 259
customer => ... => recommendation v2 from '2036617847': 260
customer => ... => recommendation v1 from '2039379827': 514
customer => ... => recommendation v1 from '2039379827': 515
customer => 503 preference => 503 misbehavior from '3416541697'
customer => ... => recommendation v1 from '2039379827': 516
customer => ... => recommendation v2 from '2036617847': 261
4.6 組合:斷路器 + 池彈出 + 重試
從上面的輸出可以看出,即使使用了池彈出,你的應用還是會有少量出錯,但你可以繼續進行優化。如果你的服務有足夠數量的副本在環境中運行,你可以將Istio的多種能力組合在一起來增強后端彈性:
-
斷路器:避免給一個實例發送過量請求
-
池彈出:從負載均衡池中將故障實例彈出
-
重試:當斷路器或池彈出發生時,將請求發往其它實例
在當前VirtualService中增加一些簡單配置,我們就能完全避免503錯誤響應。這意味着每次從被彈出實例收到503故障返回時,Istio會將請求發往另一個正常實例:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
creationTimestamp: null
name: recommendation
namespace: tutorial
spec:
hosts:
- recommendation
http:
- retries:
attempts: 3
perTryTimeout: 4.000s
route:
- destination:
host: recommendation
subset: version-v1
weight: 50
- destination:
host: recommendation
subset: version-v2
weight: 50
替代當前的VirtualService服務:
oc -n tutorial replace -f \ istiofiles/virtual-service-recommendation-v1_and_v2_retry.yml
然后向customer端點發起請求:
#!/bin/bash
while true
do curl customer-tutorial.$(minishift ip).nip.io
sleep .1
done
你不會再收到503錯誤了:
customer => ... => recommendation v1 from '2039379827': 538
customer => ... => recommendation v1 from '2039379827': 539
customer => ... => recommendation v1 from '2039379827': 540
customer => ... => recommendation v2 from '2036617847': 281
customer => ... => recommendation v1 from '2039379827': 541
customer => ... => recommendation v2 from '2036617847': 282
customer => ... => recommendation v1 from '2039379827': 542
customer => ... => recommendation v1 from '2039379827': 543
customer => ... => recommendation v2 from '2036617847': 283
customer => ... => recommendation v2 from '2036617847': 284
出故障的recommendation-v2-3416541697不會再出現,這要歸功於Istio的池彈出和重試功能。
最后做下清理,在RecommendationVertical.java中刪除延遲代碼,重新構建docker鏡像,刪除故障pod,然后刪除DestionationRule和VirtualService對象:
cd recommendation/java/vertx
mvn clean package
docker build -t example/recommendation:v2 .
oc scale deployment recommendation-v2 --replicas=1 -n tutorial
oc delete pod -l app=recommendation,version=v2 -n tutorial
oc delete virtualservice recommendation -n tutorial
oc delete destinationrule recommendation -n tutorial
現在,你已看到如何讓服務到服務之間的調用更加彈性和健壯。接下來,在第5章中,我們會介紹如何通過引入混亂來有意中斷某些服務。
書籍英文版下載鏈接為 https://developers.redhat.com/books/introducing-istio-service-mesh-microservices/,作者 Burr Sutter 和 Christian Posta。
本中文譯稿版權由本人所有。水平有限,錯誤肯定是有的,還請海涵。
感謝您的閱讀,歡迎關注我的微信公眾號: