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

 

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

前言

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

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

Pilot高層架構

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

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

 

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

統一的服務模型

Pilot定義了網格中服務的標準模型,這個標準模型獨立於各類底層平臺。因爲有了該標準模型,各個不一樣的平臺能夠經過適配器和Pilot對接,將本身特有的服務數據格式轉換爲標準格式,填充到Pilot的標準模型中。算法

例如Pilot中的Kubernetes適配器經過Kubernetes API服務器獲得kubernetes中service和pod的相關信息,而後翻譯爲標準模型提供給Pilot使用。經過適配器模式,Pilot還能夠從Mesos, Cloud Foundry, Consul等平臺中獲取服務信息,還能夠開發適配器將其餘提供服務發現的組件集成到Pilot中。docker

標準數據面 API

Pilo使用了一套起源於Envoy項目的標準數據面API[2]來將服務信息和流量規則下發到數據面的sidecar中。json

經過採用該標準API,Istio將控制面和數據面進行了解耦,爲多種數據面sidecar實現提供了可能性。事實上基於該標準API已經實現了多種Sidecar代理和Istio的集成,除Istio目前集成的Envoy外,還能夠和Linkerd, Nginmesh等第三方通訊代理進行集成,也能夠基於該API本身編寫Sidecar實現。bootstrap

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

數據面標準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自動注入原理
相關文章
相關標籤/搜索