0%

【Service Mesh】Istio 流量控制

流量控制是指对系统流量的管控,包括了对网格入口的流量、网格出口的流量以及在网格内部微服务间相互调用流量的控制。在 Istio 入门 中我们知道,Istio 架构在逻辑上分为 Control plane 和 Data plane,Control plane 负责整体管理和配置代理, Data plane 负责网格内所有微服务间的网络通信,同时还收集报告网络请求的遥测数据等。流量控制是在 Data plane 层实现。

Istio Architecture

路由和流量转移

Istio 为了控制服务请求,引入了服务版本(version)的概念,可以通过版本这一标签将服务进行区分。版本的设置是非常灵活的,以下是几种典型的设置方式:

  • 根据服务的迭代编号进行定义(如 v1、v2 版本)
  • 根据部署环境进行定义(比如 dev、staging、production)
  • 自定义的任何用于区分服务的某种标记

通过版本标签,Istio 就可以定义灵活的路由规则来控制流量,上面提到的金丝雀发布这类应用场景就很容易实现了。

下图展示了使用服务版本实现路由分配的例子。服务版本定义了版本号(v1.5、v2.0-alpha)和环境(us-prod、us-staging)两种信息。服务 B 包含了 4 个 Pod,其中 3 个是部署在生产环境的 v1.5 版本,而 Pod4 是部署在预生产环境的 v2.0-alpha 版本。运维人员可以根据服务版本来指定路由规则,使 99% 的流量流向 v1.5 版本,而 1% 的流量进入 v2.0-alpha 版本。

路由

除了上面介绍的服务间流量控制外,还能控制与网格边界交互的流量。可以在系统的入口和出口处部署 Sidecar 代理,让所有流入和流出的流量都由代理进行转发。负责入和出的代理就叫做入口网关和出口网关,它们把守着进入和流出网格的流量。下图展示了 Ingress 和 Egress 在请求流中的位置,有了他们俩,也就可以控制出入网格的流量了。

入口和出口网关

Istio 还能设置流量策略。比如可以对连接池相关的属性进行设置,通过修改最大连接等参数,实现对请求负载的控制。还可以对负载均衡策略进行设置,在轮询、随机、最少访问等方式之间进行切换。还能设置异常探测策略,将满足异常条件的实例从负载均衡池中摘除,以保证服务的稳定性。


Istio 的流量路由规则可以让您很容易的控制服务之间的流量和 API 调用。Istio 在服务层面提供了断路器,超时,重试等功能,通过这些功能可以简单地实现 A/B 测试,金丝雀发布,基于百分比的流量分割等,此外还提供了开箱即用的故障恢复功能,用于增加应用的健壮性,以应对服务故障或网络故障。这些功能都可以通过 Istio 的流量管理 API 添加流量配置来实现。

跟其他 Istio 配置一样,流量管理 API 也使用 CRD 指定。本小节主要介绍下面几个典型的流量管理 API 资源,以及这些 API 的功能和使用示例。

VirtualService

VirtualService 由一组 路由规则 组成,描述了 用户请求的目标地址服务网格中实际工作负载 之间的映射。在这个映射中,VirtualService提供了丰富的配置方式,可以为发送到这些 Workloads 的流量指定不同的路由规则。对应于具体的配置,用户请求的目标地址用 hosts 字段来表示,网格内的实际负载由每个 route 配置项中的 destination 字段指定。

graph LR
subgraph VirtualService
ClientRequests -- DifferentTrafficRoutingRules --> DestinationWorkloads
Hosts -- DifferentTrafficRoutingRules --> RouteDestination
end

VirtualService 通过解耦 用户请求的目标地址真实响应请求的目标工作负载,为服务提供了合适的统一抽象层,而由此演化设计的配置模型为管理这方面提供了一致的环境。对于原生 Kubernetes 而言,只有在 Ingress 处有这种路由规则的定义,对于集群内部不同Service的不同版本之间,并没有类似 VirtualService 的定义。

使用 VirtualService,可以为一个或多个主机名指定流量行为。在 VirtualService 中使用路由规则,告诉 Envoy如何发送 VirtualService 的流量到适当的目标。路由目标可以是相同服务的不同版本,或者是完全不同的服务。

一个典型的应用场景是将流量发送到被指定为服务子集的服务的不同版本。客户端将 VirtualService 视为一个单一实体,将请求发送至 VirtualService 主机,然后 Envoy 根据 VirtualService 规则把流量路由到不同的版本中。

这种方式可以方便地创建一种金丝雀的发布策略实现新版本流量的平滑比重升级。流量路由完全独立于实例部署,这意味着实现新版本服务的实例可以根据流量的负载来伸缩,完全不影响流量路由。相比之下,类似 Kubernetes 的容器调度平台仅支持基于部署中实例扩缩容比重的流量分发,那样会日趋复杂化。关于使用VirtualService实现金丝雀部署,可以参考 Canary

VirtualService 也提供了如下功能。

  • 通过单个 VirtualService 处理多个应用程序服务。例如,如果您的服务网格使用是 Kubernetes,您可以配置一个 VirtualService 来处理一个特定命名空间的所有服务。将单一的 VirtualService 映射为多个“真实”的服务特别有用,可以在不需要客户适应转换的情况下,将单体应用转换为微服务构建的复合应用系统。您的路由规则可以指定“请求到 monolith.com 的 URLs 跳转至 microservice A 中”。
  • 和 Gateway 一起配置流量规则来控制入口和出口流量。

在一些应用场景中,由于指定服务子集,需要配置 DestinationRule 来使用这些功能。在不同的对象中指定服务子集以及其他特定的目标策略可以帮助您在不同的 VirtualService 中清晰地复用这些功能。

下面的 VirtualService 根据是否来自于特定用户路由请求到不同的服务版本中(如果请求来自用户 jason ,则访问 v2 版本的 reviews,否则访问 v3 版本):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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: v3

下面对这些字段依次解释:

Hosts

用来配置 Downstream 访问的可寻址地址,也就是用户请求的目标地址。

1
2
hosts:
- reviews
  • VirtualService 主机名可以是 IP 地址、 DNS 域名、完全限定域名(FQDN)
  • 也可以是 依赖于平台的一个简称(例如 Kubernetes 服务的短名称)
  • 也可以使用通配符 *前缀,创建一组匹配所有服务的路由规则
  • VirtualService 的 hosts 实际上不必是 Istio 服务注册的一部分,它只是虚拟的目标地址。这可以为没有路由到网格内部的虚拟主机建模。

路由规则

http 字段用来配置路由规则,通常情况下配置一组路由规则,当请求到来时,自上而下依次进行匹配,直到匹配成功后跳出匹配。它可以对请求的 uri、method、authority、headers、port、queryParams 以及是否对 uri 大小写敏感等进行配置。

除了HTTP协议,也可以使用 tcptls 片段为 TCP 和未终止的 TLS 流量设置路由规则

1
2
3
4
5
6
7
8
9
10
11
12
13
http:
- match:
- headers:
end-user:
exact: jason
route:
- destination:
host: reviews
subset: v2
- route:
- destination:
host: reviews
subset: v3

我们推荐在每个 VirtualService 中配置一条默认「无条件的」或者基于权重的规则以确保 VirtualService 至少有一条匹配的路由。

Destination

路由片段的 destination 字段指定符合匹配条件的流量目标地址。这里不像 VirtualService 的 hosts,Destination 的 host 必须是存在于 Istio 服务注册中心的实际目标地址,否则 Envoy 不知道该将请求发送到哪里。这个目标地址可以是代理的网格服务或者作为服务入口加入的非网格服务。下面的场景中我们运行在 Kubernetes 平台上,主机名是 Kubernetes 的服务名。

1
2
3
4
route:
- destination:
host: reviews
subset: v2
1
*Note for Kubernetes users*: When short names are used (e.g. "reviews" instead of "reviews.default.svc.cluster.local"), Istio will interpret the short name based on the namespace of the rule, not the service. A rule in the "default" namespace containing a host "reviews will be interpreted as "reviews.default.svc.cluster.local", irrespective of the actual namespace associated with the reviews service. To avoid potential misconfiguration, it is recommended to always use fully qualified domain names over short names.
Match

路由规则是将特定流量子集路由到特定目标地址的强大工具。可以在流量端口、header 字段、 URL 等内容上设置匹配条件。例如,下面的VirtualService 使用户发送流量到两个独立的服务,ratings and reviews, 就好像它们是 http://bookinfo.com/ 这个更大的 VirtualService 的一部分。VirtualService 规则根据请求的 URL 和指向适当服务的请求匹配流量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: bookinfo
spec:
hosts:
- bookinfo.com
http:
- match:
- uri:
prefix: /reviews
route:
- destination:
host: reviews
- match:
- uri:
prefix: /ratings
route:
- destination:
host: ratings

对于匹配条件,您可以使用确定的值,一条前缀、或者一条正则表达式。

您可以使用 AND 向同一个 match 块添加多个匹配条件, 或者使用 OR 向同一个规则添加多个 match 块。对于任意给定的 VirtualService ,您可以配置多条路由规则。这可以使您的路由条件在一个单独的 VirtualService 中基于业务场景的复杂度来进行相应的配置。可以在 HTTPMatchRequest 参考中查看匹配条件字段和他们可能的值。

再者进一步使用匹配条件,您可以使用基于“权重”百分比分发流量。这在 A/B 测试和金丝雀部署中非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
spec:
hosts:
- reviews
http:
- route:
- destination:
host: reviews
subset: v1
weight: 75
- destination:
host: reviews
subset: v2
weight: 25

您也可以使用路由规则在流量上执行一些操作,例如

  • 扩展或者删除 headers
  • 重写 URL
  • 为调用这个目标地址设置重试策略

DestinationRule

DestinationRule 是 Istio 流量路由功能的重要组成部分。一个 VirtualService 可以看作是如何将流量分发到给定的目标地址,然后调用 DestinationRule 来配置分发到该目标地址的流量。DestinationRuleVirtualService 的路由规则之后起作用(即在 VirtualServicematch -> route -> destination 之后起作用,此时流量已经分发到真实的 Service 上),应用于真实的目标地址。

特别地,可以使用 DestinationRule 来指定命名的服务子集,例如根据版本对服务的实例进行分组,然后通过 VirtualService 的路由规则中的服务子集将控制流量分发到不同服务的实例中。

DestinationRule 允许在调用完整的目标服务或特定的服务子集(如倾向使用的负载均衡模型,TLS 安全模型或断路器)时自定义 Envoy流量策略。Istio 默认会使用轮询策略,此外 Istio 也支持如下负载均衡模型,可以在 DestinationRule 中使用这些模型,将请求分发到特定的服务或服务子集。

  • Random:将请求转发到一个随机的实例上
  • Weighted:按照指定的百分比将请求转发到实例上
  • Least requests:将请求转发到具有最少请求数目的实例上

下面的 DestinationRule 使用不同的负载均衡策略为 my-svc 目的服务配置了3个不同的 Subset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: my-destination-rule
spec:
host: my-svc
trafficPolicy: #默认的负载均衡策略模型为随机
loadBalancer:
simple: RANDOM
subsets:
- name: v1 #subset1,将流量转发到具有标签 version:v1 的 deployment 对应的服务上
labels:
version: v1
- name: v2 #subset2,将流量转发到具有标签 version:v2 的 deployment 对应的服务上,指定负载均衡为轮询
labels:
version: v2
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
- name: v3 #subset3,将流量转发到具有标签 version:v3 的 deployment 对应的服务上
labels:
version: v3

每个子集由一个或多个 labels 定义,对应 Kubernetes 中的对象(如 Pod )的 key/value 对。这些标签定义在 Kubernetes 服务的 deployment 的 metadata 中,用于标识不同的版本。

除了定义子集外,DestinationRule 还定义了该目的地中所有子集的默认流量策略,以及仅覆盖该子集的特定策略。默认的策略定义在 subset 字段之上,为 v1v3 子集设置了随机负载均衡策略,在 v2 策略中使用了轮询负载均衡。

Gateway

Gateway 用于管理进出网格的流量,指定可以进入或离开网格的流量。Gateway 配置应用于网格边缘的独立的 Envoy代理上,而不是服务负载的 Envoy 代理上。

与其他控制进入系统的流量的机制(如 Kubernetes Ingress API)不同,Istio gateway 允许利用 Istio 的流量路由的强大功能和灵活性。Istio 的 gateway 资源仅允许配置 4-6 层的负载属性,如暴露的端口,TLS 配置等等,但结合 Istio 的 VirtualService,就可以像管理 Istio 网格中的其他数据面流量一样管理 Gateway 的流量。

Gateway 主要用于管理 Ingress 流量,但也可以配置 Egress Gateway。通过 Egress Gateway 可以配置流量离开网格的特定节点,限制哪些服务可以访问外部网络,或通过 Egress 安全控制来提高网格的安全性。Gateway 可以用于配置为一个纯粹的内部代理。

Istio (通过 istio-ingressgatewayistio-egressgateway 参数)提供了一些预配置的 Gateway 代理,default profile 下仅会部署 Ingress Gateway。Gateway 可以通过部署文件进行部署,也可以单独部署。

下面是 default profile 默认安装的 Ingress

1
2
3
$ kubectl get gw
NAME AGE
bookinfo-gateway 28h

可以看到该 ingress 就是一个普通的 Pod,该 Pod 仅包含一个 Istio-proxy 容器

1
2
$ kubectl get pod -n istio-system |grep ingress
istio-ingressgateway-64f6f9d5c6-qrnw2 1/1 Running 0 4d20h

下面是一个 Gateway 的例子,用于配置外部 HTTPS 的 ingress 流量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: ext-host-gwy
spec:
selector: #指定 gateway 配置下发的代理,如具有标签 app: my-gateway-controller 的 pod
app: my-gateway-controller
servers:
- port: #gateway pod 暴露的端口信息
number: 443
name: https
protocol: HTTPS
hosts: #外部流量
- ext-host.example.com
tls:
mode: SIMPLE
serverCertificate: /tmp/tls.crt
privateKey: /tmp/tls.key

上述 Gateway 配置允许来自 ext-host.example.com 流量进入网格的 443 端口,但没有指定该流量的路由。(此时流量只能进入网格,但没有指定处理该流量的服务,因此需要与 VirtualService 进行绑定)

为了为 Gateway 指定路由,需要通过 VirtualServiceGateway 字段,将 Gateway 绑定到一个 VirtualService 上,将来自 ext-host.example.com 流量引入一个 VirtualServicehosts 可以是通配符,表示引入匹配到的流量。

1
2
3
4
5
6
7
8
9
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: virtual-svc
spec:
hosts:
- ext-host.example.com
gateways: #将 gateway "ext-host-gwy" 绑定到 virtual service "virtual-svc"上
- ext-host-gwy

Egress Gateway 提供了对网格的出口流量进行统一管控的功能,在安装 Istio 时默认是不开启的。可以使用以下命令查看是否开启。

1
$ kubectl get pod -l istio=egressgateway -n istio-system

若没有开启,使用以下命令添加。

1
2
$ istioctl manifest apply --set values.global.istioNamespace=istio-system \
--set values.gateways.istio-egressgateway.enabled=true

Egress Gateway 的一个简单示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: istio-egressgateway
spec:
selector:
istio: egressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- edition.cnn.com

可以看出,与 Ingress Gateway 不同,Egress Gateway 使用有 istio: egressgateway 标签的 Pod 来代理流量,实际上这也是一个 Envoy 代理。当网格内部需要访问 edition.cnn.com 这个地址时,流量将会统一先转发到 Egress Gateway 上,再由 Egress Gateway 将流量转发到 edition.cnn.com 上。

ServiceEntry

Istio 支持对接 Kubernetes、Consul 等多种不同的注册中心,控制平面Pilot启动时,会从指定的注册中心获取 Service Mesh 集群的服务信息和实例列表,并将这些信息进行处理和转换,然后通过 xDS 下发给对应的数据平面,保证服务之间可以互相发现并正常访问。

同时,由于这些服务和实例信息都来源于服务网格内部,Istio 无法从注册中心直接获取网格外的服务,导致不利于网格内部与外部服务之间的通信和流量管理。为此,Istio 引入 ServiceEntry 实现对外通信和管理。

使用 ServiceEntry 可以将外部的服务条目添加到 Istio 内部的服务注册表中,以便让网格中的服务能够访问并路由到这些手动指定的服务。ServiceEntry 描述了服务的属性(DNS 名称、VIP、端口、协议、端点)。这些服务可能是位于网格外部(如,web APIs),也可能是处于网格内部但不属于平台服务注册表中的条目(如,需要和 Kubernetes 服务交互的一组虚拟机服务)。

ServiceEntry 示例和属性介绍

对于网格外部的服务,下面的 ServiceEntry 示例表示网格内部的应用通过 https 访问外部的 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: google
spec:
hosts:
- www.google.com
ports:
- number: 443
name: https
protocol: HTTPS
resolution: DNS
location: MESH_EXTERNAL

对于在网格内部但不属于平台服务注册表的服务,使用下面的示例可以将一组在非托管 VM 上运行的 MongoDB 实例添加到 Istio 的注册中心,以便可以将这些服务视为网格中的任何其他服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: external-svc-mongocluster
spec:
hosts:
- mymongodb.somedomain
addresses:
- 192.192.192.192/24 # VIPs
ports:
- number: 27018
name: mongodb
protocol: MONGO
location: MESH_INTERNAL
resolution: STATIC
endpoints:
- address: 2.2.2.2
- address: 3.3.3.3

结合上面给出的示例,这里对 ServiceEntry 涉及的关键属性解释如下:

  • hosts: 表示与该 ServiceEntry 相关的主机名,可以是带有通配符前缀的 DNS 名称。
  • address: 与服务相关的虚拟 IP 地址,可以是 CIDR 前缀的形式。
  • ports: 和外部服务相关的端口,如果外部服务的 endpoints 是 Unix socket 地址,这里必须只有一个端口。
  • location: 用于指定该服务属于网格内部(MESH_INTERNAL)还是外部(MESH_EXTERNAL)。
  • resolution: 主机的服务发现模式,可以是 NONE、STATIC、DNS。
  • endpoints: 与服务相关的一个或多个端点。
  • exportTo: 用于控制 ServiceEntry 跨命名空间的可见性,这样就可以控制在一个命名空间下定义的资源对象是否可以被其他命名空间下的 Sidecar、Gateway 和 VirtualService 使用。目前支持两种选项,”.” 表示仅应用到当前命名空间,”*” 表示应用到所有命名空间。

使用 ServiceEntry 访问外部服务

Istio 提供了三种访问外部服务的方法:

  1. 允许 Sidecar 将请求传递到未在网格内配置过的任何外部服务。使用这种方法时,无法监控对外部服务的访问,也不能利用 Istio 的流量控制功能。
  2. 配置 ServiceEntry 以提供对外部服务的受控访问。这是 Istio 官方推荐使用的方法。
  3. 对于特定范围的 IP,完全绕过 Sidecar。仅当出于性能或其他原因无法使用 Sidecar 配置外部访问时,才建议使用该配置方法。

这里,我们重点讨论第 2 种方式,也就是使用 ServiceEntry 完成对网格外部服务的受控访问。

对于 Sidecar 对外部服务的处理方式,Istio 提供了两种选项:

  • ALLOW_ANY:默认值,表示 Istio 代理允许调用未知的外部服务。上面的第一种方法就使用了该配置项。
  • REGISTRY_ONLY:Istio 代理会阻止任何没有在网格中定义的 HTTP 服务或 ServiceEntry 的主机。

可以使用下面的命令查看当前所使用的模式:

1
2
$ kubectl get configmap istio -n istio-system -o yaml | grep -o "mode: ALLOW_ANY"
mode: ALLOW_ANY

如果当前使用的是 ALLOW_ANY 模式,可以使用下面的命令切换为 REGISTRY_ONLY 模式:

1
2
$ kubectl get configmap istio -n istio-system -o yaml | sed 's/mode: ALLOW_ANY/mode: REGISTRY_ONLY/g' | kubectl replace -n istio-system -f -
configmap "istio" replaced

REGISTRY_ONLY 模式下,需要使用 ServiceEntry 才能完成对外部服务的访问。当创建如下的 ServiceEntry 时,服务网格内部的应用就可以正常访问 httpbin.org 服务了。

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: httpbin-ext
spec:
hosts:
- httpbin.org
ports:
- number: 80
name: http
protocol: HTTP
resolution: DNS
location: MESH_EXTERNAL

管理外部流量

使用 ServiceEntry 可以使网格内部服务发现并访问外部服务,除此之外,还可以对这些到外部服务的流量进行管理。结合 VirtualService 为对应的 ServiceEntry 配置外部服务访问规则,如请求超时、故障注入等,实现对指定服务的受控访问。

下面的示例就是为外部服务 httpbin.org 设置了超时时间,当请求时间超过 3s 时,请求就会直接中断,避免因外部服务访问时延过高而影响内部服务的正常运行。由于外部服务的稳定性通常无法管控和监测,这种超时机制对内部服务的正常运行具有重要意义。

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: httpbin-ext
spec:
hosts:
- httpbin.org
http:
- timeout: 3s
route:
- destination:
host: httpbin.org
weight: 100

同样的,我们也可以为 ServiceEntry 设置故障注入规则,为系统测试提供基础。下面的示例表示为所有访问 httpbin.org 服务的请求注入一个403错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: httpbin-service
spec:
hosts:
- httpbin.org
http:
- route:
- destination:
host: httpbin.org
fault:
abort:
percent: 100
httpStatus: 403

Sidecar

在默认的情况下,Istio 中所有 Pod 中的 Envoy 代理都是可以被寻址的。然而在某些场景下,我们为了做资源隔离,希望只访问某些 Namespace 下的资源。这个时候,我们就可以使用 Sidecar配置来实现。下面是一个简单的示例:

1
2
3
4
5
6
7
8
9
10
apiVersion: networking.istio.io/v1alpha3
kind: Sidecar
metadata:
name: default
namespace: bookinfo
spec:
egress:
- hosts:
- "./*"
- "istio-system/*"

该示例就规定了在命名空间为 bookinfo 下的所有服务仅可以访问本命名空间下的服务以及 istio-system 命名空间下的服务。

弹性功能

除了最核心的路由和流量转移功能外,Istio 还提供了一定的弹性功能,目前支持超时、重试和熔断。

Request Timeouts

如果程序请求长时间无法返回结果,则需要设置超时机制,超过设置的时间则返回错误信息。这样做既可以节约等待时消耗的资源,也可以避免由于级联错误引起的一系列问题。

设置超时的方式也有很多种,比如通过修改代码在应用程序侧设置请求超时时间,但是这样很不灵活,也容易出现遗漏的现象,而 Istio 则可以在基础设施层解决这一问题。在 Istio 里添加超时非常简单,只需要在路由配置里添加 timeout 这个关键字就可以实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: ratings
spec:
hosts:
- ratings
http:
- route:
- destination:
host: ratings
subset: v1
timeout: 10s

Retries

在网络环境不稳定的情况下,会出现暂时的网络不可达现象,这时需要重试机制,通过多次尝试来获取正确的返回信息。重试逻辑可以写业务代码中,比如 Bookinfo 应用中的productpage服务就存在硬编码重试,而 Istio 可以通过简单的配置来实现重试功能,让开发人员无需关注重试部分的代码实现,专心实现业务代码。在 Istio 里添加超时和重试都非常简单,只需要在路由配置里添 retry 这个关键字就可以实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: ratings
spec:
hosts:
- ratings
http:
- route:
- destination:
host: ratings
subset: v1
retries:
attempts: 3
perTryTimeout: 2s

Circuit Breaking

熔断是一种非常有用的过载保护手段,可以避免服务的级联失败。在熔断器中,设置一个对服务中的单个主机调用的限制,例如并发连接的数量或对该主机调用失败的次数。一旦限制被触发,熔断器就会“跳闸”并停止连接到该主机。使用熔断模式可以快速失败而不必让客户端尝试连接到过载或有故障的主机。熔断适用于在负载均衡池中的“真实”网格目标地址,可以在 DestinationRule 中配置熔断器阈值,让配置适用于服务中的每个主机。

Istio 里面的熔断需要在自定义资源 DestinationRuleTrafficPolicy 里进行设置。下面的示例将 v1 子集的reviews服务工作负载的并发连接数限制为 100:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: reviews
spec:
host: reviews
subsets:
- name: v1
labels:
version: v1
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100

调试能力

Istio 还提供了对流量进行调试的能力,包括故障注入和流量镜像。对流量进行调试可以让系统具有更好的容错能力,也方便我们在问题排查时通过调试来快速定位原因所在。

Fault Injection

在一个微服务架构的系统中,为了让系统达到较高的健壮性要求,通常需要对系统做定向错误测试。比如电商中的订单系统、支付系统等若出现故障那将是非常严重的生产事故,因此必须在系统设计前期就需要考虑多样性的异常故障并对每一种异常设计完善的恢复策略或优雅的回退策略,尽全力规避类似事故的发生,使得当系统发生故障时依然可以正常运作。而在这个过程中,服务故障模拟一直以来是一个非常繁杂的工作,于是在这样的背景下就衍生出了故障注入技术手段,故障注入是用来模拟上游服务请求响应异常行为的一种手段。通过人为模拟上游服务请求的一些故障信息来检测下游服务的故障策略是否能够承受这些故障并进行自我恢复。

Istio 提供了一种无侵入式的故障注入机制,让开发测试人员在不用调整服务程序的前提下,通过配置即可完成对服务的异常模拟。Istio 1.5 仅支持网络层的故障模拟,即支持模拟上游服务的处理时长、服务异常状态、自定义响应状态码等故障信息,暂不支持对于服务主机内存、CPU 等信息故障的模拟。他们都是通过配置上游主机的 VirtualService 来实现的。当我们在 VirtualService 中配置了故障注入时,上游服务的 Envoy代理在拦截到请求之后就会做出相应的响应。

目前,Istio 提供两种类型的故障注入,abort 类型与 delay 类型。

  • abort:非必配项,配置一个 Abort 类型的对象。用来注入请求异常类故障。简单的说,就是用来模拟上游服务对请求返回指定异常码时,当前的服务是否具备处理能力。它对应于 Envoy过滤器中的 config.filter.http.fault.v2.FaultAbort 配置项,当 VirtualService 资源应用时,Envoy将会该配置加载到过滤器中并处理接收到的流量。
  • delay:非必配项,配置一个 Delay 类型的对象。用来注入延时类故障。通俗一点讲,就是人为模拟上游服务的响应时间,测试在高延迟的情况下,当前的服务是否具备容错容灾的能力。它对应于 Envoy过滤器中的 config.filter.fault.v2.FaultDelay 配置型,同样也是在应用 Istio 的 VirtualService 资源时,Envoy将该配置加入到过滤器中。

实际上,Istio 的故障注入正是基于 Envoy的 config.filter.http.fault.v2.HTTPFault 过滤器实现的,它的局限性也来自于 Envoy故障注入机制的局限性。对于 Envoy的 HttpFault 的详细介绍请参考 Envoy 文档。对比 Istio 故障注入的配置项与 Envoy故障注入的配置项,不难发现,Istio 简化了对于故障控制的手段,去掉了 Envoy中通过 HTTP header 控制故障注入的配置。

HTTPFaultInjection.Abort

  • httpStatus:必配项,是一个整型的值。表示注入 HTTP 请求的故障状态码。
  • percentage:非必配项,是一个 Percent 类型的值。表示对多少请求进行故障注入。如果不指定该配置,那么所有请求都将会被注入故障。
  • percent:已经废弃的一个配置,与 percentage 配置功能一样,已经被 percentage 代替。

如下的配置表示对 v1 版本的 ratings.prod.svc.cluster.local 服务访问的时候进行故障注入,0.1表示有千分之一的请求被注入故障, 400 表示故障为该请求的 HTTP 响应码为 400

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: ratings-route
spec:
hosts:
- ratings.prod.svc.cluster.local
http:
- route:
- destination:
host: ratings.prod.svc.cluster.local
subset: v1
fault:
abort:
percentage:
value: 0.1
httpStatus: 400

HTTPFaultInjection.Delay

  • fixedDelay:必配项,表示请求响应的模拟处理时间。格式为:1h/1m/1s/1ms, 不能小于 1ms
  • percentage:非必配项,是一个 Percent 类型的值。表示对多少请求进行故障注入。如果不指定该配置,那么所有请求都将会被注入故障。
  • percent:已经废弃的一个配置,与 percentage 配置功能一样,已经被 percentage 代替。

如下的配置表示对 v1 版本的 reviews.prod.svc.cluster.local 服务访问的时候进行延时故障注入,0.1 表示有千分之一的请求被注入故障,5s 表示reviews.prod.svc.cluster.local 延时 5s返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reviews-route
spec:
hosts:
- reviews.prod.svc.cluster.local
http:
- match:
- sourceLabels:
env: prod
route:
- destination:
host: reviews.prod.svc.cluster.local
subset: v1
fault:
delay:
percentage:
value: 0.1
fixedDelay: 5s

Mirroring

流量镜像(Mirroring / traffic-shadow),也叫作影子流量,就是通过复制一份请求并把它发送到镜像服务,从而实现流量的复制功能。流量镜像的主要应用场景有以下几种:最主要的就是进行线上问题排查

一般情况下,因为系统环境,特别是数据环境、用户使用习惯等问题,我们很难在开发环境中模拟出真实的生产环境中出现的棘手问题,同时生产环境也不能记录太过详细的日志,因此很难定位到问题。有了流量镜像,我们就可以把真实的请求发送到镜像服务,再打开 debug 日志来查看详细的信息。除此以外,还可以通过它来观察生产环境的请求处理能力,比如在镜像服务进行压力测试。也可以将复制的请求信息用于数据分析。流量镜像在 Istio 里实现起来也非常简单,只需要在路由配置中通添加mirror关键字即可。

流量镜像能够为我们带来什么

很多情况下,当我们对服务做了重构,或者我们对项目做了重大优化时,怎么样保证服务是健壮的呢?在传统的服务里,我们只能通过大量的测试,模拟在各种情况下服务的响应情况。虽然也有手工测试、自动化测试、压力测试等一系列手段去检测它,但是测试本身就是一个样本化的行为,即使测试人员再完善它的测试样例,无法全面的表现出线上服务的一个真实流量形态。往往当项目发布之后,总会出现一些意外,比如你服务里收到客户使用的某些数据库不认识的特殊符号,再比如用户在本该输入日期的输入框中输入了 “—” 字样的字符,又比如用户使用乱码替换你的 token 值批量恶意攻击服务等等,这样的情况屡见不鲜。数据的多样性,复杂性决定了开发人员在开发阶段根本是无法考虑周全的。

而流量镜像的设计,让这类问题得到了最大限度的解决。流量镜像讲究的不再是使用少量样本去评估一个服务的健壮性,而是在不影响线上坏境的前提下将线上流量持续的镜像到我们的预发布坏境中去,让重构后的服务在上线之前就结结实实地接受一波真实流量的冲击与考验,让所有的风险全部暴露在上线前夕,通过不断的暴露问题,解决问题让服务在上线前夕就拥有跟线上服务一样的健壮性。由于测试坏境使用的是真实流量,所以不管从流量的多样性,真实性,还是复杂性上都将能够得以展现,同时预发布服务也将表现出其最真实的处理能力和对异常的处理能力。运用这种模式,一方面,我们将不会再跟以前一样在发布服务前夕内心始终忐忑不安,只能祈祷上线之后不会出现问题。另一方面,当大量的流量流入重构服务之后,开发过程中难以评估的性能问题也将完完整整的暴露出来,此时开发人员将会考虑它服务的性能,测试人员将会更加完善他们的测试样例。通过暴露问题,解决问题,再暴露问题,再解决问题的方式循序渐进地完善预发布服务来增加我们上线的成功率。同时也变相的促进我们开发测试人员技能水平的提高。

当然,流量镜像的作用不仅仅只是解决上面这样的场景问题,我们可以根据它的特性,解决更多的问题。比如,假如我们在上线后突然发现一个线上问题,而这个问题在测试坏境中始终不能复现。那么这个时候我们就能利用它将异常流量镜像到一个分支服务中去,然后我们可以随意在这个分支服务上进行分析调试,这里所说的分支服务,可以是原服务的只用于问题分析而不处理正式业务的副本服务,也可以是一个只收集镜像流量的组件类服务。又比如突然需要收集某个时间段某些流量的特征数据做分析,像这种临时性的需求,使用流量镜像来处理非常合适,既不影响线上服务的正常运转,也达到了收集分析的目的。

流量镜像的实现原理

实际上在 Istio 中,服务间的通讯都是被 Envoy代理拦截并处理的, Istio 流量镜像的设计也是基于 Envoy特性实现的。它的流量转发如下图所示。可以看到,当流量进入到Service A时,因为在Service A的 Envoy代理上配置了流量镜像规则,那么它首先会将原始流量转发到v1版本的 Service B服务子集中去 。同时也会将相同的流量复制一份,异步地发送给v2版本的Service B 服务子集中去,可以明显的看到,Service A 发送完镜像流量之后并不关心它的响应情况。

在很多情况下,我们需要将真实的流量数据与镜像流量数据进行收集并分析,那么当我们收集完成后应该怎样区分哪些是真实流量,哪些是镜像流量呢? 实际上,Envoy团队早就考虑到了这样的场景,他们为了区分镜像流量与真实流量,在镜像流量中修改了请求标头中 host 值来标识,它的修改规则是:在原始流量请求标头中的 host 属性值拼接上“-shadow” 字样作为镜像流量的 host 请求标头。

为了能够更清晰的对比出原始流量与镜像流量的区别,我们使用以下的一个示例来说明:

如下图所示,我们发起一个http://istio.gateway.xxxx.tech/serviceB/request/info的请求,请求首先进入了istio-ingressgateway ,它是一个 Istio 的 Gateway 资源类型的服务,它本身就是一个 Envoy代理。在这个例子里,就是它对流量进行了镜像处理。可以看到,它将流量转发给v1版本Service B服务子集的同时也复制了一份流量发送到了v2版本的Service B服务子集中去。

concepts-traffic-shadow-request

在上面的请求链中,请求标头数据有什么变化呢?下图收集了它们请求标头中的所有信息,可以明显的对比出正式流量与镜像流量请求标头中host属性的区别(部分相同的属性值过长,这里只截取了前半段)。从图中我们可以看出,首先就是host属性值的不同,而区别就是多了一个“-shadow”的后缀。再者发现x-forwarded-for属性也不相同,x-forwarded-for协议头的格式是:x-forwarded-for: client1, proxy1, proxy2, 当流量经过 Envoy代理时这个协议头将会把代理服务的 IP 添加进去。实例中10.10.2.151是我们云主机的 IP,而10.10.2.121isito-ingressgateway所对应Pod的 IP 。从这里也能看到,镜像流量是由istio-ingressgatway发起的。除了这两个请求标头的不同,其他配置项是完全一样的。

concepts-traffic-shadow-header

流量镜像的配置

上面我们介绍了流量镜像的原理及使用场景,接下来我们再介绍下流量的镜像如何配置才能生效。在 Istio 架构里,镜像流量是借助于 VirtualService 这个资源中的 HTTPRoute 配置项的mirrormirrorPercent这两项子配置项来实现的,这两个配置项的定义也是非常的简单。

  • mirror:配置一个 Destination 类型的对象,这里就是我们镜像流量转发的服务地址。具体的 VirtualService 配置与DestinationRule 对象配置属性请参考相关介绍页。
  • mirrorPercent:配置一个数值,这个配置项用来指定有多少的原始流量将被转发到镜像流量服务中去,它的有效值为0~100,如果配置成0则表示不发送镜像流量。

下面的例子就是我们在示例中使用到的Service B的镜像流量配置,其中,mirror.host配置项是配置一个域名或者在Istio 注册表中注册过的服务名称,可以看到,该配置指定了镜像流量需要发送的目标服务地址为serviceBmirror.subset配置项配置一个Service B服务的服务子集名称 ,指定了要将镜像流量镜像到v2版本的Service B服务子集中去。mirror_percent配置将100%的真实流量进行镜像发送。所以下面的配置整体表示当流量到来时,将请求转发到v1版本的service B服务子集中,再以镜像的方式发送到v2版本的service B服务上一份,并将真实流量全部镜像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: serviceB
spec:
hosts:
- istio.gateway.xxxx.tech
gateways:
- ingressgateway.istio-system.svc.cluster.local
http:
- match:
- uri:
prefix: /serviceB
rewrite:
uri: /
route:
- destination:
host: serviceB
subset: v1
mirror:
host: serviceB
subset: v2
mirror_percent: 100

service B 服务对应的 DestinationRule 配置如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: serviceB
namespace: default
spec:
host: serviceB
subsets:
- name: v2
labels:
version: v2
- name: v1
labels:
version: v1