Sidecar 流量路由机制分析

流量管理是 Istio 服务网格的一项核心能力,Istio 中的很多功能,包括请求路由,负载均衡,灰度发布,流量镜像等,都是依托于其流量管理的能力实现的。在 Istio 服务网格中,Pilot 提供了控制平面的流量管理接口,而真正的流量路由则是由数据平面的 sidecar 实现的。本节将对 sidecar 的流量路由机制进行分析,以帮助读者理解 Istio 流量管理的实现原理。

备注:本节将对大量 Envoy 的配置文件内容进行分析。文中采用了 json 格式来展示 Envoy 的配置, json 本身并不支持注释,但为了向读者解释配置文件中的各部分内容的作用,本文将采用“// 注释...”的格式添加注释进行说明。另外,为了方便阅读,将重点展示配置中和流量路由相关的部分,省略部分内容。建议读者在阅读本节时参考Github中的完整配置文件,以助于对本文的理解。

基本的概念和术语

为了理解 sidecar 中的流量处理机制,我们需要理解 Envoy 中的一些基本概念。下面介绍了 Envoy 中和流量处理相关的一些术语,如果需要了解更多关于 Envoy 的内容,请参考本书的数据平面章节。

  • Host: 能够进行网络通信的实体(如移动设备、服务器上的应用程序)。在此文档中,host 是一个逻辑上的网络应用程序。一个物理硬件上可能运行有多个 host,只要它们是可以独立寻址的。在 EDS 接口中,也使用 “endpoint” 来表示一个应用实例,对应一个 IP + port 的组合。
  • Downstream: 下游 host 连接到 Envoy,发送请求并接收响应。
  • Upstream: 上游 host 接收来自 Envoy 的连接和请求,并返回响应。
  • Listener: 监听器是一个命名网络地址(例如,端口、unix domain socket等),可以被下游客户端连接。Envoy 中暴露一个或者多个给下游主机连接的监听器。在 Envoy 中,listener 可以绑定到端口上直接对外提供服务,也可以不绑定到端口上,而是接收其他 listener 转发的请求。
  • Cluster: 集群是指 Envoy 连接的一组上游主机,集群中的主机是对等的,对外提供相同的服务,这些主机一起组成了一个可以提供负载均衡和高可用的服务集群。Envoy 通过服务发现来发现集群的成员。可以选择通过主动健康检查来确定集群成员的健康状态。Envoy 通过负载均衡策略决定将请求路由到哪个集群成员。

XDS服务接口

Pilot 通过 xDS 接口向数据平面的 sidecar 下发动态配置信息,以对网格中的数据流量进行控制。xDS 中的 DS 意为 discovery service,即发现服务,表示 xDS 接口使用动态发现的方式提供数据平面所需的配置数据。而 x 则是一个代词,表示有多种 discovery service。本节不对 xDS 接口展开进行描述,关于 xDS 接口的更多内容参见本书的 xDS 章节部分的介绍。

Envoy 配置介绍

Envoy 是一个四层/七层代理,其架构非常灵活,采用了插件式的机制来实现各种功能,可以通过配置的方式对其功能进行定制。Envoy 提供了两种配置的方式:通过配置文件向 Envoy 提供静态配置,或者通过 xDS 接口向 Envoy 下发动态配置。在 Istio 中同时采用了这两种方式对 Envoy 的功能进行设置。本文假设读者对 Envoy 已有基本的了解,如需要了解 Envoy 的更多内容,请参考本书 Envoy 章节部分的介绍。

Envoy 初始化配置文件

Istio 中,Envoy 的大部分配置都来自于控制平面通过 xDS 接口下发的动态配置,包括网格中服务相关的 service cluster, listener, route 规则等。但 Envoy 是如何知道 xDS server 的地址呢?这就是在 Envoy 初始化配置文件中以静态资源的方式配置的。 Sidecar 容器中有一个 pilot-agent 进程,该进程根据启动参数生成 Envoy 的初始配置文件,并采用该配置文件来启动 Envoy 进程。

可以使用下面的命令将productpage pod中该文件导出来查看其中的内容:

kubectl exec productpage-v1-6d8bc58dd7-ts8kw -c istio-proxy cat /etc/istio/proxy/envoy-rev0.json > envoy-rev0.json

该初始化配置文件的结构如下所示:

{
    "node": {...},
    "stats_config": {...},
    "admin": {...},
    "dynamic_resources": {...},
    "static_resources": {...},
    "tracing": {...}
}

该配置文件中包含了下面的内容:

  • node: 包含了 Envoy 所在节点的相关信息,如节点的 id,节点所属的 Kubernetes 集群,节点的 IP 地址,等等。
  • admin: Envoy 的日志路径以及管理端口。
  • dynamic_resources: 动态资源,即来自 xDS 服务器下发的配置。
  • static_resources: 静态资源,包括预置的一些 listener 和 cluster,例如调用跟踪和指标统计使用到的 listener 和 cluster
  • tracing: 分布式调用追踪的相关配置。

Envoy 完整配置

Envoy 初始化配置文件中,我们可以看出 IstioEnvoy sidecar 真正的配置实际上是由两部分组成的。Pilot-agent 在启动 Envoy 时将 xDS server 信息通过静态资源的方式配置到 Envoy 的初始化配置文件中,Envoy 启动后再通过 xDS server 获取网格中的服务信息、路由规则等动态资源。

Envoy 完整配置的生成流程如下图所示:

Envoy 配置生成流程
图 2.3.2.2.1:Envoy 配置生成流程
  1. Pilot-agent 根据启动参数生成 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 等动态配置信息。
  4. Envoy 根据获取到的动态配置启动 Listener,并根据 listener 的配置,结合 route 和 cluster 对拦截到的流量进行处理。

可以看到,Envoy 中实际生效的配置是由初始化配置文件中的静态配置和从 Pilot 获取的动态配置一起组成的。因此只对 envoy-rev0 .json 进行分析并不能看到网络中流量管理的全貌。那么有没有办法可以看到 Envoy 中实际生效的完整配置呢?Envoy 提供了相应的管理接口,我们可以采用下面的命令导出 productpage-v1 服务 sidecar 的完整配置。

kubectl exec -it productpage-v1-6d8bc58dd7-ts8kw -c istio-proxy curl http://127.0.0.1:15000/config_dump > config_dump

该配置文件的内容如下:

{
 "configs": [
  {
   "@type": "type.googleapis.com/envoy.admin.v3.BootstrapConfigDump",
   "bootstrap": {},
   "last_updated": "2020-03-11T08:14:03.630Z"
   },
  {
   "@type": "type.googleapis.com/envoy.admin.v3.ClustersConfigDump",
   "version_info": "2020-03-11T08:14:06Z/23",
   "static_clusters": [...],
   "dynamic_active_clusters": [...]
  },
  {
    "@type": "type.googleapis.com/envoy.admin.v3.ListenersConfigDump",
    "version_info": "2020-03-11T08:13:39Z/22",
    "static_listeners": [...],
    "dynamic_listeners": [...]
  },
  {
   "@type": "type.googleapis.com/envoy.admin.v3.RoutesConfigDump",
   "static_route_configs": [...],
   "dynamic_route_configs": [...],
  },
  {
   "@type": "type.googleapis.com/envoy.admin.v3.SecretsConfigDump",
   "dynamic_active_secrets": [...]
   }
 ]
}

从导出的文件中可以看到 Envoy 中主要由以下几部分内容组成:

  • BootstrapConfigDump: 初始化配置,来自于初始化配置文件中配置的内容。
  • ClustersConfigDump: 集群配置,包括对应于外部服务的 outbound cluster 和 自身所在节点服务的 inbound cluster
  • ListenersConfigDump: 监听器配置,包括用于处理对外业务请求的 outbound listener,处理入向业务请求的 inbound listener,以及作为流量处理入口的 virtual listener。
  • RoutesConfigDump: 路由配置,用于 HTTP 请求的路由处理。
  • SecretsConfigDump: TLS 双向认证相关的配置,包括自身的证书以及用于验证请求方的 CA 根证书。

下面我们对该配置文件中和流量路由相关的配置一一进行详细分析。

Bootstrap

从名字可以看出这是 Envoy 的初始化配置,打开该节点,可以看到其中的内容和 envoy-rev0.json 是一致的,这里不再赘述。 需要注意的是在 bootstrap 部分配置的一些内容也会被用于其他部分,例如 clusters 部分就包含了 bootstrap 中定义的一些静态 cluster 资源。

{
 "@type": "type.googleapis.com/envoy.admin.v3.BootstrapConfigDump",
 "bootstrap": {
  "node": {...},
  "stats_config": {...},
  "admin": {...},
  "dynamic_resources": {...},
  "static_resources": {...},
  "tracing": {...}
 },
 "last_updated": "2020-03-11T08:14:03.630Z"
},

Clusters

这部分配置定义了 Envoy 中所有的 cluster,即服务集群,cluster 中包含一个到多个 endpoint,每个 endpoint 都可以提供服务,Envoy 根据负载均衡算法将请求发送到这些 endpoint 中。

从配置文件结构中可以看到,在 productpage 的 clusters 配置中包含 static_clusters 和 dynamic_active_clusters 两部分,其中 static_clusters 是来自于 envoy-rev0.json 的初始化配置中的 prometheus_stats、xDS server、zipkin server 信息。dynamic_active_clusters 是 Envoy 通过 xDS 接口从 Istio 控制平面获取的服务信息。

其中 dynamic cluster 又分为以下几类:

Outbound Cluster

这部分的 cluster 占了绝大多数,该类 cluster 对应于 Envoy 所在节点的外部服务。以 reviews 为例,对于 productpage 来说,reviews 是一个外部服务,因此其 cluster 名称中包含 outbound 字样。

从 reviews 服务对应的 cluster 配置中可以看到,其类型为 EDS,即表示该 cluster 的 endpoint 来自于动态发现,动态发现中 eds_config 则指向了ads,最终指向 static resource 中配置的 xds-grpc cluster,即 Pilot 的地址。

{
 "version_info": "2020-03-11T08:13:39Z/22",
 "cluster": {
  "@type": "type.googleapis.com/envoy.api.v2.Cluster",
  "name": "outbound|9080||reviews.default.svc.cluster.local",
  "type": "EDS",
  "eds_cluster_config": {
   "eds_config": {
    "ads": {}
   },
   "service_name": "outbound|9080||reviews.default.svc.cluster.local"
  },
  "connect_timeout": "1s",
  "circuit_breakers": {},
  "filters": [],
  "transport_socket_matches": []
 },
 "last_updated": "2020-03-11T08:14:04.664Z"
}

可以通过Pilot的调试接口获取该 cluster 的 endpoint:

curl http://10.97.222.108:15014/debug/edsz > pilot_eds_dump

从导出的文件内容可以看到,reviews cluster 配置了3个 endpoint 地址,是 reviews 的 pod ip。

{
  "clusterName": "outbound|9080||reviews.default.svc.cluster.local",
  "endpoints": [
    {
      "lbEndpoints": [
        {
          "endpoint": {
            "address": {
              "socketAddress": {
                "address": "10.40.0.15",
                "portValue": 9080
              }
            }
          },
          "metadata": {},
          "loadBalancingWeight": 1
        },
        {
          "endpoint": {
            "address": {
              "socketAddress": {
                "address": "10.40.0.16",
                "portValue": 9080
              }
            }
          },
          "metadata": {},
          "loadBalancingWeight": 1
        },
        {
          "endpoint": {
            "address": {
              "socketAddress": {
                "address": "10.40.0.17",
                "portValue": 9080
              }
            }
          },
          "metadata": {},
          "loadBalancingWeight": 1
        }
      ],
      "loadBalancingWeight": 3
    }
  ]
}
Inbound Cluster

对于 Envoy 来说,inbound cluster 对应于入向请求的 upstream 集群, 即 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": "2020-03-11T08:13:39Z/22",
 "cluster": {
  "@type": "type.googleapis.com/envoy.api.v2.Cluster",
  "name": "inbound|9080|http|productpage.default.svc.cluster.local",
  "type": "STATIC",
  "connect_timeout": "1s",
  "circuit_breakers": {
   "thresholds": []
  },
  "load_assignment": {
   "cluster_name": "inbound|9080|http|productpage.default.svc.cluster.local",
   "endpoints": [
    {
     "lb_endpoints": [
      {
       "endpoint": {
        "address": {
         "socket_address": {
          "address": "127.0.0.1",
          "port_value": 9080
         }
        }
       }
      }
     ]
    }
   ]
  }
 },
 "last_updated": "2020-03-11T08:14:04.684Z"
}
BlackHoleCluster

这是一个特殊的 cluster ,其中并没有配置后端处理请求的 host。如其名字所表明的一样,请求进入该 cluster 后如同进入了一个黑洞,将被丢弃掉,而不是发向一个 upstream host。

{
     "version_info": "2020-03-11T08:13:39Z/22",
     "cluster": {
      "@type": "type.googleapis.com/envoy.api.v2.Cluster",
      "name": "BlackHoleCluster",
      "type": "STATIC",
      "connect_timeout": "1s",
      "filters": []
     },
     "last_updated": "2020-03-11T08:14:04.665Z"
PassthroughCluster

cluster 的 type 被设置为 ORIGINAL_DST 类型, 表明任何发向该 cluster 的请求都会被直接发送到其请求中的原始目地的,Envoy 不会对请求进行重新路由,

{
 "version_info": "2020-03-11T08:13:39Z/22",
 "cluster": {
  "@type": "type.googleapis.com/envoy.api.v2.Cluster",
  "name": "PassthroughCluster",
  "type": "ORIGINAL_DST",
  "connect_timeout": "1s",
  "lb_policy": "CLUSTER_PROVIDED",
  "circuit_breakers": {
   "thresholds": []
  },
  "filters": []
 },
 "last_updated": "2020-03-11T08:14:04.666Z"
}

Listeners

Envoy 采用 listener 来接收并处理 downstream 发过来的请求,listener 采用了插件式的架构,可以通过配置不同的 filter 在 listener 中插入不同的处理逻辑。

Listener 可以绑定到 IP Socket 或者 Unix Domain Socket 上,以接收来自客户端的请求;也可以不绑定,而是接收从其他 listener 转发来的数据。Istio 利用了 Envoy listener 的这一特点,通过 VirtualOutboundListener 在一个端口接收所有出向请求,然后再按照请求的端口分别转发给不同的 listener 分别处理。

VirtualOutbound Listener

IstioEnvoy 中配置了一个在 15001 端口监听的虚拟入口监听器。Iptable 规则将 Envoy 所在 pod 的对外请求拦截后发向本地的 15001 端口,该监听器接收后并不进行业务处理,而是根据请求的目的端口分发给其他监听器处理。这就是该监听器取名为 "virtual”(虚拟)监听器的原因。

Envoy 是如何做到按请求的目的端口进行分发的呢? 从下面 VirtualOutbound listener 的配置中可以看到 use_original_dest 属性被设置为 true, 这表示该监听器在接收到来自 downstream 的请求后,会将请求转交给匹配该请求原目的地址的 listener(即名字格式为 0.0.0.0_ 请求目的端口 的 listener)进行处理。

如果在 Enovy 的配置中找不到匹配请求目的端口的 listener,则将会根据 Istio 的 outboundTrafficPolicy 全局配置选项进行处理。存在两种情况:

  • 如果 outboundTrafficPolicy 设置为 ALLOW_ANY:这表明网格允许发向任何外部服务的请求,无论该服务是否在 Pilot 的服务注册表中。在该策略下,Pilot 将会在下发给 Envoy 的 VirtualOutbound listener 加入一个 upstream cluster 为 PassthroughCluster 的 TCP proxy filter,找不到匹配端口 listener 的请求会被该 TCP proxy filter 处理,请求将会被发送到其 IP 头中的原始目的地地址。
  • 如果 outboundTrafficPolicy 设置为 REGISTRY_ONLY:只允许发向 Pilot 服务注册表中存在的服务的对外请求。在该策略下,Pilot 将会在下发给 Enovy 的 VirtualOutbound listener 加入一个 upstream cluster 为 BlackHoleCluster 的 TCP proxy filter,找不到匹配端口 listener 的请求会被该 TCP proxy filter 处理,由于 BlackHoleCluster 中没有配置 upstteam host,请求实际上会被丢弃。

下图是 bookinfo 例子中 productpage 服务中 Enovy Proxy 的 Virutal Outbound Listener 配置。由于 outboundTrafficPolicy 的默认配置为 ALLOW_ANY,因此 listener 的 filterchain 中第二个 filter chain 中是一个 upstream cluster 为 PassthroughCluster 的 TCP proxy filter。注意该 filter 没有 filter_chain_match 匹配条件,因此如果进入该 listener 的请求在配置中找不到匹配其目的端口的 listener,就会缺省进入该 filter 进行处理。

filterchain 中的第一个 filter chain 中是一个 upstream cluster 为 BlackHoleCluster 的 TCP proxy filter,该 filter 设置了 filter_chain_match 匹配条件,只有发向 10.40.0.18 这个 IP 的出向请求才会进入该 filter 处理。10.40.0.18 是 productpage 服务自身的IP地址。该 filter 的目的是为了防止服务向自己发送请求可能导致的死循环。

{
 "name": "virtualOutbound",
 "active_state": {
  "version_info": "2020-03-11T08:13:39Z/22",
  "listener": {
   "@type": "type.googleapis.com/envoy.api.v2.Listener",
   "name": "virtualOutbound",
   "address": {
    "socket_address": {
     "address": "0.0.0.0",
     "port_value": 15001
    }
   },
   "filter_chains": [
    {
     "filter_chain_match": {
      "prefix_ranges": [
       {
        "address_prefix": "10.40.0.18",
        "prefix_len": 32
       }
      ]
     },
     "filters": [
      {
       "name": "envoy.tcp_proxy",
       "typed_config": {
        "@type": "type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy",
        "stat_prefix": "BlackHoleCluster",
        "cluster": "BlackHoleCluster"
       }
      }
     ]
    },
    {
     "filters": [
      {
       "name": "envoy.tcp_proxy",
       "typed_config": {
        "@type": "type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy",
        "stat_prefix": "PassthroughCluster",
        "cluster": "PassthroughCluster",
        "access_log": []
    }
   ],
   "use_original_dst": true,
   "traffic_direction": "OUTBOUND"
  },
  "last_updated": "2020-03-11T08:14:04.929Z"
 }
},
Outbound Listener

Envoy 为网格中的外部服务按端口创建多个 Outbound listener,以用于处理出向请求。bookinfo 示例程序中使用了9080作为微服务的业务端口,因此我们这里主要分析9080这个业务端口的 listenrer。和其他所有 Outbound listener 一样,该 listener 配置了"bind_to_port”: false 属性,因此该 listener 没有被绑定到 tcp 端口上,其接收到的所有请求都转发自15001端口的 Virtual listener。

该listener 的名称为0.0.0.0_9080,因此会匹配发向任意 IP 的9080端口的请求,bookinfo 程序中的 productpage,revirews,ratings,details 四个服务都使用了9080端口,那么 Envoy 如何区别处理这四个服务呢?

备注: 根据业务逻辑,实际上 productpage 并不会调用 ratings 服务,但 Istio 并不知道各个业务之间会如何调用,因此将所有的服务信息都下发到了 Envoy 中。这样做对 Envoy 的内存占用和效率有一定影响,如果希望去掉 Envoy 配置中的无用数据,可以通过 sidecar CRDEnvoy 的 ingress 和 egress service 配置进行调整。

首先,iptables 拦截到 productpage 向外发出的 HTTP 请求,并转发到同一 pod 中的 Envoy sidecar 监听的 15001 virtualOutbound listener 进行处理。 Envoy 根据目的端口匹配到 0.0.0.0_9080 这个 Outbound listener,并转交给该 listener。

如下面的配置所示,当 0.0.0.0_9080 接收到出向请求后,并不会直接发送到一个 downstream cluster,而是配置了一个路由规则 9080,在该路由规则中会根据不同的请求目的地对请求进行处理。

{
 "name": "0.0.0.0_9080",
 "active_state": {
  "version_info": "2020-03-11T08:13:39Z/22",
  "listener": {
   "@type": "type.googleapis.com/envoy.api.v2.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",
       "typed_config": {
        "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager",
        "stat_prefix": "outbound_0.0.0.0_9080",
        "rds": {
         "config_source": {
          "ads": {}
         },
         "route_config_name": "9080"
        },
        ...
       }
      }
     ]
    }
   ],
   "deprecated_v1": {
    "bind_to_port": false
   },
   "traffic_direction": "OUTBOUND"
  },
  "last_updated": "2020-03-11T08:14:04.927Z"
 }
}
VirtualInbound Listener

在较早的版本中,Istio 采用同一个 VirtualListener 在端口 15001 上同时处理入向和出向的请求。该方案存在一些潜在的问题,例如可能会导致出现死循环,参见这个 PR。在 1.4 版本之后,IstioEnvoy 单独创建了 一个 VirtualInboundListener,在 15006 端口监听入向请求,原来的 15001 端口只用于处理出向请求。

另外一个变化是当 VirtualInboundListener 接收到请求后,将直接在 VirtualInboundListener 采用一系列 filterChain 对入向请求进行处理,而不是像 VirtualOutboundListene 一样分发给其它独立的 listener 进行处理。

这样修改后,Envoy 配置中入向和出向的请求处理流程被完全拆分开,请求处理流程更为清晰,可以避免由于配置导致的一些潜在错误。

下图是 bookinfo 例子中 reviews 服务中 Enovy Proxy 的 VirutalInbound listener 配置。 在配置中采用注释标注了各个不同作用的 filter chain。

{
 "name": "virtualInbound",
 "active_state": {
  "version_info": "2020-03-11T08:13:14Z/21",
  "listener": {
   "@type": "type.googleapis.com/envoy.api.v2.Listener",
   "name": "virtualInbound",
   "address": {
    "socket_address": {
     "address": "0.0.0.0",
     "port_value": 15006
    }
   },
   "filter_chains": [
     {...} //passthrough,启用tls
     {...} //passthrough,未启用tls
     {...} //处理发向15020端口的监控检查
     {...} //9080业务端口,启用tls
     {...} //9080业务端口,未启用tls
   ],
   "listener_filters": [
    {
     "name": "envoy.listener.original_dst"
    },
    {
     "name": "envoy.listener.tls_inspector"
    }
   ],
   "listener_filters_timeout": "1s",
   "traffic_direction": "INBOUND",
   "continue_on_listener_filters_timeout": true
  },
  "last_updated": "2020-03-11T08:13:39.372Z"
 }
}

如该配置所示,reviews 服务中一共配置了5个 filter chain,其中最后两个 filter chain 用于业务处理,一个用于处理 HTTPS 请求;一个用于处理 plain HTTP 请求。

除了 TLS 相关的配置以外,这两个业务处理的 filter chain 的处理逻辑是相同的。 我们打开 HTTPS filter chain,以查看其内部的内容:

{
 "filter_chain_match": {
  "prefix_ranges": [
   {
    "address_prefix": "10.40.0.15",
    "prefix_len": 32
   }
  ],
  "destination_port": 9080,
  "application_protocols": [
   "istio-peer-exchange",
   "istio",
   "istio-http/1.0",
   "istio-http/1.1",
   "istio-h2"
  ]
 },
 "filters": [
  {
   "name": "envoy.filters.network.metadata_exchange",
   "config": {
    "protocol": "istio-peer-exchange"
   }
  },
  {
   "name": "envoy.http_connection_manager",
   "typed_config": {
    "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager",
    "stat_prefix": "inbound_10.40.0.15_9080",
    "route_config": {
     "name": "inbound|9080|http|reviews.default.svc.cluster.local",
     "virtual_hosts": [
      {
       "name": "inbound|http|9080",
       "domains": [
        "*"
       ],
       "routes": [
        {
         "match": {
          "prefix": "/"
         },
         "route": {
          "cluster": "inbound|9080|http|reviews.default.svc.cluster.local",
          "timeout": "0s",
          "max_grpc_timeout": "0s"
         },
         "decorator": {
          "operation": "reviews.default.svc.cluster.local:9080/*"
         },
         "name": "default"
        }
       ]
      }
     ],
     "validate_clusters": false
    },
    "http_filters": [
     {
      "name": "envoy.filters.http.wasm",
      ......
     },
     {
      "name": "istio_authn",
      ......
     },
     {
      "name": "envoy.cors"
     },
     {
      "name": "envoy.fault"
     },
     {
      "name": "envoy.filters.http.wasm",
      ......
     {
      "name": "envoy.router"
     }
    ],
    "tracing": {
     ......
    },
    "server_name": "istio-envoy",
    ......
   }
  }
 ],
 },
 "transport_socket": {
  "name": "envoy.transport_sockets.tls",
  "typed_config": {
   "@type": "type.googleapis.com/envoy.api.v2.auth.DownstreamTlsContext",
   "common_tls_context": {
    "alpn_protocols": [
     "istio-peer-exchange",
     "h2",
     "http/1.1"
    ],
    "tls_certificate_sds_secret_configs": [
     {
      "name": "default",
      "sds_config": {
       "api_config_source": {
        "api_type": "GRPC",
        "grpc_services": [
         {
          "envoy_grpc": {
           "cluster_name": "sds-grpc"
          }
         }
        ]
       }
      }
     }
    ],
    "combined_validation_context": {
     "default_validation_context": {},
     "validation_context_sds_secret_config": {
      "name": "ROOTCA",
      "sds_config": {
       "api_config_source": {
        "api_type": "GRPC",
        "grpc_services": [
         {
          "envoy_grpc": {
           "cluster_name": "sds-grpc"
          }
         }
        ]
       }
      }
     }
    }
   },
   "require_client_certificate": true
  }
 }
}

该 filterchain 配置了一个 http_connection_manager filterhttp_connection_manager 中又配置了 wasm、istio_authn、envoy.router 等 http filter,Istio 中提供的一些基础能力,例如安全认证、指标收集、请求限流等,就是通过这些 filter 实现的。请求经过这些 HTTP filter 处理后,最终被转发给 inbound|9080|http|reviews.default.svc.cluster.local 这个 Inbound cluster,该 Inbound cluster 中配置的 Upstream 为 127.0.0.1:9080,因此该请求将发送到和 sidecar 同一个 pod 上的 reviews 服务的 9080 端口上进行业务处理。

transport_socket 部分配置的是 TLS 双向认证所需的证书信息,从配置中可以得知,Envoy 将通过 SDS (Secret Discovery Service) 获取自身的服务器证书和验证客户端证书所需的根证书。

如果一个入向访问的目的端口不能匹配到业务服务的 filterchain,则会进入到 passthrough 的 filter chain 进行处理,该 filter chain 对应的 clusterInboundPassthroughClusterIpv4,结合 iptables 规则, 该 cluster 将会把请求转发到其本地的原始目的端口处理。

Routes

这部分配置是 Envoy 的 HTTP 路由规则。在前面 listener 的分析中,我们看到 Outbound listener 是以端口为最小粒度来进行处理的,而不同的服务可能采用了相同的端口,因此需要通过 Route 来进一步对发向同一目的端口的不同服务的请求进行区分和处理。Istio 在下发给 sidecar 的缺省路由规则中为每个端口设置了一个路由规则,然后再根据 host 来对请求进行路由分发。

下面是 proudctpage 服务中 9080 的路由配置,从文件中可以看到对应了5个 virtual host,分别是 details、productpage、ratings、reviews 和 allow_any,前三个 virtual host 分别对应到不同服务的 outbound cluster。最后一个对应到 PassthroughCluster,即当入向的请求没有找到对应的服务时,也会让其直接通过。

