Istio流量管理實現機制深度解析

ServiceMesher發表於2018-10-08

本文由作者授權,轉載自趙化冰的部落格

Istio作為一個service mesh開源專案,其中最重要的功能就是對網格中微服務之間的流量進行管理,包括服務發現,請求路由和服務間的可靠通訊。Istio實現了ser svice mesh的控制面,並整合Envoy開源專案作為資料面的sidecar,一起對流量進行控制。

Istio體系中流量管理配置下發以及流量規則如何在資料面生效的機制相對比較複雜,通過官方文件容易管中窺豹,難以瞭解其實現原理。本文嘗試結合系統架構、配置檔案和程式碼對Istio流量管理的架構和實現機制進行分析,以達到從整體上理解Pilot和Envoy的流量管理機制的目的。

Pilot高層架構

Istio控制面中負責流量管理的元件為Pilot,Pilot的高層架構如下圖所示:

Istio流量管理實現機制深度解析

Pilot Architecture(來自Isio官網文件[1])

根據上圖,Pilot主要實現了下述功能:

統一的服務模型

Pilot定義了網格中服務的標準模型,這個標準模型獨立於各種底層平臺。由於有了該標準模型,各個不同的平臺可以通過介面卡和Pilot對接,將自己特有的服務資料格式轉換為標準格式,填充到Pilot的標準模型中。

例如Pilot中的Kubernetes介面卡通過Kubernetes API伺服器得到kubernetes中service和pod的相關資訊,然後翻譯為標準模型提供給Pilot使用。通過介面卡模式,Pilot還可以從Mesos, Cloud Foundry, Consul等平臺中獲取服務資訊,還可以開發介面卡將其他提供服務發現的元件整合到Pilot中。

標準資料面 API

Pilo使用了一套起源於Envoy專案的標準資料面API[2]來將服務資訊和流量規則下發到資料面的sidecar中。

通過採用該標準API,Istio將控制面和資料面進行了解耦,為多種資料面sidecar實現提供了可能性。事實上基於該標準API已經實現了多種Sidecar代理和Istio的整合,除Istio目前整合的Envoy外,還可以和Linkerd, Nginmesh等第三方通訊代理進行整合,也可以基於該API自己編寫Sidecar實現。

控制面和資料面解耦是Istio後來居上,風頭超過Service mesh鼻祖Linkerd的一招妙棋。Istio站在了控制面的高度上,而Linkerd則成為了可選的一種sidecar實現,可謂降維打擊的一個典型成功案例!

資料面標準API也有利於生態圈的建立,開源,商業的各種sidecar以後可能百花齊放,使用者也可以根據自己的業務場景選擇不同的sidecar和控制面整合,如高吞吐量的,低延遲的,高安全性的等等。有實力的大廠商可以根據該API定製自己的sidecar,例如螞蟻金服開源的Golang版本的Sidecar MOSN(Modular Observable Smart Netstub)(SOFAMesh中Golang版本的Sidecar);小廠商則可以考慮採用成熟的開源專案或者提供服務的商業sidecar實現。

備註:Istio和Envoy專案聯合制定了Envoy V2 API,並採用該API作為Istio控制面和資料面流量管理的標準介面。

業務DSL語言

Pilot還定義了一套DSL(Domain Specific Language)語言,DSL語言提供了面向業務的高層抽象,可以被運維人員理解和使用。運維人員使用該DSL定義流量規則並下發到Pilot,這些規則被Pilot翻譯成資料面的配置,再通過標準API分發到Envoy例項,可以在執行期對微服務的流量進行控制和調整。

Pilot的規則DSL是採用K8S API Server中的Custom Resource (CRD)[3]實現的,因此和其他資源型別如Service Pod Deployment的建立和使用方法類似,都可以用Kubectl進行建立。

通過運用不同的流量規則,可以對網格中微服務進行精細化的流量控制,如按版本分流,斷路器,故障注入,灰度釋出等。

Istio流量管理相關元件

我們可以通過下圖瞭解Istio流量管理涉及到的相關元件。雖然該圖來自Istio Github old pilot repo, 但圖中描述的元件及流程和目前Pilot的最新程式碼的架構基本是一致的。

Istio流量管理實現機制深度解析

Pilot Design Overview (來自Istio old_pilot_repo[4])

圖例說明:圖中紅色的線表示控制流,黑色的線表示資料流。藍色部分為和Pilot相關的元件。

從上圖可以看到,Istio中和流量管理相關的有以下元件:

控制面元件

Discovery Services

對應的docker為gcr.io/istio-release/pilot,程式為pilot-discovery,該元件的功能包括:

  • 從Service provider(如kubernetes或者consul)中獲取服務資訊

  • 從K8S API Server中獲取流量規則(K8S CRD Resource)

  • 將服務資訊和流量規則轉化為資料面可以理解的格式,通過標準的資料面API下發到網格中的各個sidecar中。

K8S API Server

提供Pilot相關的CRD Resource的增、刪、改、查。和Pilot相關的CRD有以下幾種:

  • Virtualservice:用於定義路由規則,如根據來源或 Header 制定規則,或在不同服務版本之間分拆流量。

  • DestinationRule:定義目的服務的配置策略以及可路由子集。策略包括斷路器、負載均衡以及 TLS 等。

  • ServiceEntry:用 ServiceEntry 可以向Istio中加入附加的服務條目,以使網格內可以向istio 服務網格之外的服務發出請求。

  • Gateway:為網格配置閘道器,以允許一個服務可以被網格外部訪問。

  • EnvoyFilter:可以為Envoy配置過濾器。由於Envoy已經支援Lua過濾器,因此可以通過EnvoyFilter啟用Lua過濾器,動態改變Envoy的過濾鏈行為。我之前一直在考慮如何才能動態擴充套件Envoy的能力,EnvoyFilter提供了很靈活的擴充套件性。

資料面元件

在資料面有兩個程式Pilot-agent和envoy,這兩個程式被放在一個docker容器gcr.io/istio-release/proxyv2中。

Pilot-agent

該程式根據K8S API Server中的配置資訊生成Envoy的配置檔案,並負責啟動Envoy程式。注意Envoy的大部分配置資訊都是通過xDS介面從Pilot中動態獲取的,因此Agent生成的只是用於初始化Envoy的少量靜態配置。在後面的章節中,本文將對Agent生成的Envoy配置檔案進行進一步分析。

Envoy

Envoy由Pilot-agent程式啟動,啟動後,Envoy讀取Pilot-agent為它生成的配置檔案,然後根據該檔案的配置獲取到Pilot的地址,通過資料面標準API的xDS介面從pilot拉取動態配置資訊,包括路由(route),監聽器(listener),服務叢集(cluster)和服務端點(endpoint)。Envoy初始化完成後,就根據這些配置資訊對微服務間的通訊進行定址和路由。

命令列工具

kubectl和Istioctl,由於Istio的配置是基於K8S的CRD,因此可以直接採用kubectl對這些資源進行操作。Istioctl則針對Istio對CRD的操作進行了一些封裝。Istioctl支援的功能參見該表格

資料面標準API

前面講到,Pilot採用了一套標準的API來向資料面Sidecar提供服務發現,負載均衡池和路由表等流量管理的配置資訊。該標準API的文件參見Envoy v2 API[5]Data Plane API Protocol Buffer Definition[6])給出了v2 grpc介面相關的資料結構和介面定義。

(備註:Istio早期採用了Envoy v1 API,目前的版本中則使用V2 API,V1已被廢棄)。

基本概念和術語

首先我們需要了解資料面API中涉及到的一些基本概念:

  • Host:能夠進行網路通訊的實體(如移動裝置、伺服器上的應用程式)。在此文件中,主機是邏輯網路應用程式。一塊物理硬體上可能執行有多個主機,只要它們是可以獨立定址的。在EDS介面中,也使用“Endpoint”來表示一個應用例項,對應一個IP+Port的組合。

  • Downstream:下游主機連線到 Envoy,傳送請求並接收響應。

  • Upstream:上游主機接收來自 Envoy 的連線和請求,並返回響應。

  • Listener:監聽器是命名網地址(例如,埠、unix domain socket等),可以被下游客戶端連線。Envoy 暴露一個或者多個監聽器給下游主機連線。在Envoy中,Listener可以繫結到埠上直接對外服務,也可以不繫結到埠上,而是接收其他listener轉發的請求。

  • Cluster:叢集是指 Envoy 連線到的邏輯上相同的一組上游主機。Envoy 通過服務發現來發現叢集的成員。可以選擇通過主動健康檢查來確定叢集成員的健康狀態。Envoy 通過負載均衡策略決定將請求路由到哪個叢集成員。

XDS服務介面

Istio資料面API定義了xDS服務介面,Pilot通過該介面向資料面sidecar下發動態配置資訊,以對Mesh中的資料流量進行控制。xDS中的DS表示discovery service,即發現服務,表示xDS介面使用動態發現的方式提供資料面所需的配置資料。而x則是一個代詞,表示有多種discover service。這些發現服務及對應的資料結構如下:

XDS服務介面的最終一致性考慮

xDS的幾個介面是相互獨立的,介面下發的配置資料是最終一致的。但在配置更新過程中,可能暫時出現各個介面的資料不匹配的情況,從而導致部分流量在更新過程中丟失。

設想這種場景:在CDS/EDS只知道cluster X的情況下,RDS的一條路由配置將指向Cluster X的流量調整到了Cluster Y。在CDS/EDS向Mesh中Envoy提供Cluster Y的更新前,這部分導向Cluster Y的流量將會因為Envoy不知道Cluster Y的資訊而被丟棄。

對於某些應用來說,短暫的部分流量丟失是可以接受的,例如客戶端重試可以解決該問題,並不影響業務邏輯。對於另一些場景來說,這種情況可能無法容忍。可以通過調整xDS介面的更新邏輯來避免該問題,對上面的情況,可以先通過CDS/EDS更新Y Cluster,然後再通過RDS將X的流量路由到Y。

一般來說,為了避免Envoy配置資料更新過程中出現流量丟失的情況,xDS介面應採用下面的順序:

  1. CDS 首先更新Cluster資料(如果有變化)

  2. EDS 更新相應Cluster的Endpoint資訊(如果有變化)

  3. LDS 更新CDS/EDS相應的Listener。

  4. RDS 最後更新新增Listener相關的Route配置。

  5. 刪除不再使用的CDS cluster和 EDS endpoints。

ADS聚合發現服務

保證控制面下發資料一致性,避免流量在配置更新過程中丟失的另一個方式是使用ADS(Aggregated Discovery Services),即聚合的發現服務。ADS通過一個gRPC流來發布所有的配置更新,以保證各個xDS介面的呼叫順序,避免由於xDS介面更新順序導致的配置資料不一致問題。

關於XDS介面的詳細介紹可參考xDS REST and gRPC protocol[7]

Bookinfo 示例程式分析

下面我們以Bookinfo為例對Istio中的流量管理實現機制,以及控制面和資料面的互動進行進一步分析。

Bookinfo程式結構

下圖顯示了Bookinfo示例程式中各個元件的IP地址,埠和呼叫關係,以用於後續的分析。

Istio流量管理實現機制深度解析

xDS介面除錯方法

首先我們看看如何對xDS介面的相關資料進行檢視和分析。Envoy v2介面採用了gRPC,由於gRPC是基於二進位制的RPC協議,無法像V1的REST介面一樣通過curl和瀏覽器進行進行分析。但我們還是可以通過Pilot和Envoy的除錯介面檢視xDS介面的相關資料。

Pilot除錯方法

Pilot在9093埠提供了下述除錯介面[8]下述方法檢視xDS介面相關資料。

PILOT=istio-pilot.istio-system:9093
​
# What is sent to envoy
# Listeners and routes
curl $PILOT/debug/adsz
​
# Endpoints
curl $PILOT/debug/edsz
​
# Clusters
curl $PILOT/debug/cdsz複製程式碼

Envoy除錯方法

Envoy提供了管理介面,預設為localhost的15000埠,可以獲取listener,cluster以及完整的配置資料匯出功能。

kubectl exec productpage-v1-54b8b9f55-bx2dq -c istio-proxy curl http://127.0.0.1:15000/help
  /: Admin home page
  /certs: print certs on machine
  /clusters: upstream cluster status
  /config_dump: dump current Envoy configs (experimental)
  /cpuprofiler: enable/disable the CPU profiler
  /healthcheck/fail: cause the server to fail health checks
  /healthcheck/ok: cause the server to pass health checks
  /help: print out list of admin commands
  /hot_restart_version: print the hot restart compatibility version
  /listeners: print listener addresses
  /logging: query/change logging levels
  /quitquitquit: exit the server
  /reset_counters: reset all counters to zero
  /runtime: print runtime values
  /runtime_modify: modify runtime values
  /server_info: print server version/status information
  /stats: print server stats
  /stats/prometheus: print server stats in prometheus format複製程式碼

進入productpage pod 中的istio-proxy(Envoy) container,可以看到有下面的監聽埠

  • 9080: productpage程式對外提供的服務埠

  • 15001: Envoy的入口監聽器,iptable會將pod的流量匯入該埠中由Envoy進行處理

  • 15000: Envoy管理埠,該埠繫結在本地環回地址上,只能在Pod內訪問。

kubectl exec t productpage-v1-54b8b9f55-bx2dq -c istio-proxy --  netstat -ln
 
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:9080            0.0.0.0:*               LISTEN      -               
tcp        0      0 127.0.0.1:15000         0.0.0.0:*               LISTEN      13/envoy        
tcp        0      0 0.0.0.0:15001           0.0.0.0:*               LISTEN      13/envoy  複製程式碼

Envoy啟動過程分析

Istio通過K8s的Admission webhook[9]機制實現了sidecar的自動注入,Mesh中的每個微服務會被加入Envoy相關的容器。下面是Productpage微服務的Pod內容,可見除productpage之外,Istio還在該Pod中注入了兩個容器gcr.io/istio-release/proxy_init和gcr.io/istio-release/proxyv2。

備註:下面Pod description中只保留了需要關注的內容,刪除了其它不重要的部分。為方便檢視,本文中後續的其它配置檔案以及命令列輸出也會進行類似處理。

ubuntu@envoy-test:~$ kubectl describe pod productpage-v1-54b8b9f55-bx2dq
​
Name:               productpage-v1-54b8b9f55-bx2dq
Namespace:          default
Init Containers:
  istio-init:
    Image:         gcr.io/istio-release/proxy_init:1.0.0
      Args:
      -p
      15001
      -u
      1337
      -m
      REDIRECT
      -i
      *
      -x
​
      -b
      9080,
      -d
​
Containers:
  productpage:
    Image:          istio/examples-bookinfo-productpage-v1:1.8.0
    Port:           9080/TCP
    
  istio-proxy:
    Image:         gcr.io/istio-release/proxyv2:1.0.0
    Args:
      proxy
      sidecar
      --configPath
      /etc/istio/proxy
      --binaryPath
      /usr/local/bin/envoy
      --serviceCluster
      productpage
      --drainDuration
      45s
      --parentShutdownDuration
      1m0s
      --discoveryAddress
      istio-pilot.istio-system:15007
      --discoveryRefreshDelay
      1s
      --zipkinAddress
      zipkin.istio-system:9411
      --connectTimeout
      10s
      --statsdUdpAddress
      istio-statsd-prom-bridge.istio-system:9125
      --proxyAdminPort
      15000
      --controlPlaneAuthPolicy
      NONE複製程式碼

Proxy_init

Productpage的Pod中有一個InitContainer proxy_init,InitContrainer是K8S提供的機制,用於在Pod中執行一些初始化任務.在Initialcontainer執行完畢並退出後,才會啟動Pod中的其它container。

我們看一下proxy_init容器中的內容:

ubuntu@envoy-test:~$ sudo docker inspect gcr.io/istio-release/proxy_init:1.0.0
[
    {
        "RepoTags": [
            "gcr.io/istio-release/proxy_init:1.0.0"
        ],
​
        "ContainerConfig": {
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "ENTRYPOINT [\"/usr/local/bin/istio-iptables.sh\"]"
            ],
            "Entrypoint": [
                "/usr/local/bin/istio-iptables.sh"
            ],
        },
    }
]複製程式碼

從上面的命令列輸出可以看到,Proxy_init中執行的命令是istio-iptables.sh,該指令碼原始碼較長,就不列出來了,有興趣可以在Istio 原始碼倉庫的tools/deb/istio-iptables.sh檢視。

該指令碼的作用是通過配置iptable來劫持Pod中的流量。結合前面Pod中該容器的命令列引數-p 15001,可以得知Pod中的資料流量被iptable攔截,併發向Envoy的15001埠。 -u 1337引數用於排除使用者ID為1337,即Envoy自身的流量,以避免Iptable把Envoy發出的資料又重定向到Envoy,形成死迴圈。

Proxyv2

前面提到,該容器中有兩個程式Pilot-agent和envoy。我們進入容器中看看這兩個程式的相關資訊。

ubuntu@envoy-test:~$ kubectl exec   productpage-v1-54b8b9f55-bx2dq -c istio-proxy -- ps -ef
​
UID        PID  PPID  C STIME TTY          TIME CMD
istio-p+     1     0  0 Sep06 ?        00:00:00 /usr/local/bin/pilot-agent proxy sidecar --configPath /etc/istio/proxy --binaryPath /usr/local/bin/envoy --serviceCluster productpage --drainDuration 45s --parentShutdownDuration 1m0s --discoveryAddress istio-pilot.istio-system:15007 --discoveryRefreshDelay 1s --zipkinAddress zipkin.istio-system:9411 --connectTimeout 10s --statsdUdpAddress istio-statsd-prom-bridge.istio-system:9125 --proxyAdminPort 15000 --controlPlaneAuthPolicy NONE
istio-p+    13     1  0 Sep06 ?        00:47:37 /usr/local/bin/envoy -c /etc/istio/proxy/envoy-rev0.json --restart-epoch 0 --drain-time-s 45 --parent-shutdown-time-s 60 --service-cluster productpage --service-node sidecar~192.168.206.23~productpage-v1-54b8b9f55-bx2dq.default~default.svc.cluster.local --max-obj-name-len 189 -l warn --v2-config-only複製程式碼

Envoy的大部分配置都是dynamic resource,包括網格中服務相關的service cluster, listener, route規則等。這些dynamic resource是通過xDS介面從Istio控制面中動態獲取的。但Envoy如何知道xDS server的地址呢?這是在Envoy初始化配置檔案中以static resource的方式配置的。

Envoy初始配置檔案

Pilot-agent程式根據啟動引數和K8S API Server中的配置資訊生成Envoy的初始配置檔案,並負責啟動Envoy程式。從ps命令輸出可以看到Pilot-agent在啟動Envoy程式時傳入了pilot地址和zipkin地址,併為Envoy生成了一個初始化配置檔案envoy-rev0.json

Pilot agent生成初始化配置檔案的程式碼: github.com/istio/istio… 137行

// WriteBootstrap generates an envoy config based on config and epoch, and returns the filename.
// TODO: in v2 some of the LDS ports (port, http_port) should be configured in the bootstrap.
func WriteBootstrap(config *meshconfig.ProxyConfig, node string, epoch int, pilotSAN []string, opts map[string]interface{}) (string, error) {
    if opts == nil {
        opts = map[string]interface{}{}
    }
    if err := os.MkdirAll(config.ConfigPath, 0700); err != nil {
        return "", err
    }
    // attempt to write file
    fname := configFile(config.ConfigPath, epoch)
​
    cfg := config.CustomConfigFile
    if cfg == "" {
        cfg = config.ProxyBootstrapTemplatePath
    }
    if cfg == "" {
        cfg = DefaultCfgDir
    }
    ......
​
    if config.StatsdUdpAddress != "" {
        h, p, err = GetHostPort("statsd UDP", config.StatsdUdpAddress)
        if err != nil {
            return "", err
        }
        StoreHostPort(h, p, "statsd", opts)
    }
​
    fout, err := os.Create(fname)
    if err != nil {
        return "", err
    }
​
    // Execute needs some sort of io.Writer
    err = t.Execute(fout, opts)
    return fname, err
}複製程式碼

可以使用下面的命令將productpage pod中該檔案匯出來檢視其中的內容:

kubectl exec productpage-v1-54b8b9f55-bx2dq -c istio-proxy -- cat /etc/istio/proxy/envoy-rev0.json > envoy-rev0.json複製程式碼

配置檔案的結構如圖所示:

Istio流量管理實現機制深度解析

其中各個配置節點的內容如下:

Node

包含了Envoy所在節點相關資訊。

"node": {
    "id": "sidecar~192.168.206.23~productpage-v1-54b8b9f55-bx2dq.default~default.svc.cluster.local",
    //用於標識envoy所代理的node(在k8s中對應為Pod)上的service cluster,來自於Envoy程式啟動時的service-cluster引數
    "cluster": "productpage",  
    "metadata": {
          "INTERCEPTION_MODE": "REDIRECT",
          "ISTIO_PROXY_SHA": "istio-proxy:6166ae7ebac7f630206b2fe4e6767516bf198313",
          "ISTIO_PROXY_VERSION": "1.0.0",
          "ISTIO_VERSION": "1.0.0",
          "POD_NAME": "productpage-v1-54b8b9f55-bx2dq",
          "istio": "sidecar"
    }
  }複製程式碼
Admin

配置Envoy的日誌路徑以及管理埠。

"admin": {
    "access_log_path": "/dev/stdout",
    "address": {
      "socket_address": {
        "address": "127.0.0.1",
        "port_value": 15000
      }
    }
  }複製程式碼
Dynamic_resources

配置動態資源,這裡配置了ADS伺服器。

"dynamic_resources": {
    "lds_config": {
        "ads": {}
    },
    "cds_config": {
        "ads": {}
    },
    "ads_config": {
      "api_type": "GRPC",
      "refresh_delay": {"seconds": 1, "nanos": 0},
      "grpc_services": [
        {
          "envoy_grpc": {
            "cluster_name": "xds-grpc"
          }
        }
      ]
    }
  }```複製程式碼
Static_resources

配置靜態資源,包括了xds-grpc和zipkin兩個cluster。其中xds-grpc cluster對應前面dynamic_resources中ADS配置,指明瞭Envoy用於獲取動態資源的伺服器地址。

"static_resources": {
    "clusters": [
    {
    "name": "xds-grpc",
    "type": "STRICT_DNS",
    "connect_timeout": {"seconds": 10, "nanos": 0},
    "lb_policy": "ROUND_ROBIN",
​
    "hosts": [
    {
    "socket_address": {"address": "istio-pilot.istio-system", "port_value": 15010}
    }
    ],
    "circuit_breakers": {
        "thresholds": [
      {
        "priority": "default",
        "max_connections": "100000",
        "max_pending_requests": "100000",
        "max_requests": "100000"
      },
      {
        "priority": "high",
        "max_connections": "100000",
        "max_pending_requests": "100000",
        "max_requests": "100000"
      }]
    },
    "upstream_connection_options": {
      "tcp_keepalive": {
        "keepalive_time": 300
      }
    },
    "http2_protocol_options": { }
    } ,
      {
        "name": "zipkin",
        "type": "STRICT_DNS",
        "connect_timeout": {
          "seconds": 1
        },
        "lb_policy": "ROUND_ROBIN",
        "hosts": [
          {
            "socket_address": {"address": "zipkin.istio-system", "port_value": 9411}
          }
        ]
      }
      
    ]
  }複製程式碼
Tracing

配置分散式鏈路跟蹤。

"tracing": {
    "http": {
      "name": "envoy.zipkin",
      "config": {
        "collector_cluster": "zipkin"
      }
    }
  }複製程式碼
Stats_sinks

這裡配置的是和Envoy直連的metrics收集sink,和Mixer telemetry沒有關係。Envoy自帶stats格式的metrics上報。

"stats_sinks": [
    {
      "name": "envoy.statsd",
      "config": {
        "address": {
          "socket_address": {"address": "10.103.219.158", "port_value": 9125}
        }
      }
    }
  ]複製程式碼

在Gist gist.github.com/zhaohuabing…

Envoy配置分析

通過管理介面獲取完整配置

從Envoy初始化配置檔案中,我們可以大致看到Istio通過Envoy來實現服務發現和流量管理的基本原理。即控制面將xDS server資訊通過static resource的方式配置到Envoy的初始化配置檔案中,Envoy啟動後通過xDS server獲取到dynamic resource,包括網格中的service資訊及路由規則。

Envoy配置初始化流程:

Istio流量管理實現機制深度解析

  1. Pilot-agent根據啟動引數和K8S API Server中的配置資訊生成Envoy的初始配置檔案envoy-rev0.json,該檔案告訴Envoy從xDS server中獲取動態配置資訊,並配置了xDS server的地址資訊,即控制面的Pilot。

  2. Pilot-agent使用envoy-rev0.json啟動Envoy程式。

  3. Envoy根據初始配置獲得Pilot地址,採用xDS介面從Pilot獲取到Listener,Cluster,Route等d動態配置資訊。

  4. Envoy根據獲取到的動態配置啟動Listener,並根據Listener的配置,結合Route和Cluster對攔截到的流量進行處理。

可以看到,Envoy中實際生效的配置是由初始化配置檔案中的靜態配置和從Pilot獲取的動態配置一起組成的。因此只對envoy-rev0 .json進行分析並不能看到Mesh中流量管理的全貌。那麼有沒有辦法可以看到Envoy中實際生效的完整配置呢?答案是可以的,我們可以通過Envoy的管理介面來獲取Envoy的完整配置。

kubectl exec -it productpage-v1-54b8b9f55-bx2dq -c istio-proxy curl http://127.0.0.1:15000/config_dump > config_dump複製程式碼

該檔案內容長達近7000行,本文中就不貼出來了,在Gist gist.github.com/zhaohuabing… 中可以檢視到全文。

Envoy配置檔案結構

Istio流量管理實現機制深度解析

檔案中的配置節點包括:

Bootstrap

從名字可以大致猜出這是Envoy的初始化配置,開啟該節點,可以看到檔案中的內容和前一章節中介紹的envoy-rev0.json是一致的,這裡不再贅述。

Istio流量管理實現機制深度解析

Clusters

在Envoy中,Cluster是一個服務叢集,Cluster中包含一個到多個endpoint,每個endpoint都可以提供服務,Envoy根據負載均衡演算法將請求傳送到這些endpoint中。

在Productpage的clusters配置中包含static_clusters和dynamic_active_clusters兩部分,其中static_clusters是來自於envoy-rev0.json的xDS server和zipkin server資訊。dynamic_active_clusters是通過xDS介面從Istio控制面獲取的動態服務資訊。

Istio流量管理實現機制深度解析

Dynamic Cluster中有以下幾類Cluster:

Outbound Cluster

這部分的Cluster佔了絕大多數,該類Cluster對應於Envoy所在節點的外部服務。以details為例,對於Productpage來說,details是一個外部服務,因此其Cluster名稱中包含outbound字樣。

從details 服務對應的cluster配置中可以看到,其型別為EDS,即表示該Cluster的endpoint來自於動態發現,動態發現中eds_config則指向了ads,最終指向static Resource中配置的xds-grpc cluster,即Pilot的地址。

{
 "version_info": "2018-09-06T09:34:19Z",
 "cluster": {
  "name": "outbound|9080||details.default.svc.cluster.local",
  "type": "EDS",
  "eds_cluster_config": {
   "eds_config": {
    "ads": {}
   },
   "service_name": "outbound|9080||details.default.svc.cluster.local"
  },
  "connect_timeout": "1s",
  "circuit_breakers": {
   "thresholds": [
    {}
   ]
  }
 },
 "last_updated": "2018-09-06T09:34:20.404Z"
}複製程式碼

可以通過Pilot的除錯介面獲取該Cluster的endpoint:

curl http://10.96.8.103:9093/debug/edsz > pilot_eds_dump複製程式碼

匯出的檔案長達1300多行,本文只貼出details服務相關的endpoint配置,完整檔案參見:gist.github.com/zhaohuabing…

從下面的檔案內容可以看到,details cluster配置了1個endpoint地址,是details的pod ip。

{
  "clusterName": "outbound|9080||details.default.svc.cluster.local",
  "endpoints": [
    {
      "locality": {
​
      },
      "lbEndpoints": [
        {
          "endpoint": {
            "address": {
              "socketAddress": {
                "address": "192.168.206.21",
                "portValue": 9080
              }
            }
          },
          "metadata": {
            "filterMetadata": {
              "istio": {
                  "uid": "kubernetes://details-v1-6764bbc7f7-qwzdg.default"
                }
            }
          }
        }
      ]
    }
  ]
}複製程式碼
Inbound Cluster

該類Cluster對應於Envoy所在節點上的服務。如果該服務接收到請求,當然就是一個入站請求。對於Productpage Pod上的Envoy,其對應的Inbound Cluster只有一個,即productpage。該cluster對應的host為127.0.0.1,即環回地址上productpage的監聽埠。由於iptable規則中排除了127.0.0.1,入站請求通過該Inbound cluster處理後將跳過Envoy,直接傳送給Productpage程式處理。

{
   "version_info": "2018-09-14T01:44:05Z",
   "cluster": {
    "name": "inbound|9080||productpage.default.svc.cluster.local",
    "connect_timeout": "1s",
    "hosts": [
     {
      "socket_address": {
       "address": "127.0.0.1",
       "port_value": 9080
      }
     }
    ],
    "circuit_breakers": {
     "thresholds": [
      {}
     ]
    }
   },
   "last_updated": "2018-09-14T01:44:05.291Z"
}複製程式碼
BlackHoleCluster

這是一個特殊的Cluster,並沒有配置後端處理請求的Host。如其名字所暗示的一樣,請求進入後將被直接丟棄掉。如果一個請求沒有找到其對的目的服務,則被髮到cluste。

{
   "version_info": "2018-09-06T09:34:19Z",
   "cluster": {
    "name": "BlackHoleCluster",
    "connect_timeout": "5s"
   },
   "last_updated": "2018-09-06T09:34:20.408Z"
}複製程式碼

Listeners

Envoy採用listener來接收並處理downstream發過來的請求,listener的處理邏輯是外掛式的,可以通過配置不同的filter來插入不同的處理邏輯。Istio就在Envoy中加入了用於policy check和metric report的Mixer filter。

Listener可以繫結到IP Socket或者Unix Domain Socket上,也可以不繫結到一個具體的埠上,而是接收從其他listener轉發來的資料。Istio就是利用了Envoy listener的這一特點實現了將來發向不同服務的請求轉交給不同的listener處理。

Virtual Listener

Envoy建立了一個在15001埠監聽的入口監聽器。Iptable將請求擷取後發向15001埠,該監聽器接收後並不進行業務處理,而是根據請求目的地分發給其他監聽器處理。該監聽器取名為”virtual”(虛擬)監聽器也是這個原因。

Envoy是如何做到按服務分發的呢? 可以看到該Listener的配置項use_original_dest設定為true,該配置要求監聽器將接收到的請求轉交給和請求原目的地址關聯的listener進行處理。

從其filter配置可以看到,如果找不到和請求目的地配置的listener進行轉交,則請求將被髮送到BlackHoleCluster,由於BlackHoleCluster並沒有配置host,因此找不到對應目的地對應監聽器的請求實際上會被丟棄。

    {
     "version_info": "2018-09-06T09:34:19Z",
     "listener": {
      "name": "virtual",
      "address": {
       "socket_address": {
        "address": "0.0.0.0",
        "port_value": 15001
       }
      },
      "filter_chains": [
       {
        "filters": [
         {
          "name": "envoy.tcp_proxy",
          "config": {
           "stat_prefix": "BlackHoleCluster",
           "cluster": "BlackHoleCluster"
          }
         }
        ]
       }
      ],
      "use_original_dst": true
     },
     "last_updated": "2018-09-06T09:34:26.262Z"
    }複製程式碼
Inbound Listener

在Productpage Pod上的Envoy建立了Listener 192.168.206.23_9080,當外部呼叫Productpage服務的請求到達Pod上15001的”Virtual” Listener時,Virtual Listener根據請求目的地匹配到該Listener,請求將被轉發過來。

    {
     "version_info": "2018-09-14T01:44:05Z",
     "listener": {
      "name": "192.168.206.23_9080",
      "address": {
       "socket_address": {
        "address": "192.168.206.23",
        "port_value": 9080
       }
      },
      "filter_chains": [
       {
        "filters": [
         {
          "name": "mixer",
          "config": {
           "transport": {
            "check_cluster": "outbound|9091||istio-policy.istio-system.svc.cluster.local",
            "network_fail_policy": {
             "policy": "FAIL_CLOSE"
            },
            "report_cluster": "outbound|9091||istio-telemetry.istio-system.svc.cluster.local",
            "attributes_for_mixer_proxy": {
             "attributes": {
              "source.uid": {
               "string_value": "kubernetes://productpage-v1-54b8b9f55-bx2dq.default"
              }
             }
            }
           },
           "mixer_attributes": {
            "attributes": {
             "destination.port": {
              "int64_value": "9080"
             },
             "context.reporter.uid": {
              "string_value": "kubernetes://productpage-v1-54b8b9f55-bx2dq.default"
             },
             "destination.namespace": {
              "string_value": "default"
             },
             "destination.ip": {
              "bytes_value": "AAAAAAAAAAAAAP//wKjOFw=="
             },
             "destination.uid": {
              "string_value": "kubernetes://productpage-v1-54b8b9f55-bx2dq.default"
             },
             "context.reporter.kind": {
              "string_value": "inbound"
             }
            }
           }
          }
         },
         {
          "name": "envoy.tcp_proxy",
          "config": {
           "stat_prefix": "inbound|9080||productpage.default.svc.cluster.local",
           "cluster": "inbound|9080||productpage.default.svc.cluster.local"
          }
         }
        ]
       }
      ],
      "deprecated_v1": {
       "bind_to_port": false
      }
     },
     "last_updated": "2018-09-14T01:44:05.754Z"
    }複製程式碼

從上面的配置”bind_to_port”: false可以得知該listener建立後並不會被繫結到tcp埠上直接接收網路上的資料,因此其所有請求都轉發自15001埠。

該listener配置的envoy.tcp_proxy filter對應的cluster為“inbound|9080||productpage.default.svc.cluster.local”,該cluster配置的host為127.0.0.1:9080,因此Envoy會將該請求發向127.0.0.1:9080。由於iptable設定中127.0.0.1不會被攔截,該請求將傳送到Productpage程式的9080埠進行業務處理。

除此以外,Listenter中還包含Mixer filter的配置資訊,配置了策略檢查(Mixer check)和Metrics上報(Mixer report)伺服器地址,以及Mixer上報的一些attribute取值。

Outbound Listener

Envoy為網格中的外部服務按埠建立多個Listener,以用於處理出向請求。

Productpage Pod中的Envoy建立了多個Outbound Listener

  • 0.0.0.0_9080 :處理對details,reviews和rating服務的出向請求

  • 0.0.0.0_9411 :處理對zipkin的出向請求

  • 0.0.0.0_15031 :處理對ingressgateway的出向請求

  • 0.0.0.0_3000 :處理對grafana的出向請求

  • 0.0.0.0_9093 :處理對citadel、galley、pilot、(Mixer)policy、(Mixer)telemetry的出向請求

  • 0.0.0.0_15004 :處理對(Mixer)policy、(Mixer)telemetry的出向請求

  • ……

除了9080這個Listener用於處理應用的業務之外,其他listener都是Istio用於處理自身元件之間通訊使用的,有的控制面元件如Pilot,Mixer對應多個listener,是因為該元件有多個埠提供服務。

我們這裡主要分析一下9080這個業務埠的Listenrer。和Outbound Listener一樣,該Listener同樣配置了”bind_to_port”: false屬性,因此該listener也沒有被繫結到tcp埠上,其接收到的所有請求都轉發自15001埠的Virtual listener。

監聽器name為0.0.0.0_9080,推測其含義應為匹配發向任意IP的9080的請求,從bookinfo程式結構可以看到該程式中的productpage,revirews,ratings,details四個service都是9080埠,那麼Envoy如何區別處理這四個service呢?

首先需要區分入向(傳送給productpage)請求和出向(傳送給其他幾個服務)請求:

  • 發給productpage的入向請求,virtual listener根據其目的IP和Port首先匹配到192.168.206.23_9080這個listener上,不會進入0.0.0.0_9080 listener處理。

  • 從productpage外發給reviews、details和ratings的出向請求,virtual listener無法找到和其目的IP完全匹配的listener,因此根據通配原則轉交給0.0.0.0_9080處理。

備註: 1. 該轉發邏輯為根據Envoy配置進行的推測,並未分析Envoy程式碼進行驗證。歡迎瞭解Envoy程式碼和實現機制的朋友指正。 2.根據業務邏輯,實際上productpage並不會呼叫ratings服務,但Istio並不知道各個業務之間會如何呼叫,因此將所有的服務資訊都下發到了Envoy中。這樣做對效率和效能理論上有一定影響,存在一定的優化空間。

由於對應到reviews、details和Ratings三個服務,當0.0.0.0_9080接收到出向請求後,並不能直接傳送到一個downstream cluster中,而是需要根據請求目的地進行不同的路由。

在該listener的配置中,我們可以看到並沒有像inbound listener那樣通過envoy.tcp_proxy直接指定一個downstream的cluster,而是通過rds配置了一個路由規則9080,在路由規則中再根據不同的請求目的地對請求進行處理。

{
     "version_info": "2018-09-06T09:34:19Z",
     "listener": {
      "name": "0.0.0.0_9080",
      "address": {
       "socket_address": {
        "address": "0.0.0.0",
        "port_value": 9080
       }
      },
      "filter_chains": [
       {
        "filters": [
         {
          "name": "envoy.http_connection_manager",
          "config": {
           "access_log": [
            {
             "name": "envoy.file_access_log",
             "config": {
              "path": "/dev/stdout"
             }
            }
           ],
           "http_filters": [
            {
             "name": "mixer",
             "config": {
              
              ......
​
             }
            },
            {
             "name": "envoy.cors"
            },
            {
             "name": "envoy.fault"
            },
            {
             "name": "envoy.router"
            }
           ],
           "tracing": {
            "operation_name": "EGRESS",
            "client_sampling": {
             "value": 100
            },
            "overall_sampling": {
             "value": 100
            },
            "random_sampling": {
             "value": 100
            }
           },
           "use_remote_address": false,
           "stat_prefix": "0.0.0.0_9080",
           "rds": {
            "route_config_name": "9080",
            "config_source": {
             "ads": {}
            }
           },
           "stream_idle_timeout": "0.000s",
           "generate_request_id": true,
           "upgrade_configs": [
            {
             "upgrade_type": "websocket"
            }
           ]
          }
         }
        ]
       }
      ],
      "deprecated_v1": {
       "bind_to_port": false
      }
     },
     "last_updated": "2018-09-06T09:34:26.172Z"
    },
    複製程式碼

Routes

配置Envoy的路由規則。Istio下發的預設路由規則中對每個埠設定了一個路由規則,根據host來對請求進行路由分發。

下面是9080的路由配置,從檔案中可以看到對應了3個virtual host,分別是details、ratings和reviews,這三個virtual host分別對應到不同的outbound cluster

{
     "version_info": "2018-09-14T01:38:20Z",
     "route_config": {
      "name": "9080",
      "virtual_hosts": [
       {
        "name": "details.default.svc.cluster.local:9080",
        "domains": [
         "details.default.svc.cluster.local",
         "details.default.svc.cluster.local:9080",
         "details",
         "details:9080",
         "details.default.svc.cluster",
         "details.default.svc.cluster:9080",
         "details.default.svc",
         "details.default.svc:9080",
         "details.default",
         "details.default:9080",
         "10.101.163.201",
         "10.101.163.201:9080"
        ],
        "routes": [
         {
          "match": {
           "prefix": "/"
          },
          "route": {
           "cluster": "outbound|9080||details.default.svc.cluster.local",
           "timeout": "0s",
           "max_grpc_timeout": "0s"
          },
          "decorator": {
           "operation": "details.default.svc.cluster.local:9080/*"
          },
          "per_filter_config": {
           "mixer": {
            ......
​
           }
          }
         }
        ]
       },
       {
        "name": "ratings.default.svc.cluster.local:9080",
        "domains": [
         "ratings.default.svc.cluster.local",
         "ratings.default.svc.cluster.local:9080",
         "ratings",
         "ratings:9080",
         "ratings.default.svc.cluster",
         "ratings.default.svc.cluster:9080",
         "ratings.default.svc",
         "ratings.default.svc:9080",
         "ratings.default",
         "ratings.default:9080",
         "10.99.16.205",
         "10.99.16.205:9080"
        ],
        "routes": [
         {
          "match": {
           "prefix": "/"
          },
          "route": {
           "cluster": "outbound|9080||ratings.default.svc.cluster.local",
           "timeout": "0s",
           "max_grpc_timeout": "0s"
          },
          "decorator": {
           "operation": "ratings.default.svc.cluster.local:9080/*"
          },
          "per_filter_config": {
           "mixer": {
           ......
​
            },
            "disable_check_calls": true
           }
          }
         }
        ]
       },
       {
        "name": "reviews.default.svc.cluster.local:9080",
        "domains": [
         "reviews.default.svc.cluster.local",
         "reviews.default.svc.cluster.local:9080",
         "reviews",
         "reviews:9080",
         "reviews.default.svc.cluster",
         "reviews.default.svc.cluster:9080",
         "reviews.default.svc",
         "reviews.default.svc:9080",
         "reviews.default",
         "reviews.default:9080",
         "10.108.25.157",
         "10.108.25.157:9080"
        ],
        "routes": [
         {
          "match": {
           "prefix": "/"
          },
          "route": {
           "cluster": "outbound|9080||reviews.default.svc.cluster.local",
           "timeout": "0s",
           "max_grpc_timeout": "0s"
          },
          "decorator": {
           "operation": "reviews.default.svc.cluster.local:9080/*"
          },
          "per_filter_config": {
           "mixer": {
            ......
​
            },
            "disable_check_calls": true
           }
          }
         }
        ]
       }
      ],
      "validate_clusters": false
     },
     "last_updated": "2018-09-27T07:17:50.242Z"
    }複製程式碼

Bookinfo端到端呼叫分析

通過前面章節對Envoy配置檔案的分析,我們瞭解到Istio控制面如何將服務和路由資訊通過xDS介面下發到資料面中;並介紹了Envoy上生成的各種配置資料的結構,包括listener,cluster,route和endpoint。

下面我們來分析一個端到端的呼叫請求,通過呼叫請求的流程把這些配置串連起來,以從全域性上理解Istio控制面的流量控制是如何在資料面的Envoy上實現的。

下圖描述了一個Productpage服務呼叫Details服務的請求流程:

Istio流量管理實現機制深度解析

  1. Productpage發起對Details的呼叫:http://details:9080/details/0

  2. 請求被Pod的iptable規則攔截,轉發到15001埠。

  3. Envoy的Virtual Listener在15001埠上監聽,收到了該請求。

  4. 請求被Virtual Listener根據原目標IP(通配)和埠(9080)轉發到0.0.0.0_9080這個listener。

    {
     "version_info": "2018-09-06T09:34:19Z",
     "listener": {
      "name": "virtual",
      "address": {
       "socket_address": {
        "address": "0.0.0.0",
        "port_value": 15001
       }
      }
      ......
    ​
      "use_original_dst": true //請求轉發給和原始目的IP:Port匹配的listener
     },複製程式碼
  5. 根據0.0.0.0_9080 listener的http_connection_manager filter配置,該請求採用“9080” route進行分發。

    {
     "version_info": "2018-09-06T09:34:19Z",
     "listener": {
      "name": "0.0.0.0_9080",
      "address": {
       "socket_address": {
        "address": "0.0.0.0",
        "port_value": 9080
       }
      },
      "filter_chains": [
       {
        "filters": [
         {
          "name": "envoy.http_connection_manager",
          "config": {
          ......
    ​
           "rds": {
            "route_config_name": "9080",
            "config_source": {
             "ads": {}
            }
           },
    ​
         }
        ]
       }
      ],
      "deprecated_v1": {
       "bind_to_port": false
      }
     },
     "last_updated": "2018-09-06T09:34:26.172Z"
    },
    ​
    {
     },複製程式碼
  6. “9080”這個route的配置中,host name為details:9080的請求對應的cluster為outbound|9080||details.default.svc.cluster.local

    {
     "version_info": "2018-09-14T01:38:20Z",
     "route_config": {
      "name": "9080",
      "virtual_hosts": [
       {
        "name": "details.default.svc.cluster.local:9080",
        "domains": [
         "details.default.svc.cluster.local",
         "details.default.svc.cluster.local:9080",
         "details",
         "details:9080",
         "details.default.svc.cluster",
         "details.default.svc.cluster:9080",
         "details.default.svc",
         "details.default.svc:9080",
         "details.default",
         "details.default:9080",
         "10.101.163.201",
         "10.101.163.201:9080"
        ],
        "routes": [
         {
          "match": {
           "prefix": "/"
          },
          "route": {
           "cluster": "outbound|9080||details.default.svc.cluster.local",
           "timeout": "0s",
           "max_grpc_timeout": "0s"
          },
            ......
    ​
           }
          }
         }
        ]
       },
           ......
    ​
    {
     },複製程式碼
  7. outbound|9080||details.default.svc.cluster.local cluster為動態資源,通過eds查詢得到其endpoint為192.168.206.21:9080。

    {
    "clusterName": "outbound|9080||details.default.svc.cluster.local",
    "endpoints": [
    {
      "locality": {
    ​
      },
      "lbEndpoints": [
        {
          "endpoint": {
            "address": {
              "socketAddress": {
                "address": "192.168.206.21",
                "portValue": 9080
              }
            }
          },
         ......  
        }
      ]
    }
    ]
    }複製程式碼
  8. 請求被轉發到192.168.206.21,即Details服務所在的Pod,被iptable規則攔截,轉發到15001埠。

  9. Envoy的Virtual Listener在15001埠上監聽,收到了該請求。

  10. 請求被Virtual Listener根據請求原目標地址IP(192.168.206.21)和埠(9080)轉發到192.168.206.21_9080這個listener。

  11. 根據92.168.206.21_9080 listener的http_connection_manager filter配置,該請求對應的cluster為 inbound|9080||details.default.svc.cluster.local 。

    {
     "version_info": "2018-09-06T09:34:16Z",
     "listener": {
      "name": "192.168.206.21_9080",
      "address": {
       "socket_address": {
        "address": "192.168.206.21",
        "port_value": 9080
       }
      },
      "filter_chains": [
       {
        "filters": [
         {
          "name": "envoy.http_connection_manager",
          ......
              
          "route_config": {
            "name": "inbound|9080||details.default.svc.cluster.local",
            "validate_clusters": false,
            "virtual_hosts": [
             {
              "name": "inbound|http|9080",
              "routes": [
                ......
                    
                "route": {
                 "max_grpc_timeout": "0.000s",
                 "cluster": "inbound|9080||details.default.svc.cluster.local",
                 "timeout": "0.000s"
                },
                ......
                    
                "match": {
                 "prefix": "/"
                }
               }
              ],
              "domains": [
               "*"
              ]
             }
            ]
           },
            ......
                
           ]
          }
         }
        ]
       }
      ],
      "deprecated_v1": {
       "bind_to_port": false
      }
     },
     "last_updated": "2018-09-06T09:34:22.184Z"
    }複製程式碼
  12. inbound|9080||details.default.svc.cluster.local cluster配置的host為127.0.0.1:9080。

  13. 請求被轉發到127.0.0.1:9080,即Details服務進行處理。

上述呼叫流程涉及的完整Envoy配置檔案參見:

小結

本文介紹了Istio流量管理相關元件,Istio控制面和資料面之間的標準介面,以及Istio下發到Envoy的完整配置資料的結構和內容。然後通過Bookinfo示例程式的一個端到端呼叫分析了Envoy是如何實現服務網格中服務發現和路由轉發的,希望能幫助大家透過概念更進一步深入理解Istio流量管理的實現機制。

參考資料

  1. Istio Traffic Managment Concept

  2. Data Plane API

  3. kubernetes Custom Resource

  4. Istio Pilot Design Overview

  5. Envoy V2 API Overview

  6. Data Plane API Protocol Buffer Definition

  7. xDS REST and gRPC protocolgithub.com/istio/istio…

  8. Pilot Debug interface

  9. Istio Sidecar自動注入原理


相關文章