【问题大排查】通用Linux环境的网络排障(部分包含容器环境)


因为最近一段时间,一直在处理各种网路问题,所以痛定思痛从头梳理一般运维环境下的网络状况

网络不通(持续性)

这个情况没啥好说的,只要不是 DNS 问题,就是服务器挂掉或者端口被禁

端口监听挂掉

如果容器内的端口已经没有进程监听了,内核就会返回 Reset 包,客户端就会报错连接被拒绝,可以进容器 netns 检查下端口是否存活:

1
netstat -tunlp

iptables 规则问题

检查报文是否有命中丢弃报文的 iptables 规则:

1
2
3
4
5
iptables -t filter -nvL
iptables -t nat -nvL
iptables -t raw -nvL
iptables -t mangle -nvL
iptables-save

网络偶尔丢包

网络丢包一般现象就是偶尔不通或者速度很慢.

高并发 NAT 导致 conntrack 插入冲突

如果高并发并且做了 NAT,比如使用了 ip-masq-agent,对集群外的网段或公网进行 SNAT,又或者集群内访问 Service 被做了 DNAT, 再加上高并发的话,内核就会高并发进行 NAT 和 conntrack 插入,当并发 NAT 后五元组冲突,最终插入的时候只有先插入的那个成功,另外冲突的就会插入失败,然后就丢包了。

可以通过 conntrack -S 确认,如果 insert_failed 计数在增加,说明有 conntrack 插入冲突。

看内核日志:

1
2
3
# demsg
$ journalctl -k | grep "nf_conntrack: table full"
nf_conntrack: nf_conntrack: table full, dropping packet

若有以上报错,证明 conntrack 表满了,需要调大 conntrack 表:

1
sysctl -w net.netfilter.nf_conntrack_max=1000000

socket buffer 满导致丢包 netstat -s | grep "buffer errors"的计数统计在增加,说明流量较大,socket buffer 不够用,需要调大下 buffer 容量:

1
2
3
4
5
6
net.ipv4.tcp_wmem = 4096        16384   4194304
net.ipv4.tcp_rmem = 4096        87380   6291456
net.ipv4.tcp_mem = 381462       508616  762924
net.core.rmem_default = 8388608
net.core.rmem_max = 26214400
net.core.wmem_max = 26214400

arp 表爆满

看内核日志:

1
2
3
# demsg
$ journalctl -k | grep "neighbor table overflow"
arp_cache: neighbor table overflow!

若有以上报错,证明 arp 表满了,查看当前 arp 记录数:

1
2
$ arp -an | wc -l
1335

查看 arp gc 阀值:

1
2
3
4
5
6
7
$ sysctl -a | grep gc_thresh
## 系统在 ARP 表中的条目数量低于该值时,垃圾回收不被触发,表示最小保持的 ARP 条目数
net.ipv4.neigh.default.gc_thresh1 = 128
## ARP 表中的条目数超过该值时,系统会尝试回收一些条目,这是回收的软阈值
net.ipv4.neigh.default.gc_thresh2 = 512
## ARP 表的最大容量,超过此值后,新的 ARP 条目将无法被添加
net.ipv4.neigh.default.gc_thresh3 = 1024

调大 arp 表:

1
2
3
sysctl -w net.ipv4.neigh.default.gc_thresh1=80000
sysctl -w net.ipv4.neigh.default.gc_thresh2=90000
sysctl -w net.ipv4.neigh.default.gc_thresh3=100000

这个情况极大可能出现在kubernetes集群里的某些服务,例如对中大型集群(1000个节点)内做巡检的Pod,如果有类似情况建议在个别节点上调大arp

然后给对应的Node打上标签 kubectl label node host1 arp_cache=large

然后用 nodeSelector 或 nodeAffnity 让这部分需要内核有大 arp_cache 容量的 Pod 只调度到这部分节点,推荐使用 nodeAffnity,yaml 示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  template:
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: arp_cache
                operator: In
                values:
                - large

MTU 不一致导致丢包

如果容器内网卡 MTU 比另一端宿主机内的网卡 MTU 不一致(通常是 CNI 插件问题),数据包就可能被截断导致一些数据丢失:

  1. 如果容器内的 MTU 更大,发出去的包如果超过 MTU 可能就被丢弃了(通常节点内核不会像交换机那样严谨会分片发送)。
  2. 同样的,如果容器内的 MTU 更小,进来的包如果超过 MTU 可能就被丢弃。

tcp 协商 mss 的时候,主要看的是进程通信两端网卡的 MTU。

MTU 大小可以通过ip address showifconfig来确认。

QoS 限流丢包

在云厂商的云主机环境,有可能会在底层会对某些包进行 QoS 限流,比如为了防止公共 DNS 被 DDoS 攻击, 限制 UDP 53 端口的包的流量,超过特定速度阈值就丢包,导致部分 DNS 请求丢包而超时。

PPS 限速对包

网卡的速度始终是有上限的,在云环境下,不同机型不同规格的云主机的 PPS 上限也不一样,超过阈值后就不保证能正常转发,可能就丢包了。

连接队列满导致丢包

对于 TCP 连接,三次握手建立连接,没建连成功前存储在半连接队列,建连成功但还没被应用层 accept 之前,存储在全连接队列。队列大小是有上限的,如果慢了就会丢包:

查看丢包统计:

1
2
3
4
5
netstat -s | grep -E 'drop|overflow'

$ cat /proc/net/netstat | awk '/TcpExt/ { print $21,$22 }'
ListenOverlows ListenDrops
20168 20168

不同内核版本的列号可能有差别

如果有现场,还可以观察全连接队列阻塞情况 (Rec-Q):

1
ss -lnt

通过以下内核参数可以调整队列大小 (namespace隔离):

1
2
net.ipv4.tcp_max_syn_backlog = 8096 # 调整半连接队列上限
net.core.somaxconn = 32768 # 调整全连接队列上限

需要注意的是,somaxconn只是调整了队列最大的上限,但实际队列大小是应用在listen时传入的backlog大小, 大多编程语言默认会自动读取somaxconn的值作为listen系统调用的backlog参数的大小。

如果是用nginx,backlog的值需要在nginx.conf配置中显示指定,否则会用它自己的默认值511

源端口耗尽

当作为 client 发请求,或外部流量从NodePort进来时进行SNAT,会从当前 netns 中选择一个端口作为源端口, 端口范围由net.ipv4.ip_local_port_range这个内核参数决定,如果并发量大,就可能导致源端口耗尽,从而丢包。

tcp_tw_recycle 导致丢包

在低版本内核中(比如 3.10),支持使用 tcp_tw_recycle 内核参数来开启 TIME_WAIT 的快速回收, 但如果 client 也开启了 timestamp (一般默认开启),同时也就会导致在 NAT 环境丢包, 甚至没有 NAT 时,稍微高并发一点,也会导致 PAWS 校验失败,导致丢包:

1
2
3
4
5
6
# 看 SYN 丢包是否全都是 PAWS 校验失败
$ cat /proc/net/netstat | grep TcpE| awk '{print $15, $22}'
PAWSPassive ListenDrops
96305 96305
## PAWSPassive PAWS 是为防止序列号回绕而使用的保护机制,如果包的时间戳有问题,PAWS 可能会导致包被丢弃
## ListenDrops 监听队列中的连接因队列已满而被丢弃的次数

TCP协议的PAWS机制可自行百度