《云原生网络数据面可观测性最佳实践》——五、 典型问题华山论剑——1 某客户nginx ingress偶发性出现4xx or 5xx(上)

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: 《云原生网络数据面可观测性最佳实践》——五、 典型问题华山论剑——1 某客户nginx ingress偶发性出现4xx or 5xx(上)

问题背景

客户反馈在某时间段连续分钟级别出现5xx


排查过程

查看ingressPod 日志其中status :499, request_time: 5.000,  upstream_response_time:4.999根据这几个点说明问题发生在了ingress 转发到后端pod这一段客户端未在5s内收到body,主动断开了连接

此时怀疑两个方面:后端应用负载太高,发生了限流或应用本身队列等溢出了查看了cgroup监控,pod并未触发限流,所以需要从其他方面入手

可以依照 4.3.2小节 《Nginx Ingress 499/502/503/504相关问题场景配置相关net-exporter采集指标

image.png

image.png

image.png

问题时间点,passiveopens有一个补偿态变化,先降低后上升,但是变化区间不大,可以认为下游外部访问流量没有特别大变化

问题时间点,activeopens有明显突增,tcptimeouts和synretrans都有明显增长,结合sock分配数变化,此时应该有ingress侧大量建立连接重试动作

 

综上推测此时upstream与ingress之间存在一些问题,导致ingress访问upstream出现连接建立失败并批量重试的功能。

image.png

 查看ingress 后端SVC的pod的net-exporter指标listendrops和listenoverflow均大于0,并且幅度增长较大,可以明确的是存在连接队列溢出的问题。

 

根因原理

TCP协议作为一个有状态的协议,对于服务端来说,提供应用层完成一次标准的报文交互需要经历几个过程,在这期间,需要依赖不同类型的sock数据结构完成不同的任务,按照顺序依次如下:



创建一个socket,并且调用bind()和listen()系统调用开始监听这个端口上服务,此时创建了一个inet_connection_sock,inet_connection_sock保存着建立连接所需要一些信息以及连接队列

当有合法syn报文被提交给inet_connection_sock进行处理时,就进入了TCP三次握手状态机:

    服务端第一次收到syn报文后,会创建一个request_sock(真实分配了sock内存),存放少量基础信息,这个request_sock会被放入inet_connection_sockicsk_accept_queue队列中

   服务端第收到第三次握手合法报文时,会查找icsk_accept_queue中request_sock,如果有合法request_sock,则会完成一系列操作,分配一个代表已经处于TCP_ESTABLISHEDsock



看到上面的分析,大概就能知道,我们所说的“队列溢出”,指的是用于listen的socket的用于存放request_sock的队列和tcp_sock的队列产生了溢出。

 

看到上面的分析,大概就能知道,我们所说的“队列溢出”,指的是用于listen的socket的用于存放request_sock的队列和tcp_sock的队列产生了溢出。在不同版本的内核中,这两个”队列“起步并不总是以队列的形式存在,以我们alinux内核4.19版本的代码为例,内核在判断这两处的溢出时的核心代码逻辑如下:

 

request_sock的队列,request_sock是一个轻量级的用于记录一个连接回话信息的sock类型(mini sock to represent a connection request),对于一个inet_connection_sock来说,会专门分配一个成员队列来存放所有处于连接状态的request_sock,即icsk_accept_queue,也就是俗称的"半连接队列",所以判断这个队列是否会溢出,需要对icsk_accept_queue的长度进行判断:

static inline void inet_csk_reqsk_queue_added(struct sock *sk)
{
  reqsk_queue_added(&inet_csk(sk)->icsk_accept_queue);
}
static inline int inet_csk_reqsk_queue_len(const struct sock *sk)
{
  return reqsk_queue_len(&inet_csk(sk)->icsk_accept_queue);
}
static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
{
  return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}

尽管在不同内核版本中有实现上差异,但是对于request_sock队列,内核采用inet_csk_reqsk_queue_is_full进行判断,判断方式就是获取request_sock队列长度并且与sock中存放sk_max_ack_backlog进行比对那么什么时候会进行这个检查呢?

 

