跳转至

流量管理

在前面我们成功搭建并部署了 Istio 及其其 Bookinfo 示例应用:

bookinfo homepage

目前搭建 Bookinfo 应用我们只用到了下面两个资源文件:

samples/bookinfo/platform/kube/bookinfo.yaml
samples/bookinfo/networking/bookinfo-gateway.yaml

前者就是通常的 Kubernetes 定义的 Deployment 和 Service 的资源清单文件,只是在部署时使用 istioctl kube-inject 对这个文件定义的 Pod 注入了 sidecar 代理,后者定义了这个应用的外部访问入口gateway,以及应用内部 productpage 服务的 VirtualService 规则,而其他内部服务的访问规则还没有被定义。

现在访问应用界面并刷新,会看到 Reviews 有时不会显示评分,有时候会显示不同样式的评分,这是因为后面有3个不同的 Reviews 服务版本,而没有配置该服务的路由规则 route rule 的情况下,该服务的几个实例会被随机访问到,有的版本服务会进一步调用 Ratings 服务,有的不会。

到这里我们已经接触到了 Istio 中两个非常重要的流量管理的资源对象了:

  • VirtualService 是用来在 Istio 中定义路由规则,控制流量路由到服务上的各种行为。
  • Gateway 我为 HTTP/TCP 流量配置负载均衡器的。

接下来我们来对 Bookinfo 服务的访问规则进行修改。

不同服务版本访问规则

对 Reviews 服务添加一条路由规则,启用 samples/bookinfo/networking/virtual-service-reviews-v3.yaml 定义的 VirtualService 规则,内容如下:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
  - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v3

这样,所有访问 reviews 服务的流量就会被引导到 reviews 服务对应的 subset 为 v3 的 Pod 中。启用这条规则:

$ kubectl apply -f  samples/bookinfo/networking/virtual-service-reviews-v3.yaml
virtualservice.networking.istio.io/reviews created

然后查看所有的路由规则:

$ kubectl get virtualservices
NAME       GATEWAYS             HOSTS       AGE
bookinfo   [bookinfo-gateway]   [*]         158d
reviews                         [reviews]   20s

我们可以看到 reviews 的 VirtualService 已经创建成功了,此时我们去刷新应用的页面,发现访问 Reviews 失败了:

bookinfo reviews v3 failed

这是因为我们还没有创建 DestinationRule 对象,DestinationRule 对象是 VirtualService 路由生效后,配置应用与请求的策略集,用来将 VirtualService 中指定的 subset 与对应的 Pod 关联起来。

samples/bookinfo/networking/destination-rule-all.yaml 文件中有定义所有该应用中要用到的所有 DestinationRule 资源对象,其中有一段就是对 Reviews 相关的 DestinationRule 的定义:

---
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

我们可以看到 DestinationRule 中定义了 subsets 集合,其中 labels 就和我们之前 Service 的 labelselector 一样是去匹配 Pod 的 labels 标签的,比如我们这里 subsets 中就包含一个名为 v3 的 subset,而这个 subset 匹配的就是具有 version=v3 这个 label 标签的 Pod 集合,再回到之前的 samples/bookinfo/platform/kube/bookinfo.yaml 文件中,我们可以发现 reviews 的 Deployment 确实有声明不同的 labels->version:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: reviews-v3
  labels:
    app: reviews
    version: v3
spec:
  replicas: 1
  selector:
    matchLabels:
      app: reviews
      version: v3  
  template:
    metadata:
      labels:
        app: reviews
        version: v3 # 声明version=v3的标签
    spec:
      serviceAccountName: bookinfo-reviews
      containers:
      - name: reviews
        image: docker.io/istio/examples-bookinfo-reviews-v3:1.15.1
        imagePullPolicy: IfNotPresent
        env:
        - name: LOG_DIR
          value: "/tmp/logs"
        ports:
        - containerPort: 9080
        volumeMounts:
        - name: tmp
          mountPath: /tmp
        - name: wlp-output
          mountPath: /opt/ibm/wlp/output
      volumes:
      - name: wlp-output
        emptyDir: {}
      - name: tmp
        emptyDir: {}

这样我们就通过 DestinationRule 将 VirtualService 与 Service 不同的版本关联起来了。现在我们直接创建 DestinationRule 资源:

$ 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

创建完成后,我们就可以查看目前我们网格中的 DestinationRules:

$ kubectl get destinationrule
NAME          HOST          AGE
details       details       18s
productpage   productpage   18s
ratings       ratings       18s
reviews       reviews       18s

此时再访问应用就成功了,多次刷新页面发现 Reviews 都展示的是 v3 版本带红色星的 Ratings,说明我们VirtualService 的配置成功了。

reviews v3

基于权重的服务访问规则

刚刚我们演示的基于不同服务版本的服务网格的控制,接下来我们来演示下基于权重的服务访问规则的使用。

首先移除刚刚创建的 VirtualService 对象,排除对环境的影响:

$ kubectl delete virtualservice reviews
virtualservice.networking.istio.io "reviews" deleted
$ kubectl get virtualservice
NAME       GATEWAYS             HOSTS   AGE
bookinfo   [bookinfo-gateway]   [*]     11m

现在我们再去访问 Bookinfo 应用又回到最初随机访问 Reviews 的情况了。现在我们查看文件 samples/bookinfo/networking/virtual-service-reviews-80-20.yaml 的定义:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
  - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v1
      weight: 80
    - destination:
        host: reviews
        subset: v2
      weight: 20

这个规则定义了 80% 的对 Reviews 的流量会落入 v1 这个 subset,就是没有 Ratings 的这个服务,20% 会落入 v2 带黑色 Ratings 的这个服务,然后我们创建这个资源对象:

$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-80-20.yaml
virtualservice.networking.istio.io/reviews created
$ kubectl get virtualservice
NAME       GATEWAYS             HOSTS       AGE
bookinfo   [bookinfo-gateway]   [*]         12m
reviews                         [reviews]   13s

我们查看当前网格中的 VirtualService 对象,可以看到已经有 reviews 了,证明已经创建成功了,由于上面我们已经将应用中所有的 DestinationRules 都已经创建过了,所以现在我们直接访问应用就可以了,我们多次刷新,可以发现没有出现 Ratings 的次数与出现黑色星 Ratings 的比例大概在4:1左右,并且没有红色星的 Ratings 的情况出现,说明我们配置的基于权重的 VirtualService 访问规则配置生效了。

基于请求内容的服务访问规则

除了上面基于服务版本和服务权重的方式控制服务访问之外,我们还可以基于请求内容来进行访问控制。

同样,将上面创建的 VirtualService 对象删除:

$ kubectl delete virtualservice reviews
virtualservice.networking.istio.io "reviews" deleted
$ kubectl get virtualservice
NAME       GATEWAYS             HOSTS   AGE
bookinfo   [bookinfo-gateway]   [*]     14m

查看文件 samples/bookinfo/networking/virtual-service-reviews-jason-v2-v3.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: v3

这个 VirtualService 对象定义了对 reviews 服务访问的 match 规则。意思是如果当前请求的 header 中包含 jason 这个用户信息,则只会访问到 v2 的 reviews 这个服务版本,即都带黑星的样式,如果不包含该用户信息,则都直接将流量转发给 v3 这个 reviews 的服务。

我们先不启用这个 VirtualService,先去访问下 Bookinfo 这个应用: bookinfo login

右上角有登录按钮,在没有登录的情况下刷新页面,reviews 服务是被随机访问的,可以看到有带星不带星的样式,点击登录,在弹窗中 User Name 输入 jason,Password 为空,登录:

bookinfo logined

再刷新页面,可以看到跟未登录前的访问规则一样,也是随机的。

现在我们来创建上面的 VirtualService 这个对象:

$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-jason-v2-v3.yaml
virtualservice.networking.istio.io/reviews created
$ kubectl get virtualservice
NAME       GATEWAYS             HOSTS       AGE
bookinfo   [bookinfo-gateway]   [*]         22m
reviews                         [reviews]   12s

此时再回去刷新页面,发现一直都是黑星的 Reviews 版本(v2)被访问到了。注销退出后再访问,此时又一直是红星的版本(v3)被访问了。

说明我们基于 headers->end-user->exact:jason 的控制规则生效了。在 productpage 服务调用 reviews 服务时,登录的情况下会在 header 中带上用户信息,通过 exact 规则匹配到相关信息后,流量被引向了上面配置的 v2 版本中。

这里要说明一下 match 的匹配规则:

All conditions inside a single match block have AND semantics, while the list of match blocks have OR semantics. The rule is matched if any one of the match blocks succeed.

意思是一个 match 块里的条件是需要同时满足才算匹配成功的,如下面是 url 前缀和端口都必须都满足才算成功:

- match:
    - uri:
        prefix: "/wpcatalog"
      port: 443

多个 match 块之间是只要有一个 match 匹配成功了,就会被路由到它指定的服务版本去,而忽略其他的。我们的示例中在登录的条件下,满足第一个 match,所以服务一直会访问到 v2 版本。退出登录后,没有 match 规则满足匹配,所以就走最后一个 route 规则,即 v3 版本。

到这里,我们就和大家一起学习了基于不同服务版本、权重以及请求内容来控制服务流量的配置。

延迟访问故障注入

首先移除之前创建的 VirtualService:

$ kubectl delete virtualservice reviews
virtualservice.networking.istio.io "reviews" deleted
$ kubectl get virtualservice
NAME       GATEWAYS             HOSTS   AGE
bookinfo   [bookinfo-gateway]   [*]     16d

然后我们查看 istio 样例文件夹下面的文件 samples/bookinfo/networking/virtual-service-ratings-test-delay.yaml 内容:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ratings
spec:
  hosts:
  - ratings
  http:
  - match:
    - headers:
        end-user:
          exact: jason
    fault:
      delay:
        percentage:
          value: 100.0
        fixedDelay: 7s
    route:
    - destination:
        host: ratings
        subset: v1
  - route:
    - destination:
        host: ratings
        subset: v1

这个 VirtualService 定义了一个在 jason 登录的情况下,访问 ratings 服务的 100% 的 7s 访问延迟。前面我们知道,Bookinfo 这个示例 productpage 服务调用 reviews,reviews 的不同版本会对 ratings 进行不同的调用,其中 reviews-v1 不调用 ratings,reviews-v2 和 reviews-v3 会调用 ratings,并做不同样式的渲染。并且在 productpage 访问 reviews 时,代码中有硬编码 6s 中的访问超时限制,而 reviews 访问 ratings 编码了 10s 的访问超时限制。

了解这一点后,我们现在来创建这个 VirtualService 资源对象:

$ kubectl apply -f samples/bookinfo/networking/virtual-service-ratings-test-delay.yaml
virtualservice.networking.istio.io/ratings created
$ kubectl get virtualservice
NAME       GATEWAYS             HOSTS       AGE
bookinfo   [bookinfo-gateway]   [*]         16d
ratings                         [ratings]   13s

创建完成后,前往 Bookinfo 应用,登录 jason,打开浏览器的 Network,刷新页面,发现请求加载很慢,大约 6s 后,出现如下界面:

Ratings service unavailable

可以看到 Ratings 服务出现了 unavailable 的提示信息,这是因为此时 reviews 请求 ratings 的访问超过了 6s 还没有响应,使得 productpage 中的硬编码的超时设置生效了。

当然有的时候我们也能成功访问到 reviews-v1 版本,因为此时并没有进一步访问 ratings 服务,所以一切都是正常的,会显示不带星的界面:

bookinfo reviews v1

通过这种超时故障注入,可以帮助我们方便地发现服务间相互访问中存在的潜在问题。

中断访问故障注入

首先移除之前创建的 VirtualService:

$ kubectl delete virtualservice ratings
virtualservice.networking.istio.io "ratings" deleted
$ kubectl get virtualservice
NAME       GATEWAYS             HOSTS   AGE
bookinfo   [bookinfo-gateway]   [*]     16d

然后我们查看 istio 样例文件夹下面的文件 samples/bookinfo/networking/virtual-service-ratings-test-abort.yaml 的内容:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ratings
spec:
  hosts:
  - ratings
  http:
  - match:
    - headers:
        end-user:
          exact: jason
    fault:
      abort:
        percentage:
          value: 100.0
        httpStatus: 500
    route:
    - destination:
        host: ratings
        subset: v1
  - route:
    - destination:
        host: ratings
        subset: v1

通过上面的这个 yaml 文件我们可以看出这个 VirtualService 资源对象配置了在 jason 登录时,reviews 对 ratings 访问时 100% 的返回一个500错误响应。

然后同样创建这个资源对象:

$ kubectl apply -f samples/bookinfo/networking/virtual-service-ratings-test-abort.yaml
virtualservice.networking.istio.io/ratings created
$ kubectl get virtualservice
NAME       GATEWAYS             HOSTS       AGE
bookinfo   [bookinfo-gateway]   [*]         16d
ratings                         [ratings]   34s

现在我们回到 BookInfo 应用,登录 jason,刷新页面,有时候可以很快就看到 Rating 服务不可用的提示信息:

bookinfo error

服务网格外的流量管理

为了控制服务网格外的服务的流量访问,外部的服务必须首先使用一个 ServiceEntry 对象加入到 istio 的内部 service registry 中,服务网格才会知道如何导向这些外部服务的流量。

为了测试这个功能,我们使用 istio 样例中的 sleep 应用来验证改功能,查看 samples/sleep/sleep.yaml 文件内容:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: sleep
---
apiVersion: v1
kind: Service
metadata:
  name: sleep
  labels:
    app: sleep
spec:
  ports:
  - port: 80
    name: http
  selector:
    app: sleep
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sleep
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sleep
  template:
    metadata:
      labels:
        app: sleep
    spec:
      serviceAccountName: sleep
      containers:
      - name: sleep
        image: governmentpaas/curl-ssl
        command: ["/bin/sleep", "3650d"]
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - mountPath: /etc/sleep/tls
          name: secret-volume
      volumes:
      - name: secret-volume
        secret:
          secretName: sleep-secret
          optional: true
---

这其实就是一个简单的应用,通过 Deployment 进行控制,通过 Service 暴露服务,现在我们来部署该应用:

$ kubectl apply -f <(istioctl kube-inject -f samples/sleep/sleep.yaml)
serviceaccount/sleep created
service/sleep created
deployment.apps/sleep created

Istio 有一个安装选项,global.outboundTrafficPolicy.mode,它配置 sidecar 对外部服务(那些没有在 Istio 的内部服务注册中定义的服务)的处理方式。如果这个选项设置为 ALLOW_ANY,Istio 代理允许调用未知的服务。如果这个选项设置为 REGISTRY_ONLY,那么 Istio 代理会阻止任何没有在网格中定义的 HTTP 服务或 service entry 的主机。ALLOW_ANY 是默认值,不控制对外部服务的访问。但是这种方式有一个缺点,即丢失了对外部服务流量的 Istio 监控和控制,比如,外部服务的调用没有记录到 Mixer 的日志中。

这里为了测试 ServiceEntry 功能,我们将其更改为 REGISTRY_ONLY 模式:

$ kubectl edit configmap istio -n istio-system 
.......
apiVersion: v1
data:
  mesh: |-
    accessLogEncoding: TEXT
    accessLogFile: /dev/stdout
    accessLogFormat: ""
    outboundTrafficPolicy:  # 修改为REGISTRY_ONLY模式
      mode: REGISTRY_ONLY
    defaultConfig:
      concurrency: 2
      configPath: ./etc/istio/proxy
      connectTimeout: 10s
......

ConfigMap 修改完成后我们就可以接管外部 URL 的流量了。现在我们进入上面创建的 sleep 应用容器内部执行一些测试操作:

$ export SLEEP_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})
$ kubectl exec -it $SLEEP_POD -c sleep -- curl -sL -o /dev/null -D - http://edition.cnn.com/politics
HTTP/1.1 502 Bad Gateway
date: Sun, 28 Jun 2020 08:46:26 GMT
server: envoy
content-length: 0

可以看到会返回 502 信息,因为该域名不在当前的服务网格中,因为现在我们不允许访问服务网格外部的 URL,服务网格中对未知的服务请求会丢弃。这就需要我们来创建一个 ServiceEntry 对象,将外部的访问服务引入到服务网格中来。

例如下面的规则定义了一个访问 edition.cnn.com 的服务的 ServiceEntry:

# cnn-service-entry.yaml
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: cnn
spec:
  hosts:
  - edition.cnn.com
  ports:
  - number: 80
    name: http-port
    protocol: HTTP
  - number: 443
    name: https
    protocol: HTTPS
  resolution: DNS

现在我们来部署上面的 ServiceEntry 资源:

$ kubectl apply -f cnn-service-entry.yaml
serviceentry.networking.istio.io/cnn created
$ kubectl get serviceentry
NAME   HOSTS               LOCATION   RESOLUTION   AGE
cnn    [edition.cnn.com]              DNS          27s

现在我们再去上面的 sleep 容器中执行上面的测试请求:

$ kubectl exec -it $SLEEP_POD -c sleep -- curl -sL -o /dev/null -D - http://edition.cnn.com/politics
HTTP/1.1 301 Moved Permanently
server: envoy
retry-after: 0
content-length: 0
cache-control: public, max-age=600
location: https://edition.cnn.com/politics
......

HTTP/2 200
......
vary: x-fastab-0, Accept-Encoding
content-length: 1291663

现在我们发现可以正常返回内容了,返回200,证明请求成功了。

说明

-L 参数让 curl 跟随连接进行重定向。这里服务器直接返回的 301 重定向响应,要求客户端再使用 HTTPS 的方式对 https://edition.cnn.com/politics 地址进行访问,第二次访问才返回了200的成功码。

除此之外,我们还可以进一步配置egress gateway,使这些对外部的流量访问经由 egress 去到外部。

现在为 edition.cnn.com 端口 80 创建 egress Gateway。并为指向 egress gateway 的流量创建一个 destination rule:

# cnn-egress-gateway.yaml
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
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: egressgateway-for-cnn
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
  - name: cnn

除了上面的 engress gateway 对象之外,我们还需要创建 VirtualService 资源对象,将流量从 sidecar 引导至 egress gateway,再从 egress gateway 引导至外部服务:

# cnn-virtual.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: direct-cnn-through-egress-gateway
spec:
  hosts:
  - edition.cnn.com
  gateways:
  - istio-egressgateway
  - mesh  # mesh 表示网格中的所有 Sidecar,如果没有指定 gateways,则默认为 mesh
  http:
  - match:
    - gateways:
      - mesh
      port: 80
    route:
    - destination:  # 将服务路由到什么地方去
        host: istio-egressgateway.istio-system.svc.cluster.local
        subset: cnn  # subset 配置流量目的地的子集
        port:
          number: 80
      weight: 100
  - match:
    - gateways:
      - istio-egressgateway
      port: 80
    route:
    - destination:
        host: edition.cnn.com
        port:
          number: 80
      weight: 100

然后创建上面3个资源对象:

$ kubectl apply -f cnn-virtual.yaml
virtualservice.networking.istio.io/direct-cnn-through-egress-gateway created
$ kubectl apply -f cnn-egress-gateway.yaml
gateway.networking.istio.io/istio-egressgateway created
destinationrule.networking.istio.io/egressgateway-for-cnn created

创建完成后,现在我们再去 sleep 容器执行下上面的请求:

$ kubectl exec -it $SLEEP_POD -c sleep -- curl -sL -o /dev/null -D - http://edition.cnn.com/politics
HTTP/1.1 301 Moved Permanently
server: envoy
retry-after: 0
content-length: 0
cache-control: public, max-age=600
location: https://edition.cnn.com/politics
......

HTTP/2 200
......
vary: x-fastab-0, Accept-Encoding
content-length: 1291663

正常输出结果和上面一次访问是一样的。这时我们去查看 egressgateway 的 Pod 中的容器日志:

$ kubectl logs $(kubectl get pod -l istio=egressgateway -n istio-system -o jsonpath='{.items[0].metadata.name}') -n istio-system | tail
......
[2018-10-15T17:33:57.261Z] "GET /politics HTTP/2" 301 - 0 0 437 429 "10.244.4.206" "curl/7.35.0" "6d4f2ac3-0957-97a5-961a-241c7fd72536" "edition.cnn.com" "151.101.129.67:80"
......

可以看到有一条上面的 GET /politics 的日志信息,说明访问经过了 egress gateway 出去了。不过需要注意的是我们这里只定义了 egress gateway 的 80 端口流量,如果我们通过访问 HTTPS 则会直接跳转到 edition.cnn.com

用 Egress Gateway 处理 HTTPS 请求

接下来尝试使用 Egress Gateway 发起 HTTPS 请求。我们需要在相应的 ServiceEntry、Egress Gateway 和 VirtualService 中指定 TLS 协议的端口 443。

首先先清理前面定义的资源对象:

$ kubectl delete gateway istio-egressgateway
$ kubectl delete serviceentry cnn
$ kubectl delete virtualservice direct-cnn-through-egress-gateway
$ kubectl delete destinationrule egressgateway-for-cnn

这个时候如果我们去访问 https 的地址是不被允许的:

$ export SLEEP_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})
$ kubectl exec -it $SLEEP_POD -c sleep -- curl -sL -o /dev/null -D - https://edition.cnn.com/politics
command terminated with exit code 35

因为还没有定义 ServiceEntry 对象,所以接下来需要创建一个 HTTPS 版本的 ServiceEntry 对象:

# cnn-https-service-entry.yaml
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: cnn
spec:
  hosts:
  - edition.cnn.com
  ports:
  - number: 443
    name: tls
    protocol: TLS
  resolution: DNS

直接创建上面的资源对象:

$ kubectl apply -f cnn-https-service-entry.yaml
serviceentry.networking.istio.io/cnn created
$ kubectl get serviceentry
NAME        HOSTS               LOCATION   RESOLUTION   AGE
cnn   [edition.cnn.com]              DNS          37s

然后同样使用上面创建的 sleep 容器来测试访问 https 地址:

$ kubectl exec -it $SLEEP_POD -c sleep -- curl -sL -o /dev/null -D - https://edition.cnn.com/politics
HTTP/2 200
......
content-length: 1291663

可以看到直接访问 https 地址可以得到正确的结果了。然后同样为 edition.cnn.com 创建一个 egress Gateway。除此之外还需要创建一个 destination rule 和一个 virtual service,用来引导流量通过 egress gateway,并通过 egress gateway 与外部服务通信。

# cnn-https-service-rule.yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
spec:
  selector:
    istio: egressgateway
  servers:
  - port:
      number: 443
      name: tls
      protocol: TLS
    hosts:
    - edition.cnn.com
    tls:  # PASSTHROUGH 表示 gateway 按原样通过入口流量,而不终止 TLS
      mode: PASSTHROUGH
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: egressgateway-for-cnn
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
  - name: cnn
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: direct-cnn-through-egress-gateway
spec:
  hosts:
  - edition.cnn.com
  gateways:
  - mesh 
  - istio-egressgateway
  tls:
  - match:
    - gateways:
      - mesh
      port: 443
      sniHosts:
      - edition.cnn.com
    route:
    - destination:
        host: istio-egressgateway.istio-system.svc.cluster.local
        subset: cnn
        port:
          number: 443
  - match:
    - gateways:
      - istio-egressgateway
      port: 443
      sniHosts:
      - edition.cnn.com
    route:
    - destination:
        host: edition.cnn.com
        port:
          number: 443
      weight: 100

直接创建上面的3个对象即可:

$ kubectl apply -f cnn-https-service-rule.yaml
gateway.networking.istio.io/istio-egressgateway created
destinationrule.networking.istio.io/egressgateway-for-cnn created
virtualservice.networking.istio.io/direct-cnn-through-egress-gateway created

现在再使用上面创建的 sleep 容器来测试访问 https 地址:

$ kubectl exec -it $SLEEP_POD -c sleep -- curl -sL -o /dev/null -D - https://edition.cnn.com/politics
HTTP/2 200
......
content-length: 1291663

正常和上面返回的内容是一样的,但是这个时候这时我们去查看 egressgateway 的 Pod 中的容器日志:

$ kubectl logs $(kubectl get pod -l istio=egressgateway -n istio-system -o jsonpath='{.items[0].metadata.name}') -n istio-system | tail
......
[2020-06-28T11:12:41.064Z] "- - -" 0 - "-" "-" 875 758858 76501 - "-" "-" "-" "-" "151.101.1.67:443" outbound|443||edition.cnn.com 10.244.8.175:50194 10.244.8.175:8443 10.244.2.87:56520 edition.cnn.com -

就可以看到 https://edition.cnn.com 的请求日志了。不过需要注意,Istio 中定义的 Egress Gateway 本身并没有为其所在的节点提供任何特殊处理。集群管理员或云提供商可以在专用节点上部署 Egress gateway,并引入额外的安全措施,从而使这些节点比网格中的其他节点更安全。

由于 Istio 无法强制让所有出站流量都经过 egress gateway,Istio 只是通过 sidecar 代理实现了这种流向。攻击者只要绕过 sidecar 代理,就可以不经 egress gateway 直接与网格外的服务进行通信,从而避开了 Istio 的控制和监控。出于安全考虑,集群管理员和云供应商必须确保网格所有的出站流量都要经过 egress gateway。这需要通过 Istio 之外的机制来满足这一要求。例如,集群管理员可以配置防火墙,拒绝 egress gateway 以外的所有流量。Kubernetes 网络策略也能禁止所有不是从 egress gateway 发起的出站流量。此外,集群管理员和云供应商还可以对网络进行限制,让运行应用的节点只能通过 gateway 来访问外部网络。要实现这一限制,可以只给 gateway Pod 分配公网 IP,并且可以配置 NAT 设备,丢弃来自 egress gateway pod 之外的所有流量。

TCP 流量转移

上面展示了多种流量管理的方式,该部分主要讲解在 Istio 中如何将 TCP 流量从一个版本迁移到另一个版本。在 Istio 中,我们可以通过配置规则来实现该功能,可以按指定的百分比将流量路由到不同的服务。和前面我们使用的 HTTP 服务非常类似。

这里我们先将所有的 TCP 请求路由到 tcp-echo:v1 这个服务,然后在通过配置 Istio 的路由权重把 20% 的流量分配到 tcp-echo:v2 这个版本的服务上。

首先,部署微服务 tcp-echo 的 v1 版本,同样直接使用示例 samples/tcp-echo/tcp-echo-services.yaml

# samples/tcp-echo/tcp-echo-services.yaml
apiVersion: v1
kind: Service
metadata:
  name: tcp-echo
  labels:
    app: tcp-echo
spec:
  ports:
  - name: tcp
    port: 9000
  - name: tcp-other
    port: 9001
  # Port 9002 is omitted intentionally for testing the pass through filter chain.
  selector:
    app: tcp-echo
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tcp-echo-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tcp-echo
      version: v1
  template:
    metadata:
      labels:
        app: tcp-echo
        version: v1
    spec:
      containers:
      - name: tcp-echo
        image: docker.io/istio/tcp-echo-server:1.2
        imagePullPolicy: IfNotPresent
        args: [ "9000,9001,9002", "one" ]
        ports:
        - containerPort: 9000
        - containerPort: 9001
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tcp-echo-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tcp-echo
      version: v2
  template:
    metadata:
      labels:
        app: tcp-echo
        version: v2
    spec:
      containers:
      - name: tcp-echo
        image: docker.io/istio/tcp-echo-server:1.2
        imagePullPolicy: IfNotPresent
        args: [ "9000,9001,9002", "two" ]
        ports:
        - containerPort: 9000
        - containerPort: 9001

上面部署了两个版本的 tcp-echo 服务,要注意的是 Service 对象中的 Label Selector 是 app=tcp-echo,这样该对象就会匹配到上面的两个版本的对象。使用如下命令安装:

$ kubectl apply -f <(istioctl kube-inject -f samples/tcp-echo/tcp-echo-services.yaml) service/tcp-echo created
deployment.apps/tcp-echo-v1 created
deployment.apps/tcp-echo-v2 created
$ kubectl get pods -l app=tcp-echo
NAME                          READY   STATUS    RESTARTS   AGE
tcp-echo-v1-fcfbd5dd5-p9tlw   2/2     Running   0          3m41s
tcp-echo-v2-6c6d456db-mtdxp   2/2     Running   0          3m41s

istioctl kube-inject 命令用于在创建 Deployments 之前注入 Istio 的 Sidecar 容器,同样我们也可以直接将当前的命名空间打上一个 istio-injection=enabled 的 Label 标签,这样该命名空间就开启了 Sidecar 容器自动注入功能。

接下来, 将微服务 tcp-echo 的 TCP 流量全部路由到 v1 版本上,直接使用 samples/tcp-echo/tcp-echo-all-v1.yaml 这个示例文件中的对象,如下所示:

# samples/tcp-echo/tcp-echo-all-v1.yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: tcp-echo-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 31400
      name: tcp
      protocol: TCP
    hosts:
    - "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: tcp-echo-destination
spec:
  host: tcp-echo
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: tcp-echo
spec:
  hosts:
  - "*"
  gateways:
  - tcp-echo-gateway
  tcp:
  - match:
    - port: 31400
    route:
    - destination:
        host: tcp-echo
        port:
          number: 9000
        subset: v1

这里创建了一个 Gateway 对象,用来路由 TCP 的请求,要注意这里匹配的是 istio=ingressgateway 这个 Service 对象的 31400 这个 TCP 端口:

$ kubectl get svc -n istio-system -l istio=ingressgateway -o yaml
......
- name: tcp
  nodePort: 31871
  port: 31400
  protocol: TCP
  targetPort: 31400
......

然后在 DestinationRule 对象中声明了 v1v2 两个子集服务,在 VirtualService 对象中声明具体的路由规则,我们可以看到是直接全都路由到了 v1 这个子集中去,直接创建上面的资源对象:

$ kubectl apply -f samples/tcp-echo/tcp-echo-all-v1.yaml
gateway.networking.istio.io/tcp-echo-gateway created
destinationrule.networking.istio.io/tcp-echo-destination created
virtualservice.networking.istio.io/tcp-echo created

创建完成后我们就可以通过 istio-ingressgateway 去访问上面的 TCP 服务了,要在外部访问该服务,同样我们可以通过 istio-ingressgateway 的 NodePort 来访问,这里的 TCP 对应的 nodePort 端口是 31871,这里可以使用如下所示的命令来测试:

$ export INGRESS_PORT=31871  # TCP 服务的 nodePort 端口
$ export INGRESS_HOST=k8s.qikqiak.com  # 任意一个节点的 IP

然后向微服务 tcp-echo 发送一些 TCP 请求:

$ for i in {1..10}; do \
> docker run -e INGRESS_HOST=$INGRESS_HOST -e INGRESS_PORT=$INGRESS_PORT -it --rm busybox sh -c "(date; sleep 1) | nc $INGRESS_HOST $INGRESS_PORT"; \
> done
one Thu Jul  2 07:20:20 UTC 2020
one Thu Jul  2 07:20:24 UTC 2020
one Thu Jul  2 07:20:27 UTC 2020
one Thu Jul  2 07:20:31 UTC 2020
one Thu Jul  2 07:20:34 UTC 2020
one Thu Jul  2 07:20:37 UTC 2020
one Thu Jul  2 07:20:41 UTC 2020
one Thu Jul  2 07:20:44 UTC 2020
one Thu Jul  2 07:20:47 UTC 2020
one Thu Jul  2 07:20:50 UTC 2020

从上面的日志可以看出,所有时间戳的前缀都是 one,这意味着所有流量都被路由到了 tcp-echo 服务的 v1 版本中。

然后使用新的路由规则将 20% 的流量从 tcp-echo:v1 转移到 tcp-echo:v2,对应的示例文件为 samples/tcp-echo/tcp-echo-20-v2.yaml,内容如下所示:

# samples/tcp-echo/tcp-echo-20-v2.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: tcp-echo
spec:
  hosts:
  - "*"
  gateways:
  - tcp-echo-gateway
  tcp:
  - match:
    - port: 31400
    route:
    - destination:
        host: tcp-echo
        port:
          number: 9000
        subset: v1
      weight: 80
    - destination:
        host: tcp-echo
        port:
          number: 9000
        subset: v2
      weight: 20

上面的 VirtualService 对象也很简单,给 v1 子集服务加了 80% 的权重,另外有 20% 的权重被路由到了 v2 这个子集服务中去了,同样直接创建上面的资源对象更新路由规则:

$ kubectl apply -f samples/tcp-echo/tcp-echo-20-v2.yaml
virtualservice.networking.istio.io/tcp-echo configured

等待几秒钟,让新的路由规则生效。然后同样用上面的测试命令向 tcp-echo 服务发送 TCP 请求:

$ $ for i in {1..10}; do \
> docker run -e INGRESS_HOST=$INGRESS_HOST -e INGRESS_PORT=$INGRESS_PORT -it --rm busybox sh -c "(date; sleep 1) | nc $INGRESS_HOST $INGRESS_PORT"; \
> done
two Thu Jul  2 07:27:03 UTC 2020
one Thu Jul  2 07:27:09 UTC 2020
one Thu Jul  2 07:27:13 UTC 2020
one Thu Jul  2 07:27:16 UTC 2020
one Thu Jul  2 07:27:21 UTC 2020
two Thu Jul  2 07:27:24 UTC 2020
one Thu Jul  2 07:27:27 UTC 2020
one Thu Jul  2 07:27:32 UTC 2020
two Thu Jul  2 07:27:36 UTC 2020
one Thu Jul  2 07:27:39 UTC 2020

现在我们可以发现,有大约 20% 的流量时间戳前缀是 two ,这意味着有 80% 的 TCP 流量路由到了 tcp-echo 服务的 v1 版本,与此同时有 20% 流量路由到了 v2 版本。

这样我们实现了对 TCP 流量的转移,和前面我们使用的 HTTP 服务非常类似。

熔断

熔断是创建弹性微服务应用程序的重要模式,熔断能够使我们的应用程序具备应对来自故障、潜在峰值和其他未知网络因素影响的能力,熔断在微服务框架中也是必备的一个功能。

同样在 Istio 中也是原生就支持熔断功能的,首先部署示例服务 samples/httpbin/httpbin.yaml

# samples/httpbin/httpbin.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: httpbin
---
apiVersion: v1
kind: Service
metadata:
  name: httpbin
  labels:
    app: httpbin
spec:
  ports:
  - name: http
    port: 8000
    targetPort: 80
  selector:
    app: httpbin
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: httpbin
      version: v1
  template:
    metadata:
      labels:
        app: httpbin
        version: v1
    spec:
      serviceAccountName: httpbin
      containers:
      - image: docker.io/kennethreitz/httpbin
        imagePullPolicy: IfNotPresent
        name: httpbin
        ports:
        - containerPort: 80

该应用也很简单,直接使用如下所示命令部署即可:

$ kubectl apply -f <(istioctl kube-inject -f samples/httpbin/httpbin.yaml)
serviceaccount/httpbin created
service/httpbin created
deployment.apps/httpbin created
$ kubectl get pods -l app=httpbin
NAME                       READY   STATUS    RESTARTS   AGE
httpbin-7b7585564c-bt9kp   2/2     Running   0          16m

应用部署完成后,接着创建一个目标规则,在调用 httpbin 服务时配置熔断设置。这里通过一个 DestinationRule 对象来创建一个如下所示的资源对象:

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: httpbin
spec:
  host: httpbin
  trafficPolicy:  
    connectionPool:
      tcp:
        maxConnections: 1  # 最大连接数为1
      http:
        http1MaxPendingRequests: 1  # 最大请求数为1
        maxRequestsPerConnection: 1
    outlierDetection:
      consecutiveErrors: 1
      interval: 1s
      baseEjectionTime: 3m
      maxEjectionPercent: 100
EOF

目标规则创建完成后,接着我们来创建一个客户端应用来发送流量请求到 httpbin 服务,这里我们使用一个名为 Fortio 的测试客户端应用,该应用可以控制连接数、并发数及发送 HTTP 请求的延迟,通过 Fortio 能够有效的触发前面在 DestinationRule 中设置的熔断策略。

客户端应用资源清单文件位于 samples/httpbin/sample-client/fortio-deploy.yaml,内容如下所示:

# samples/httpbin/sample-client/fortio-deploy.yaml
apiVersion: v1
kind: Service
metadata:
  name: fortio
  labels:
    app: fortio
spec:
  ports:
  - port: 8080
    name: http
  selector:
    app: fortio
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fortio-deploy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: fortio
  template:
    metadata:
      annotations:
        # This annotation causes Envoy to serve cluster.outbound statistics via 15000/stats
        # in addition to the stats normally served by Istio.  The Circuit Breaking example task
        # gives an example of inspecting Envoy stats.
        sidecar.istio.io/statsInclusionPrefixes: cluster.outbound,cluster_manager,listener_manager,http_mixer_filter,tcp_mixer_filter,server,cluster.xds-grpc
      labels:
        app: fortio
    spec:
      containers:
      - name: fortio
        image: fortio/fortio:latest_release
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
          name: http-fortio
        - containerPort: 8079
          name: grpc-ping

直接部署上面的客户端应用,记得注入 Istio Sidecar 代理,以便 Istio 对其网络交互进行管理:

$ kubectl apply -f <(istioctl kube-inject -f samples/httpbin/sample-client/fortio-deploy.yaml)
service/fortio created
deployment.apps/fortio-deploy created
$ kubectl get pods -l app=fortio
NAME                             READY   STATUS    RESTARTS   AGE
fortio-deploy-5cddbb9586-wrm6f   2/2     Running   0          6m23s

部署完成后我们可以进入到客户端应用中使用 Fortio 工具调用 httpbin 服务。-curl 参数表明发送一次调用:

$ FORTIO_POD=$(kubectl get pod | grep fortio | awk '{ print $1 }')
$ kubectl exec -it $FORTIO_POD  -c fortio -- /usr/bin/fortio load -curl  http://httpbin:8000/get
HTTP/1.1 200 OK
server: envoy
date: Thu, 02 Jul 2020 07:58:53 GMT
content-type: application/json
content-length: 621
access-control-allow-origin: *
access-control-allow-credentials: true
x-envoy-upstream-service-time: 49

{
  "args": {},
  "headers": {
    "Content-Length": "0",
    "Host": "httpbin:8000",
    "User-Agent": "fortio.org/fortio-1.3.1",
    "X-B3-Parentspanid": "4b6b58f3e7f6fbef",
    "X-B3-Sampled": "1",
    "X-B3-Spanid": "65503dee233ed2e8",
    "X-B3-Traceid": "ae79fbcbcc1d58244b6b58f3e7f6fbef",
    "X-Envoy-Attempt-Count": "1",
    "X-Forwarded-Client-Cert": "By=spiffe://cluster.local/ns/default/sa/httpbin;Hash=9df7ef19dd6325879dcaa5cef793f936931624bded10a063dd7a927afb77adb5;Subject=\"\";URI=spiffe://cluster.local/ns/default/sa/default"
  },
  "origin": "127.0.0.1",
  "url": "http://httpbin:8000/get"
}

可以看到调用后端服务的请求是成功的,然后我们来测试下熔断。在 DestinationRule 配置中,我们定义了 maxConnections: 1http1MaxPendingRequests: 1,表示如果并发的连接和请求数超过一个,则在 istio-proxy 进行进一步的请求和连接时,后续的请求或连接将被阻止。

比如这里我们发送并发数为 2 的连接(-c 2),请求 20 次(-n 20)来观察下现象:

$ kubectl exec -it $FORTIO_POD  -c fortio /usr/bin/fortio -- load -c 2 -qps 0 -n 20 -loglevel Warning http://httpbin:8000/get
15:59:27 I logger.go:97> Log level is now 3 Warning (was 2 Info)
Fortio 1.3.1 running at 0 queries per second, 4->4 procs, for 20 calls: http://httpbin:8000/get
Starting at max qps with 2 thread(s) [gomax 4] for exactly 20 calls (10 per thread + 0)
15:59:27 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
15:59:27 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
15:59:27 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
15:59:27 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
15:59:27 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
Ended after 147.547078ms : 20 calls. qps=135.55
Aggregated Function Time : count 20 avg 0.01428141 +/- 0.0143 min 0.000651885 max 0.061146337 sum 0.285628198
# range, mid point, percentile, count
>= 0.000651885 <= 0.001 , 0.000825943 , 5.00, 1
> 0.002 <= 0.003 , 0.0025 , 10.00, 1
> 0.003 <= 0.004 , 0.0035 , 15.00, 1
> 0.005 <= 0.006 , 0.0055 , 30.00, 3
> 0.006 <= 0.007 , 0.0065 , 50.00, 4
> 0.008 <= 0.009 , 0.0085 , 55.00, 1
> 0.011 <= 0.012 , 0.0115 , 60.00, 1
> 0.012 <= 0.014 , 0.013 , 70.00, 2
> 0.016 <= 0.018 , 0.017 , 75.00, 1
> 0.018 <= 0.02 , 0.019 , 80.00, 1
> 0.025 <= 0.03 , 0.0275 , 90.00, 2
> 0.035 <= 0.04 , 0.0375 , 95.00, 1
> 0.06 <= 0.0611463 , 0.0605732 , 100.00, 1
# target 50% 0.007
# target 75% 0.018
# target 90% 0.03
# target 99% 0.0609171
# target 99.9% 0.0611234
Sockets used: 7 (for perfect keepalive, would be 2)
Code 200 : 15 (75.0 %)
Code 503 : 5 (25.0 %)
Response Header Sizes : count 20 avg 172.85 +/- 99.8 min 0 max 231 sum 3457
Response Body/Total Sizes : count 20 avg 698.85 +/- 264.3 min 241 max 852 sum 13977
All done 20 calls (plus 0 warmup) 14.281 ms avg, 135.5 qps

可以看到大部分请求还是都完成了,但是并不是一半的请求失败,这是因为 istio-proxy 并不是100%准确的:

Code 200 : 15 (75.0 %)
Code 503 : 5 (25.0 %)

然后我们将并发连接数提高到 3 个:

$ kubectl exec -it $FORTIO_POD  -c fortio /usr/bin/fortio -- load -c 3 -qps 0 -n 30 -loglevel Warning http://httpbin:8000/get
16:04:52 I logger.go:97> Log level is now 3 Warning (was 2 Info)
Fortio 1.3.1 running at 0 queries per second, 4->4 procs, for 30 calls: http://httpbin:8000/get
Starting at max qps with 3 thread(s) [gomax 4] for exactly 30 calls (10 per thread + 0)
16:04:52 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
16:04:52 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
16:04:52 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
16:04:52 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
16:04:52 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
16:04:52 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
16:04:52 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
16:04:52 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
16:04:52 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
16:04:52 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
16:04:52 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
16:04:53 W http_client.go:679> Parsed non ok code 503 (HTTP/1.1 503)
Ended after 118.870596ms : 30 calls. qps=252.38
Aggregated Function Time : count 30 avg 0.007441411 +/- 0.006978 min 0.001160642 max 0.036414088 sum 0.223242331
# range, mid point, percentile, count
>= 0.00116064 <= 0.002 , 0.00158032 , 23.33, 7
> 0.002 <= 0.003 , 0.0025 , 40.00, 5
> 0.007 <= 0.008 , 0.0075 , 56.67, 5
> 0.008 <= 0.009 , 0.0085 , 70.00, 4
> 0.009 <= 0.01 , 0.0095 , 80.00, 3
> 0.01 <= 0.011 , 0.0105 , 83.33, 1
> 0.011 <= 0.012 , 0.0115 , 90.00, 2
> 0.012 <= 0.014 , 0.013 , 93.33, 1
> 0.018 <= 0.02 , 0.019 , 96.67, 1
> 0.035 <= 0.0364141 , 0.035707 , 100.00, 1
# target 50% 0.0076
# target 75% 0.0095
# target 90% 0.012
# target 99% 0.0359899
# target 99.9% 0.0363717
Sockets used: 14 (for perfect keepalive, would be 3)
Code 200 : 18 (60.0 %)
Code 503 : 12 (40.0 %)
Response Header Sizes : count 30 avg 138.06667 +/- 112.7 min 0 max 231 sum 4142
Response Body/Total Sizes : count 30 avg 607.06667 +/- 298.9 min 241 max 852 sum 18212
All done 30 calls (plus 0 warmup) 7.441 ms avg, 252.4 qps

我们可以看到大部分的请求都被熔断器拦截:

Code 200 : 18 (60.0 %)
Code 503 : 12 (40.0 %)

我们可以通过 istio-proxy 状态来了解更多关于熔断的详情,使用如下命令进行查看:

$ kubectl exec $FORTIO_POD -c istio-proxy -- pilot-agent request GET stats | grep httpbin | grep pending
cluster.outbound|8000||httpbin.default.svc.cluster.local.circuit_breakers.default.rq_pending_open: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.circuit_breakers.high.rq_pending_open: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_active: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_failure_eject: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_overflow: 17
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_total: 37

其中 upstream_rq_pending_overflow 值为17,这表示目前为止已有17个调用被标记为熔断了。

流量镜像

流量镜像也称为影子流量,是一个以尽可能低的风险为生产带来变化的强大的功能,镜像会将实时流量的副本发送到镜像服务。前面我们在学习 Traefik 的时候就测试过类似的功能。

这里我们先把流量全部路由到 v1 版本的测试服务中,然后添加路由规则将一部分流量镜像到 v2 版本中去,来测试流量镜像功能。

首先部署 v1 版本的 httpbin 服务:

$ cat <<EOF | istioctl kube-inject -f - | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: httpbin
      version: v1
  template:
    metadata:
      labels:
        app: httpbin
        version: v1
    spec:
      containers:
      - image: docker.io/kennethreitz/httpbin
        imagePullPolicy: IfNotPresent
        name: httpbin
        command: ["gunicorn", "--access-logfile", "-", "-b", "0.0.0.0:80", "httpbin:app"]
        ports:
        - containerPort: 80
EOF

然后部署 v2 版本的 httpbin 服务:

$ cat <<EOF | istioctl kube-inject -f - | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: httpbin
      version: v2
  template:
    metadata:
      labels:
        app: httpbin
        version: v2
    spec:
      containers:
      - image: docker.io/kennethreitz/httpbin
        imagePullPolicy: IfNotPresent
        name: httpbin
        command: ["gunicorn", "--access-logfile", "-", "-b", "0.0.0.0:80", "httpbin:app"]
        ports:
        - containerPort: 80
EOF

然后创建一个 httpbin 的 Service 对象,关联上面的两个版本服务:

$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: httpbin
  labels:
    app: httpbin
spec:
  ports:
  - name: http
    port: 8000
    targetPort: 80
  selector:
    app: httpbin
EOF

然后在不是一个 sleep 服务,这样就可以使用 curl 来提供负载了:

$ cat <<EOF | istioctl kube-inject -f - | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sleep
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sleep
  template:
    metadata:
      labels:
        app: sleep
    spec:
      containers:
      - name: sleep
        image: tutum/curl
        command: ["/bin/sleep","infinity"]
        imagePullPolicy: IfNotPresent
EOF

默认情况下,Kubernetes 在 httpbin 服务的两个版本之间进行负载均衡。这里我们首先创建一个默认路由规则,将所有流量路由到服务的 v1 版本中去:

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
  - httpbin
  http:
  - route:
    - destination:
        host: httpbin
        subset: v1
      weight: 100
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: httpbin
spec:
  host: httpbin
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
EOF

上面对象创建完成后,所有流量都会转到 httpbin:v1 服务下面去,使用如下所示的命令来测试以下:

$ export SLEEP_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})
$ kubectl exec -it $SLEEP_POD -c sleep -- sh -c 'curl  http://httpbin:8000/headers' | python -m json.tool
{
    "headers": {
        "Accept": "*/*",
        "Content-Length": "0",
        "Host": "httpbin:8000",
        "User-Agent": "curl/7.35.0",
        "X-B3-Parentspanid": "2f5164f4206f2958",
        "X-B3-Sampled": "1",
        "X-B3-Spanid": "233ea866989fc458",
        "X-B3-Traceid": "336d07e6e360544d2f5164f4206f2958",
        "X-Envoy-Attempt-Count": "1",
        "X-Forwarded-Client-Cert": "By=spiffe://cluster.local/ns/default/sa/default;Hash=af8a4f153a257b346fcf8e88418d21781f145305c8ad98943ef43134ed147b57;Subject=\"\";URI=spiffe://cluster.local/ns/default/sa/default"
    }
}

然后可以分别查看 httpbin 服务 v1v2 两个 Pods 的日志,正常情况下只会看到 v1 会产生日志,v1 中没有:

$ export V1_POD=$(kubectl get pod -l app=httpbin,version=v1 -o jsonpath={.items..metadata.name})
$ kubectl logs -f $V1_POD -c httpbin
127.0.0.1 - - [02/Jul/2020:16:27:47 +0800] "GET /headers HTTP/1.1" 200 553 "-" "curl/7.35.0"
$ export V2_POD=$(kubectl get pod -l app=httpbin,version=v2 -o jsonpath={.items..metadata.name})
$ kubectl logs -f $V2_POD -c httpbin

这是因为上面我们创建的路由规则是将所有的请求都路由到了 v1 这个版本的服务中去。接下来我们更改下流量规则,将流量镜像到 v2 版本的服务中去:

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
  - httpbin
  http:
  - route:
    - destination:
        host: httpbin
        subset: v1
      weight: 100
    mirror:
      host: httpbin
      subset: v2
    mirror_percent: 100
EOF

这个路由规则会发送 100% 流量到 v1 这个子集服务,然后通过 mirror 属性配置了将流量也 100% 镜像到了 httpbin:v2 服务。当流量被镜像时,请求将发送到镜像服务中,并在 headers 中的 Host/Authority 属性值上追加 -shadow

需要注意的是这些被镜像的流量是『 即发即弃』的,也就是说镜像请求的响应会被丢弃。

现在我们在用上面的命令来测试发送一次请求:

$ kubectl exec -it $SLEEP_POD -c sleep -- sh -c 'curl  http://httpbin:8000/headers' | python -m json.tool

现在就可以看到 v1v2 中都有了访问日志,v2 中的访问日志就是由镜像流量产生的,这些请求的实际目标是 v1

$ kubectl logs -f $V1_POD -c httpbin
127.0.0.1 - - [02/Jul/2020:16:27:47 +0800] "GET /headers HTTP/1.1" 200 553 "-" "curl/7.35.0"
127.0.0.1 - - [02/Jul/2020:16:32:06 +0800] "GET /headers HTTP/1.1" 200 553 "-" "curl/7.35.0"
$ kubectl logs -f $V2_POD -c httpbin
127.0.0.1 - - [02/Jul/2020:16:32:06 +0800] "GET /headers HTTP/1.1" 200 593 "-" "curl/7.35.0"

到这里我们就实现了流量镜像的功能。