{
 "version_info": "2020-03-11T08:13:39Z/22",
 "route_config": {
  "@type": "type.googleapis.com/envoy.api.v2.RouteConfiguration",
  "name": "9080",
  "virtual_hosts": [
   {
    "name": "allow_any",
    "domains": [
     "*"
    ],
    "routes": [
     {
      "match": {
       "prefix": "/"
      },
      "route": {
       "cluster": "PassthroughCluster",
       "timeout": "0s"
      }
     }
    ]
   },
   {
    "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.96.60.140",
     "10.96.60.140:9080"
    ],
    "routes": [
     {
      "match": {
       "prefix": "/"
      },
      "route": {
       "cluster": "outbound|9080||details.default.svc.cluster.local",
       "timeout": "0s",
       "retry_policy": {
        "retry_on": "connect-failure,refused-stream,unavailable,cancelled,resource-exhausted,retriable-status-codes",
        "num_retries": 2,
        "retry_host_predicate": [
         {
          "name": "envoy.retry_host_predicates.previous_hosts"
         }
        ],
        "host_selection_retry_max_attempts": "5",
        "retriable_status_codes": [
         503
        ]
       },
       "max_grpc_timeout": "0s"
      },
      "decorator": {
       "operation": "details.default.svc.cluster.local:9080/*"
      },
      "name": "default"
     }
    ]
   },
   {
    "name": "productpage.default.svc.cluster.local:9080",
    "domains": [
     "productpage.default.svc.cluster.local",
     ......
    ],
    "routes": [
     {
      "match": {
       "prefix": "/"
      },
      "route": {
       "cluster": "outbound|9080||productpage.default.svc.cluster.local",
       ......
    ]
   },
   {
    "name": "ratings.default.svc.cluster.local:9080",
    "domains": [
     "ratings.default.svc.cluster.local",
     ......
    ],
    "routes": [
     {
      "match": {
       "prefix": "/"
      },
      "route": {
       "cluster": "outbound|9080||ratings.default.svc.cluster.local",
       ......
    ]
   },
   {
    "name": "reviews.default.svc.cluster.local:9080",
    "domains": [
     "reviews.default.svc.cluster.local",
     ......
    ],
    "routes": [
     {
      "match": {
       "prefix": "/"
      },
      "route": {
       "cluster": "outbound|9080||reviews.default.svc.cluster.local",
       ......
},

Bookinfo 端到端调用分析

通过前面对 Envoy 配置文件的分析,我们对 Envoy 上生成的各种配置数据的结构,包括 listener、cluster、route 和 endpoint 有了一定的了解。那么这些配置是如何有机地结合在一起,以对经过网格中的流量进行路由的呢?

下面我们通过 bookinfo 示例程序中一个端到端的调用请求把这些相关的配置串连起来,使用该完整的调用流程来帮助理解 Istio 控制平面的流量控制能力是如何在数据平面的 Envoy 上实现的。

下图描述了 bookinfo 示例程序中 productpage 服务调用 reviews 服务的请求流程:

Bookinfo 服务调用请求流程
图 2.3.2.2.2:Bookinfo 服务调用请求流程
  1. Productpage 发起对 reviews 服务的调用:http://reviews:9080/reviews/0
  2. 请求被 productpage Pod 的 iptable 规则拦截,重定向到本地的 15001 端口。
  3. 在 15001 端口上监听的 VirtualOutbound listener 收到了该请求。
  4. 请求被 VirtualOutbound listener 根据原目标 IP(通配)和端口(9080)转发到 0.0.0.0_9080 这个 outbound listener。

    {
    "name": "virtualOutbound",
    "active_state": {
    "version_info": "2020-03-11T08:13:39Z/22",
    "listener": {
    "@type": "type.googleapis.com/envoy.api.v2.Listener",
    "name": "virtualOutbound",
    "address": {
     "socket_address": {
      "address": "0.0.0.0",
      "port_value": 15001
     }
    },
    ......
    
    "use_original_dst": true,
    "traffic_direction": "OUTBOUND"
    },
    "last_updated": "2020-03-11T08:14:04.929Z"
    }
    
  5. 根据 0.0.0.0_9080 listener 的 http_connection_manager filter 配置,该请求采用 9080 route 进行分发。
    {
      "name": "0.0.0.0_9080",
      "active_state": {
       "version_info": "2020-03-11T08:13:39Z/22",
       "listener": {
        "@type": "type.googleapis.com/envoy.api.v2.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",
            "typed_config": {
             "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager",
             "stat_prefix": "outbound_0.0.0.0_9080",
             "rds": {
              "config_source": {
               "ads": {}
              },
              "route_config_name": "9080"
             },
             "http_filters": [
              {
               "name": "envoy.filters.http.wasm",
               ......
              },
              {
               "name": "istio.alpn",
               ......
              },
              {
               "name": "envoy.cors"
              },
              {
               "name": "envoy.fault"
              },
              {
               "name": "envoy.filters.http.wasm",
               ......
              },
              {
               "name": "envoy.router"
              }
             ],
             "tracing": {
              "client_sampling": {
               "value": 100
              },
              "random_sampling": {
               "value": 100
              },
              "overall_sampling": {
               "value": 100
              }
             },
             ......           
            }
           }
          ]
         }
        ],
        "deprecated_v1": {
         "bind_to_port": false
        },
        "traffic_direction": "OUTBOUND"
       },
       "last_updated": "2020-03-11T08:14:04.927Z"
      }
     },
    
  6. 9080 这个 route 的配置中,host name 为 reviews:9080 的请求对应的 clusteroutbound|9080||reviews.default.svc.cluster.local
    {
    "version_info": "2020-03-11T08:13:39Z/22",
    "route_config": {
    "@type": "type.googleapis.com/envoy.api.v2.RouteConfiguration",
    "name": "9080",
    "virtual_hosts": [
    ......
     "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.102.90.243",
      "10.102.90.243:9080"
     ],
     "routes": [
      {
       "match": {
        "prefix": "/"
       },
       "route": {
        "cluster": "outbound|9080||ratings.default.svc.cluster.local",
        "timeout": "0s",
        "retry_policy": {
         "retry_on": "connect-failure,refused-stream,unavailable,cancelled,resource-exhausted,retriable-status-codes",
         "num_retries": 2,
         "retry_host_predicate": [
          {
           "name": "envoy.retry_host_predicates.previous_hosts"
          }
         ],
         "host_selection_retry_max_attempts": "5",
         "retriable_status_codes": [
          503
         ]
        },
        "max_grpc_timeout": "0s"
       },
       "decorator": {
        "operation": "ratings.default.svc.cluster.local:9080/*"
       },
       "name": "default"
      }
     ]
    },
    {
     "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.107.156.4",
      "10.107.156.4:9080"
     ],
     "routes": [
      {
       "match": {
        "prefix": "/"
       },
       "route": {
        "cluster": "outbound|9080||reviews.default.svc.cluster.local",
        "timeout": "0s",
        "retry_policy": {
         "retry_on": "connect-failure,refused-stream,unavailable,cancelled,resource-exhausted,retriable-status-codes",
         "num_retries": 2,
         "retry_host_predicate": [
          {
           "name": "envoy.retry_host_predicates.previous_hosts"
          }
         ],
         "host_selection_retry_max_attempts": "5",
         "retriable_status_codes": [
          503
         ]
        },
        "max_grpc_timeout": "0s"
       },
       "decorator": {
        "operation": "reviews.default.svc.cluster.local:9080/*"
       },
       "name": "default"
      }
     ]
    }
    ],
    "validate_clusters": false
    },
    "last_updated": "2020-03-11T08:14:04.971Z"
    }
    
  7. outbound|9080||reviews.default.svc.cluster.local cluster 为动态资源,通过 EDS 查询得到该 cluster 中有3个 endpoint。
    {
    "clusterName": "outbound|9080||reviews.default.svc.cluster.local",
    "endpoints": [
     {
       "lbEndpoints": [
         {
           "endpoint": {
             "address": {
               "socketAddress": {
                 "address": "10.40.0.15",
                 "portValue": 9080
               }
             }
           },
           "metadata": {},
           "loadBalancingWeight": 1
         },
         {
           "endpoint": {
             "address": {
               "socketAddress": {
                 "address": "10.40.0.16",
                 "portValue": 9080
               }
             }
           },
           "metadata": {},
           "loadBalancingWeight": 1
         },
         {
           "endpoint": {
             "address": {
               "socketAddress": {
                 "address": "10.40.0.17",
                 "portValue": 9080
               }
             }
           },
           "metadata": {},
           "loadBalancingWeight": 1
         }
       ],
       "loadBalancingWeight": 3
     }
    ]
    }
    
  8. 请求被转发到其中一个 endpoint 10.40.0.15,即 reviews-v1 所在的 Pod
  9. 然后该请求被 iptable 规则拦截,重定向到本地的 15006 端口。
  10. 在 15006 端口上监听的 VirtualInbound listener 收到了该请求。
  11. 根据匹配条件,请求被 VirtualInbound listener 内部配置的 Http connection manager filter 处理,该 filter 设置的路由配置为将其发送给 inbound|9080|http|reviews.default.svc.cluster.local 这个 inbound cluster
    {
    "name": "virtualInbound",
    "active_state": {
    "version_info": "2020-03-11T08:13:14Z/21",
    "listener": {
    "@type": "type.googleapis.com/envoy.api.v2.Listener",
    "name": "virtualInbound",
    "address": {
    "socket_address": {
     "address": "0.0.0.0",
     "port_value": 15006
    }
    },
    "filter_chains": [
    {
     "filter_chain_match": {
      "prefix_ranges": [
       {
        "address_prefix": "10.40.0.15",
        "prefix_len": 32
       }
      ],
      "destination_port": 9080,
      "application_protocols": [
       "istio-peer-exchange",
       "istio",
       "istio-http/1.0",
       "istio-http/1.1",
       "istio-h2"
      ]
     },
     "filters": [
      {
       "name": "envoy.filters.network.metadata_exchange",
       "config": {
        "protocol": "istio-peer-exchange"
       }
      },
      {
       "name": "envoy.http_connection_manager",
       "typed_config": {
        "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager",
        "stat_prefix": "inbound_10.40.0.15_9080",
        "route_config": {
         "name": "inbound|9080|http|reviews.default.svc.cluster.local",
         "virtual_hosts": [
          {
           "name": "inbound|http|9080",
           "domains": [
            "*"
           ],
           "routes": [
            {
             "match": {
              "prefix": "/"
             },
             "route": {
              "cluster": "inbound|9080|http|reviews.default.svc.cluster.local",
              "timeout": "0s",
              "max_grpc_timeout": "0s"
             },
             "decorator": {
              "operation": "reviews.default.svc.cluster.local:9080/*"
             },
             "name": "default"
            }
           ]
          }
         ],
         "validate_clusters": false
        },
        "http_filters": [
         {
          "name": "envoy.filters.http.wasm",
          ......
         },
         {
          "name": "istio_authn",
          ......
         },
         {
          "name": "envoy.cors"
         },
         {
          "name": "envoy.fault"
         },
         {
          "name": "envoy.filters.http.wasm",
          ......
         },
         {
          "name": "envoy.router"
         }
        ],
        ......
       }
      }
     ],
     "metadata": {...},
     "transport_socket": {...}
    ],
    ......
    }
    }
    
  12. inbound|9080|http|reviews.default.svc.cluster.local cluster 配置的 host 为 127.0.0.1:9080
    {
    "version_info": "2020-03-11T08:13:14Z/21",
    "cluster": {
    "@type": "type.googleapis.com/envoy.api.v2.Cluster",
    "name": "inbound|9080|http|reviews.default.svc.cluster.local",
    "type": "STATIC",
    "connect_timeout": "1s",
    "circuit_breakers": {
    "thresholds": [
    {
     "max_connections": 4294967295,
     "max_pending_requests": 4294967295,
     "max_requests": 4294967295,
     "max_retries": 4294967295
    }
    ]
    },
    "load_assignment": {
    "cluster_name": "inbound|9080|http|reviews.default.svc.cluster.local",
    "endpoints": [
    {
     "lb_endpoints": [
      {
       "endpoint": {
        "address": {
         "socket_address": {
          "address": "127.0.0.1",
          "port_value": 9080
         }
        }
       }
      }
     ]
    }
    ]
    }
    },
    "last_updated": "2020-03-11T08:13:39.118Z"
    }
    
  13. 请求被转发到 127.0.0.1:9080,即 reviews 服务进行业务处理。

小结

本节介绍了 Istiosidecar 内部配置的数据结构和内容,并通过 bookinfo 示例程序的一个端到端调用分析了 Envoy 是 如何实现服务网格中的流量路由的。虽然文中未涉及按照版本对请求路由、流量镜像、故障注入、熔断等高级流量管理功能,但读者也可以参考本节介绍的方法对这些功能进行分析,以透过表面的概念更进一步深入理解 Istio 流量管理的实现机制。

Copyright © servicemesher.com 2018-2020 all right reserved,powered by Gitbook Updated at 2020-09-15 15:03:35

results matching ""

    No results matching ""