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 导致的。
-
https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts#tcp
The TCP proxy idle_timeout is the amount of time that the TCP proxy will allow a connection to exist with no upstream or downstream activity. The default idle timeout if not otherwise specified is 1 hour.
TCP 代理空闲超时是 TCP 代理允许连接在没有上游或下游活动的情况下存在的时间量。如果没有另外指定,默认空闲超时为 1 小时。
那是不是 enovy 关闭了连接,导致了这个问题呢?下面,我们将通过设计一个简单的复现场景来验证这个问题。
设计复现
- 准备一个 k8s 集群 ,并安装好 istio
- 在集群外部署一个 redis 服务
- 设置一个较小的 tcp idle timeout 参数(10s),便于观察
- 在集群内部署一个两个 POD, 一个接入 istio,一个不接入 istio(两个 POD 都通过 telnet 连接 redis 服务)
- 调整 istio 的 tcp_idle_timeout 参数,观察连接情况
使用到的相关文件参见 https://github.com/yeqown/playground/tree/master/k8s/istio-idle-timeout
- EnovyFilter 修改 tcp_idle_timeout 参数
kubectl apply -f envoyfilter-10s.yaml
- 部署 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
- 连接外部 redis 服务
在本地通过 minikube 启动 k8s 集群,并且在本机部署一个 redis 服务。
time telnet host.minikube.internal 3306
观察并配置调整
- 没有 istio sidecar 的 POD, 连接不会断开。
- 有 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 参数的影响后,那么可以通过调大这个参数来解决这个问题。比如设置为比客户端超时时间更大的值,比服务端超时时间更大的值。
其他
- 可能相关的日志输出
# 调整日志级别
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
- 应用 envoy filter 超时设置后,已经建立的连接不会应用新的超时设置,只有新的连接才会应用新的超时时间?
- 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: