Istio流量管理之请求路由分析

k8s技术圈

共 33664字,需浏览 68分钟

 ·

2023-11-12 08:46

前面我们了解了 GatewayVirtualService 资源对象的作用,以及它们是如何影响 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 对象,分别是 productpagereviewsratingsdetails,它们分别对应着 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 版本的微服务到底在哪里。

bookinfo error

这个时候就需要用到另外一个资源对象 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 对象,分别是 productpagereviewsratingsdetails 几个服务的目标规则,它们分别对应着 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,而此版本的服务不访问星级评分服务。

v1版本review

这样我们就成功将流量路由到服务的某一个版本上了。

原理分析

前面章节中我们只定义了一个名为 bookinfoVirtualService 资源对象就可以正常访问了:

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 的代码来验证这一点:

productpage

我们可以再次查看 Bookinfo 在网格内的请求架构图:

BookInfo 架构

当我们在浏览器中访问 http://<gateway url>/productpage 时,请求将进入网格中的 istio-ingressgateway 服务,然后将请求转发到 productpage 服务。productpage 服务将调用 reviewsdetails 服务来填充页面的内容,然后将其返回给用户。(reviews 服务包括 3 个不同版本的应用,可以通过 version 标签区分)

现在我们只想将流量路由到 reviews:v1 版本去,按照传统的方法只需要将 reviews 的 Service 对象去强制关联 version: v1 这个标签即可,现在我们所有的服务都被注入了一个 Envoy 的 Sidecar 代理,通过 Envoy 很容易就可以实现这个路由功能,而相应的在 Istio 中我们只需要通过 VirtualServiceDestinationRule 这两个资源对象就可以来实现了。上面我们创建的关于 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。

到这里我们是不是就实现了通过 VirtualServiceDestinationRule 对象将流量路由到了指定的版本上面了,上面的整个过程就是请求从 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 版本的服务,即没有星标的:

Bookinfo

然后我们点击页面右上角的 Sign in 按钮,使用 jason 进行登录,登录后页面就会出现带有黑色星标的 v2 版本的评论服务,即使多刷新几次依然如此:

Bookinfo jason

如果我们选择使用其他用户进行登录或者注销则星标就会消失,这是因为除了 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 这个子集,所以最后请求会被路由到带有黑色星标的评论服务去。

Kiali Dashboard

到这里我们就明白了要通过 Istio 实现服务的流量管理,需要用到 GatewayVirtualServiceDestinationRule 三个 CRD 对象,这些对象其实最终都是去拼凑 Envoy 的配置,每个对象管理 Envoy 配置的一部分,把这个关系搞清楚我们就能更好的掌握 Istio 的使用了。

浏览 1743
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报