int tcp_conn_request(struct request_sock_ops *rsk_ops,
         const struct tcp_request_sock_ops *af_ops,
         struct sock *sk, struct sk_buff *skb)
{
  /* TW buckets are converted to open requests without
   * limitations, they conserve resources and peer is
   * evidently real one.
   */
  if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
       inet_csk_reqsk_queue_is_full(sk)) && !isn) {
    want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name);
    if (!want_cookie)
      goto drop;
  }
  if (sk_acceptq_is_full(sk)) {
    NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
    goto drop;
  }
  if (!want_cookie && !isn) {
    /* Kill the following clause, if you dislike this way. */
    if (!net->ipv4.sysctl_tcp_syncookies &&
        (net->ipv4.sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
         (net->ipv4.sysctl_max_syn_backlog >> 2)) &&
        !tcp_peer_is_proven(req, dst)) {
      pr_drop_req(req, ntohs(tcp_hdr(skb)->source),
            rsk_ops->family);
      goto drop_and_release;
    }
    isn = af_ops->init_seq(skb);
  }
drop_and_release:
  dst_release(dst);
drop_and_free:
  reqsk_free(req);
drop:
  tcp_listendrop(sk);
  return 0;

 内核在tcp_conn_request中进行了inet_csk_reqsk_queue_is_full检查,也就是当LISTEN状态sock收到了一个合法syn报文时所谓“半连接队列溢出”,也就是syn队列溢出,其实就是还处在握手阶段未就绪连接过多导致,这里逻辑在不同版本内核上有着比较明显变化,尤其是在容器时代到来,许多sysctl都成为了net namespace级别后

 

ESTABLISHED状态tcpsock队列溢出,通常特被称之为accept队列或者俗称“全连接队列”,在内核中,使用以下方法用于判断溢出现象是否发生:

static inline void sk_acceptq_removed(struct sock *sk)
{
  sk->sk_ack_backlog--;
}
static inline void sk_acceptq_added(struct sock *sk)
{
  sk->sk_ack_backlog++;
}
static inline bool sk_acceptq_is_full(const struct sock *sk)
{
  return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}

 可以看到,尽管也被称为“队列”,但是在内核的实际行为中,其实并没有真正存在一个队列,而是改为通过sk_ack_backlog计数来进行判断。那么内核在什么情况下会对已经完成连接建立的socket进行溢出的判断呢?在上面request_sock的溢出判断中,其实已经存在了一个,在第一次收到SYN报文是,如果这个LISTEN的地址已经有了足够多的就绪的连接,那么就会选择丢弃掉这个还未完全建立的连接,除此之外,还有一处也会进行判断:

/*
 * The three way handshake has completed - we got a valid synack -
 * now create the new socket.
 */
struct sock *tcp_v4_syn_recv_sock(const struct sock *sk, struct sk_buff *skb,
          struct request_sock *req,
          struct dst_entry *dst,
          struct request_sock *req_unhash,
          bool *own_req)
{
  if (sk_acceptq_is_full(sk))
    goto exit_overflow;
  newsk = tcp_create_openreq_child(sk, req, skb);
  if (!newsk)
    goto exit_nonewsk;
  *own_req = inet_ehash_nolisten(newsk, req_to_sk(req_unhash));
  if (likely(*own_req)) {
    tcp_move_syn(newtp, req);
    ireq->ireq_opt = NULL;
  } else {
    newinet->inet_opt = NULL;
  }
  return newsk;
exit_overflow:
  NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
exit_nonewsk:
  dst_release(dst);
exit:
  tcp_listendrop(sk);
  return NULL;
put_and_exit:
  newinet->inet_opt = NULL;
  inet_csk_prepare_forced_close(newsk);
  tcp_done(newsk);
  goto exit;

从代码的名称不难发现,tcp_v4_syn_recv_sock正是内核处理第三次握手的ACK报文的核心逻辑,这里通过sk_acceptq_is_full判断了是否当前已经就绪的连接数量已经超过了设定值,如果没有超过,则会通过核心的inet_ehash_nolisten增加一个新的ESTABLISHED状态的tcpsock,后续就不会由LISTEN状态的inet_connection_sock来处理报文了。



回答这一小节最初的问题,连接队列溢出的直接原因是什么呢?

 

连接队列溢出可以分request_sock队列溢出(syn-queue/半连接队列)和就绪连接(accept-queue/全连接队列)超出限制,其中前者指的是还在握手过程中,完成了第一次握手但是没有完成第三次握手的会话过多,后者则是单纯的单个LISTEN地址的就绪连接太多了。

 

在了解了原因之后,我们再看一下如何站在直接的原因的基础上去解决这个问题。从上面原因的分析来看,对于客户正常的业务遇到的问题,排除SYN-Flooding等攻击行为的干扰,解决问题的核心,其实是让单个LISTEN地址可以容纳更多的就绪连接,从上文中可以发现,真正核心的代码其实是如下一行:

sk->sk_ack_backlog > sk->sk_max_ack_backlog;


更多精彩内容,欢迎观看:

《云原生网络数据面可观测性最佳实践》——五、 典型问题华山论剑——1 某客户nginx ingress偶发性出现4xx or 5xx(下):https://developer.aliyun.com/article/1221272?groupCode=supportservice

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
1月前
|
Kubernetes 负载均衡 应用服务中间件
k8s学习--ingress详细解释与应用(nginx ingress controller))
k8s学习--ingress详细解释与应用(nginx ingress controller))
205 0
|
5月前
|
Kubernetes 测试技术 应用服务中间件
基于 Nginx Ingress + 云效 AppStack 实现灰度发布
本文将演示结合云效 AppStack,来看下如何在阿里云 ACK 集群上进行应用的 Ingress 灰度发布。
65106 25
|
6月前
|
机器学习/深度学习 监控 数据可视化
数据分享|电信行业客户流失预测:KNN、朴素贝叶斯、逻辑回归、LDA/QDA、随机森林、支持向量机、CART、神经网络
数据分享|电信行业客户流失预测:KNN、朴素贝叶斯、逻辑回归、LDA/QDA、随机森林、支持向量机、CART、神经网络
|
6月前
|
算法 数据可视化 搜索推荐
数据分享|Python用Apriori算法关联规则分析亚马逊购买书籍关联推荐客户和网络图可视化
数据分享|Python用Apriori算法关联规则分析亚马逊购买书籍关联推荐客户和网络图可视化
|
6月前
|
机器学习/深度学习 监控 数据可视化
R语言SOM神经网络聚类、多层感知机MLP、PCA主成分分析可视化银行客户信用数据实例2
R语言SOM神经网络聚类、多层感知机MLP、PCA主成分分析可视化银行客户信用数据实例
|
6月前
|
机器学习/深度学习 数据可视化 算法
使用自组织映射神经网络(SOM)进行客户细分
使用自组织映射神经网络(SOM)进行客户细分
|
6月前
|
机器学习/深度学习 数据可视化 算法
R语言SOM神经网络聚类、多层感知机MLP、PCA主成分分析可视化银行客户信用数据实例1
R语言SOM神经网络聚类、多层感知机MLP、PCA主成分分析可视化银行客户信用数据实例
|
6月前
|
机器学习/深度学习 PyTorch 算法框架/工具
Python中用PyTorch机器学习神经网络分类预测银行客户流失模型
Python中用PyTorch机器学习神经网络分类预测银行客户流失模型
|
6月前
|
机器学习/深度学习 数据可视化 算法
R语言使用自组织映射神经网络(SOM)进行客户细分
R语言使用自组织映射神经网络(SOM)进行客户细分
|
6月前
|
Kubernetes 负载均衡 应用服务中间件
Ingress Nginx 安装【亲测可用】
Ingress Nginx 安装【亲测可用】
481 2
下一篇
无影云桌面