如何在Kubernetes中將Envoy用做負載均衡器

在當今分佈式的世界中,單體架構愈來愈多地被多個,更小,相互鏈接的服務(不論是好是壞)所取代,代理和負載平衡技術彷佛正在復興。除了老玩家之外,近年來還涌現出幾種新的代理技術,它們以各類技術實現,並以不一樣的功能進行普及,例如易於集成到某些雲提供商(「雲原生」),高性能和低內存佔用,或動態配置。git

能夠說,兩種最流行的「經典」代理技術是NGINX(C)和HAProxy(C),而其中的一些新成員是Zuul(Java),Linkerd(Rust),Traefik(Go),Caddy(Go)和Envoy(C++)。github

全部這些技術具備不一樣的功能集,而且針對某些特定場景或託管環境(例如,Linkerd通過微調,可在Kubernetes中使用)。
在本文中,我將不作這些比較,而只是關注一個特定的場景:如何將Envoy用做Kubernetes中運行的服務的負載平衡器。算法

Envoy是最初在Lyft實施的「高性能C++分佈式代理」,但此後獲得了普遍的採用。它性能高,資源佔用少,支持由「控制平面」 API管理的動態配置,並提供了一些高級功能,例如各類負載平衡算法,限流,熔斷和影子鏡像。docker

因爲多種緣由,我選擇Envoy做爲負載平衡器代理:shell

  • 除了能夠經過控制平面API動態控制以外,它還支持基於YAML的簡單,硬編碼配置,這對我而言很方便,而且易於入門。
  • 它內置了對稱爲STRICT_DNS的服務發現技術的支持,該技術基於查詢DNS記錄,並指望看到上游羣集每一個節點都有IP地址的A記錄。這使得Kubernetes中的無頭服務變得易於使用。
  • 它支持各類負載平衡算法,其中包括「最少請求」。

在開始使用Envoy以前,我是經過類型爲LoadBalancer的服務對象訪問Kubernetes中的服務的,這是從Kubernetes中從外部訪問服務的一種很是典型的方法。負載均衡器服務的確切工做方式取決於託管環境。我使用的是Google Kubernetes引擎,其中每一個負載平衡器服務都映射到TCP級別的Google Cloud負載平衡器,該負載平衡器僅支持循環負載平衡算法。服務器

就我而言,這是一個問題,由於個人服務具備如下特徵:架構

  • 這些請求長期運行,響應時間從100ms到秒不等。
  • 請求的處理佔用大量CPU,實際上一個請求的處理使用了一個CPU內核的100%。
  • 並行處理許多請求會下降響應時間。 (這是因爲該服務的工做原理的內部緣由,它不能有效地並行運行少數幾個請求。)

因爲上述特性,輪循負載均衡算法不太適合,由於常常(偶然)多個請求最終在同一節點上結束,這使得平均響應時間比羣集的平均響應時間差得多。因此須要分配更均勻的負載。app

在本文的其他部分中,我將描述將Envoy部署爲在Kubernetes中運行的服務以前用做負載平衡器的必要步驟。負載均衡

1. 爲咱們的應用建立headless服務

在Kubernetes中,有一種稱爲headless服務的特定服務,剛好與Envoy的STRICT_DNS服務發現模式一塊兒使用時很是方便。less

Headless服務不會爲底層Pod提供單個IP和負載平衡,而只是具備DNS配置,該配置爲咱們提供了一個A記錄,其中包含與標籤選擇器匹配的全部Pod的Pod IP地址。咱們但願在實現負載平衡並本身維護與上游Pod的鏈接的狀況下使用此服務類型,這正是咱們使用Envoy能夠作到的。

咱們能夠經過將.spec.clusterIP字段設置爲「None」來建立headless服務。所以,假設咱們的應用程序pod的標籤app的值爲myapp,咱們可使用如下yaml建立headless服務。
b-01.jpg

服務的名稱沒必要等於咱們的應用程序名稱或應用程序標籤,但這是一個很好的約定。

