Files
baim0-learn/k8sFunny/05 ipvs 1s超时.md
T

214 lines
8.8 KiB
Markdown
Raw Normal View History

2024-05-25 17:01:37 +08:00
> 原文链接:[](https://zhuanlan.zhihu.com/p/630430941)
某个客户在Kubernetes中部署了大规模的业务,因此选用IPVS作为`kube-proxy`的负载均衡转发模式。以追求更高的转发性能和更新规则的速度。
但是,IPVS有一个存在了很久的连接重用问题,当客户发布服务,因为存在大量TCP短连接,客户端出现了`No route to host`报错,业务服务出现故障。
这是一个存在已久的issue:[kubernetes#81775](https://link.zhihu.com/?target=https%3A//github.com/kubernetes/kubernetes/issues/81775)。我们今天就来简单分析一下并介绍解决方案。
### 复现
故障必须经过复现才有排查的手段,我这里介绍一个简单的复现方法。
首先,准备一个测试用的Kubernetes集群,kube-proxy使用IPVS模式,并且为了复现故障,把内核参数`net.ipv4.vs.conn_reuse_mode`设为`0`(在一些低版本的`kube-proxy`或低版本内核中,你会发现它默认为`0`):
```bash
echo 0 > /proc/sys/net/ipv4/vs/conn_reuse_mode
```
部署一个简单的HTTP服务,包含10个副本,并定义一个`LoadBalancer`类型的Service以模拟负载均衡的场景:
```yaml
apiVersion: v1
kind: Service
metadata:
annotations:
service.beta.kubernetes.io/ucloud-load-balancer-vserver-protocol: "TCP"
service.beta.kubernetes.io/ucloud-load-balancer-type: "outer"
name: httpgraceful
spec:
type: LoadBalancer
ports:
- name: http
port: 80
protocol: TCP
targetPort: 8080
selector:
app: httpgraceful
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: httpgraceful
labels:
app: httpgraceful
spec:
replicas: 10
selector:
matchLabels:
app: httpgraceful
template:
metadata:
labels:
app: httpgraceful
spec:
containers:
- name: http
image: uhub.service.ucloud.cn/wxyz/httpgraceful:1.0.0
imagePullPolicy: Always
ports:
- containerPort: 8080
readinessProbe:
exec:
command:
- cat
- /tmp/healthz
initialDelaySeconds: 3
periodSeconds: 3
```
等待Pod运行起来后,查看Service地址(这里假设为`106.75.10.10`):
```bash
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
httpgraceful LoadBalancer 172.17.43.192 106.75.10.10 80:31984/TCP 48s
```
确认该Service可以访问:
```bash
$ curl http://106.75.10.10
hello world
version:1.0.0
```
现在,使用`ab`命令,创建大量对该Service的短连接:
```bash
ab -c 10 -n 2000000 http://106.75.10.10/
```
同时,将HTTP服务的副本数改为5,以模拟服务发布的场景:
```bash
kubectl scale deploy httpgraceful --replicas=5
```
很快,你就可以看到`ab`命令输出错误:
```text
Benchmarking 106.75.10.10 (be patient)
apr_socket_recv: No route to host (113)
Total of 5251 requests completed
```
`No route to host`出现了!复现出了客户的错误。
这时候,登录机器,查看IPVS规则,你会发现该Service对应的RS出现了很多权重为0的项:
```bash
$ ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 106.75.35.59:80 rr
-> 10.9.19.40:8080 Masq 1 5 2
-> 10.9.26.56:8080 Masq 0 1 0
-> 10.9.82.126:8080 Masq 0 1 1
-> 10.9.99.18:8080 Masq 0 1 0
-> 10.9.127.64:8080 Masq 0 2 0
-> 10.9.136.110:8080 Masq 1 6 1
-> 10.9.157.207:8080 Masq 1 5 0
-> 10.9.161.247:8080 Masq 1 5 0
-> 10.9.187.95:8080 Masq 1 5 0
```
没错,这就是导致这次问题的根因,我们会在下面进行分析。
### 原因分析
当业务进行缩容或者滚动迭代过程中,一定有旧的Pod被销毁。`No route to host`的直接原因是,IPVS把访问流量转发到了一个被销毁的Pod上面。
具体地说,当一个Pod被销毁后,`kube-controller-manager`中的`Endpoint Controller`会随之把对应的Pod Ip从`Endpoints`对象中摘除。kube-proxy随即通过informer获取到旧Pod Ip被摘除的事件,相应地更新ipvs转发规则:
- 先将旧的IP对应的RS权重置为0,防止新的连接调度到这个IP上。
- 等存量连接归零后(ActiveConn+InactiveConn=0),再彻底摘除掉这个旧IP。
从上面`ipvsadm -Ln`的结果也可以看到,几个权重为0的RS都还有连接,所以没有被摘除。
kube-proxy不直接摘除RS是为了实现Pod的优雅退出,以让Pod在处理完所有请求之后再退出。
但我们碰到的问题实际上是,权重被置为0后,依然有新连接被调度到旧IP上。这样存量连接计数永远不会归零,旧RS永远无法被摘除。而由于旧的IP实际已经不存在,就出现了`No route to host`报错。只有把流量撤走一段时间后,连接计数终于为0,旧RS被摘除。
这一切的罪魁祸首是一个内核参数:`net.ipv4.vs.conn_reuse_mode`。
众所周知,在大量TCP短连接的场景下,会有很多连接处于`TIME_WAIT`状态,为了减少资源占用,内核会重用这些连接的端口。
而如果`net.ipv4.vs.conn_reuse_mode`为0,并且客户端的源端口被复用了,那么IPVS会将流量直接转发到之前RS,而绕过了正常的负载均衡调度。这样即使RS的权重为0,也可能会有流量被转发上去。
在大量TCP短连接时,端口复用的频率会非常高,这样即使RS的权重被设为了0,还是会有部分流量打上来,而其背后的Pod已经被删除了(优雅退出有超时时间,超过之后Kubernetes会强制删除Pod),就导致了流量被转发到了一个不存在的IP上,出现`No route to host`错误。
那么其实只要将`net.ipv4.vs.conn_reuse_mode`设为1,强制让复用的连接也经过负载均衡转发不就可以了?
可惜,在`5.9`之前的内核版本中,`net.ipv4.vs.conn_reuse_mode`设为1并使用了`conntrack`时,如果出现了连接复用,IPVS会 DROP 掉第一个 SYN 包,导致 SYN 的重传,有 1s 延迟。而 Kube-proxy 在 IPVS 模式下,使用了 iptables 进行`MASQUERADE`,也正好开启了`net.ipv4.vs.conntrack`。
所以:
- 如果将`net.ipv4.vs.conn_reuse_mode`设为0,当出现大量短TCP连接时,会出现部分流量被转发到销毁Pod的问题。
- 如果将`net.ipv4.vs.conn_reuse_mode`设为1,当出现大量短TCP连接时,会有部分连接多出了1s的延迟的问题。
### 解决
也就是说,在以前的内核版本中,理想的情况是:
- `net.ipv4.vs.conn_reuse_mode`设为1,强制复用连接走负载均衡。
- `net.ipv4.vs.conntrack`设为0,防止IPVS对复用连接进行DROP SYNC操作。
但是,很可惜,这实现不了。**因此,如果你的内核版本低于`5.9`,并且存在大量TCP短连接场景,不建议使用IPVS,建议替换为iptables模式。**
`5.9`的一个[patch](https://link.zhihu.com/?target=http%3A//patchwork.ozlabs.org/project/netfilter-devel/patch/20200701151719.4751-1-ja%40ssi.bg)修复了在启用`conntrack`时的1s延迟问题,因此,如果内核版本大于等于`5.9`,将`net.ipv4.vs.conn_reuse_mode`设为1是安全的,保持这个内核参数为1即可解决问题。
事实上,新的kube-proxy会自动根据你的内核版本来判断是否要修改这个内核参数:
```go
const connReuseFixedKernelVersion = "5.9"
if kernelVersion.LessThan(version.MustParseGeneric(connReuseMinSupportedKernelVersion)) {
klog.ErrorS(nil, "Can't set sysctl, kernel version doesn't satisfy minimum version requirements", "sysctl", sysctlConnReuse, "minimumKernelVersion", connReuseMinSupportedKernelVersion)
} else if kernelVersion.AtLeast(version.MustParseGeneric(connReuseFixedKernelVersion)) {
// https://github.com/kubernetes/kubernetes/issues/93297
klog.V(2).InfoS("Left as-is", "sysctl", sysctlConnReuse)
} else {
// Set the connection reuse mode
if err := utilproxy.EnsureSysctl(sysctl, sysctlConnReuse, 0); err != nil {
return nil, err
}
}
```
而因为我们提供给客户的内核版本经过了魔改,虽然其已经包含了这个patch,但是版本小于`5.9`,所以需要在kube-proxy启动之后强制将`net.ipv4.vs.conn_reuse_mode`设为1。
如果使用systemd部署kube-proxy,可以用`ExecStartPost`来完成内核参数的修改:
```text
[Service]
...
ExecStartPost=/sbin/sysctl -w net.ipv4.vs.conn_reuse_mode=1
```
如果用Pod部署kube-proxy,可以使用PostStart完成:
```yaml
lifecycle:
postStart:
exec:
command:
- /sbin/sysctl
- -w
- net.ipv4.vs.conn_reuse_mode=1
```
https://mp.weixin.qq.com/s/H5zrelsEVtAmy96ObhuokA