Istio 的频率限制

本来这个应该是作为第三天“零散功能点”介绍的,结果目标规则部分遇到一个 bug 一直没得到修正,就拖 着了——然后后来发现自己这个想法挺无知的——零散的功能点非常多,非常大,而且文档非常弱,很难搞,只好 逐个介绍了。

简介

调用频率限制这个功能其实也是比较常见的东西了。这里就不多做介绍了。下面简单粗暴的介绍一下测试要完 成的目标。

测试中我们将使用两个服务,服务叫 workload,客户端叫 sleep,workload 服务正常返回群众喜闻乐见 的 “Hello World”,而 sleep 仅用来做 Shell 方便测试。

测试过程将为 workload 建立规则:

  • 对于任意访问,十秒钟之内仅能访问两次;
  • 对于来自 sleep 的访问,十秒钟之内仅能访问一次

下面所有内容都在 default 命名空间进行

建立测试环境

编辑 workload.yaml 以及 sleep.yaml 之后, 我们使用如下命令运行(这两部分源码,只有些乌七八糟的标签和命名有点问题,其他很普通。 见最后页)

istioctl kube-inject -f workload.yaml | kubectl apply -f -
istioctl kube-inject -f sleep.yaml | kubectl apply -f -

kubectl get pods 确定 Pod 成功启动,也可以使用 curl 测试 NodePort,会得到 40x 的错误 页面。

建立规则

quota.yaml

这部分内容规则来自:https://istio.io/docs/reference/config/mixer/template/quota.html

这个对象用于设置检测服务来源、目标的标准维度,下面我们设置的是利用服务名称来检测。


这部分可能存在 bug,官方介绍的 source 定义是 source.labels["app"] | source.service | "unknown" 我的理解是说首先获取服务 Pod 的 app 标签内容,如果没有,再获取服务名称。但是后面我们会看到, 删除source.labels["app"]的定义之后,最终生效的依旧是 app 标签,这也是为什么给 workload

加上这么多奇怪标签的原因。

apiVersion: config.istio.io/v1alpha2
kind: quota
metadata:
  name: requestcount
spec:
  dimensions:
    source: source.service | "unknown"
    sourceVersion: source.labels["version"] | "unknown"
    destination:  destination.service | "unknown"

handler.yaml

这一对象的定义来自:https://istio.io/docs/reference/config/mixer/adapters/memquota.html

下面的代码对应我们之前的目的,任意访问都是每 10 秒钟两次,而特定源和目标的访问则是每 10 秒钟一 次。

其中 quotas 部分,name 字段的来源是上面的 quota.yaml

这个不知道是不是 bug,必须写上半个 fqdn。

dimentions 字段则需要使用 quota.yaml 定义的维度。这里的维度可以比 quota.yaml 中定义的 维度宽松(也就是少,例如定义三个,只使用其中的两个),可以理解,这也符合匹配的规则。

apiVersion: config.istio.io/v1alpha2
kind: memquota
metadata:
  name: handler
spec:
  quotas:
  - name: requestcount.quota.default
    maxAmount: 2
    validDuration: 10s
    overrides:
    - dimensions:
        destination: workload-pod.default.svc.cluster.local
        source: sleep.default.svc.cluster.local
      maxAmount: 1
      validDuration: 10s

rule.yaml

这部分的定义参考:https://istio.io/docs/reference/config/mixer/policy-and-telemetry-rules.html#istio.mixer.v1.config.Rule

这个对象的作用就是,对于符合筛选条件的服务调用,使用对应的 handler 进行处理。

这里的 handler 和 requestcount 两个字段的内容,也需要是 实力名称.对象类型的方式。

apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: quota
spec:
  actions:
  - handler: handler.memquota
    instances:
    - requestcount.quota

测试运行

集群外

使用 curl 重复访问 NodePort 三次,会发现第三次出现:RESOURCE_EXHAUSTED:Quota is exhausted for: RequestCount#

这说明我们设置的通用规则生效了。

集群内

在 sleep pod 中同样执行 curl 三次,发现第二次就开始出现超限说明。

上面两种情况,在超过时间窗口限制之后,都会自动恢复。

幕后

定义 quota 对象,在系统中会展现为一系列的计数器,计数器的维度就是quota 中的维度的笛卡尔积。 如果在 validDuration 的时间窗口过期之前调用次数超过了 maxAmount 规定,Mixer 就会返回 RESOURCE_EXHAUSTED 给 Envoy,Envoy 则会反馈 429 代码给调用方。

结论

文档不完善的开源系统。。坑真大啊!

源码

Workload 的 Deployment 和 Service:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    app: workload-app
    version: http200
  name: workload-200
spec:
  replicas: 1
  selector:
    matchLabels:
      app: workload-pod
      version: http200
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: workload-pod
        version: http200
    spec:
      containers:
      - image: php:7.0-apache
        imagePullPolicy: IfNotPresent
        name: http200
        ports:
        - containerPort: 80
          name: http
          protocol: TCP
      dnsPolicy: ClusterFirst
      restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: workload-service
    version: http200
  name: workload-200
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
    nodePort: 30200
  selector:
    app: workload-pod
    version: http200
  sessionAffinity: None
  type: NodePort

Sleep 的定义

apiVersion: v1
kind: Service
metadata:
  name: sleep
  labels:
    app: sleep
spec:
  ports:
  - port: 80
    name: http
  selector:
    app: sleep
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: sleep
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: sleep
    spec:
      containers:
      - name: sleep
        image: tutum/curl
        command: ["/bin/sleep","infinity"]
        imagePullPolicy: IfNotPresent