Istio流量管理之请求路由分析
前面我们了解了 Gateway
和 VirtualService
资源对象的作用,以及它们是如何影响 Envoy 的配置的,那么这些资源对象又是如何影响流量的呢?通过 Istio 如何实现流量管理的呢?
流量管理概述
Istio 的流量路由规则可以很容易的控制服务之间的流量和 API 调用。Istio 简化了服务级别属性的配置,比如熔断器、超时和重试,并且能轻松的设置重要的任务,如 A/B 测试、金丝雀发布、基于流量百分比切分的分阶段发布等。它还提供了开箱即用的故障恢复特性, 有助于增强应用的健壮性,从而更好地应对被依赖的服务或网络发生故障的情况。
为了在网格中路由,Istio 需要知道所有的 endpoint 在哪以及它们属于哪些服务。为了定位到 service registry(服务注册中心),Istio 会连接到一个服务发现系统。如果在 Kubernetes 集群上安装了 Istio,那么它将自动检测该集群中的服务和 endpoint。
请求路由
首先我们来实现下最基本的流量请求路由的功能,这里我们将学习如何将请求动态路由到微服务的多个版本。
我们知道 Bookinfo
示例包含四个独立的微服务,每个微服务都有多个版本。其中 reviews
服务的三个不同版本已经部署并同时运行。我们可以在浏览器中访问 Bookinfo
应用程序并刷新几次。正常会看到三种不同的 reviews
服务版本的输出,有时书评的输出包含星级评分,有时则不包含。这是因为没有明确的默认服务版本可路由,Istio 将以循环方式将请求路由到所有可用版本。
我们首先来将所有流量路由到微服务的 v1
版本,稍后,您将应用规则根据 HTTP 请求 header 的值路由流量。
路由到指定版本
要只路由到一个版本,则需要为微服务设置默认版本的 VirtualService
。
应用规则
Istio 使用 VirtualService
来定义路由规则,只需要应用下面的资源对象即可:
$ kubectl apply -f samples/bookinfo/networking/virtual-service-all-v1.yaml
virtualservice.networking.istio.io/productpage created
virtualservice.networking.istio.io/reviews created
virtualservice.networking.istio.io/ratings created
virtualservice.networking.istio.io/details created
该资源清单中定义了四个 VirtualService
对象,分别是 productpage
、reviews
、ratings
、details
,它们分别对应着 Bookinfo
应用中的四个微服务,完整的清单如下所示:
# virtual-service-all-v1.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: productpage
spec:
hosts:
- productpage
http:
- route:
- destination:
host: productpage
subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reviews
spec:
hosts:
- reviews
http:
- route:
- destination:
host: reviews
subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: ratings
spec:
hosts:
- ratings
http:
- route:
- destination:
host: ratings
subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: details
spec:
hosts:
- details
http:
- route:
- destination:
host: details
subset: v1
---
我们可以看到这里的 VirtualService
对象中都定义了 subset
字段,这个字段就是用来指定微服务的版本的,这里我们将所有的微服务都指定为 v1
版本,这样所有的流量都会被路由到 v1
版本的微服务中,包括 reviews
服务,这样我们就不会再看到星级评分了。
但是如果我们现在直接去访问 Bookinfo
应用的话,是不能正常访问的,因为我们压根就还没指定这些 v1
版本的微服务到底在哪里。
这个时候就需要用到另外一个资源对象 DestinationRule
了,我们需要为每个微服务创建一个 DestinationRule
对象,用来指定这些微服务的实际地址,这样 VirtualService
对象才能将流量路由到这些微服务中。Istio 在 DestinationRule
目标规则中使用 subsets
定义服务的版本,运行以下命令为 Bookinfo
服务创建默认的目标规则即可:
$ kubectl apply -f samples/bookinfo/networking/destination-rule-all.yaml
destinationrule.networking.istio.io/productpage created
destinationrule.networking.istio.io/reviews created
destinationrule.networking.istio.io/ratings created
destinationrule.networking.istio.io/details created
该资源清单中定义了四个 DestinationRule
对象,分别是 productpage
、reviews
、ratings
、details
几个服务的目标规则,它们分别对应着 Bookinfo
应用中的四个微服务,完整的清单如下所示:
# destination-rule-all.yaml
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: productpage
spec:
host: productpage
subsets:
- name: v1
labels:
version: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: reviews
spec:
host: reviews
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
- name: v3
labels:
version: v3
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: ratings
spec:
host: ratings
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
- name: v2-mysql
labels:
version: v2-mysql
- name: v2-mysql-vm
labels:
version: v2-mysql-vm
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: details
spec:
host: details
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
---
现在我们就可以正常访问 Bookinfo 应用了,并且无论刷新多少次,页面的评论部分都不会显示评级星标,这是因为我们将 Istio 配置为将 reviews 服务的所有流量路由到版本 reviews:v1
,而此版本的服务不访问星级评分服务。
这样我们就成功将流量路由到服务的某一个版本上了。
原理分析
前面章节中我们只定义了一个名为 bookinfo
的 VirtualService
资源对象就可以正常访问了:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: bookinfo
namespace: default
spec:
gateways:
- bookinfo-gateway
hosts:
- "*"
http:
- match:
- uri:
exact: /productpage
- uri:
prefix: /static
- uri:
exact: /login
- uri:
exact: /logout
- uri:
prefix: /api/v1/products
route:
- destination:
host: productpage
port:
number: 9080
很明显上面这个虚拟服务对象是我们访问 Bookinfo 应用的入口路由规则,所以这个虚拟服务对象实际上是为 istio-ingressgateway
入口网关服务定义的。 它将所有的流量都路由到了 productpage
这个服务上,而 productpage
这个服务又会去调用其他的服务来获取数据,在 productpage
服务中调用其他微服务 其实就是直接通过服务名称来调用的,比如调用 reviews
服务就是直接通过 reviews:9080
这个服务来调用的,我们可以查看 productpage
的代码来验证这一点:
我们可以再次查看 Bookinfo 在网格内的请求架构图:
当我们在浏览器中访问 http://<gateway url>/productpage
时,请求将进入网格中的 istio-ingressgateway
服务,然后将请求转发到 productpage
服务。productpage
服务将调用 reviews
和 details
服务来填充页面的内容,然后将其返回给用户。(reviews
服务包括 3 个不同版本的应用,可以通过 version
标签区分)
现在我们只想将流量路由到 reviews:v1
版本去,按照传统的方法只需要将 reviews
的 Service 对象去强制关联 version: v1
这个标签即可,现在我们所有的服务都被注入了一个 Envoy 的 Sidecar 代理,通过 Envoy 很容易就可以实现这个路由功能,而相应的在 Istio 中我们只需要通过 VirtualService
和 DestinationRule
这两个资源对象就可以来实现了。上面我们创建的关于 reviews 服务的这两个对象如下所示:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reviews
spec:
hosts:
- reviews
http:
- route:
- destination:
host: reviews
subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: reviews
spec:
host: reviews
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
- name: v3
labels:
version: v3
那么这两个对象是如何来影响 Envoy Sidecar 的呢?前面我们已经分析了流量从 istio-ingressgateway
进来后被路由到了 productpage
服务,那么 productpage
又该如何去访问其他微服务呢?同样我们可以使用 istioctl proxy-config
来查看 productpage
服务的 Envoy 配置。
每个 Envoy Sidecar 都有一个绑定到 0.0.0.0:15001
的监听器,然后利用 IP tables 将 pod 的所有入站和出站流量路由到这里,此监听器会配置一个 useOriginalDst: true
,这意味着它将请求交给最符合请求原始目标的监听器。如果找不到任何匹配的虚拟监听器,它会将请求发送给返回 404 的 BlackHoleCluster
,我们可以查看下 15001 端口的监听器配置:
$ istioctl proxy-config listeners productpage-v1-564d4686f-wwqqf --port 15001 -oyaml
- address:
socketAddress:
address: 0.0.0.0
portValue: 15001
filterChains:
- filterChainMatch:
destinationPort: 15001
filters:
- name: istio.stats
typedConfig:
'@type': type.googleapis.com/stats.PluginConfig
- name: envoy.filters.network.tcp_proxy
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
cluster: BlackHoleCluster
statPrefix: BlackHoleCluster
name: virtualOutbound-blackhole
- filters:
- name: istio.stats
typedConfig:
'@type': type.googleapis.com/stats.PluginConfig
- name: envoy.filters.network.tcp_proxy
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
# ......
cluster: PassthroughCluster
statPrefix: PassthroughCluster
name: virtualOutbound-catchall-tcp
name: virtualOutbound
trafficDirection: OUTBOUND
useOriginalDst: true
实际上我们的请求是到 9080 端口(productpage 服务绑定 9080 端口)的 HTTP 出站请求,这意味着它被切换到 0.0.0.0:9080
虚拟监听器。所以我们查看下 9080 端口的监听器配置:
# productpage 默认访问其他服务的 9080 端口
$ istioctl proxy-config listeners productpage-v1-564d4686f-wwqqf --port 9080 -oyaml
- address:
socketAddress:
address: 0.0.0.0
portValue: 9080
# ......
rds:
configSource:
ads: {}
initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: "9080" # RDS的路由配置名称
# ......
name: 0.0.0.0_9080
trafficDirection: OUTBOUND # 出流量
可以看到此监听器在其配置的 RDS 中查找名为 9080
的路由配置,我们可以使用 istioctl proxy-config routes
命令来查看这个路由配置的详细信息:
# 查看 9080 这个路由配置
$ istioctl proxy-config routes productpage-v1-564d4686f-wwqqf --name 9080 -oyaml
- name: "9080"
virtualHosts:
- domains:
- details.default.svc.cluster.local
- details
- details.default.svc
- details.default
- 10.111.83.224
name: details.default.svc.cluster.local:9080
routes:
- decorator:
operation: details.default.svc.cluster.local:9080/*
match:
prefix: /
metadata:
filterMetadata:
istio:
config: /apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/details
route:
cluster: outbound|9080|v1|details.default.svc.cluster.local
# ......
- domains:
- productpage.default.svc.cluster.local
- productpage
- productpage.default.svc
- productpage.default
- 10.97.120.23
name: productpage.default.svc.cluster.local:9080
routes:
- decorator:
operation: productpage.default.svc.cluster.local:9080/*
match:
prefix: /
name: default
route:
cluster: outbound|9080||productpage.default.svc.cluster.local
# ......
- domains:
- ratings.default.svc.cluster.local
- ratings
- ratings.default.svc
- ratings.default
- 10.101.184.235
name: ratings.default.svc.cluster.local:9080
routes:
- decorator:
operation: ratings.default.svc.cluster.local:9080/*
match:
prefix: /
metadata:
filterMetadata:
istio:
config: /apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/ratings
route:
cluster: outbound|9080|v1|ratings.default.svc.cluster.local
# ......
- domains:
- reviews.default.svc.cluster.local
- reviews
- reviews.default.svc
- reviews.default
- 10.97.120.56
name: reviews.default.svc.cluster.local:9080
routes:
- decorator:
operation: reviews.default.svc.cluster.local:9080/*
match:
prefix: /
metadata:
filterMetadata:
istio:
config: /apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/reviews
route:
cluster: outbound|9080|v1|reviews.default.svc.cluster.local
# ......
- domains:
- '*'
name: allow_any
routes:
- match:
prefix: /
name: allow_any
route:
cluster: PassthroughCluster
# ......
这个路由配置中其实包含了 K8s Service 对象中监听 9080 端口的所有服务,如果没有创建对应的 VirtualService
对象,对应的路由配置就没有 metadata.filterMetadata.istio.config
这个属性。比如现在我们正在通过 productpage
请求前往 reviews
服务,因此 Envoy 将选择我们的请求与域匹配的虚拟主机。一旦在域上匹配,Envoy 会查找与请求匹配的第一条路径,我们这里没有任何高级路由,因此只有一条路由匹配所有内容。这条路由告诉 Envoy 将请求发送到 outbound|9080|v1|reviews.default.svc.cluster.local
集群,因为前面我们创建的 reviews
这个 VirtualService
对象配置了的 destination.subset: v1
,所以这里的集群命名上多了一个 subset
。
需要注意的是我们在 VirtualService
对象里面配置了 destination.subset: v1
,那么必须要有对应的 subset
存在才行,否则不会生成对应的 Envoy 集群配置,那么就不能正常访问该服务了,而该 subset
就是通过前面的 DestinationRule
对象来定义的,现在我们就可以来查看这个集群配置了:
$ istioctl proxy-config cluster productpage-v1-564d4686f-wwqqf --fqdn reviews.default.svc.cluster.local -o yaml
- edsClusterConfig:
edsConfig:
ads: {}
initialFetchTimeout: 0s
resourceApiVersion: V3
serviceName: outbound|9080||reviews.default.svc.cluster.local
lbPolicy: LEAST_REQUEST
metadata:
filterMetadata:
istio:
config: /apis/networking.istio.io/v1alpha3/namespaces/default/destination-rule/reviews
services:
- host: reviews.default.svc.cluster.local
name: reviews
namespace: default
# ......
name: outbound|9080||reviews.default.svc.cluster.local
type: EDS
- edsClusterConfig:
edsConfig:
ads: {}
initialFetchTimeout: 0s
resourceApiVersion: V3
serviceName: outbound|9080|v1|reviews.default.svc.cluster.local
lbPolicy: LEAST_REQUEST
metadata:
filterMetadata:
istio:
config: /apis/networking.istio.io/v1alpha3/namespaces/default/destination-rule/reviews
services:
- host: reviews.default.svc.cluster.local
name: reviews
namespace: default
subset: v1
name: outbound|9080|v1|reviews.default.svc.cluster.local
# ......
type: EDS
- edsClusterConfig:
edsConfig:
ads: {}
initialFetchTimeout: 0s
resourceApiVersion: V3
serviceName: outbound|9080|v2|reviews.default.svc.cluster.local
filters:
- name: istio.metadata_exchange
typedConfig:
'@type': type.googleapis.com/envoy.tcp.metadataexchange.config.MetadataExchange
protocol: istio-peer-exchange
lbPolicy: LEAST_REQUEST
metadata:
filterMetadata:
istio:
config: /apis/networking.istio.io/v1alpha3/namespaces/default/destination-rule/reviews
services:
- host: reviews.default.svc.cluster.local
name: reviews
namespace: default
subset: v2
name: outbound|9080|v2|reviews.default.svc.cluster.local
# ......
type: EDS
- edsClusterConfig:
edsConfig:
ads: {}
initialFetchTimeout: 0s
resourceApiVersion: V3
serviceName: outbound|9080|v3|reviews.default.svc.cluster.local
filters:
- name: istio.metadata_exchange
typedConfig:
'@type': type.googleapis.com/envoy.tcp.metadataexchange.config.MetadataExchange
protocol: istio-peer-exchange
lbPolicy: LEAST_REQUEST
metadata:
filterMetadata:
istio:
config: /apis/networking.istio.io/v1alpha3/namespaces/default/destination-rule/reviews
services:
- host: reviews.default.svc.cluster.local
name: reviews
namespace: default
subset: v3
name: outbound|9080|v3|reviews.default.svc.cluster.local
# ......
type: EDS
从上面配置可以看到里面一共包含了 4 个 reviews
相关的集群,一个是原始的不包含 subset 的,而另外三个就是前面我们在 DestinationRule
对象中配置的 3 个 subset,所以其实 DestinationRule 映射到 Envoy 的配置文件中就是 Cluster。
最后我们同样还可以查看每个集群下面包含的 endpoint 有哪些:
$ istioctl proxy-config endpoint productpage-v1-564d4686f-wwqqf --cluster "outbound|9080||reviews.default.svc.cluster.local" -o yaml
- edsServiceName: outbound|9080||reviews.default.svc.cluster.local
- address:
socketAddress:
address: 10.244.2.84
portValue: 9080
# ......
weight: 1
- address:
socketAddress:
address: 10.244.2.83
portValue: 9080
# ......
weight: 1
- address:
socketAddress:
address: 10.244.2.88
portValue: 9080
# ......
weight: 1
name: outbound|9080||reviews.default.svc.cluster.local
observabilityName: outbound|9080||reviews.default.svc.cluster.local
$ istioctl proxy-config endpoint productpage-v1-564d4686f-wwqqf --cluster "outbound|9080|v1|reviews.default.svc.cluster.local" -o yaml
- edsServiceName: outbound|9080|v1|reviews.default.svc.cluster.local
hostStatuses:
- address:
socketAddress:
address: 10.244.2.84
portValue: 9080
weight: 1
name: outbound|9080|v1|reviews.default.svc.cluster.local
observabilityName: outbound|9080|v1|reviews.default.svc.cluster.local
# 过滤 version=v1 的 reviews pod
$ kubectl get pod -l app=reviews,version=v1 -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
reviews-v1-86896b7648-zjh2n 2/2 Running 4 (5h18m ago) 6d17h 10.244.2.84 node2 <none> <none>
可以看到不包含 subset 的集群下面的 endpoint 其实就是 reviews
这个 Service 对象的 endpoint 集合,包含 subset 就只有和该子集匹配的后端实例了。到了这一步,一切皆明了,后面的事情就跟之前的套路一样了,具体的 Endpoint 对应打了标签 version=v1
的 Pod。
到这里我们是不是就实现了通过 VirtualService
和 DestinationRule
对象将流量路由到了指定的版本上面了,上面的整个过程就是请求从 productpage 到 reviews 的过程,从 reviews 到网格内其他应用的流量与上面类似,就不展开讨论了。
基于用户身份的路由
接下来我们继续更改路由配置,将来自特定用户的所有流量路由到特定服务版本。我们这里将配置来自名为 Jason
的用户的所有流量被路由到服务 reviews:v2
。
注意 Istio 对用户身份没有任何特殊的内置机制,
productpage
服务在所有到reviews
服务的 HTTP 请求中都增加了一个自定义的end-user
请求头来实现该效果:headers['end-user'] = session['user']
。
要实现该功能,只需要创建下面的资源对象即可:
$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-test-v2.yaml
virtualservice.networking.istio.io/reviews configured
该资源清单文件创建了一个如下所示的 VirtualService
资源对象:
# virtual-service-reviews-test-v2.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reviews
spec:
hosts:
- reviews
http:
- match:
- headers:
end-user:
exact: jason
route:
- destination:
host: reviews
subset: v2
- route:
- destination:
host: reviews
subset: v1
该对象设置了一条路由规则,它会根据 productpage 服务发起的请求的 end-user
自定义请求头内容进行匹配,如果有该内容且为 jason
则会将流量路由到 reviews
服务的 v2
版本,其余的还是被路由到 v1
版本去。
现在我们可以前往浏览器访问 Bookinfo 应用,多刷新几次可以看到评论始终访问到的是 v1
版本的服务,即没有星标的:
然后我们点击页面右上角的 Sign in
按钮,使用 jason
进行登录,登录后页面就会出现带有黑色星标的 v2
版本的评论服务,即使多刷新几次依然如此:
如果我们选择使用其他用户进行登录或者注销则星标就会消失,这是因为除了 Jason
之外,所有用户的流量都被路由到 reviews:v1
。
同样的我们可以去查看下对应的 Envoy Sidecar 配置的变化,因为这里我们只更新了一个 VirtualService
对象,所以只会对 Envoy 的路由表产生影响,查看对应的路由配置即可:
$ istioctl proxy-config routes productpage-v1-564d4686f-wwqqf --name 9080 -oyaml
- name: "9080"
validateClusters: false
virtualHosts:
# ......
- domains:
- reviews.default.svc.cluster.local
- reviews
- reviews.default.svc
- reviews.default
- 10.97.120.56
includeRequestAttemptCount: true
name: reviews.default.svc.cluster.local:9080
routes:
- decorator:
operation: reviews.default.svc.cluster.local:9080/*
match:
caseSensitive: true
headers:
- name: end-user
stringMatch:
exact: jason
prefix: /
metadata:
filterMetadata:
istio:
config: /apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/reviews
route:
cluster: outbound|9080|v2|reviews.default.svc.cluster.local
maxGrpcTimeout: 0s
# ......
- decorator:
operation: reviews.default.svc.cluster.local:9080/*
match:
prefix: /
metadata:
filterMetadata:
istio:
config: /apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/reviews
route:
cluster: outbound|9080|v1|reviews.default.svc.cluster.local
maxGrpcTimeout: 0s
# ......
从配置上我们可以看到现在的 Envoy 配置中新增了一条路由规则,如下所示:
match:
caseSensitive: true
headers:
- name: end-user
stringMatch:
exact: jason
prefix: /
route:
cluster: outbound|9080|v2|reviews.default.svc.cluster.local
当请求头中包含 end-user:jason
的时候请求会被路由到 outbound|9080|v2|reviews.default.svc.cluster.local
这个 Envoy Cluster 集群,这个集群就是前面我们通过 DestinationRule
创建的 v2
这个子集,所以最后请求会被路由到带有黑色星标的评论服务去。
到这里我们就明白了要通过 Istio 实现服务的流量管理,需要用到 Gateway
、VirtualService
、DestinationRule
三个 CRD 对象,这些对象其实最终都是去拼凑 Envoy 的配置,每个对象管理 Envoy 配置的一部分,把这个关系搞清楚我们就能更好的掌握 Istio 的使用了。