Skip to content

第36讲:什么是服务重试与熔断器

上一课时对 Istio 中的虚拟服务、目的地规则和网关的基本概念,以及使用方式进行了介绍,本课时继续介绍 Istio 与流量控制相关的功能,包括服务超时、重试和熔断器等。

服务超时

在一个微服务内部,当调用其他内部微服务或第三方服务的 API 时,都应该设置超时时间,这样,可以避免调用请求长时间处于等待状态。超时时间的设置取决于服务之间的服务级别协议(Service-level Agreement,SLA)。每个服务提供者都应该声明它在响应时间上的保证,比如,保证 90% 的请求都在 1 秒内给出响应。服务消费者根据服务级别协议来设置超时时间,比如,如果希望保证 90% 的请求都不超时,那么超时时间应该设置为 1 秒。

如果没有服务级别协议,我们只能根据经验来进行推断,然后得出一个估计值。更好的做法是利用服务网格收集的性能指标数据,从历史记录中推断出比较合理的超时时间。

过长或过短的超时时间都可能造成问题。当超时时间过长时,没有办法及时对可能阻塞的请求进行处理;如果超时时间过短,当服务提供者的负载变大,而造成响应时间增加时,有些本来可以成功完成的请求,会由于超时而被提前终止,影响应用的可用性。

在没有服务网格之前,需要在代码中直接设置超时时间,一般通过服务客户端的 API 来完成。常见的做法是把超时时间设置以系统属性或环境变量的形式暴露出来,允许在运行时进行修改。不过这种方式的弊端在于,每次修改配置都会造成应用的重启。在 Istio 的虚拟服务资源中,HTTP 类型的路由可以使用 timeout 属性来设置超时时间。

在下面代码中,地址管理服务的 API 调用的超时时间被设置为 500 毫秒。如果对目的地服务的调用没有在超时时间内返回,那么请求方会接收到 HTTP 504 错误。

yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: address-service
spec:
  hosts:
    - address-service.happyride.svc.cluster.local
  http:
    - name: "address-service-http"
      route:
        - destination:
            host: address-service.happyride.svc.cluster.local
      timeout: 500ms

使用 Istio 来设置虚拟服务的超时时间,提供了在运行时进行动态修改的便利,并不需要对服务本身进行修改。当预期对服务的调用请求增多时,比如在大型促销活动之前,可以提前增加超时时间,以减少出错的次数。在运行时,如果检测到对某个服务的调用频繁超时,也可以适当增加超时时间,以提高调用的成功率。这种便利性不仅方便了运维人员,也减少了开发人员的工作量。

请求重试

当调用服务的请求出现错误时,如果该请求是幂等的,也可以对该请求进行重试。Istio 的服务代理提供了自动重试的功能,只需要在虚拟服务中进行配置即可。虚拟服务资源的 HTTP 路由的 retries 属性可以配置重试策略,具体的配置属性如下表所示:

属性说明
attempts进行重试的最大次数
perTryTimeout每次重试的超时时间
retryOn重试发生的条件
retryRemoteLocalities是否尝试访问其他地理位置来进行重试

上表中的 retryOn 属性所指定的条件根据 HTTP 或 gRPC 协议而有所不同,HTTP 协议支持的条件如下表所示。如果需要指定多个重试条件,以逗号进行分隔即可。

条件说明
5xxHTTP 响应的状态码以 5 开头
gateway-errorHTTP 响应的状态码为 502、503 或 504
reset目标服务器没有响应,包括连接被断开、重置或读取超时
connect-failure无法连接到目标服务器,包括连接超时或产生了以 5 开头的错误
retriable-4xxHTTP 响应的状态码为可以重试的以 4 开头的错误,目前仅包括状态码 409
refused-stream目标服务器拒绝连接,对应于 HTTP/2 中的 REFUSED_STREAM 错误
retriable-status-codesHTTP 响应的状态码包含在允许重试的状态码列表中
retriable-headersHTTP 响应中包含允许重试的 HTTP 头

对于上表中的最后两种重试条件,允许重试的状态码和 HTTP 头列表分别通过请求中的 HTTP 头 x-envoy-retriable-status-codes 和 x-envoy-retriable-header-names 来设置。

对于 gRPC 来说,重试的条件取决于响应中的状态码。可以使用的状态码如下表所示:

状态码状态值说明
cancelled1操作被调用者取消
deadline-exceeded4操作在给定的期限内没有完成
internal13出现内部错误
resource-exhausted8没有足够的资源来完成操作
unavailable14服务当前不可用

在下面代码中的虚拟服务的资源定义中,为地址管理服务的 API 添加了重试策略。

yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: address-service
spec:
  hosts:
    - address-service.happyride.svc.cluster.local
  http:
    - name: "address-service-http"
      route:
        - destination:
            host: address-service.happyride.svc.cluster.local
      retries:
        attempts: 3
        perTryTimeout: 200ms
        retryOn: gateway-error,connect-failure,refused-stream

与一般的重试库不同的是,Istio 的服务代理在重试时,并不能配置重试之间的时间间隔。实际的重试时间间隔由 Istio 来确定,并且至少为 25 毫秒,由 Istio 来自动管理重试的间隔,可以避免重试风暴的问题,也简化了相关的配置。

熔断器

熔断器是保证服务调用快速恢复能力的一个重要模式。在服务网格实现中,服务代理会监控与对目标服务调用相关的一些指标,比如当前的并发连接数量和等待处理的请求数量等。如果这些指标超过预先设定的阈值,则说明目标服务当前处于过载的状态,在这种情况下,应该暂时中断对目标服务的请求,使得后续对该服务的请求都直接快速失败,而不是继续等待。

在添加了熔断器之后,当目标服务过载时,与之对应的熔断器处于打开状态,终止后续的相关请求。在一段时间过后,熔断器会闭合,允许再次尝试调用目标服务。通过熔断器模式,可以避免服务的调用方等待过长的时间,而造成请求积压,从而导致服务的级联崩溃。

Istio 的服务代理提供了对熔断器模式的支持,与之相关的配置在目的地规则的资源定义中。在 DestinationRule 资源的表示流量控制策略的 trafficPolicy 属性中,可以使用 connectionPool 来控制目标服务的连接池的属性,并设置相关指标的上限。

connectionPool 的 tcp 属性表示 TCP 和 HTTP 连接的公共配置,与熔断器相关的配置如下表所示:

属性默认值说明
maxConnections2^32^-1连接数量的最大值

而 connectionPool 的 http 属性表示 HTTP 连接的配置,与熔断器相关的配置如下表所示:

属性默认值说明
http1MaxPendingRequests2^32^-1HTTP 1 协议中处于等待状态的请求数量的最大值
http2MaxRequests2^32^-1HTTP 2 协议中请求数量的最大值
maxRequestsPerConnection0(无限制)每个连接的请求数量的最大值
maxRetries2^32^-1允许同时进行的请求重试数量的最大值

当配置中给定的最大值被超过时,熔断器处于打开状态,后续请求会直接出错。下面代码中的目的地规则对应于乘客界面的 GraphQL API 服务,添加了阈值的设置。从中可以看到,这些阈值设置的值很小,可以方便模拟熔断器生效的情况。

yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: passenger-api-graphql-service-cb
spec:
  host: passenger-api-graphql.happyride.svc.cluster.local
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 2
      http:
        http1MaxPendingRequests: 5
        maxRequestsPerConnection: 5
        maxRetries: 2

当请求由于熔断器打开的原因而失败时,HTTP 响应中会包含头 x-envoy-overloaded: true 来表明,并且 HTTP 状态码为 503。为了测试熔断器的工作模式,可以使用 Apache JMeter 这样的压力测试工具来进行测试。我们使用 20 个线程模拟并发的访问,从下图的测试结果中可以看到,在 8274 个请求中,有 97.32% 的请求出现错误。整个应用的吞吐量很高,因为大部分的请求都快速出错。

异常主机处理

Istio 服务代理会维护一个对目标服务的连接池,包含目标服务的全部可用主机,对这个连接池中的主机,服务代理可以追踪它们的状态。如果对主机的访问出现异常情况,那么该主机会被从连接池中暂时移除,不会作为负载均衡时的备选。

当需要配置移除连接池中主机的策略时,可以使用目的地规则资源的流量控制策略中的 outlierDetection 属性。下表给出了相关的配置项:

属性默认值说明
consecutiveGatewayErrors主机被移除之前允许出现连续的网关错误的数量
consecutive5xxErrors5主机被移除之前允许出现连续的以 5 开头的错误数量
interval10s检查主机状态的时间间隔
baseEjectionTime30s主机被移除的基础时间间隔
maxEjectionPercent10%可以被移除的主机数量的最大百分比
minHealthPercent0%当连接池中的健康主机数量不低于该百分比时,状态检查才会被启用;否则的话,服务代理会忽略主机状态,而使用全部主机进行负载均衡

下面代码中的目的地规则资源添加了与异常主机处理相关的配置。

yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: address-service-cb
spec:
  host: address-service.happyride.svc.cluster.local
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 1
      http:
        http1MaxPendingRequests: 1
        maxRequestsPerConnection: 1
        maxRetries: 1
    outlierDetection:
      consecutiveGatewayErrors: 10
      baseEjectionTime: 20s
      interval: 5s

错误注入

在配置服务网格时,一个很重要的问题是如何测试这些配置的有效性。比如,当请求出现超时时,自动重试的配置是否有效;当负载过大时,熔断器是否被触发。比较常见的做法是通过压力测试工具来模拟负载过大的情况,并检查请求的响应情况。不过压力测试一般只对入口网关发送请求,没有办法有效测试服务之间的调用。另外一个问题是,压力测试通常模拟的是单一用户场景的重复调用,得出的结果缺乏全面性。

Istio 提供了对自动注入错误的支持,可以模拟不同的错误场景。错误注入策略由虚拟服务资源的 HTTP 路由的 fault 属性来配置。Istio 支持两种出错策略,即延迟(Delay)和终止(Abort)。

1. 延迟

延迟指的是在请求发送到目的地之前,服务代理会延迟一段时间,从而模拟出现网络延迟或目标服务负载过大等情况。延迟通过 delay 属性来配置,相关的配置项如下表所示。

属性说明
fixedDelay固定的延迟时间
percentage添加延迟的请求数量的百分比

在下面代码的虚拟服务资源中,全部请求都会被延迟 1 秒钟。

yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: address-service
spec:
  hosts:
    - address-service.happyride.svc.cluster.local
  http:
    - name: "address-service-http"
      route:
        - destination:
            host: address-service.happyride.svc.cluster.local       
      fault:
        delay:
          fixedDelay: 1s
          percentage:
            value: 100

2. 终止

终止指的是用指定的 HTTP 状态码来终止请求。请求并不会被发送到目的地服务,而是直接以特定的 HTTP 状态码来作为响应。终止通过 abort 属性来配置,相关的配置项如下表所示:

属性说明
httpStatus终止请求时使用的 HTTP 状态码
percentage终止的请求数量的百分比

在下面代码的虚拟服务资源中,有一半的请求会被直接以 503 错误终止。

yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: address-service
spec:
  hosts:
    - address-service.happyride.svc.cluster.local
  http:
    - name: "address-service-http"
      route:
        - destination:
            host: address-service.happyride.svc.cluster.local       
      fault:
        abort:
          httpStatus: 503
          percentage:
            value: 50

在集成测试中,Istio 的错误注入功能可以帮助模拟不同的场景,从而测试应用对于错误的处理方式。在乘客界面的 GraphQL API 实现中,查询乘客地址的详细信息需要调用地址管理服务的 API。如果地址管理服务不可用,那么 API 应该返回可用的部分数据。在集成测试中,通过终止机制可以把对于地址管理服务的全部请求都以 503 错误来回应,然后再调用 API 来验证返回的结果是否正确地包含了部分数据。

总结

Istio 服务网格提供了对于流量控制的复杂策略,只需要虚拟服务和目的地规则进行配置即可。通过本课时的学习,你可以了解如何通过 Istio 来添加服务调用的超时时间和自动重试,以及如何通过熔断器来保护服务,如何通过错误注入来模拟不同的错误场景。