`
March 25, 2024 本文阅读量

Istio Idle Timeout问题复现和解决

在使用 istio 时,有时候会遇到连接超时的问题,这个问题可能是由于 envoy 的 tcp_idle_timeout 导致的,本文将介绍如何复现和解决这个问题。

更新 2024-04-01

通过调整 tcp_proxy 的 idle_timeout 参数后,部分中间件(redis, mongo)的异常问题不再出现,但是 mysql(sharding-spere) 和 memcached 仍然存在 “invalid connection” 错误,所以还需要找到能够解决 mysql(sharding-spere) 和 memcached 的方法。

上述描述的现象非常主观,不一定正确也不能作为最终结论,但是 idle_timeout 配置确实没有解决所有的问题。

这里,需要知道 istio 在 inject 时会通过 iptables 对应用的流量进行劫持,对于 outbound 的流量 iptables 规则拦截转发到 OUTPUT 链。OUTPUT 的链转发流量到 ISTIO_OUTPUT,这个链会决定服务访问外部服务的流量发往何处。

这样产生的效果是,除了应用自己建立的连接之外 envoy 也会创建一个代理连接。

$ netstat -notp | grep 3307
tcp         0             172.23.105.25:41030        x.x.x.x:3307         ESTABLISHED -      off(0.00/0/0)
tcp         0             172.23.105.25:41022        x.x.x.x:3307         ESTABLISHED 1/./app      keepalive(0.88/0/0)

我遇到的问题可以确定问题就出在 envoy 建立的连接上,因为应用自己建立的连接是正常的,同时还可以看到应用自己的连接开启了 keepalive, 而 envoy 建立的连接没有开启。这样可能会出现这个连接会因为超时而被关闭的情况,或者其他原因导致连接被释放。那如果可以避免将中间件的流量通过 envoy 代理,这样就可以避免这个问题。

在官方的文档中也提到,https://istio.io/latest/zh/docs/tasks/traffic-management/egress/egress-control/ 对于外部服务,可以通过配置 excludeOutboundPorts 或者 excludeOutboundIPRanges 来使得某些服务的流量不经过 istio sidecar。其背后的原理是通过 iptables 的规则中排除这些端口或者 IP 地址。

经过实验,发现这个方法可以 解决(绕过)这个问题。还是没有定位到 envoy 代理过程中发生了什么导致连接被关闭的原因,这个问题还需要进一步的分析。

问题描述

在 kubernetes 集群中使用 istio 时,开发同学反馈经常会出现 “invalid connection” 错误,导致业务逻辑错误,而在没有使用 istio 的情况下,这个问题并不会出现。通过查看日志,发现这种错误常发生在集群外的服务连接,如:memcached、redis, mysql, mongodb 等。

通过上述的描述,首先怀疑的是 istio 的 sidecar 代理导致的问题,因为这种错误只有在使用 istio 时才会出现。

Istio 和 envoy 的基本概念

Istio 服务网格从逻辑上分为数据面和控制面,其中数据面由 Envoy 代理组成,控制面由 Pilot、Citadel、Galley 等组件组成。数据面是由 Envoy 代理组成的,负责实际的流量代理和控制。

也就是说,当我们在 kubernetes 集群中部署了 istio 时,每个 pod 都会有一个 sidecar 容器,这个容器中就包含了 Envoy 代理,会劫持 pod 中的所有流量。再回到上面的问题,在没有这个代理的情况下,这个问题并不会出现,所以这个问题很有可能是由 Envoy 代理导致的。

猜测问题原因

数据库连接超时

这里使用的开发语言为 go,“invalid connection” 错误往往发生在“连接超时”的情况下(连接被服务端单方面关闭,而客户端还在使用)。在数据库连接场景中,往往还会有“连接池”这个概念,连接池会在连接空闲一段时间后关闭连接,与此同时服务端也会设置相应的超时时间,当连接空闲时间超过这个时间时,服务端会主动关闭连接。

如:

  • mysql 的 wait_timeout 参数,当连接空闲时间超过这个时间时,服务端会主动关闭连接。
  • mongodb 的 maxIdleTimeMS 参数,连接在池中可保持空闲状态的最大毫秒数,超过这个时间后,连接会被删除或关闭。

在外部没有代理的情况时,如果出现 “invalid connection” 错误,很有可能是由于连接超时导致的。可以检查下客户端和服务端的连接超时时间是否合理设置了(服务端的超时时间要大于客户端的超时时间)。

envoy 关闭连接

根据上面的描述,我们可以猜测这个问题是由于 Envoy 代理产生的,那么经过在网上查找资料,发现这个问题可能是由于 Envoy 的 tcp_idle_timeout 导致的。

那是不是 enovy 关闭了连接,导致了这个问题呢?下面,我们将通过设计一个简单的复现场景来验证这个问题。

设计复现

  1. 准备一个 k8s 集群 ,并安装好 istio
  2. 在集群外部署一个 redis 服务
  3. 设置一个较小的 tcp idle timeout 参数(10s),便于观察
  4. 在集群内部署一个两个 POD, 一个接入 istio,一个不接入 istio(两个 POD 都通过 telnet 连接 redis 服务)
  5. 调整 istio 的 tcp_idle_timeout 参数,观察连接情况

使用到的相关文件参见 https://github.com/yeqown/playground/tree/master/k8s/istio-idle-timeout

  1. EnovyFilter 修改 tcp_idle_timeout 参数
kubectl apply -f envoyfilter-10s.yaml
  1. 部署 POD
kubectl create ns istio-idle-timeout && kubectl label ns istio-idle-timeout istio-injection=enabled

# 部署没有 istio 的 POD
kubectl apply -f deployment.yaml -n default
# 部署有 istio 的 POD
kubectl apply -f deployment.yaml -n istio-idle-timeout
  1. 连接外部 redis 服务

在本地通过 minikube 启动 k8s 集群,并且在本机部署一个 redis 服务。

time telnet host.minikube.internal 3306

观察并配置调整

  1. 没有 istio sidecar 的 POD, 连接不会断开。
  2. 有 istio sidecar 的 POD, 10s 后连接会断开。
/ # time telnet 192.168.105.1 6379
Connected to 192.168.105.1
Connection closed by foreign host
Command exited with non-zero status 1
real	0m 10.00s
user	0m 0.00s
sys	0m 0.00s

清除 idle_timeout 参数

kubectl delete -f envoyfilter-remove.yaml

重新连接到 redis ,观察连接情况。

/ # time telnet 192.168.105.1 6379
Connected to 192.168.105.1
Connection closed by foreign host
Command exited with non-zero status 1
real	1h 0m 00s
user	0m 0.00s
sys	0m 0.00s

总结

enovy 的 tcp_idle_timeout 参数默认为 1h,这个参数会导致一些连接超时的问题,可以通过修改 istio EnvoyFilter 来调整这个参数。

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: idle-timeout
  namespace: istio-system
spec:
  configPatches:
    - applyTo: NETWORK_FILTER
      match:
        context: SIDECAR_OUTBOUND
        listener:
          filterChain:
            filter:
              name: envoy.filters.network.tcp_proxy
      patch:
        operation: MERGE
        value:
          name: envoy.filters.network.tcp_proxy
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
            idle_timeout: 10s

需要注意的是,这里的 envov filter 是作用在 sidecar 的 outbound 上的,所以只会影响 sidecar 代理的连接。但是 envoy 还会作为 ingress 和 egress 的代理,这个参数不会影响到这两个场景。

解决

知道 envoy 的 tcp_idle_timeout 参数的影响后,那么可以通过调大这个参数来解决这个问题。比如设置为比客户端超时时间更大的值,比服务端超时时间更大的值。

其他

  1. 可能相关的日志输出
# 调整日志级别
curl -X POST http://127.0.0.1:15000/logging?level=debug

通过日志发现,连接在 10s 后断开时伴随着 invoking idle callbacks 的日志输出。如下:(前后一共测试了4次)

➜  istio-1.19.3 klf istio-idle-timeout-demo-5b4894b67d-tdpb5 -n istio-idle-timeout -c istio-proxy | grep "invoking idle callbacks"
2024-03-01T07:22:21.481180Z	debug	envoy pool external/envoy/source/common/conn_pool/conn_pool_base.cc:454	invoking idle callbacks - is_draining_for_deletion_=false	thread=22
2024-03-01T07:22:21.481310Z	debug	envoy pool external/envoy/source/common/conn_pool/conn_pool_base.cc:454	invoking idle callbacks - is_draining_for_deletion_=false	thread=22
2024-03-01T07:24:03.947896Z	debug	envoy pool external/envoy/source/common/conn_pool/conn_pool_base.cc:454	invoking idle callbacks - is_draining_for_deletion_=false	thread=23
2024-03-01T07:24:03.947903Z	debug	envoy pool external/envoy/source/common/conn_pool/conn_pool_base.cc:454	invoking idle callbacks - is_draining_for_deletion_=false	thread=23


2024-03-01T07:25:04.884340Z	debug	envoy pool external/envoy/source/common/conn_pool/conn_pool_base.cc:454	invoking idle callbacks - is_draining_for_deletion_=false	thread=22
2024-03-01T07:25:04.884394Z	debug	envoy pool external/envoy/source/common/conn_pool/conn_pool_base.cc:454	invoking idle callbacks - is_draining_for_deletion_=false	thread=22
2024-03-01T07:25:17.629907Z	debug	envoy pool external/envoy/source/common/conn_pool/conn_pool_base.cc:454	invoking idle callbacks - is_draining_for_deletion_=false	thread=22
2024-03-01T07:25:17.629912Z	debug	envoy pool external/envoy/source/common/conn_pool/conn_pool_base.cc:454	invoking idle callbacks - is_draining_for_deletion_=false	thread=22
  1. 应用 envoy filter 超时设置后,已经建立的连接不会应用新的超时设置,只有新的连接才会应用新的超时时间?
  2. EnovyFilter 设置超时后,可以通过设置为空来清除超时设置。
      patch:
        operation: MERGE
        value:
          name: envoy.filters.network.tcp_proxy
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
            idle_timeout: