如何使用 Istio 动态的对服务通信进行速率限制。
kubectl apply -f samples/bookinfo/networking/virtual-service-all-v1.yaml
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
---
此任务中会根据客户的 IP 地址,针对对目标为 productpage 的流量配置 Istio 的速率限制。客户端使用X-Forwarded-For 请求 Header 存放客户端 IP 地址。此外还会针对用户的登录情况进行有条件的速率限制。为方便起见,可以使用 memquota 适配器完成速率限制。但是在生产系统上就需要提供 Redis 服务,然后配置 redisquota 适配器。 memquota 和 redisquota 适配器都支持 quota template,因此,在两个适配器上启用速率限制的配置是相同的。
速率限制配置分为两部分。
kubectl apply -f samples/bookinfo/policy/mixer-rule-productpage-ratelimit.yaml
YAML内容具体如下:
apiVersion: config.istio.io/v1alpha2
kind: handler
metadata:
name: quotahandler
namespace: istio-system
spec:
compiledAdapter: memquota
params:
quotas:
- name: requestcountquota.instance.istio-system
maxAmount: 500
validDuration: 1s
# The first matching override is applied.
# A requestcount instance is checked against override dimensions.
overrides:
# The following override applies to 'reviews' regardless
# of the source.
- dimensions:
destination: reviews
maxAmount: 1
validDuration: 5s
# The following override applies to 'productpage' when
# the source is a specific ip address.
- dimensions:
destination: productpage
source: "10.28.11.20"
maxAmount: 500
validDuration: 1s
# The following override applies to 'productpage' regardless
# of the source.
- dimensions:
destination: productpage
maxAmount: 2
validDuration: 5s
---
apiVersion: config.istio.io/v1alpha2
kind: instance
metadata:
name: requestcountquota
namespace: istio-system
spec:
compiledTemplate: quota
params:
dimensions:
source: request.headers["x-forwarded-for"] | "unknown"
destination: destination.labels["app"] | destination.service.name | "unknown"
destinationVersion: destination.labels["version"] | "unknown"
---
apiVersion: config.istio.io/v1alpha2
kind: QuotaSpec
metadata:
name: request-count
namespace: istio-system
spec:
rules:
- quotas:
- charge: 1
quota: requestcountquota
---
apiVersion: config.istio.io/v1alpha2
kind: QuotaSpecBinding
metadata:
name: request-count
namespace: istio-system
spec:
quotaSpecs:
- name: request-count
namespace: istio-system
services:
- name: productpage
namespace: default
# - service: '*' # Uncomment this to bind *all* services to request-count
---
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: quota
namespace: istio-system
spec:
# quota only applies if you are not logged in.
# match: match(request.headers["cookie"], "user=*") == false
actions:
- handler: quotahandler
instances:
- requestcountquota
kubectl apply -f samples/bookinfo/policy/mixer-rule-productpage-ratelimit-crd.yaml
YAML具体内容如下:
apiVersion: config.istio.io/v1alpha2
kind: memquota
metadata:
name: handler
namespace: istio-system
spec:
quotas:
- name: requestcount.quota.istio-system
maxAmount: 500
validDuration: 1s
# The first matching override is applied.
# A requestcount instance is checked against override dimensions.
overrides:
# The following override applies to 'reviews' regardless
# of the source.
- dimensions:
destination: reviews
maxAmount: 1
validDuration: 5s
# The following override applies to 'productpage' when
# the source is a specific ip address.
- dimensions:
destination: productpage
source: "10.28.11.20"
maxAmount: 500
validDuration: 1s
# The following override applies to 'productpage' regardless
# of the source.
- dimensions:
destination: productpage
maxAmount: 2
validDuration: 5s
---
apiVersion: config.istio.io/v1alpha2
kind: quota
metadata:
name: requestcount
namespace: istio-system
spec:
dimensions:
source: request.headers["x-forwarded-for"] | "unknown"
destination: destination.labels["app"] | destination.service.name | "unknown"
destinationVersion: destination.labels["version"] | "unknown"
---
apiVersion: config.istio.io/v1alpha2
kind: QuotaSpec
metadata:
name: request-count
namespace: istio-system
spec:
rules:
- quotas:
- charge: 1
quota: requestcount
---
apiVersion: config.istio.io/v1alpha2
kind: QuotaSpecBinding
metadata:
name: request-count
namespace: istio-system
spec:
quotaSpecs:
- name: request-count
namespace: istio-system
services:
- name: productpage
namespace: default
# - service: '*' # Uncomment this to bind *all* services to request-count
---
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: quota
namespace: istio-system
spec:
# quota only applies if you are not logged in.
# match: match(request.headers["cookie"], "user=*") == false
actions:
- handler: handler.memquota
instances:
- requestcount.quota
如果 destination 的值是 reviews,则限制每 5 秒(validDuration)1 (maxAmount 字段)个请求。如果 destination 是 productpage,每 5 秒 2 个请求。当处理请求时,会按照自顶向下的顺序,选择第一个符合条件的 override 进行速率限制。或者
kubectl apply -f samples/bookinfo/policy/mixer-rule-productpage-redis-quota-rolling-window.yaml
YAML具体内容如下:
apiVersion: config.istio.io/v1alpha2
kind: handler
metadata:
name: redishandler
namespace: istio-system
spec:
compiledAdapter: redisquota
params:
redisServerUrl: redis-release-master:6379
connectionPoolSize: 10
quotas:
- name: requestcountquota.instance.istio-system
maxAmount: 500
validDuration: 1s
bucketDuration: 500ms
rateLimitAlgorithm: ROLLING_WINDOW
# The first matching override is applied.
# A requestcount instance is checked against override dimensions.
overrides:
# The following override applies to 'reviews' regardless
# of the source.
- dimensions:
destination: reviews
maxAmount: 1
# The following override applies to 'productpage' when
# the source is a specific ip address.
- dimensions:
destination: productpage
source: "10.28.11.20"
maxAmount: 500
# The following override applies to 'productpage' regardless
# of the source.
- dimensions:
destination: productpage
maxAmount: 2
---
apiVersion: config.istio.io/v1alpha2
kind: instance
metadata:
name: requestcountquota
namespace: istio-system
spec:
compiledTemplate: quota
params:
dimensions:
source: request.headers["x-forwarded-for"] | "unknown"
destination: destination.labels["app"] | destination.workload.name | "unknown"
destinationVersion: destination.labels["version"] | "unknown"
---
apiVersion: config.istio.io/v1alpha2
kind: QuotaSpec
metadata:
name: request-count
namespace: istio-system
spec:
rules:
- quotas:
- charge: 1
quota: requestcountquota
---
apiVersion: config.istio.io/v1alpha2
kind: QuotaSpecBinding
metadata:
name: request-count
namespace: istio-system
spec:
quotaSpecs:
- name: request-count
namespace: istio-system
services:
- name: productpage
namespace: default
# - service: '*' # Uncomment this to bind *all* services to request-count
---
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: quota
namespace: istio-system
spec:
# quota only applies if you are not logged in.
# match: match(request.headers["cookie"], "session=*") == false
actions:
- handler: redishandler
instances:
- requestcountquota
---
根据实际情况替换 rate_limit_algorithm 和 redis_server_url 的值。redisquota Handler 定义了 4 个不同的速率限制模式。缺省限制为每秒 500 个请求,使用 ROLLING_WINDOW 算法进行配额检查,该算法的 bucketDuration 定义为 500 毫秒。还定义了三个 override:
当处理请求时,会按照自顶向下的顺序,选择第一个符合条件的 override 进行速率限制。
确认已经创建 quota instance:
kubectl -n istio-system get instance requestcountquota -o yaml
quota 模板定义了三个 dimension,在 memquota 或者 redisquota 中可以根据这些定义,对符合属性要求的请求进行覆盖。destination 会在 destination.labels["app"]、destination.service.host 或 "unknown" 中选择第一个非空值。
确认 quota rule 已经创建:
kubectl -n istio-system get rule quota -o yaml
rule 告诉 Mixer,调用 memquota 或者 redisquota Handler,并使用上面创建的 requestcountquota Instance 生成对象传递给 Handler。这个步骤会使用 quota 模板对dimension 进行映射。
确认 QuotaSpec 已经创建:
kubectl -n istio-system get QuotaSpec request-count -o yaml
QuotaSpec 中声明,上面定义的 requestcountquota 的消耗倍数为 1。
确认 QuotaSpecBinding 已经创建:
kubectl -n istio-system get QuotaSpecBinding request-count -o yaml
QuotaSpecBinding 把前面的 QuotaSpec 绑定到需要应用限流的服务上。productpage 被显式的绑定到了 request-count 上。因为 QuotaSpecBinding 所属命名空间和这些服务是不一致的,所以这里必须定义每个服务的 namespace。如果去掉最后一行的注释标志,service: '*' 会把所有的服务绑定到 QuotaSpec 上,第一行就无效了。
在浏览器中刷新 productpage 页面。
request-count 配额适用于 productpage ,每 5 秒允许 2 个请求。如果你不断刷新页面,你会看到 RESOURCE_EXHAUSTED:Quota is exhausted for: requestcount。
在上面的例子里,我们有效的把 productpage 的速率限制为每客户端 IP 2 rps。如果我们想要放弃对已登录用户的速率限制。在 bookinfo 的例子中,我们使用 session=
可以为 quota rule 添加一个基于 cookie 的匹配条件:
kubectl -n istio-system edit rules quota
...
match: match(request.headers["cookie"], "session=*") == false
...
memquota 或者 redisquota 适配器仅在 session=
用 jason 的身份登录,重复刷新 productpage。应该不会出现任何问题。
未登录用户受到速率限制。从 jason 的身份登出,重复刷新 productpage,会看到 RESOURCE_EXHAUSTED:Quota is exhausted for: requestcount。
在前面的例子中演示了 Mixer 根据条件对请求实施速率限制的过程。
每个有名称的 Quota 实例,例如前面的 requestcount,都代表了一套计数器。这一个集合就是所有 Quota dimensions 的笛卡尔积定义的。如果上一个 expiration 区间内的请求数量超过了 maxAmount,Mixer 就会返回 RESOURCE_EXHAUSTED 信息给 Proxy。Proxy 则返回 HTTP 429 给调用方。
memquota 适配器使用一个为亚秒级分辨率的滑动窗口来实现速率限制。
redisquota 适配器能够通过配置来选择使用 ROLLING_WINDOW 或者 FIXED_WINDOW 算法来进行速率限制。
适配器配置中的 maxAmount 设置了关联到 Quota 实例中的所有计数器的缺省限制。如果所有 override 条目都无法匹配到一个请求,就只能使用 maxAmount 限制了。memquota/redisquota 会选择适合请求的第一条 override。override 条目无需定义所有 quota dimension,例如例子中的 0.2 qps 条目在 4 条 quota dimensions 中只选用了三条。
如果要把上面的策略应用到某个命名空间而非整个 Istio 网格,可以把所有 istio-system 替换成为给定的命名空间。