如今,若是咱們在Kubernetes集羣中檢查服務的DNS記錄,咱們將看到帶有IP地址的單獨的A記錄。若是咱們有3個Pod,則會看到與此相似的DNS摘要。

$ nslookup myapp 
Server: 10.40.0.10 
Address: 10.40.0.10#53 

Non-authoritative answer: 
Name: myapp.namespace.svc.cluster.local Address: 10.36.224.5 
Name: myapp.namespace.svc.cluster.local Address: 10.38.187.17 
Name: myapp.namespace.svc.cluster.local Address: 10.38.1.8

Envoy的STRICT_DNS服務發現的工做方式是,它維護DNS返回的全部A記錄的IP地址,而且每隔幾秒鐘刷新一次IP組。

2. 建立Envoy鏡像

在不以動態API形式提供控制平面的狀況下使用Envoy的最簡單方法是將硬編碼配置添加到靜態yaml文件中。

如下是一個基本配置,該配置將負載均衡到域名myapp給定的IP地址。

b-02.jpg

注意如下幾個部分:

  • type: STRICT_DNS:在這裏,咱們指定服務發現類型。將其設置爲STRICT_DNS很重要,由於它能夠與咱們設置的無頭服務一塊兒使用。
  • lb_policy:LEAST_REQUEST:咱們能夠從各類負載平衡算法中進行選擇,可能ROUND_ROBIN和LEAST_REQUEST是最多見的。(請記住,LEAST_REQUEST不會檢查全部上游節點,而只會從2個隨機選擇的選項中進行選擇。)
  • hosts: [{ socket_address: { address: myapp, port_value: 80 }}]:這是咱們在其中用address字段指定Envoy必須從中獲取要發送到A記錄的域名的部分。

您能夠在文檔中找到有關各類配置參數的更多信息。

如今,咱們必須將如下Dockerfile放在envoy.yaml配置文件同一目錄層級。

FROM envoyproxy/envoy:latest 
COPY envoy.yaml /etc/envoy.yaml 
CMD /usr/local/bin/envoy -c /etc/envoy.yaml

最後一步是構建鏡像,並將其推送到某個地方(例如Dockerhub或雲提供商的容器註冊表),以便可以從Kubernetes使用它。

假設我想將此推送到個人我的Docker Hub賬戶,可使用如下命令來完成。

$ docker build -t markvincze/myapp-envoy:1 .  
$ docker push markvincze/myapp-envoy:1

3. 可選項: 使Envoy鏡像可參數化

若是咱們但願可以使用環境變量自定義Envoy配置的某些部分而無需重建Docker鏡像,則能夠在yaml配置中進行一些env var替換。假設咱們但願可以自定義要代理的headless服務的名稱以及負載均衡器算法,而後咱們必須按如下方式修改yaml配置。

b-03.jpg

而後實施一個小shell腳本(docker-entrypoint.sh),在其中執行環境變量替換。

#!/bin/sh  
set -e 

echo  "Generating envoy.yaml config file..." 
cat /tmpl/envoy.yaml.tmpl | envsubst \$ENVOY_LB_ALG,\$SERVICE_NAME > /etc/envoy.yaml 

echo  "Starting Envoy..." 
/usr/local/bin/envoy -c /etc/envoy.yaml

並更改咱們的Dockerfile以運行此腳本,而不是直接啓動Envoy。

FROM envoyproxy/envoy:latest 
COPY envoy.yaml /tmpl/envoy.yaml.tmpl 
COPY docker-entrypoint.sh / 

RUN chmod 500 /docker-entrypoint.sh 

RUN apt-get update && \
    apt-get install gettext -y 
    
ENTRYPOINT ["/docker-entrypoint.sh"]

請記住,若是使用這種方法,則必須在Kubernetes部署中指定這些環境變量,不然它們將爲空。

4. 建立Envoy deployment

最後,咱們必須爲Envoy自己建立一個部署。
b-06.jpg

b-07.jpg

