Istio Idle Timeout问题复现和解决

Istio 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:
访问量 访客数