借助 Istio 让服务更具弹性
整体介绍
弹性是指系统能够优雅地处理故障并从故障中恢复。为了保持弹性,必须快速有效地检测故障并进行恢复。尽管我们服务设计时会尽量的做高可用方案,但是服务出现故障或者服务间的通信网络出现故障的情况是无法避免的,当故障出现时,我们应该尽量保证服务的可用性,不再让故障变的更严重,影响整个应用的稳定性,这就要求我们要让服务更具弹性。
Istio 提供了许多开箱即用的提升服务弹性的功能,包括负载均衡、连接池、健康检测、服务熔断、服务超时、服务重试、服务限流。这些功能都是服务治理的必备功能。通过这些功能,我们可以让服务更具弹性。当我们服务流量突增,并发突然变高,如果我们没有对服务进行限流,就很有可能导致服务出现故障。当网络出现瞬时故障导致服务调用失败,如果服务没有重试机制就可能导致调用方服务故障,当一个服务调用的后端服务已经出现大面积调用失败的情况,如果我们没有对调用的服务进行熔断处理,此时又开启了服务调用重试机制,这很有可能会导致后端服务压力成倍增加,进而压垮后端服务。虽然一个服务出现故障的概率可能很低,但是当服务数量变多,低概率的事件可能就会必然发生,当服务数量持续增长,服务间的调用关系复杂,如果不做服务故障处理,提升服务的弹性,就很有可能因为一个服务的故障导致其他服务也出现故障,进行导致级联故障,甚至可能导致整个应用出现不可用的现象,严重影响我们应用拆分为微服务后的服务可用性指标。应用用户体验变差,导致用户流失,进而影响业务发展,如果经常出现这种故障,这对业务来说无疑是毁灭性的打击。
实验前的准备
进行实验前,需要先执行如下的前置步骤。
下载实验时用到的源码仓库:
$ sudo yum install -y git $ git clone https://github.com/mgxian/istio-lab Cloning into 'istio-lab'... remote: Enumerating objects: 252, done. remote: Counting objects: 100% (252/252), done. remote: Compressing objects: 100% (177/177), done. remote: Total 779 (delta 157), reused 166 (delta 74), pack-reused 527 Receiving objects: 100% (779/779), 283.37 KiB | 243.00 KiB/s, done. Resolving deltas: 100% (451/451), done. $ cd istio-lab
开启 default 命名空间的自动注入功能:
$ kubectl label namespace default istio-injection=enabled namespace/default labeled
部署用于测试的服务:
$ kubectl apply -f service/go/service-go.yaml $ kubectl get pod NAME READY STATUS RESTARTS AGE service-go-v1-7cc5c6f574-lrp2h 2/2 Running 0 76s service-go-v2-7656dcc478-svn5c 2/2 Running 0 76s
服务实例负载均衡
Istio 可以通过设置 DestinationRule 来指定服务的负载均衡策略,Istio 提供了两类常用的负载均衡策略,分别是简单的负载均衡(simple)和一致性哈希负载均衡(consistentHash)。可以为服务设置默认的负载均衡策略,也可以在单独的服务子集(subset)中设置负载均衡策略,服务子集中的设置会覆盖服务设置的默认负载均衡策略。我们还可以设置端口级别的负载均衡策略(portLevelSettings),可以为服务设置默认的端口级别的负载均衡策略,当然我们也可以在单独的服务子集上设置端口级别的负载均衡策略。
简单负载均衡
简单的负载均衡策略提供了如下的4种负载均衡算法:
- 轮询(ROUND_ROBIN ):把请求依次转发给后端健康实例,默认算法。
- 最少连接(LEAST_CONN):把请求转发给活跃请求最少的后端健康实例,此处的活跃请求数是 Istio 自己维护的,是 Istio 调用后端实例且正在等待返回响应的请求数,由于实例可能还有其他客户端在调用,没有经过 Istio 统计,所以 Istio 维护的活跃请求数并不是此时实例真正的活跃请求数。
- 随机(RANDOM):把请求随机转发给后端健康实例。
- 直连(PASSTHROUGH):将连接转发到调用方请求的原始 IP 地址,而不进行任何形式的负载平衡,高级用法,一般情况下不会使用。
使用示例:
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: service-go spec: host: service-go trafficPolicy: loadBalancer: simple: ROUND_ROBIN subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2 trafficPolicy: loadBalancer: simple: LEAST_CONN portLevelSettings: - port: number: 80 loadBalancer: simple: RANDOM
7-9 行定义了默认的负载均衡策略为轮询方式。
17-19 行定义了对于名称为 v2 的实例子集负载均衡策略为最少连接方式。
20-24 行定义了端口级别的负载均衡策略,指定 80 端口的负载均衡策略为随机方式。
一致性哈希负载均衡
一致性哈希负载均衡策略只适合于 HTTP 协议的请求,可以基于请求头,Cookie 或者来源 IP 做会话粘连,让同一用户的请求一直转发到后端同一实例,当实例出现故障时会选择新的实例。当添加删除新实例时,会有部分用户重新会话粘连失效。
使用示例:
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: service-go spec: host: service-go trafficPolicy: loadBalancer: consistentHash: httpHeaderName: x-lb-test subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2
7-9行定义了默认的负载均衡策略为一致性哈希,基于 x-lb-test 头进行一致性哈希负载均衡。
实验
创建测试 Pod:
$ kubectl apply -f kubernetes/dns-test.yaml
创建 service-go 服务的 virtualservice 规则
$ kubectl apply -f istio/route/virtual-service-go.yaml
没有创建负载均衡规则的访问测试:
$ kubectl exec dns-test -c dns-test -- curl -s -H "X-lb-test: 1" http://service-go/env {"message":"go v1"} $ kubectl exec dns-test -c dns-test -- curl -s -H "X-lb-test: 1" http://service-go/env {"message":"go v2"}
创建用于测试的一致性哈希负载均衡规则:
$ kubectl apply -f istio/resilience/destination-rule-go-lb-hash.yaml
创建负载均衡规则后的访问测试:
$ kubectl exec dns-test -c dns-test -- curl -s -H "X-lb-test: 1" http://service-go/env {"message":"go v2"} $ kubectl exec dns-test -c dns-test -- curl -s -H "X-lb-test: 2" http://service-go/env {"message":"go v2"} $ kubectl exec dns-test -c dns-test -- curl -s -H "X-lb-test: 3" http://service-go/env {"message":"go v1"}
总结
从以上的实验结果可以看出,实验部署了两个版本的 service-go 服务实例,每个版本一个 Pod ,默认情况下访问 service-go 服务会轮询的转发到后端 Pod 上,因此多次访问你会看到两个版本的响应结果,当配置了一致性哈希负载均衡规则以后,以固定的 X-lb-test 请求头值时,多次访问你只能获取到同一个版本的服务实例响应信息。
注意:实验结果可能与上面的结果并不完全一致,但是结论是一致的,使用同样的请求头会访问 service-go 服务,只会得到同一个版本的服务响应结果。
清理测试:
$ kubectl delete -f kubernetes/dns-test.yaml $ kubectl delete -f istio/route/virtual-service-go.yaml $ kubectl delete -f istio/resilience/destination-rule-go-lb-hash.yaml
服务实例连接池
Istio 可以通过设置 DestinationRule 来指定服务的连接池的设置,Istio 提供了两类常用协议的连接池配置,分别是 TCP 连接池配置和 HTTP 连接池配置。与负载均衡策略设置相似,连接池的配置也支持服务默认级别的配置,服务子集的配置以及端口级别的连接池配置。TCP 连接池和 HTTP 连接池可以一同设置。
TCP 连接池
TCP 连接池对 TCP 和 HTTP 协议均提供支持,示例如下:
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: service-go spec: host: service-go trafficPolicy: connectionPool: tcp: maxConnections: 10 connectTimeout: 30ms subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2
8-11 行设置 TCP 连接池中的最大连接数为 10,连接超时时间为 30 毫秒,当连接池中连接不够用时,服务调用会返回 503 响应码。
HTTP 连接池
HTTP 连接池对 HTTP 和 GRPC 协议均提供支持,示例如下:
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: service-go spec: host: service-go trafficPolicy: connectionPool: http: http2MaxRequests: 10 http1MaxPendingRequests: 5 maxRequestsPerConnection: 2 maxRetries: 3 subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2
8-13 行设置 HTTP 连接池的后端实例的最大并发请求数为 10,每个目标的最大待处理请求数为 5,连接池中每个连接最多处理 2 个请求后就关闭,并根据需要重新创建连接池中的连接,请求在服务后端实例集群中失败后的最大重试次数为 3。
http2MaxRequests 对后端的最大并发请求数,默认值 1024。
maxRequestsPerConnection 设置为 1 时表示关闭 keep alive 特性,每次请求都创建一个新的请求。
http1MaxPendingRequests 为每个目标的最大待处理请求数,这里的目标指的是 virtualservice 路由规则中配置的 destination,当连接池中连接不够用时请求就处于待处理状态。默认值 1024。
maxRetries 请求后端失败后重试其他后端实例的总次数。默认值 3。
实验
启动用于并发测试的 Pod:
$ kubectl apply -f kubernetes/fortio.yaml
创建 service-go 服务的 virtualservice 规则:
$ kubectl apply -f istio/route/virtual-service-go.yaml
创建用于测试的连接池规则:
$ kubectl apply -f istio/resilience/destination-rule-go-pool-http.yaml
访问测试:
$ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -curl http://service-go/env HTTP/1.1 200 OK content-type: application/json; charset=utf-8 date: Wed, 16 Jan 2019 10:12:35 GMT content-length: 19 x-envoy-upstream-service-time: 4 server: envoy {"message":"go v2"} # 10 并发 $ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -c 10 -qps 0 -n 100 -loglevel Error http://service-go/env 09:40:38 I logger.go:97> Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 0 queries per second, 2->2 procs, for 100 calls: http://service-go/env Aggregated Function Time : count 100 avg 0.01652562 +/- 0.013 min 0.002576677 max 0.064653438 sum 1.65256199 # target 50% 0.0119375 # target 75% 0.018 # target 90% 0.035 # target 99% 0.06 # target 99.9% 0.0641881 Sockets used: 15 (for perfect keepalive, would be 10) Code 200 : 95 (95.0 %) Code 503 : 5 (5.0 %) All done 100 calls (plus 0 warmup) 16.526 ms avg, 563.4 qps # 20 并发 $ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -c 20 -qps 0 -n 200 -loglevel Error http://service-go/env 09:41:32 I logger.go:97> Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 0 queries per second, 2->2 procs, for 200 calls: http://service-go/env Aggregated Function Time : count 200 avg 0.023987068 +/- 0.01622 min 0.001995258 max 0.067905383 sum 4.79741353 # target 50% 0.0194286 # target 75% 0.0357692 # target 90% 0.05 # target 99% 0.0626351 # target 99.9% 0.0673784 Sockets used: 43 (for perfect keepalive, would be 20) Code 200 : 177 (88.5 %) Code 503 : 23 (11.5 %) All done 200 calls (plus 0 warmup) 23.987 ms avg, 711.9 qps # 30 并发 $ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -c 30 -qps 0 -n 300 -loglevel Error http://service-go/env 09:42:05 I logger.go:97> Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 0 queries per second, 2->2 procs, for 300 calls: http://service-go/env Aggregated Function Time : count 300 avg 0.034233818 +/- 0.02268 min 0.002354402 max 0.114700368 sum 10.2701455 # target 50% 0.0285417 # target 75% 0.0446667 # target 90% 0.0686957 # target 99% 0.1 # target 99.9% 0.11323 Sockets used: 137 (for perfect keepalive, would be 30) Code 200 : 192 (64.0 %) Code 503 : 108 (36.0 %) All done 300 calls (plus 0 warmup) 34.234 ms avg, 702.1 qps
从压测结果可以看出,当并发逐渐增大时,服务不可用的 503 响应码所占比例逐渐升高,说明我们配置的 HTTP 连接池参数已经生效。
清理测试:
$ kubectl delete -f kubernetes/fortio.yaml $ kubectl delete -f istio/route/virtual-service-go.yaml $ kubectl delete -f istio/resilience/destination-rule-go-pool-http.yaml
服务实例健康检测
Istio 可以通过设置 DestinationRule 来指定服务实例健康检测的配置,可以设置服务实例健康检测计算的时间间隔,实例移出连接池的条件,实例移出连接池时间的基础值以及服务实例移出连接池的最大比例值。设置移出连接池的最大比例,可以防止异常情况移出连接池过多的服务实例,导致服务实例不够,剩余实例承受流量过大,压跨整个服务。与负载均衡策略设置相似,服务实例健康检测的配置也支持服务默认级别的配置,服务子集的配置以及端口级别的服务实例健康检测配置。
对于 HTTP 协议的服务,当后端实例返回 5xx 的响应码时,代表后端实例出现错误,后端实例的错误计数会增加。对于 TCP 协议的服务,连接超时、连接失败会被认为后端实例出现错误,后端实例的错误计数会增加。
配置示例:
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: service-go spec: host: service-go trafficPolicy: outlierDetection: consecutiveErrors: 3 interval: 10s baseEjectionTime: 30s maxEjectionPercent: 10 subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2
8-12为后端实例健康检测配置的定义,配置最大实例移出比例(maxEjectionPercent)不超过 10%,基础移出时间(baseEjectionTime)为 30s,当实例恢复健康加入集群中后,再次出现故障被移出时,移出时间会根据此值增加移出时间,每隔 10s 检测一次后端实例是否应该被移出连接池,当在 10s 内出现3次错误时,实例会被移出连接池。
consecutiveErrors 表示服务实例移出连接池之前的最大出错数阀值,当连接池中实例在指定的时间间隔内出错次数到达该值时,会被移出连接池。
interval 表示两次后端实例健康检查时间间隔,默认值为 10s。
baseEjectionTime 表示移出连接池的基础时间,默认值为 30s。
maxEjectionPercent 表示后端实例最大移出百分比,默认值为 10。
服务熔断
后端服务调用偶尔出现失败是正常情况,但是当对后端服务的调用出现大比例的调用失败,此时可能由于后端服务已经无法承受当前压力,如果我们还是继续调用后端服务,我们不仅不能得到响应,还有可能会把后端服务整个压跨。如果当服务出现大比例的调用失败的时候,我们停止调用后端服务,经过短暂时间间隔后,我们尝试让部分请求调用后端服务,如果服务返回正常,我们就可以让更多的请求调用后端服务,直到恢复到正常情况,如果仍然出现无法响应的情况,我们将再次停止调用服务,这种处理后端服务调用的机制就叫做熔断。
Istio 通过结合连接池和实例健康检测机制,可以实现熔断机制。当后端实例出现故障时移出连接池,当连接池中无可用健康实例时,服务请求会立即得到服务不可用的响应,此时服务就处于熔断状态了。当服务实例被移出的时间结束时,服务实例会被再次加入连接池中,等待下一轮的服务健康检测。
配置示例:
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: service-go spec: host: service-go trafficPolicy: connectionPool: tcp: maxConnections: 10 http: http2MaxRequests: 10 maxRequestsPerConnection: 10 outlierDetection: consecutiveErrors: 3 interval: 3s baseEjectionTime: 3m maxEjectionPercent: 100 subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2
8-13 定义连接池配置,并发请求设置为 10。
14-18 定义后端实例健康检测配置,允许全部实例移出连接池。
实验
启动用于并发测试的 Pod:
$ kubectl apply -f kubernetes/fortio.yaml
创建 service-go 服务的 virtualservice 规则:
$ kubectl apply -f istio/route/virtual-service-go.yaml
创建用于测试的熔断规则:
$ kubectl apply -f istio/resilience/destination-rule-go-cb.yaml
访问测试:
$ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -curl http://service-go/env HTTP/1.1 200 OK content-type: application/json; charset=utf-8 date: Wed, 16 Jan 2019 10:22:35 GMT content-length: 19 x-envoy-upstream-service-time: 3 server: envoy {"message":"go v2"} # 20 并发 $ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -c 20 -qps 0 -n 200 -loglevel Error http://service-go/env 10:25:21 I logger.go:97> Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 0 queries per second, 2->2 procs, for 200 calls: http://service-go/env Aggregated Function Time : count 200 avg 0.023687933 +/- 0.01781 min 0.002302379 max 0.082312522 sum 4.73758658 # target 50% 0.0175385 # target 75% 0.029375 # target 90% 0.0533333 # target 99% 0.0766667 # target 99.9% 0.08185 Sockets used: 22 (for perfect keepalive, would be 20) Code 200 : 198 (99.0 %) Code 503 : 2 (1.0 %) All done 200 calls (plus 0 warmup) 23.688 ms avg, 631.3 qps # 30 并发 $ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -c 30 -qps 0 -n 300 -loglevel Error http://service-go/env 10:25:49 I logger.go:97> Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 0 queries per second, 2->2 procs, for 300 calls: http://service-go/env Aggregated Function Time : count 300 avg 0.055940327 +/- 0.04215 min 0.001836339 max 0.207798702 sum 16.782098 # target 50% 0.0394737 # target 75% 0.0776471 # target 90% 0.123333 # target 99% 0.18 # target 99.9% 0.205459 Sockets used: 94 (for perfect keepalive, would be 30) Code 200 : 236 (78.7 %) Code 503 : 64 (21.3 %) All done 300 calls (plus 0 warmup) 55.940 ms avg, 486.3 qps # 40 并发 $ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -c 40 -qps 0 -n 400 -loglevel Error http://service-go/env 10:26:17 I logger.go:97> Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 0 queries per second, 2->2 procs, for 400 calls: http://service-go/env Aggregated Function Time : count 400 avg 0.034048003 +/- 0.02541 min 0.001808212 max 0.144268023 sum 13.6192011 # target 50% 0.028587 # target 75% 0.0415789 # target 90% 0.0588889 # target 99% 0.132 # target 99.9% 0.143414 Sockets used: 203 (for perfect keepalive, would be 40) Code 200 : 225 (56.2 %) Code 503 : 175 (43.8 %) All done 400 calls (plus 0 warmup) 34.048 ms avg, 951.0 qps # 查看 istio-proxy 状态 $ kubectl exec fortio -c istio-proxy -- curl -s localhost:15000/stats | grep service-go | grep pending cluster.outbound|80|v1|service-go.default.svc.cluster.local.upstream_rq_pending_active: 0 cluster.outbound|80|v1|service-go.default.svc.cluster.local.upstream_rq_pending_failure_eject: 0 cluster.outbound|80|v1|service-go.default.svc.cluster.local.upstream_rq_pending_overflow: 0 cluster.outbound|80|v1|service-go.default.svc.cluster.local.upstream_rq_pending_total: 0 cluster.outbound|80|v2|service-go.default.svc.cluster.local.upstream_rq_pending_active: 0 cluster.outbound|80|v2|service-go.default.svc.cluster.local.upstream_rq_pending_failure_eject: 0 cluster.outbound|80|v2|service-go.default.svc.cluster.local.upstream_rq_pending_overflow: 0 cluster.outbound|80|v2|service-go.default.svc.cluster.local.upstream_rq_pending_total: 0 cluster.outbound|80||service-go.default.svc.cluster.local.upstream_rq_pending_active: 0 cluster.outbound|80||service-go.default.svc.cluster.local.upstream_rq_pending_failure_eject: 0 cluster.outbound|80||service-go.default.svc.cluster.local.upstream_rq_pending_overflow: 551 cluster.outbound|80||service-go.default.svc.cluster.local.upstream_rq_pending_total: 1282
实验部署了两个版本的 service-go 实例,每个版本一个 Pod,每个 Pod 的并发为 10,所有设置的总并发就为 20。从压测结果可以看出,当并发逐渐增大时,服务不可用的 503 响应码所占比例逐渐升高。但是从结果看熔断器并不是非常准确的拦截了高于设置并发值的请求,Istio 允许有部分请求遗漏。
清理测试:
$ kubectl delete -f kubernetes/fortio.yaml $ kubectl delete -f istio/route/virtual-service-go.yaml $ kubectl delete -f istio/resilience/destination-rule-go-cb.yaml
服务超时
设置服务调用的超时时间,当服务没有在规定的时间内返回数据,就直接取消此次请求,返回服务超时的响应。给服务设置超时时间可以防止服务提供方拖垮服务调用方,防止请求的总耗时时间过长。
使用示例:
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: service-node spec: hosts: - service-node http: - route: - destination: host: service-node timeout: 500ms
12 行指定服务的调用时间不能超过 500 毫秒,当调用 service-node 服务时如果超过 500 毫秒请求没有完成就直接返回超时错误给调用方。
实验
部署 service-node 服务:
$ kubectl apply -f service/node/service-node.yaml $ kubectl get pod NAME READY STATUS RESTARTS AGE service-go-v1-7cc5c6f574-lrp2h 2/2 Running 0 4m service-go-v2-7656dcc478-svn5c 2/2 Running 0 4m service-node-v1-d44b9bf7b-ppn26 2/2 Running 0 24s service-node-v2-86545d9796-rgmb7 2/2 Running 0 24s
启动用于并发测试的 Pod:
$ kubectl apply -f kubernetes/fortio.yaml
创建 service-node 服务的超时 virtualservice 规则:
$ kubectl apply -f istio/resilience/virtual-service-node-timeout.yaml
访问测试:
$ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -curl http://service-node/env HTTP/1.1 200 OK content-type: application/json; charset=utf-8 content-length: 77 date: Wed, 16 Jan 2019 10:33:57 GMT x-envoy-upstream-service-time: 18 server: envoy {"message":"node v1","upstream":[{"message":"go v1","response_time":"0.01"}]} # 10 并发 $ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -c 10 -qps 0 -n 100 -loglevel Error http://service-node/env 11:08:24 I logger.go:97> Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 0 queries per second, 2->2 procs, for 100 calls: http://service-node/env Aggregated Function Time : count 100 avg 0.19270902 +/- 0.1403 min 0.009657651 max 0.506141264 sum 19.2709017 # target 50% 0.173333 # target 75% 0.3 # target 90% 0.421429 # target 99% 0.505118 # target 99.9% 0.506039 Sockets used: 15 (for perfect keepalive, would be 10) Code 200 : 94 (94.0 %) Code 504 : 6 (6.0 %) All done 100 calls (plus 0 warmup) 192.709 ms avg, 45.4 qps # 20 并发 $ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -c 20 -qps 0 -n 200 -loglevel Error http://service-node/env 11:08:47 I logger.go:97> Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 0 queries per second, 2->2 procs, for 200 calls: http://service-node/env Aggregated Function Time : count 200 avg 0.44961158 +/- 0.122 min 0.006904922 max 0.524347684 sum 89.9223153 # target 50% 0.50864 # target 75% 0.516494 # target 90% 0.521206 # target 99% 0.524034 # target 99.9% 0.524316 Sockets used: 163 (for perfect keepalive, would be 20) Code 200 : 46 (23.0 %) Code 504 : 154 (77.0 %) All done 200 calls (plus 0 warmup) 449.612 ms avg, 39.2 qps
当并发逐渐增大时,服务的响应时间逐渐增大,服务响应超时的 504 响应码所占比例逐渐升高。这说明我们配置的服务超时时间已经生效。
清理:
$ kubectl delete -f kubernetes/fortio.yaml $ kubectl delete -f service/node/service-node.yaml $ kubectl delete -f istio/resilience/virtual-service-node-timeout.yaml
服务重试
当网格出现抖动,或者被调用的服务出现瞬时故障,由于这种问题都是偶发瞬时的,只需要再次调用后端服务,可能请求就会正常,这种情况下我就需要服务的重试功能,防止由于被调用方的服务偶发瞬时故障,导致调用后端服务实现出现不可用的情况,影响整体的应用稳定性。
使用示例:
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: service-node spec: hosts: - service-node http: - route: - destination: host: service-node retries: attempts: 3 perTryTimeout: 2s
12-14 行定义了重试规则,当调用 service-node 服务时,如果服务出错可以进行重试,最多可以重试 3 次,每次调用超时为 2s,每次重试的时间间隔由 Istio 决定,重试时间间隔一般大于 25 毫秒。
实验
创建测试 Pod:
$ kubectl apply -f kubernetes/fortio.yaml
部署 httpbin 服务:
$ kubectl apply -f kubernetes/httpbin.yaml $ kubectl get pod -l app=httpbin NAME READY STATUS RESTARTS AGE httpbin-b67975b8f-vmbtv 2/2 Running 0 49s
创建 httpbin 服务路由规则:
$ kubectl apply -f istio/route/virtual-service-httpbin.yaml
访问测试:
$ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -curl http://httpbin:8000/status/200 HTTP/1.1 200 OK server: envoy date: Wed, 16 Jan 2019 14:03:00 GMT content-type: text/html; charset=utf-8 access-control-allow-origin: * access-control-allow-credentials: true content-length: 0 x-envoy-upstream-service-time: 33 $ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -c 10 -qps 0 -n 100 -loglevel Error http://httpbin:8000/status/200%2C200%2C200%2C200%2C500 14:18:37 I logger.go:97> Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 0 queries per second, 2->2 procs, for 100 calls: http://httpbin:8000/status/200%2C200%2C200%2C200%2C500 Aggregated Function Time : count 100 avg 0.24802899 +/- 0.06426 min 0.016759858 max 0.390472066 sum 24.8028985 # target 50% 0.252941 # target 75% 0.289706 # target 90% 0.326667 # target 99% 0.376981 # target 99.9% 0.389123 Sockets used: 30 (for perfect keepalive, would be 10) Code 200 : 78 (78.0 %) Code 500 : 22 (22.0 %) All done 100 calls (plus 0 warmup) 248.029 ms avg, 38.5 qps
创建 httpbin 服务重试路由规则:
$ kubectl apply -f istio/resilience/virtual-service-httpbin-retry.yaml
访问测试:
$ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -curl http://httpbin:8000/status/200 HTTP/1.1 200 OK server: envoy date: Wed, 16 Jan 2019 14:19:14 GMT content-type: text/html; charset=utf-8 access-control-allow-origin: * access-control-allow-credentials: true content-length: 0 x-envoy-upstream-service-time: 5 $ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -c 10 -qps 0 -n 100 -loglevel Error http://httpbin:8000/status/200%2C200%2C200%2C200%2C500 14:19:32 I logger.go:97> Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 0 queries per second, 2->2 procs, for 100 calls: http://httpbin:8000/status/200%2C200%2C200%2C200%2C500 Aggregated Function Time : count 100 avg 0.23708609 +/- 0.1323 min 0.017537636 max 0.793965189 sum 23.7086086 # target 50% 0.226471 # target 75% 0.275 # target 90% 0.383333 # target 99% 0.7 # target 99.9% 0.784569 Sockets used: 13 (for perfect keepalive, would be 10) Code 200 : 97 (97.0 %) Code 500 : 3 (3.0 %) All done 100 calls (plus 0 warmup) 237.086 ms avg, 35.5 qps
从上面的测试结果可以看出来,当没有开启服务重试时,服务有大概 1/4 的请求失败,当开启服务重试之后,服务只有少部分请求失败。
清理:
$ kubectl delete -f kubernetes/fortio.yaml $ kubectl delete -f kubernetes/httpbin.yaml $ kubectl delete -f istio/resilience/virtual-service-httpbin-retry.yaml
服务限流
当服务并发请求激增,流量增大,如果此时我们没有使用任何限流措施,这很有可能导致我们的服务无法承受如此多的请求,进而导致服务崩溃,还可能会影响整个应用的稳定性。如果我们的服务有限流功能,当请求数过多时,可以直接丢掉过多的流量,防止服务被压垮,保证服务稳定。Istio 提供两种限流的实现,基于内存存储限流数据和使用 Redis 存储限流数据的方式,基于内存存储限流数据的方式只能适用于只部署一个 Mixer 的集群,而且由于使用内存存储限流数据,Mixer 重启后限流数据会丢失,生产环境建议使用 Redis 存储限流数据的限流实现。在 Istio 中服务被限流的请求会得到 429(Too Many Requests)的响应码。
限流的配置分为客户端和 Mixer 端两个部分:
-
客户端配置:
- QuotaSpec 定义了 quota 实例名称和对应的每次请求消耗的配额数。
- QuotaSpecBinding 将 QuotaSpec 与一个或多个服务相关联绑定,只有被关联绑定的服务限流才会生效。
-
Mixer 端配置:
- quota 实例定义了 Mixer 如何区别度量一个请求的限流配额,用来描述请求数据收集的维度。
- memquota/redisquota 适配器定义了 memquota/redisquota 的配置,根据 quota 实例定义的请求数据收集维度来区分并定义一个或多个限流配额数量。
- rule 规则定义了 quota 实例应该何时分发给 memquota/redisquota 适配器处理。
基于内存的限流使用示例如下:
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: memquota metadata: name: handler namespace: istio-system spec: quotas: - name: requestcount.quota.istio-system maxAmount: 500 validDuration: 1s overrides: - dimensions: destination: service-go maxAmount: 50 validDuration: 1s - dimensions: destination: service-node source: "10.28.11.20" maxAmount: 50 validDuration: 1s - dimensions: destination: service-node maxAmount: 20 validDuration: 1s - dimensions: destination: service-python maxAmount: 2
validDuration: 5s
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: quota
namespace: istio-system
spec:
actions:
– handler: handler.memquota
instances:
– requestcount.quota
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: service-go
namespace: default
– name: service-node
namespace: default
– name: service-python
namespace: default
1-10 定义了名为 requestcount 的 quota 实例,获取请求的 source、destination、destinationVersion 的值供 memquota 适配器来区分请求的限流配额。取值规则如下:
- source 获取请求的 x-forwarded-for 请求头的值作为 source 的取值,不存在时 source 取值 “unknown”。
- destination 获取请求的目标服务标签中的 app 标签的值,不存在时取目标服务的 service.name 字段的值,否则 destination 取值 “unknown”。
- destinationVersion 获取请求目标服务标签中的 version 标签的值,不存在时 destinationVersion 取值 “unknown”。
12-39 行定义了名为 handler 的 memquota 适配器,19 行中的 name 字段值为上面定义的 quota 实例名称。20 行定义了默认的限流配额为500,21 行定义默认的限流计算周期为1s,即默认情况下每秒最高 500 个请求。23-39行为具体的限流配置,23-26 行定义了当 destination 是 service-go 时,每秒不能高于 50 个请求。27-31 定义了当 destination 是 service-node 且 source 是 “10.28.11.20” 时,每秒不能高于 50 个请求。32-35 行定义了当 destination 是 service-node 时,每秒不能高于 20 个请求。36-39 行定义了当 destination 是 service-python 时,每 5 秒内不能高于 2 个请求。
41-50 行定义了名为 quota 的 rule 规则,由于没有指定条件,会把所有相关联的服务请求都分发给 memquota 适配器处理。
52-61 行定义了名为 request-count 的 QuotaSpec ,指定了名为 requestcount 的 quota 实例每次消耗一个配额。
63-78 行定义了名为 request-count 的 QuotaSpecBinding ,把 default 命名空间的 service-go、service-node、service-python 服务与名为 request-count 的 QuotaSpec 关联起来。
在 memquota 适配器配置的所有限流规则中,执行限流时会从第一条限流规则开始匹配,当遇到第一条匹配的规则后,后面的规则不再匹配,如果没有匹配到任何具体的规则,则使用默认的规则。所以 27-31 行的规则不能与 32-35 规则交换位置,如果交换位置就会导致 27-31 行的规则永远不会被匹配到,所以配置限流规则的时候应该把越具体的匹配规则放在越靠前的位置,否则可能会出现达不到预期的限流效果。
quota 实例具体可以使用获取哪些值用于区分请求,可以参考官方文档,链接如下: https://istio.io/docs/referenc … lary/
。
基于 Redis 的限流使用示例如下:
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.workload.name | "unknown"
destinationVersion: destination.labels[“version”] | “unknown”
apiVersion: “config.istio.io/v1alpha2”
kind: redisquota
metadata:
name: handler
namespace: istio-system
spec:
redisServerUrl: redis-ratelimit.istio-system:6379
connectionPoolSize: 10
quotas:
– name: requestcount.quota.istio-system
maxAmount: 500
validDuration: 1s
bucketDuration: 500ms
rateLimitAlgorithm: ROLLING_WINDOW
overrides:
– dimensions:
destination: service-go
maxAmount: 50
– dimensions:
destination: service-node
source: “10.28.11.20”
maxAmount: 50
– dimensions:
destination: service-node
maxAmount: 20
– dimensions:
destination: service-python
maxAmount: 2
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: quota
namespace: istio-system
spec:
actions:
– handler: handler.redisquota
instances:
– requestcount.quota
—
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: service-go
namespace: default
– name: service-node
namespace: default
– name: service-python
namespace: default
12-39 行定义了名为 handler 的 redisquota 适配器,18 行定义了 Redis 的连接地址,19 行定义了 Redis 的连接池大小。
22 行定义了默认配额为 500,23 行定义了默认限流周期为 1s,即默认情况下每秒最高 500 个请求。
25 行定义了使用的限流算法有 FIXEDWINDOW 和 ROLLINGWINDOW 两种,FIXED_WINDOW 为默认的算法。
- FIXED_WINDOW 算法可以允许2倍的设置的请求速率峰值。
- ROLLING_WINDOW 算法可以提高更高的精确度,这也会额外消耗 Redis 的资源。
27-39 行定义了具体的限流规则,与 memquota 不同,这里不允许再单独为限流规则设置限流周期,只能使用默认的限流周期。
其余部分的配置与 memquota 的配置保持一致。
基于条件的限流
如下配置只对 cookie 中不存在 user 的请求做限流。
apiVersion: config.istio.io/v1alpha2 kind: rule metadata: name: quota namespace: istio-system spec: match: match(request.headers["cookie"], "user=*") == false actions: - handler: handler.memquota instances: - requestcount.quota
对所有服务限流:
apiVersion: config.istio.io/v1alpha2 kind: QuotaSpecBinding metadata: name: request-count namespace: istio-system spec: quotaSpecs: - name: request-count namespace: istio-system services: - service: '*'
实验
本次实验使用基于内存的 memquota 适配器来进行服务限流测试。如果使用基于 Redis 的 redisquota 适配器进行实验,可能会由于实验环境机器性能问题,导致 Mixer 访问 Redis 出现错误,进而导致 qps 还没有到达设置值时就出现被限流有情况,影响实验结果。
部署其他服务:
$ kubectl apply -f service/node/service-node.yaml $ kubectl apply -f service/lua/service-lua.yaml $ kubectl apply -f service/python/service-python.yaml $ kubectl get pod NAME READY STATUS RESTARTS AGE service-go-v1-7cc5c6f574-488rs 2/2 Running 0 15m service-go-v2-7656dcc478-bfq5x 2/2 Running 0 15m service-lua-v1-5c9bcb7778-d7qwp 2/2 Running 0 3m12s service-lua-v2-75cb5cdf8-g9vht 2/2 Running 0 3m12s service-node-v1-d44b9bf7b-z7vbr 2/2 Running 0 3m11s service-node-v2-86545d9796-rgtxw 2/2 Running 0 3m10s service-python-v1-79fc5849fd-xgfkn 2/2 Running 0 3m9s service-python-v2-7b6864b96b-5w6cj 2/2 Running 0 3m15s
启动用于并发测试的 Pod:
$ kubectl apply -f kubernetes/fortio.yaml
创建限流规则:
$ kubectl apply -f istio/resilience/quota-mem-ratelimit.yaml
测试 service-go 服务的限流是否生效:
$ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -curl http://service-go/env HTTP/1.1 200 OK content-type: application/json; charset=utf-8 date: Wed, 16 Jan 2019 15:33:02 GMT content-length: 19 x-envoy-upstream-service-time: 226 server: envoy {"message":"go v1"}
30 qps
$ kubectl exec fortio -c fortio /usr/local/bin/fortio — load -qps 30 -n 300 -loglevel Error http://service-go/env
15:33:36 I logger.go:97> Log level is now 4 Error (was 2 Info)
Fortio 1.0.1 running at 30 queries per second, 2->2 procs, for 300 calls: http://service-go/env
Aggregated Function Time : count 300 avg 0.0086544419 +/- 0.005944 min 0.002929143 max 0.065596074 sum 2.59633258
target 50% 0.007375
target 75% 0.00938095
target 90% 0.0115
target 99% 0.0325
target 99.9% 0.0647567
Sockets used: 4 (for perfect keepalive, would be 4)
Code 200 : 300 (100.0 %)
All done 300 calls (plus 0 warmup) 8.654 ms avg, 30.0 qps
50 qps
$ kubectl exec fortio -c fortio /usr/local/bin/fortio — load -qps 50 -n 500 -loglevel Error http://service-go/env
15:34:17 I logger.go:97> Log level is now 4 Error (was 2 Info)
Fortio 1.0.1 running at 50 queries per second, 2->2 procs, for 500 calls: http://service-go/env
Aggregated Function Time : count 500 avg 0.0086848862 +/- 0.005076 min 0.00307391 max 0.05419281 sum 4.34244311
target 50% 0.0075
target 75% 0.00959459
target 90% 0.0132857
target 99% 0.03
target 99.9% 0.0531446
Sockets used: 4 (for perfect keepalive, would be 4)
Code 200 : 500 (100.0 %)
All done 500 calls (plus 0 warmup) 8.685 ms avg, 50.0 qps
60 qps
$ kubectl exec fortio -c fortio /usr/local/bin/fortio — load -qps 60 -n 600 -loglevel Error http://service-go/env
15:35:28 I logger.go:97> Log level is now 4 Error (was 2 Info)
Fortio 1.0.1 running at 60 queries per second, 2->2 procs, for 600 calls: http://service-go/env
Aggregated Function Time : count 600 avg 0.0090870522 +/- 0.008314 min 0.002537502 max 0.169680378 sum 5.45223134
target 50% 0.00748529
target 75% 0.0101538
target 90% 0.0153548
target 99% 0.029375
target 99.9% 0.163872
Sockets used: 23 (for perfect keepalive, would be 4)
Code 200 : 580 (96.7 %)
Code 429 : 20 (3.3 %)
All done 600 calls (plus 0 warmup) 9.087 ms avg, 59.9 qps
测试 service-node 服务的限流是否生效:
$ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -curl http://service-node/env HTTP/1.1 200 OK content-type: application/json; charset=utf-8 content-length: 77 date: Wed, 16 Jan 2019 15:36:13 GMT x-envoy-upstream-service-time: 1187 server: envoy {"message":"node v2","upstream":[{"message":"go v1","response_time":"0.51"}]} # 20 qps $ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -qps 20 -n 200 -loglevel Error http://service-node/env 15:37:51 I logger.go:97> Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 20 queries per second, 2->2 procs, for 200 calls: http://service-node/env Aggregated Sleep Time : count 196 avg -0.21285915 +/- 1.055 min -4.8433788589999995 max 0.190438028 sum -41.7203939 # range, mid point, percentile, count >= -4.84338 0.003 0.011 0.015 0.069 0.089 0.099 0.119 0.139 0.159 0.179 Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 30 queries per second, 2->2 procs, for 300 calls: http://service-node/env Aggregated Sleep Time : count 296 avg 0.035638851 +/- 0.1206 min -0.420611573 max 0.132597685 sum 10.5491 # range, mid point, percentile, count >= -0.420612 -0.001 Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 30 queries per second, 2->2 procs, for 300 calls: http://service-node/env Aggregated Sleep Time : count 296 avg -1.4901022 +/- 1.952 min -6.08576837 max 0.123485559 sum -441.070241 # range, mid point, percentile, count >= -6.08577 Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 50 queries per second, 2->2 procs, for 500 calls: http://service-node/env Aggregated Sleep Time : count 496 avg 0.0015264793 +/- 0.1077 min -0.382731569 max 0.078526418 sum 0.757133711 # range, mid point, percentile, count >= -0.382732 -0.001 0.069 Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 60 queries per second, 2->2 procs, for 600 calls: http://service-node/env Aggregated Sleep Time : count 596 avg -0.081667759 +/- 0.1592 min -0.626635518 max 0.064876123 sum -48.6739846 # range, mid point, percentile, count >= -0.626636 0 0.059 <= 0.0648761 , 0.0619381 , 100.00, 14 # target 50% -0.0133888 WARNING 51.01% of sleep were falling behind Aggregated Function Time : count 600 avg 0.04532505 +/- 0.04985 min 0.001904423 max 0.304644243 sum 27.1950299 # target 50% 0.0208163 # target 75% 0.07 # target 90% 0.1025 # target 99% 0.233333 # target 99.9% 0.303251 Sockets used: 19 (for perfect keepalive, would be 4) Code 200 : 585 (97.5 %) Code 429 : 15 (2.5 %) All done 600 calls (plus 0 warmup) 45.325 ms avg, 59.9 qps
测试 service-python 服务的限流是否生效:
$ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -curl http://service-python/env HTTP/1.1 200 OK content-type: application/json content-length: 178 server: envoy date: Wed, 16 Jan 2019 15:47:30 GMT x-envoy-upstream-service-time: 366 {"message":"python v2","upstream":[{"message":"lua v2","response_time":0.19},{"message":"node v2","response_time":0.18,"upstream":[{"message":"go v1","response_time":"0.02"}]}]} $ kubectl exec fortio -c fortio /usr/local/bin/fortio -- load -qps 1 -n 10 -loglevel Error http://service-python/env 15:48:02 I logger.go:97> Log level is now 4 Error (was 2 Info) Fortio 1.0.1 running at 1 queries per second, 2->2 procs, for 10 calls: http://service-python/env Aggregated Function Time : count 10 avg 0.45553668 +/- 0.5547 min 0.003725253 max 1.4107851249999999 sum 4.55536678 # target 50% 0.18 # target 75% 1.06846 # target 90% 1.27386 # target 99% 1.39709 # target 99.9% 1.40942 Sockets used: 6 (for perfect keepalive, would be 4) Code 200 : 5 (50.0 %) Code 429 : 5 (50.0 %) All done 10 calls (plus 0 warmup) 455.537 ms avg, 0.6 qps
从上面的实验结果,可以得出如下的结论:
对于 service-go 服务,当 qps 低于50时请求几乎全部正常通过,当 qps 大于50时会有部分于请求得到 429 的响应码,这说明我们配置的限流规则已经生效。
对于 service-node 服务,对于普通调用,当 qps 大于20时就会出现部分于请求得到 429 响应码,但是当添加 “x-forwarded-for: 10.28.11.20” 请求头时,只有 qps 大于50时才会出现部分于请求得到 429 响应码,说明我们配置的关于 service-node 的两条限流规则都已经生效。
对于 service-python 服务,我们限定每 5s 只允许 2 次请求的限制,当以每秒 1qps 请求时,10 个请求只有 3 个请求通过,其他请求均得到 429 响应码。这说明我们对于 service-python 配置的限流规则也已经生效。
Istio 通过 quota 实现限流,但是限流并不是完全准确的,可能会存在部分误差,使用时需要注意。
清理:
$ kubectl delete -f kubernetes/fortio.yaml $ kubectl delete -f istio/resilience/quota-mem-ratelimit.yaml $ kubectl delete -f service/node/service-node.yaml $ kubectl delete -f service/lua/service-lua.yaml $ kubectl delete -f service/python/service-python.yaml
总结
借助 Istio 提供的负载均衡,连接池,熔断和限流机制,我们可以使得我们的服务更具弹性,在遇到故障能更好的应对,以及快速的从故障中恢复过来,而这些几乎不需要人来参与其中。
以上内容摘自《Istio入门与实战》一书,经出版方授权发布。