3、Istio流量管理
Istio流量管理
目录
[toc]
本节实战
实战名称 |
---|
🚩 实战:路由到版本1-2023.11.11(测试成功) |
🚩 实战:基于用户身份的路由-2023.11.11(测试成功) |
🚩 实战:注入 HTTP 延迟故障-2023.11.12(测试成功) |
🚩 实战:注入 HTTP abort 故障-2023.11.12(测试成功) |
🚩 实战:流量拆分-2023.11.12(测试成功) |
🚩 实战:流量镜像-2023.11.12(测试成功) |
🚩 实战:熔断示例-2023.11.13(测试成功) |
🚩 实战:TCP 流量拆分-2023.11.15(测试成功) |
流量管理概述
上面我们了解了 Gateway
和 VirtualService
资源对象的作用,以及它们是如何影响 Envoy 的配置的,那么这些资源对象又是如何影响流量的呢?通过 Istio 如何实现流量管理的呢?
Istio 的流量路由规则可以很容易的控制服务之间的流量和 API 调用。Istio 简化了服务级别属性的配置,比如熔断器、超时和重试,并且能轻松的设置重要的任务,如 A/B 测试、金丝雀发布、基于流量百分比切分的分阶段发布等。它还提供了开箱即用的故障恢复特性, 有助于增强应用的健壮性,从而更好地应对被依赖的服务或网络发生故障的情况。
为了在网格中路由,Istio 需要知道所有的 endpoint 在哪以及它们属于哪些服务。为了定位到 service registry(服务注册中心),Istio 会连接到一个服务发现系统。如果在 Kubernetes 集群上安装了 Istio,那么它将自动检测该集群中的服务和 endpoint。
使用此服务注册中心,Envoy 代理可以将流量定向到相关服务。大多数基于微服务的应用程序,每个服务的工作负载都有多个实例来处理流量,称为负载均衡池。默认情况下,Envoy 代理基于轮询调度模型在服务的负载均衡池内分发流量,按顺序将请求发送给池中每个成员, 一旦所有服务实例均接收过一次请求后,就重新回到第一个池成员。
Istio 基本的服务发现和负载均衡能力提供了一个可用的服务网格,但它能做到的远比这多的多。在许多情况下我们可能希望对网格的流量情况进行更细粒度的控制。作为 A/B 测试的一部分,可能想将特定百分比的流量定向到新版本的服务,或者为特定的服务实例子集应用不同的负载均衡策略。可能还想对进出网格的流量应用特殊的规则,或者将网格的外部依赖项添加到服务注册中心。通过使用 Istio 的流量管理 API 将流量配置添加到 Istio,就可以完成所有这些甚至更多的工作。
请求路由
首先我们来实现下最基本的流量请求路由的功能,这里我们将学习如何将请求动态路由到微服务的多个版本。
我们知道 Bookinfo
示例包含四个独立的微服务,每个微服务都有多个版本。其中 reviews
服务的三个不同版本已经部署并同时运行。我们可以在浏览器中访问 Bookinfo
应用程序并刷新几次。正常会看到三种不同的 reviews
服务版本的输出,有时书评的输出包含星级评分,有时则不包含。这是因为没有明确的默认服务版本可路由,Istio 将以循环方式将请求路由到所有可用版本。
我们首先来将所有流量路由到微服务的 v1
版本,稍后,您将应用规则根据 HTTP 请求 header 的值路由流量。
1.路由到版本1
==🚩 实战:路由到版本1-2023.11.11(测试成功)==
实验环境:
k8sv1.27.6(containerd:[root@master1 ~]#istioctl versionclientversion:1.19.3controlplaneversion:1.19.3dataplaneversion:1.19.3(8 proxies)
实验软件:
链接:https:A[实战步骤] -->B(1️⃣ 部署VirtualService)A[实战步骤] -->C(2️⃣ DestinationRule)A[实战步骤] -->D(3️⃣ 验证)
要只路由到一个版本,则需要为微服务设置默认版本的 VirtualService
。
🍀
应用规则
Istio 使用 VirtualService
来定义路由规则,只需要应用下面的资源对象即可:
$kubectlapply-fsamples/bookinfo/networking/virtual-service-all-v1.yamlvirtualservice.networking.istio.io/productpagecreatedvirtualservice.networking.istio.io/reviewscreatedvirtualservice.networking.istio.io/ratingscreatedvirtualservice.networking.istio.io/detailscreated
该资源清单中定义了四个 VirtualService
对象,分别是 productpage
、reviews
、ratings
、details
,它们分别对应着 Bookinfo
应用中的四个微服务,完整的清单如下所示:
# virtual-service-all-v1.yamlapiVersion:networking.istio.io/v1alpha3kind:VirtualServicemetadata:name:productpagespec:hosts:- productpagehttp:- route:- destination:host:productpagesubset:v1---apiVersion:networking.istio.io/v1alpha3kind:VirtualServicemetadata:name:reviewsspec:hosts:- reviewshttp:- route:- destination:host:reviewssubset:v1---apiVersion:networking.istio.io/v1alpha3kind:VirtualServicemetadata:name:ratingsspec:hosts:- ratingshttp:- route:- destination:host:ratingssubset:v1---apiVersion:networking.istio.io/v1alpha3kind:VirtualServicemetadata:name:detailsspec:hosts:- detailshttp:- route:- destination:host:detailssubset:v1---
我们可以看到这里的 VirtualService
对象中都定义了 subset
字段,这个字段就是用来指定微服务的版本的,这里我们将所有的微服务都指定为 v1
版本,这样所有的流量都会被路由到 v1
版本的微服务中,包括 reviews
服务,这样我们就不会再看到星级评分了。
🍀
但是如果我们现在直接去访问 Bookinfo
应用的话,是不能正常访问的,因为我们压根就还没指定这些 v1
版本的微服务到底在哪里。
🍀
这个时候就需要用到另外一个资源对象 DestinationRule
了,我们需要为每个微服务创建一个 DestinationRule
对象,用来指定这些微服务的实际地址,这样 VirtualService
对象才能将流量路由到这些微服务中。Istio 在 DestinationRule
目标规则中使用 subsets
定义服务的版本,运行以下命令为 Bookinfo
服务创建默认的目标规则即可:
$kubectlapply-fsamples/bookinfo/networking/destination-rule-all.yamldestinationrule.networking.istio.io/productpagecreateddestinationrule.networking.istio.io/reviewscreateddestinationrule.networking.istio.io/ratingscreateddestinationrule.networking.istio.io/detailscreated
该资源清单中定义了四个 DestinationRule
对象,分别是 productpage
、reviews
、ratings
、details
几个服务的目标规则,它们分别对应着 Bookinfo
应用中的四个微服务,完整的清单如下所示:
# destination-rule-all.yamlapiVersion:networking.istio.io/v1alpha3kind:DestinationRulemetadata:name:productpagespec:host:productpagesubsets:- name:v1labels:version:v1---apiVersion:networking.istio.io/v1alpha3kind:DestinationRulemetadata:name:reviewsspec:host:reviewssubsets:- name:v1labels:version:v1- name:v2labels:version:v2- name:v3labels:version:v3---apiVersion:networking.istio.io/v1alpha3kind:DestinationRulemetadata:name:ratingsspec:host:ratingssubsets:- name:v1labels:version:v1- name:v2labels:version:v2- name:v2-mysqllabels:version:v2-mysql- name:v2-mysql-vmlabels:version:v2-mysql-vm---apiVersion:networking.istio.io/v1alpha3kind:DestinationRulemetadata:name:detailsspec:host:detailssubsets:- name:v1labels:version:v1- name:v2labels:version:v2---
🍀
现在我们就可以正常访问 Bookinfo 应用了,并且无论刷新多少次,页面的评论部分都不会显示评级星标,这是因为我们将 Istio 配置为将 reviews 服务的所有流量路由到版本 reviews:v1
,而此版本的服务不访问星级评分服务。
这样我们就成功将流量路由到服务的某一个版本上了。
原理分析
🍀
前面章节中我们只定义了一个名为 bookinfo
的 VirtualService
资源对象就可以正常访问了:
apiVersion:networking.istio.io/v1beta1kind:VirtualServicemetadata:name:bookinfonamespace:defaultspec:gateways:- bookinfo-gatewayhosts:- "*"http:- match:- uri:exact:/productpage- uri:prefix:/static- uri:exact:/login- uri:exact:/logout- uri:prefix:/api/v1/productsroute:- destination:host:productpageport:number:9080
很明显上面这个虚拟服务对象是我们访问 Bookinfo 应用的入口路由规则,所以这个虚拟服务对象实际上是为 istio-ingressgateway
入口网关服务定义的。 它将所有的流量都路由到了 productpage
这个服务上,而 productpage
这个服务又会去调用其他的服务来获取数据,在 productpage
服务中调用其他微服务其实就是直接通过服务名称来调用的,比如调用 reviews
服务就是直接通过 reviews:9080
这个服务来调用的,我们可以查看 productpage
的代码来验证这一点:
🍀
我们可以再次查看 Bookinfo 在网格内的请求架构图:
当我们在浏览器中访问 http:kind:VirtualServicemetadata:name:reviewsspec:hosts:- reviewshttp:- route:- destination:host:reviewssubset:v1---apiVersion:networking.istio.io/v1alpha3kind:DestinationRulemetadata:name:reviewsspec:host:reviewssubsets:- name:v1labels:version:v1- name:v2labels:version:v2- name:v3labels: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 端口的监听器配置:
[root@master1 istio-1.19.3]#istioctl proxy-config listeners productpage-v1-564d4686f-7vhks --port 15001 -oyaml- accessLog:- filter:responseFlagFilter:flags:- NRname:envoy.access_loggers.filetypedConfig:'@type':type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLoglogFormat:textFormatSource:inlineString:|[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%"%RESPONSE_CODE% %RESPONSE_FLAGS% %RESPONSE_CODE_DETAILS% %CONNECTION_TERMINATION_DETAILS% "%UPSTREAM_TRANSPORT_FAILURE_REASON%"%BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%""%REQ(USER-AGENT)%""%REQ(X-REQUEST-ID)%""%REQ(:AUTHORITY)%""%UPSTREAM_HOST%"%UPSTREAM_CLUSTER% %UPSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_REMOTE_ADDRESS% %REQUESTED_SERVER_NAME% %ROUTE_NAME%path:/dev/stdoutaddress:socketAddress:address:0.0.0.0portValue:15001filterChains:- filterChainMatch:destinationPort:15001filters:- name:istio.statstypedConfig:'@type':type.googleapis.com/stats.PluginConfig- name:envoy.filters.network.tcp_proxytypedConfig:'@type':type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxycluster:BlackHoleClusterstatPrefix:BlackHoleClustername:virtualOutbound-blackhole- filters:- name:istio.statstypedConfig:'@type':type.googleapis.com/stats.PluginConfig- name:envoy.filters.network.tcp_proxytypedConfig:'@type':type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxyaccessLog:- name:envoy.access_loggers.filetypedConfig:'@type':type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLoglogFormat:textFormatSource:inlineString:|[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%"%RESPONSE_CODE% %RESPONSE_FLAGS% %RESPONSE_CODE_DETAILS% %CONNECTION_TERMINATION_DETAILS% "%UPSTREAM_TRANSPORT_FAILURE_REASON%"%BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%""%REQ(USER-AGENT)%""%REQ(X-REQUEST-ID)%""%REQ(:AUTHORITY)%""%UPSTREAM_HOST%"%UPSTREAM_CLUSTER% %UPSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_REMOTE_ADDRESS% %REQUESTED_SERVER_NAME% %ROUTE_NAME%path:/dev/stdoutcluster:PassthroughClusterstatPrefix:PassthroughClustername:virtualOutbound-catchall-tcpname:virtualOutboundtrafficDirection:OUTBOUNDuseOriginalDst:true[root@master1 istio-1.19.3]#
🍀
实际上我们的请求是到 9080 端口(productpage 服务绑定 9080 端口)的 HTTP 出站请求,这意味着它被切换到 0.0.0.0:9080
虚拟监听器。所以我们查看下 9080 端口的监听器配置:
# productpage 默认访问其他服务的 9080 端口$istioctlproxy-configlistenersproductpage-v1-564d4686f-wwqqf--port9080-oyaml-address:socketAddress:address:0.0.0.0portValue:9080# ......rds:configSource:ads:{}initialFetchTimeout:0sresourceApiVersion:V3routeConfigName:"9080"# RDS的路由配置名称# ......name:0.0.0.0_9080trafficDirection:OUTBOUND# 出流量
可以看到此监听器在其配置的 RDS 中查找名为 9080
的路由配置,我们可以使用 istioctl proxy-config routes
命令来查看这个路由配置的详细信息:
# 查看 9080 这个路由配置$istioctlproxy-configroutesproductpage-v1-564d4686f-wwqqf--name9080-oyaml-name:"9080"virtualHosts:-domains:-details.default.svc.cluster.local-details-details.default.svc-details.default-10.111.83.224name:details.default.svc.cluster.local:9080routes:-decorator:operation:details.default.svc.cluster.local:9080/*match:prefix:/metadata:filterMetadata:istio:config:/apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/detailsroute:cluster:outbound|9080|v1|details.default.svc.cluster.local# ......-domains:-productpage.default.svc.cluster.local-productpage-productpage.default.svc-productpage.default-10.97.120.23name:productpage.default.svc.cluster.local:9080routes:-decorator:operation:productpage.default.svc.cluster.local:9080/*match:prefix:/name:defaultroute:cluster:outbound|9080||productpage.default.svc.cluster.local# ......-domains:-ratings.default.svc.cluster.local-ratings-ratings.default.svc-ratings.default-10.101.184.235name:ratings.default.svc.cluster.local:9080routes:-decorator:operation:ratings.default.svc.cluster.local:9080/*match:prefix:/metadata:filterMetadata:istio:config:/apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/ratingsroute:cluster:outbound|9080|v1|ratings.default.svc.cluster.local# ......-domains:-reviews.default.svc.cluster.local-reviews-reviews.default.svc-reviews.default-10.97.120.56name:reviews.default.svc.cluster.local:9080routes:-decorator:operation:reviews.default.svc.cluster.local:9080/*match:prefix:/metadata:filterMetadata:istio:config:/apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/reviewsroute:cluster:outbound|9080|v1|reviews.default.svc.cluster.local# ......-domains:-'*'name:allow_anyroutes:-match:prefix:/name:allow_anyroute: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
对象来定义的,现在我们就可以来查看这个集群配置了:
$istioctlproxy-configclusterproductpage-v1-564d4686f-wwqqf--fqdnreviews.default.svc.cluster.local-oyaml-edsClusterConfig:edsConfig:ads:{}initialFetchTimeout:0sresourceApiVersion:V3serviceName:outbound|9080||reviews.default.svc.cluster.locallbPolicy:LEAST_REQUESTmetadata:filterMetadata:istio:config:/apis/networking.istio.io/v1alpha3/namespaces/default/destination-rule/reviewsservices:-host:reviews.default.svc.cluster.localname:reviewsnamespace:default# ......name:outbound|9080||reviews.default.svc.cluster.localtype:EDS-edsClusterConfig:edsConfig:ads:{}initialFetchTimeout:0sresourceApiVersion:V3serviceName:outbound|9080|v1|reviews.default.svc.cluster.locallbPolicy:LEAST_REQUESTmetadata:filterMetadata:istio:config:/apis/networking.istio.io/v1alpha3/namespaces/default/destination-rule/reviewsservices:-host:reviews.default.svc.cluster.localname:reviewsnamespace:defaultsubset:v1name:outbound|9080|v1|reviews.default.svc.cluster.local# ......type:EDS-edsClusterConfig:edsConfig:ads:{}initialFetchTimeout:0sresourceApiVersion:V3serviceName:outbound|9080|v2|reviews.default.svc.cluster.localfilters:-name:istio.metadata_exchangetypedConfig:'@type':type.googleapis.com/envoy.tcp.metadataexchange.config.MetadataExchangeprotocol:istio-peer-exchangelbPolicy:LEAST_REQUESTmetadata:filterMetadata:istio:config:/apis/networking.istio.io/v1alpha3/namespaces/default/destination-rule/reviewsservices:-host:reviews.default.svc.cluster.localname:reviewsnamespace:defaultsubset:v2name:outbound|9080|v2|reviews.default.svc.cluster.local# ......type:EDS-edsClusterConfig:edsConfig:ads:{}initialFetchTimeout:0sresourceApiVersion:V3serviceName:outbound|9080|v3|reviews.default.svc.cluster.localfilters:-name:istio.metadata_exchangetypedConfig:'@type':type.googleapis.com/envoy.tcp.metadataexchange.config.MetadataExchangeprotocol:istio-peer-exchangelbPolicy:LEAST_REQUESTmetadata:filterMetadata:istio:config:/apis/networking.istio.io/v1alpha3/namespaces/default/destination-rule/reviewsservices:-host:reviews.default.svc.cluster.localname:reviewsnamespace:defaultsubset:v3name:outbound|9080|v3|reviews.default.svc.cluster.local# ......type:EDS
从上面配置可以看到里面一共包含了 4 个 reviews
相关的集群,一个是原始的不包含 subset 的,而另外三个就是前面我们在 DestinationRule
对象中配置的 3 个 subset,所以其实 DestinationRule 映射到 Envoy 的配置文件中就是 Cluster。
🍀
最后我们同样还可以查看每个集群下面包含的 endpoint 有哪些:
$istioctlproxy-configendpointproductpage-v1-564d4686f-wwqqf--cluster"outbound|9080||reviews.default.svc.cluster.local"-oyaml-edsServiceName:outbound|9080||reviews.default.svc.cluster.local-address:socketAddress:address:10.244.2.84portValue:9080# ......weight:1-address:socketAddress:address:10.244.2.83portValue:9080# ......weight:1-address:socketAddress:address:10.244.2.88portValue:9080# ......weight:1name:outbound|9080||reviews.default.svc.cluster.localobservabilityName:outbound|9080||reviews.default.svc.cluster.local$istioctlproxy-configendpointproductpage-v1-564d4686f-wwqqf--cluster"outbound|9080|v1|reviews.default.svc.cluster.local"-oyaml-edsServiceName:outbound|9080|v1|reviews.default.svc.cluster.localhostStatuses:-address:socketAddress:address:10.244.2.84portValue:9080weight:1name:outbound|9080|v1|reviews.default.svc.cluster.localobservabilityName:outbound|9080|v1|reviews.default.svc.cluster.local# 过滤 version=v1 的 reviews pod$kubectlgetpod-lapp=reviews,version=v1-owideNAMEREADYSTATUSRESTARTSAGEIPNODENOMINATEDNODEREADINESSGATESreviews-v1-86896b7648-zjh2n2/2Running4(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 到网格内其他应用的流量与上面类似,就不展开讨论了。
2.基于用户身份的路由
==🚩 实战:基于用户身份的路由-2023.11.11(测试成功)==
实验环境:
k8sv1.27.6(containerd:[root@master1 ~]#istioctl versionclientversion:1.19.3controlplaneversion:1.19.3dataplaneversion:1.19.3(8 proxies)
实验软件:
链接:https:A[实战步骤] -->B(1️⃣ 更新VirtualService)A[实战步骤] -->C(2️⃣ 测试)