Istio流量管理实现机制深度解析

本文由做者受权,转载自赵化冰的博客node

Istio做为一个service mesh开源项目,其中最重要的功能就是对网格中微服务之间的流量进行管理,包括服务发现,请求路由和服务间的可靠通讯。Istio实现了ser svice mesh的控制面,并整合Envoy开源项目做为数据面的sidecar,一块儿对流量进行控制。git

Istio体系中流量管理配置下发以及流量规则如何在数据面生效的机制相对比较复杂,经过官方文档容易管中窥豹,难以了解其实现原理。本文尝试结合系统架构、配置文件和代码对Istio流量管理的架构和实现机制进行分析,以达到从总体上理解Pilot和Envoy的流量管理机制的目的。github

Pilot高层架构

Istio控制面中负责流量管理的组件为Pilot,Pilot的高层架构以下图所示:web

Pilot Architecture(来自Isio官网文档[1])算法

根据上图,Pilot主要实现了下述功能:docker

统一的服务模型

Pilot定义了网格中服务的标准模型,这个标准模型独立于各类底层平台。因为有了该标准模型,各个不一样的平台能够经过适配器和Pilot对接,将本身特有的服务数据格式转换为标准格式,填充到Pilot的标准模型中。json

例如Pilot中的Kubernetes适配器经过Kubernetes API服务器获得kubernetes中service和pod的相关信息,而后翻译为标准模型提供给Pilot使用。经过适配器模式,Pilot还能够从Mesos, Cloud Foundry, Consul等平台中获取服务信息,还能够开发适配器将其余提供服务发现的组件集成到Pilot中。bootstrap

标准数据面 API

Pilo使用了一套起源于Envoy项目的标准数据面API[2]来将服务信息和流量规则下发到数据面的sidecar中。ubuntu

经过采用该标准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生成初始化配置文件的代码: 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复制代码

配置文件的结构如图所示:

其中各个配置节点的内容以下:

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配置初始化流程:

  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配置文件结构

文件中的配置节点包括:

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配置,完整文件参见: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服务的请求流程:

  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自动注入原理

相关文章
相关标签/搜索