更新 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: