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


 

https://zhaohuabing.com/post/2018-09-25-istio-traffic-management-impl-intro/TOC

前言

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

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

Pilot高層架構

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

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的最新代碼的架構基本是一致的。

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地址,端口和調用關系,以用於后續的分析。

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生成初始化配置文件的代碼: https://github.com/istio/istio/blob/release-1.0/pkg/bootstrap/bootstrap_config.go 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

配置文件的結構如圖所示:

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

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 https://gist.github.com/zhaohuabing/14191bdcf72e37bf700129561c3b41ae中可以查看該配置文件的完整內容。

Envoy配置分析

通過管理接口獲取完整配置

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

Envoy配置初始化流程:

  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 https://gist.github.com/zhaohuabing/034ef87786d290a4e89cd6f5ad6fcc97 中可以查看到全文。

Envoy配置文件結構

文件中的配置節點包括:

Bootstrap

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

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控制面獲取的動態服務信息。

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配置,完整文件參見:https://gist.github.com/zhaohuabing/a161d2f64746acd18097b74e6a5af551

從下面的文件內容可以看到,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服務的請求流程:

  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 protocolhttps://github.com/istio/istio/tree/master/pilot/pkg/proxy/envoy/v2
  8. Pilot Debug interface
  9. Istio Sidecar自動注入原理


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM