k8s常见问题总结

k8s常见问题总结

  • 网络丢包

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    1、iptables 规则丢包

    2、listen了源port_range范围内的端口
    比如 net.ipv4.ip_local_port_range="1024 65535",但又listen了 9100 端口,当作为client发请求时,选择一个port_range范围内的端口作为源端口,就可能选到9100,但这个端口已经被listen了,就会选取失败,导致丢包。

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

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

    5、MTU 不一致导致丢包
    如果容器内网卡 MTU 比另一端宿主机内的网卡 MTU 不一致(通常是 CNI 插件问题),数据包就可能被截断导致一些数据丢失
    如果容器内的MTU更大,发出去的包如果超过MTU可能就被丢弃了(通常节点内核不会像交换机那样严谨会分片发送)。
    同样的,如果容器内的MTU更小,进来的包如果超过MTU可能就被丢弃。
    MTU 大小可以通过 ip address show 或 ifconfig 来确认。

    6、连接队列满导致丢包
    对于TCP连接,三次握手建立连接,没建立成功前存储在半连接队列,建立成功但还没被应用层accept之前,存储在全连接队列。队列大小是有上限的,如果满了就会丢包
    如果并发太高或机器负载过高,半连接队列可能会满,新来的SYN建连包会被丢包
    如果应用层accept连接过慢,会导致全连接队列堆积,满了就会丢包,通常是并发高、机器负载高或应用夯死等原因
    可通过netstat -s | grep -E 'drop|overflow'确认
    全连接队列可通过ss -lnt 观察Rec-Q
    net.ipv4.tcp_max_syn_backlog = 8096 #半连接队列长度
    net.core.somaxconn = 32768 #全连接队列长度

    7、源端口耗尽
    当client发起请求或外部流量通过NodePort进来时会进行SNAT,会从当前netns中选择一个端口作为源端口,端口范围由net.ipv4.ip_local_port_range参数决定,如果并发大就可能导致端口耗尽从而丢包
  • ipvs连接复用引起系列问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    当client对service发起大量新建TCP连接时,新的连接被转发到Terminating已销毁的Pod上,导致持续丢包(报错no route to host),其根本原因是内核ipvs连接复用引发

    当client_ip:client_port复用发生时,对于TIME_WAIT状态下的ip_vs_conn进行重新调度使得connection在rs上分布更均衡以提高性能
    如果conn_reuse_mode=0则会复用旧ip_vs_conn里的rs,使得连接更不均衡

    如果conn_reuse_mode为1表示不复用,每次新建的连接都会重新调度rs并新建ip_vs_conn,但在新建连接时(SYN包),如果client_ip:client_port匹配到了ipvs旧连接(TIMEWAIT状态)且使用了conntrack,就会丢掉第一SYN包,等待重传后(1s)才能连接成功,从而导致连接性能急剧下降。

    如果conn_reuse_mode=0表示复用
    只要client_ip:client_port匹配上ip_vs_conn(发生复用),就会直接发给对应rs,不管rs当前是什么状态,即使rs的weight为0(通常是TIME_WAIT状态也会转发,而TIME_WAIT的rs通常是Terminating状态已销毁的Pod,故转发连接出现异常。
    高并发下大量复用,没有为新连接重新调度rs,直接转发到复用连接对应rs上,从而导致新连接被固化到部分rs上。

    实际业务场景:
    1、滚动更新连接异常,被访问服务滚动更新时,Pod有新建也有销毁,ipvs发生连接复用时转发到已销毁的Pod导致连接异常(no route to host)
    2、滚动更新负载不均,由于复用时不会重新调度连接,导致新连接被固化到部分Pod上。
    3、新扩容的Pod接收流量少,同样也是由于复用时不会重新调度连接,导致新连接被固化到扩容之前这些Pod上

    如何规避:
    集群外调用
    1、使用SLB直通Pod,通常通过NodePort暴露端口,前面SLB将流量转发到NodePort上,在通过ipvs转发到后端Pod上,云厂商都支持SLB直通Pod,直接将请求转发到Pod,不经过NodePort,从而没有ipvs转发,从而在流量接入层规避次问题。
    2、使用ingress转发,在集群内部署ingress controller(如nginx ingress),流量到达ingress再向后转发时(直接转发到集群内的Pod),不会经过service转发而且直接转发到service对应的Pod IP:Port,从而没有ipvs转发,ingress controller本身结合方案一SLB直通Pod方式部署。
    集群内调用
    集群内的服务间调用,默认会走ipvs转发,对于高并发场景的业务,可考虑Service Mesh(如istio)来治理流量,服务间转发由sidecar代理,从而没有ipvs转发。

    终结方案:内核修复
  • 优雅重启

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    容器终止流程:
    1、Pod被删除,状态标记为Terminating
    2、kube-proxy更新转发规则,将Pod从service的endpoint列表中删除,新的流量不再转发到次Pod
    3、如果Pod配置了preStop钩子,将执行此钩子
    4、kubelet对Pod中各个container发送SIGTERM信号以通知容器进程开始优雅停止
    5、等待容器进程完全停止,如果在TerminationGracePeriodSeconds内(默认30s)还没完全停止,就发送SIGKILL信号强制杀死进程
    6、所有容器进程终止,清理Pod资源

    业务代码处理SIGTERM信号,主要逻辑不接受新增连接,继续处理留存连接,所有连接处理完后才退出

    注意:如果容器启动入口使用脚本,容器主进程是shell,业务进程是通过shell启动,成了shell进程的子进程;
    shell进程默认不会处理SIGTERM信号,也不会将信号传递给子进程。

    解决方案:
    1、shell使用exec启动,exec让该程序进程替代当前shell进程,即让新启动进程成为主进程
    #!/bin/bash
    exec /opt/cmdb
    2、多进程场景,使用trap传递信号
    #!/bin/bash
    /opt/app1 & pid1="$!"
    /opt/app1 & pid2="$!"

    handle_sigterm() {
    kill -SIGTERM $pid1 $pid2
    wait $pid1 $pid2
    }
    trap handle_sigterm SIGTERM

    wait

    最优方案:使用init系统
    install -y dumb-init
    ENTRYPOINT ["dumb-init", "--"]
    CMD ["/start.sh"]

    使用preStop钩子
    某些情况下,Pod被删除的一小段时间内,仍然可能有新连接转发过来,因为kubelet与kube-proxy同时watch到Pod被删除,kubelet有可能在kube-proxy同步完规则前已经停止容器,这就导致一些新的连接被转到正在删除的Pod,而通常情况下,当程序收到SIGTERM信号后不再接收新连接,只保持存量连接继续处理,故导致Pod删除瞬间部分请求失败。此类情况,可利用preStop钩子先sleep会儿,等待kube-proxy同步完规则再开始停止容器内进程
    lifecycle:
    preStop:
    exec:
    command:
    - sleep
    - 5

    调整优雅时间
    如果需要优雅终止时间较长(preStop+业务进程停止超过默认30s),可自定义terminationGracePeriodSeconds,避免过早被SIGKILL杀死