在互联网系统中,服务提供方(upstream)因访问压力过大而响应变慢或失败,服务发起方(downstream)为了保护系统整体的可用性,可以临时暂停对服务提供方的调用,这种牺牲局部,保全整体的措施就叫做熔断。
限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。一般也会算在熔断中。
可靠性是微服务架构的关键,熔断(Circuit breakers)是减少服务异常和降低服务延迟的一种设计模式,如果在一定时间内服务累计发生错误的次数超过了预先定义的阈值,就会将该错误的服务从负载均衡池中移除(如下图),当超过一段时间后,又会将服务再移回到服务负载均衡池。
熔断主要是无感的处理服务异常并保证不会发生级联甚至雪崩的服务异常。在微服务方面体现是对异常的服务情况进行快速失败,它对已经调用失败的服务不再会继续调用,如果仍需要调用此异常服务,它将立刻返回失败。与此同时,它一直监控服务的健康状况,一旦服务恢复正常,则立刻恢复对此服务的正常访问。这样的快速失败策略可以降低服务负载压力,很好地保护服务免受高负载的影响。
Hystrix vs Istio 两大熔断器对比:
Istio是使用了黑盒方式来作为一种代理管理工具。它实现起来很简单,不依赖于底层技术栈,而且可以在部署后也可以进行配置和修改。Hystrix需要在代码级别处理断路器,可以设置级联和一些附加功能,它需要在开发阶段时做出降级决策,后期优化配置成本高。
Istio无需对代码进行任何更改就可以为应用增加熔断和限流功能。Istio中熔断和限流在DestinationRule的CRD资源的TrafficPolicy中设置,一般设置连接池(ConnectionPool)限流方式和异常检测(outlierDetection)熔断方式。两者ConnectionPool和outlierDetection各自配置部分参数,其中参数有可能存在对方的功能,并没有很严格的区分出来,如主要进行限流设置的ConnectionPool中的maxPendingRequests参数,最大等待请求数,如果超过则也会暂时的熔断。
ConnectionPool可以对上游服务的并发连接数和请求数进行限制,适用于TCP和HTTP。ConnectionPool又称之是限流。
设置在DestinationRule中的配置如下图:
Tcp连接池设置http和tcp上游连接的设置。相关参数设置如下:
maxConnections:到目标主机的HTTP1/TCP最大连接数量,只作用于http1.1,不作用于http2,因为后者只建立一次连接。
connectTimeout:tcp连接超时时间,默认单位秒。也可以写其他单位,如ms。
tcpKeepalive:如果在套接字上设置SO_KEEPALIVE可以确保TCP 存活
TCP的TcpKeepalive设置:
Probes:在确定连接已死之前,在没有响应的情况下发送的keepalive探测的最大数量。默认值是使用系统级别的配置(除非写词参数覆盖,Linux默认值为9)。
Time:发送keep-alive探测前连接存在的空闲时间。默认值是使用系统的配置(除非写此参数,Linux默认值为7200s(即2小时)。
interval:探测活动之间的时间间隔。默认值是使用系统的配置(除非被覆盖,Linux默认值为75秒)。
http连接池设置用于http1.1/HTTP2/GRPC连接。
http1MaxPendingRequests:http请求pending状态的最大请求数,从应用容器发来的HTTP请求的最大等待转发数,默认是1024。
http2MaxRequests:后端请求的最大数量,默认是1024。
maxRequestsPerConnection:在一定时间内限制对后端服务发起的最大请求数,如果超过了这个限制,就会开启限流。如果将这一参数设置为 1 则会禁止 keepalive 特性;
idleTimeout:上游连接池连接的空闲超时。空闲超时被定义为没有活动请求的时间段。如果未设置,则没有空闲超时。当达到空闲超时时,连接将被关闭。注意,基于请求的超时意味着HTTP/2ping将无法保持有效连接。适用于HTTP1.1和HTTP2连接;
maxRetries:在给定时间内,集群中所有主机都可以执行的最大重试次数。默认为3。
熔断和限流是分布式系统的重要组成部分,优点是快速失败并迅速向下游反馈。Istio是通过Envoy Proxy 来实现熔断和限流机制的,Envoy 强制在网络层面配置熔断和限流策略,这样就不必为每个应用程序单独配置或重新编程。Envoy支持各种类型的全分布式(不协调)限流。
IstioConnectionPool与 Envoy 的限流参数对照表:
Envoy paramether | Envoy upon object | Istio parameter | Istio upon ojbect |
---|---|---|---|
max_connections | cluster.circuit_breakers | maxConnections | TCPSettings |
max_pending_requests | cluster.circuit_breakers | http1MaxPendingRequests | HTTPSettings |
max_requests | cluster.circuit_breakers | http2MaxRequests | HTTPSettings |
max_retries | cluster.circuit_breakers | maxRetries | HTTPSettings |
connect_timeout_ms | cluster | connectTimeout | TCPSettings |
max_requests_per_connection | cluster | maxRequestsPerConnection | HTTPSettings |
maxConnections: 表示在任何给定时间内,Envoy 与上游集群建立的最大连接数,限制对后端服务发起的 HTTP/1.1 连接数。该配置仅适用于 HTTP/1.1 协议,因为HTTP/2 协议可以在同一个 TCP 连接中发送多个请求,而 HTTP/1.1 协议在同一个连接中只能处理一个请求。如果超过了这个限制(即断路器溢出),集群的upstream_cx_overflow计数器就会增加。
maxPendingRequests: 表示待处理请求队列的长度,如果超过了这个限制,就会开启限流。因为HTTP/2 是通过单个连接并发处理多个请求的,因此该策略仅在创建初始 HTTP/2 连接时有用,之后的请求将会在同一个 TCP 连接上多路复用。对于HTTP/1.1 协议,只要没有足够的上游连接可用于立即分派请求,就会将请求添加到待处理请求队列中,因此该断路器将在该进程的生命周期内保持有效。如果该断路器溢出,集群的upstream_rq_pending_overflow计数器就会增加。
maxRequestsPerConnection: 表示在任何给定时间内,上游集群中主机可以处理的最大请求数,限制对后端服务发起的HTTP/2 请求数。实际上,这适用于仅 HTTP/2 集群,因为 HTTP/1.1 集群由最大连接数断路器控制。如果该断路器溢出,集群的upstream_rq_pending_overflow 计数器就会递增。
maxRetries:在任何给定时间内,集群中所有主机都可以执行的最大重试次数。一般情况下,建议对偶尔的故障积极地进行断路重试,因为总体重试容量不会爆炸并导致大规模级联故障。如果这个断路器溢出,则集群的upstream_rq_retry_overflow计数器将增加。
envoy新加参数(后期istio可能会增加)
maximumconcurrent connection pools:可以并发实例化的连接池的最大数量。一些特性,比如Original SrcListener Filter,可以创建无限数量的连接池。当集群耗尽其并发连接池时将会回收空闲连接。如果不能回收,断路器就会溢出。这与连接池中的集群最大连接不同,连接池中的连接通常不会超时。Connections自动清理;连接池不需要。注意,为了使连接池发挥作用,它至少需要一个上游连接,因此这个值应该小于集群最大连接。
在上游集群和优先级上针对不同的组件,都可以分别进行单独的配置参数进行请求限制。通过统计可以观察到这些断路器的状态,包括断路器打开前剩余数量的断路器。注意,在HTTP请求下将会重新设置路由过滤器的x-envoy-overloaded报头。
使用istio的sample中自带的httpbin案例分析。设置maxConnections:1 以及http1MaxPendingRequests: 1,表示如果超过了一个连接同时发起请求,Istio 就会限流,阻止后续的请求或连接。
先尝试通过单线程(NUM_THREADS=1 )创建一个连接,并进行 5 次调用(默认值: NUM_CALLS_PER_CLIENT=5 ):
$ CLIENT_POD=$(kubectl get pod | grep httpbin-client | awk '{ print $1 }')
$ kubectl exec -it $CLIENT_POD -c httpbin-client -- sh -c 'export URL_UNDER_TEST=http://httpbin:8000/get export NUM_THREADS=1 && java -jar http-client.jar'
using num threads: 1
Starting pool-1-thread-1 with numCalls=5 delayBetweenCalls=0 url=http://localhost:15001/get mixedRespTimes=false
pool-1-thread-1: successes=[5], failures=[0], duration=[545ms]
可以看到所有请求都通过了:successes=[5]
我们可以查询istio-proxy 的状态,获取更多相关信息:
$ kubectl exec -it $CLIENT_POD -c istio-proxy -- sh -c 'curl localhost:15000/stats' | grep httpbin
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_cx_http1_total: 5
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_cx_overflow: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_total: 5
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_200: 5
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_2xx: 5
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_overflow: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_retry: 0
可以看出总共发送了 5 个HTTP/1.1 连接,也就是 5 个请求,响应码均为 200。
下面尝试把线程数提高到 2的结果如下:
kubectl exec -it $CLIENT_POD -c httpbin-client -- sh -c 'export URL_UNDER_TEST=http://httpbin:8000/get export NUM_THREADS=2 && java -jar http-client.jar'
using num threads: 2
Starting pool-1-thread-1 with numCalls=5 parallelSends=false delayBetweenCalls=0 url=http://httpbin:8000/get mixedRespTimes=false
Starting pool-1-thread-2 with numCalls=5 parallelSends=false delayBetweenCalls=0 url=http://httpbin:8000/get mixedRespTimes=false
pool-1-thread-1: successes=[3], failures=[2], duration=[96ms]
pool-1-thread-2: successes=[4], failures=[1], duration=[87ms]
查看istio-proxy 的状态:
$ kubectl exec -it $CLIENT_POD -c istio-proxy -- sh -c 'curl localhost:15000/stats' | grep httpbin
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_cx_http1_total: 7
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_cx_overflow: 3
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_total: 10
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_200: 7
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_2xx: 7
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_503: 3
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_5xx: 3
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_overflow:
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_retry:0
...
总共发送了 10 个HTTP/1 连接,只有 7 个被允许通过,剩余请求被断路器拦截了。其中upstream_cx_overflow 的值为 3,表明 maxConnections 断路器起作用了。Istio-proxy 允许一定的冗余,你可以将线程数提高到 3,限流的效果会更明显。
再来测试一下maxPendingRequests 断路器。前面已经将 maxPendingRequests 的值设置为 1,现在按照预想,我们只需要模拟在单个HTTP/1.1 连接中同时发送多个请求,就可以触发该断路器开启限流。由于 HTTP/1.1 同一个连接只能处理一个请求,剩下的请求只能放到待处理请求队列中。通过限制待处理请求队列的长度,可以对恶意请求、DoS 和系统中的级联错误起到一定的缓解作用。
现在尝试通过单线程(NUM_THREADS=1 )创建一个连接,并同时发送 20 个请求( PARALLEL_SENDS=true ,NUM_CALLS_PER_CLIENT=20 ):
$ kubectl exec-it $CLIENT_POD -c httpbin-client -- sh -c 'export URL_UNDER_TEST=http://httpbin:8000/get export NUM_THREADS=1 export PARALLEL_SENDS=true export NUM_CALLS_PER_CLIENT=20 && java -jar http-client.jar'
using num threads:1
Starting pool-1-thread-1with numCalls=20 parallelSends=true delayBetweenCalls=0 url=http://httpbin:8000/get mixedRespTimes=false
finished batch 0
finished batch 5
finished batch 10
finished batch 15
pool-1-thread-1: successes=[16], failures=[4], duration=[116ms]
查询istio-proxy 的状态:
$ kubectl exec-it $CLIENT_POD -c istio-proxy -- sh -c 'curl localhost:15000/stats'| grep httpbin | grep pending
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:4
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_total:16
upstream_rq_pending_overflow的值是 4 ,说明有 4 次调用触发了maxPendingRequests 断路器的熔断策略,被标记为熔断。
熔断策略对集群中压力过大的上游服务起到一定的保护作用,有一种情况是集群中的某些节点完全崩溃,这种情况我们并不知晓。istio引入了异常检测来完成熔断的功能,通过周期性的动态的异常检测来确定上游集群中的某些主机是否异常,如果发现异常,就将该主机从连接池中隔离出去,这就是异常值检测。也是一种熔断的实现,用于跟踪上游服务的状态。适用于HTTP和TCP服务。对于HTTP服务,API调用连续返回5xx错误,则在一定时间内连接池拒绝此服务。对于TCP服务,一个主机连接超时次数或者连接失败次数达到一定次数时就认为是连接错误。
1. 检测到了某个主机异常。
2.如果到目前为止负载均衡池中还没有主机被隔离出去,将会立即隔离该异常主机;如果已经有主机被隔离出去,就会检查当前隔离的主机数是否低于设定的阈值(通过envoy中的 outlier_detection.max_ejection_percent 指定),如果当前被隔离的主机数量不超过该阈值,就将该主机隔离出去,否则不隔离。
3.隔离不是永久的,会有一个时间限制。当主机被隔离后,该主机就会被标记为不健康,并且不会被加入到负载均衡池中,除非负载均衡处于恐慌模式。隔离时间等于envoy中的outlier_detection.base_ejection_time_ms的值乘以主机被隔离的次数。所以如果某个主机连续出现故障,会导致它被隔离的时间越来越长。
4.经过了规定的隔离时间之后,被隔离的主机将会自动恢复过来,重新接受调用方的远程调用。通常异常检测会与主动健康检查一起用于全面的健康检查解决方案。
连续 5xx 响应:如果上游主机连续返回一定数量的 5xx 响应,该主机就会被驱逐。注意,这里的 5xx 响应不仅包括返回的 5xx 状态码,也包括 HTTP路由返回的一个事件(如连接超时和连接错误)。隔离主机所需的 5xx 响应数量由 consecutive_5xx 的值控制。
连续网关故障:如果上游主机连续返回一定数量的 "gatewayerrors" ( 502 , 503 或 504 状态码),该主机就会被驱逐。这里同样也包括 HTTP 路由返回的一个事件(如连接超时和连接错误)。隔离主机所需的连续网关故障数量由consecutive_gateway_failure的值控制。
调用成功率:基于调用成功率的异常检测类型会聚合集群中每个主机的调用成功率,然后根据统计的数据以给定的周期来隔离主机。如果该主机的请求数量小于success_rate_request_volume指定的值,则不会为该主机计算调用成功率,因此聚合的统计数据中不会包括该主机的调用成功率。如果在给定的周期内具有最小所需请求量的主机数小于success_rate_minimum_hosts 指定的值,则不会对该集群执行调用成功率检测。
Istio outlierDetection与Envoy 的异常检测参数对照表如下所示:
Envoy paramether | Envoy upon object | Istio parameter | Istio upon ojbect |
---|---|---|---|
consecutive_gateway_failure | cluster.outlier_detection | consecutiveErrors | outlierDetection |
interval | cluster.outlier_detection | interval | outlierDetection |
baseEjectionTime | cluster.outlier_detection | baseEjectionTime | outlierDetection |
maxEjectionPercent | cluster.outlier_detection | maxEjectionPercent | outlierDetection |
常用的配置事例入下图:
consecutiveErrors:从连接池开始拒绝连接,已经连接失败的次数。当通过HTTP访问时,返回代码是502、503或504则视为错误。当访问不透明的TCP连接时,连接超时和连接错误/失败也会都视为错误。即将实例从负载均衡池中剔除,需要连续的错误(HTTP5XX或者TCP断开/超时)次数。默认是5。
Interval:拒绝访问扫描的时间间隔,即在interval(1s)内连续发生1个consecutiveErrors错误,则触发服务熔断,格式是1h/1m/1s/1ms,但必须大于等于1ms。即分析是否需要剔除的频率,多久分析一次,默认10秒。
baseEjectionTime:最短拒绝访问时长。这个时间主机将保持拒绝访问,且如果决绝访问达到一定的次数。这允许自动增加不健康服务的拒绝访问时间,时间为baseEjectionTime*驱逐次数。格式:1h/1m/1s/1ms,但必须大于等于1ms。实例被剔除后,至少多久不得返回负载均衡池,默认是30秒。
maxEjectionPercent:服务在负载均衡池中被拒绝访问(被移除)的最大百分比,负载均衡池中最多有多大比例被剔除,默认是10%。
minHealthPercent:在健康模式下,外部检测可以和负载均衡池一样,有最小健康百分比的阈值。当健康主机百分比低于这个阈值,外部检测将禁用,同时proxy将会对所有主机进行负载均衡,包含健康和不健康的主机。通常minHealthPercent+maxEjectionPercent<= 100%。默认值是50%。
上面例子是设置tcp的连接池大小为100个连接,可以有1000个并发HTTP2请求。“reviews”服务的请求连接比不大于10。此外,配置拒绝访问的时间间隔是5分钟,同时,任何连续7次返回5XX码的主机,将会拒绝访问15分钟。
举例:设置的参数如下,该配置表示每秒钟扫描一次上游主机,连续失败1 次返回 5xx 错误码的所有主机会被移出连接池 3 分钟。
我们通过调用一个 URL 来指定httpbin 服务返回 502 状态码,以此来触发连续网关故障异常检测。总共发起 3 次调用,因为 outlierDetection中的配置要求 Envoy 的异常检测机制必须检测到两个连续的网关故障才会将httpbin 服务移除负载均衡池。
kubectl exec-it $CLIENT_POD -c httpbin-client -- sh -c 'export URL_UNDER_TEST=http://httpbin:8000/status/502 export NUM_CALLS_PER_CLIENT=3 && java -jar http-client.jar'
using num threads:1
Starting pool-1-thread-1with numCalls=3 parallelSends=false delayBetweenCalls=0 url=http://httpbin:8000/status/502 mixedRespTimes=false
pool-1-thread-1: successes=[0], failures=[3], duration=[99ms]
查看istio-proxy 的状态:
kubectl exec -it $CLIENT_POD -c istio-proxy -- sh -c 'curl localhost:15000/stats' | grep httpbin | grep outlier
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_active: 1
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_consecutive_5xx: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_detected_consecutive_5xx: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_detected_consecutive_gateway_failure: 1
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_detected_success_rate: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_enforced_consecutive_5xx: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_enforced_consecutive_gateway_failure: 1
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_enforced_success_rate: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_enforced_total: 1
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_overflow: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_success_rate: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_total: 0
确实检测到了连续网关故障,consecutive_gateway_failure 的值为 1。
例子2:
kind: DestinationRuleapiVersion: networking.istio.io/v1alpha3metadata: name: testhttpspec: host: httpbin.org trafficPolicy: connectionPool: http: http1MaxPendingRequests: 1024 http2MaxRequests: 1024 outlierDetection: baseEjectionTime: 10m consecutiveErrors: 1 interval: 1s maxEjectionPercent: 50 minHealthPercent: 50 subsets: - labels: version: v1 name: v1 - labels: version: v2 name: v2 |
---|
上述配置在实际测试中生效,subset的v1中一个pod和subset的v2中一个pod返回200(健康),subset的v1中另一个pod返回503(不健康),则subset-v1健康实例百分比是:
pod-v1-2/(pod-v1-1 + pod-v1-2) = 1/2 = 50%>= minHealthPercent=50%,
subset-v1不健康实例百分比:
pod-v1-1/(pod-v1-1 + pod-v1-2) = 1/2 = 50% <=maxEjectionPercent=50%,则subset的v1中健康的实例百分比50%>=50%(minHealthPercent)且不健康的实例百分比50%<=50%(maxEjectionPercent),则服务熔断触发,异常检测生效,v1的一个pod返回503,服务实例被从服务负载均衡池中移除,实际观察到的现象就是subset的v1中另一个pod继续提供服务而v1的pod在接受1-2个请求之后便不再接收请求,而subset的v2中pod未受到影响,继续提供服务。
以上是个人对Istio中Circuit Breaker的简单学习,很乐意在云原生技术社区与各位技术同仁互相交流,共同提高!