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

简介: 《云原生网络数据面可观测性最佳实践》——五、 典型问题华山论剑——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

相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。 &nbsp; &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
相关文章
|
4月前
|
存储 缓存 分布式计算
StarRocks x Iceberg:云原生湖仓分析技术揭秘与最佳实践
本文将深入探讨基于 StarRocks 和 Iceberg 构建的云原生湖仓分析技术,详细解析两者结合如何实现高效的查询性能优化。内容涵盖 StarRocks Lakehouse 架构、与 Iceberg 的性能协同、最佳实践应用以及未来的发展规划,为您提供全面的技术解读。 作者:杨关锁,北京镜舟科技研发工程师
StarRocks x Iceberg:云原生湖仓分析技术揭秘与最佳实践
|
12月前
|
人工智能 安全 Cloud Native
阿里云云原生安全能力全线升级,护航百万客户云上安全
【重磅发布】9月20日,在杭州云栖大会上,阿里云宣布云原生安全能力全线升级,首次发布云原生网络检测与响应产品NDR(Network Detection Response,简称NDR)。同时,阿里云还宣布将持续增加免费的安全防护能力,帮助中小企业客户以极低投入完成基础的云上安全风险治理。
318 15
|
6月前
|
负载均衡 容灾 Cloud Native
云原生应用网关进阶:阿里云网络ALB Ingress 全面增强
云原生应用网关进阶:阿里云网络ALB Ingress 全面增强
145 6
|
6月前
|
运维 容灾 API
云栖大会 | 阿里云网络持续演进之路:简单易用的智能云网络,让客户专注业务创新
云栖大会 | 阿里云网络持续演进之路:简单易用的智能云网络,让客户专注业务创新
234 2
|
6月前
|
人工智能 运维 API
第七届 SD-WAN&SASE大会暨云网络大会 | 简单易用的智能云网络,让客户专注业务创新
第七届 SD-WAN&SASE大会暨云网络大会 | 简单易用的智能云网络,让客户专注业务创新
|
11月前
|
人工智能 Cloud Native 安全
从云原生到 AI 原生,网关的发展趋势和最佳实践
本文整理自阿里云智能集团资深技术专家,云原生产品线中间件负责人谢吉宝(唐三)在云栖大会的精彩分享。讲师深入浅出的分享了软件架构演进过程中,网关所扮演的各类角色,AI 应用的流量新特征对软件架构和网关所提出的新诉求,以及基于阿里自身实践所带来的开源贡献和商业能力。
732 102
|
8月前
|
负载均衡 容灾 Cloud Native
云原生应用网关进阶:阿里云网络ALB Ingress 全能增强
在过去半年,ALB Ingress Controller推出了多项高级特性,包括支持AScript自定义脚本、慢启动、连接优雅中断等功能,增强了产品的灵活性和用户体验。此外,还推出了ingress2Albconfig工具,方便用户从Nginx Ingress迁移到ALB Ingress,以及通过Webhook服务实现更智能的配置校验,减少错误配置带来的影响。在容灾部署方面,支持了多集群网关,提高了系统的高可用性和容灾能力。这些改进旨在为用户提供更强大、更安全的云原生网关解决方案。
1120 25
|
12月前
|
Cloud Native 关系型数据库 Serverless
基于阿里云函数计算(FC)x 云原生 API 网关构建生产级别 LLM Chat 应用方案最佳实践
本文带大家了解一下如何使用阿里云Serverless计算产品函数计算构建生产级别的LLM Chat应用。该最佳实践会指导大家基于开源WebChat组件LobeChat和阿里云函数计算(FC)构建企业生产级别LLM Chat应用。实现同一个WebChat中既可以支持自定义的Agent,也支持基于Ollama部署的开源模型场景。
1544 111
|
9月前
|
Kubernetes 网络协议 应用服务中间件
Kubernetes Ingress:灵活的集群外部网络访问的利器
《Kubernetes Ingress:集群外部访问的利器-打造灵活的集群网络》介绍了如何通过Ingress实现Kubernetes集群的外部访问。前提条件是已拥有Kubernetes集群并安装了kubectl工具。文章详细讲解了Ingress的基本组成(Ingress Controller和资源对象),选择合适的版本,以及具体的安装步骤,如下载配置文件、部署Nginx Ingress Controller等。此外,还提供了常见问题的解决方案,例如镜像下载失败的应对措施。最后,通过部署示例应用展示了Ingress的实际使用方法。
274 2
|
10月前
|
Kubernetes Cloud Native Ubuntu
庆祝 .NET 9 正式版发布与 Dapr 从 CNCF 毕业:构建高效云原生应用的最佳实践
2024年11月13日,.NET 9 正式版发布,Dapr 从 CNCF 毕业,标志着云原生技术的成熟。本文介绍如何使用 .NET 9 Aspire、Dapr 1.14.4、Kubernetes 1.31.0/Containerd 1.7.14、Ubuntu Server 24.04 LTS 和 Podman 5.3.0-rc3 构建高效、可靠的云原生应用。涵盖环境准备、应用开发、Dapr 集成、容器化和 Kubernetes 部署等内容。
522 8

热门文章

最新文章