僅當咱們使Envoy Docker鏡像可參數化時,才須要env變量。

Apply此Yaml後,Envoy代理應該能夠運行,而且您能夠經過將請求發送到Envoy服務的主端口來訪問基礎服務。

在此示例中,我僅添加了類型爲ClusterIP的服務,可是若是要從羣集外部訪問代理,還可使用LoadBalancer服務或Ingress對象。

下圖說明了整個設置的體系結構。

b-08.png

該圖僅顯示一個Envoy Pod,可是若是須要,您能夠將其擴展以具備更多實例。固然,您能夠根據須要使用Horizo​​ntal Pod Autoscaler自動建立更多副本。 (全部實例將是自治的且彼此獨立。)
實際上,與基礎服務相比,代理所需的實例可能要少得多。在當前使用Envoy的生產應用程序中,咱們在 〜400個上游Pod上提供了〜1000個請求/秒,可是咱們只有3個Envoy實例在運行,CPU負載約爲10%。

故障排除和監視

在Envoy配置文件中,您能夠看到admin:部分,用於配置Envoy的管理端點。可用於檢查有關代理的各類診斷信息。

若是您沒有發佈admin端口的服務,默認狀況下爲9901,您仍然能夠經過端口轉發到帶有kubectl的容器來訪問它。假設其中一個Envoy容器稱爲myapp-envoy-656c8d5fff-mwff8,那麼您可使用命令kubectl port-forward myapp-envoy-656c8d5fff-mwff8 9901開始端口轉發。而後您能夠訪問http://localhost:9901上的頁面。

一些有用的端點:

  • /config_dump:打印代理的完整配置,這對於驗證正確的配置是否最終在Pod上頗有用。
  • /clusters:顯示Envoy發現的全部上游節點,以及爲每一個上游節點處理的請求數。例如,這對於檢查負載平衡算法是否正常工做頗有用。

進行監視的一種方法是使用Prometheus從代理pods獲取統計信息。 Envoy對此提供了內置支持,Prometheus統計信息在管理端口上的/ stats/prometheus路由上發佈。

您能夠從該存儲庫下載可視化這些指標的Grafana儀表板,這將爲您提供如下圖表。

b-08.png

關於負載均衡算法

負載平衡算法會對集羣的總體性能產生重大影響。對於須要均勻分配負載的服務(例如,當服務佔用大量CPU並很容易超載時),使用最少請求算法多是有益的。另外一方面,最少請求的問題在於,若是某個節點因爲某種緣由開始發生故障,而且故障響應時間很快,那麼負載均衡器會將不成比例的大部分請求發送給故障節點,循環負載均衡算法不會有問題。

我使用dummy API進行了一些基準測試,並比較了輪詢和最少請求LB算法。事實證實,最少的請求能夠帶來總體性能的顯着提升。

我使用不斷增長的輸入流量對API進行了約40分鐘的基準測試。在整個基準測試中,我收集了如下指標:

  • 服務器執行的請求數("requests in flight")
  • 每臺服務器平均正在執行的請求數
  • 請求速率(每5分鐘增長一次)
  • 錯誤率(一般沒有,可是當事情開始放慢時,這開始顯示出超時)
  • 服務器上記錄的響應時間百分位數(0.50、0.90和0.99)

ROUND_ROBIN的統計數據看起來像這樣:
b-09.png

這些是LEAST_REQUEST的結果:
b-10.png

您能夠從結果中看到LEAST_REQUEST能夠致使流量在節點之間的分配更加順暢,從而在高負載降低低了平均響應時間。

確切的改進取決於實際的API,所以,我絕對建議您也使用本身的服務進行基準測試,以便作出決定。

總結

我但願此介紹對在Kubernetes中使用Envoy有所幫助。順便說一下,這不是在Kubernetes上實現最少請求負載平衡的惟一方法。能夠執行相同操做的各類ingress控制器(其中一個是在Envoy之上構建的Ambassador)。

相關文章
相關標籤/搜索