Kubernetes web 网站无法访问

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Kubernetes web 网站无法访问


网络异常,图片无法展示
|

开篇点题, 这其实是一次深入探索问题本质的一次排查故事,之所以想写这个,是因为这个问题的现象和最后分析出来的原因看起来有点千差万别。因为感觉排查过程可以抽象成一个通用的排查思维逻辑, 所以各位看完后可以这个抽象是否做成功了


起(问题发生)


故事的起因和大多数排查故事一样, 并没有什么特别的.就是普通的一天早上,正带着愉快心情上班时,突然被拉了一个会议,然后老板在会议中特别着急的表达了问题以及严重性,于是我也特别着急的开始了排查。

问题也是很普通,外部大客户发现一个容器里的应用无法响应请求了,特别着急的找到了我们这边。

从会议中听到的内容总结了一下, 大致是容器里的一个 server 进程没法响应 http 请求,我包括其他同学理所当然的以为容器网络可能出问题了,然后我登录到宿主机上, 按套路查看容器网络联通性,路由等,发现网络正常没有任何问题,折腾完了之后完全一脸懵,不知道到底是啥情况


承(开始排查,居然不是网络问题)


按正常套路排查没有任何结果后,我又咨询了上层应用同学关于服务的信息, 希望从用户的部署服务类型看出一点信息,上层应用同学从 k8s 集群中查看了 pod 的信息,发现是一个普通的 java 应用,参数也没有奇怪的地方.

这里简直没有头绪,我又问了一下问题出现前线上有没有做变更,结果果然有做,昨天晚上刚更新了容器引擎的版本,也就是说容器引擎被重启了.正是因为我对容器太了解了,理所当然的觉得容器引擎重启不会对已运行的容器有任何影响,所以暂时对这个线索不是很上心.

到了这,线上的排查基本结束,线上的问题只能先重新创建 pod 来解决.当时对 k8s 还不熟,我们请上层同学先尝试复现问题,然后再进行线下的排查.也多亏了一位同学线下复现出了问题,排查才又有了进展

复现时用 kubectl 查看 pod 日志时偶然发现当应用请求卡住的时候,容器的标准输出也断了.结合线上用户的 bug 案例,作出了一个简单的分析,应该是应用进程在响应用户请求时需要打印一些内容,当这个步骤卡住时,就无法继续响应请求,表面上看就是用户的请求卡住了.排查进入到这里,距离发现最终 bug 的根因就比较近了


转(定位根因)


问题的触发的条件

1.   进程要向容器标准输出打印日志

2.   容器引擎重启

问题触发是因为容器引擎重启触发的,重启后发现容器的标准输出就断了,容器里的进程也无法响应请求了,并且通过 debug 发现容器收到了 SIGPIPE 的信号.再结合容器是如何转发 stdio 到容器引擎的原理,基本上定位了原因,原来是容器用来转发 stdio 的 fifo(linux 命名管道)断了.去看了线上 shim 打开的 fifo fd 已经被关闭也确认了这点

原因定位了,但是代码 bug 还没有找出来,虽然我当时对容器非常熟,但是我对 fifo 的工作原理可非常不熟,如果当时我对 fifo 的原理了解的话,可能下午就定位出了问题,不至于用了一天的时间(这个问题后续又发生了一次,也是 fifo 的问题,但是是另外一个 bug,第二次等我自己线下复现之后,下午就定位出来了).

下面先介绍一下和问题相关的 fifo 部分的工作原理

不打开 fifo 读端或多次重新打开读端, 只写方式打开 fifo 写端, 若写入 fifo 里的数据超过缓冲区,fifo 写端报 EPIPE(Broken pipe)错误退出, 发出 SIGPIPE 的信号.如果读写方式打开 fifo 写端,就不会有这个问题

对比了问题代码,打开 fifo 的方式正是 O_WRONLY 的方式,之前没有出问题居然是因为从来没有更新过容器引擎,昨天晚上第一次更新直接触发了这个问题.困扰大家一天的问题竟然只需要改一个单词,把 O_WRONLY -> O_RDWR 就可以了

也可以用下面这段简单的代码来自行验证一下


网络异常,图片无法展示
|

 

好了,问题分析完了,下面我要开始写容器引擎接管 stdio 的原理了,对容器部分原理没有兴趣的同学可以直接跳到"合"的章节了


原理解析(容器创建原理及接管 stdio)


稍微提一下,出问题的不是 runc 容器,是 kata 安全容器, runc 容器毕竟用的多 bug 也比较少了

 

以下原理解析我都以 pouch(https://github.com/alibaba/pouch)+ containerd(https://github.com/containerd/containerd)+ runc(https://github.com/opencontainers/runc.git)的方式来做分析

从低向上容器 1 号进程 IO 的流转

进程的 stdio 指向 pipe 一端 -> shim 进程打开的 pipe 另一端 ->shim 进程打开的 fifo 写端 -> pouch 打开的 fifo 写端 -> pouch 指定的 IO 输出地址,默认是 json 文件

容器 IO 的创建和是否需要 terminal,是否有 stdin 有关系,为了简单起见,我们下面的流程介绍都是后台运行一个容器为例来讲解,即只会创建容器的 stdout 和 stderr,用 pouch 命令来表述,就是执行下面的命令后,如何从 pouch logs 看到进程的日志输出


pouch run -d nigix


pouch 创建容器与初始化 IO


简单介绍一下 pouch 创建容器的流程,pouch 和 dokcer 一样,也是基于 containerd 去管理容器的,即 pouch 启动会拉起一个 containerd 进程,pouch 发起各种容器相关的请求时,通过 grpc 和 containerd 通信,containerd 收到请求后,调用对应的 runtime 接口操作容器,这里的 runtime 可以有很多类型,大家最常用的就是 runc 了,当然也可以是上方案例中的 kata 安全容器,你也可以按照 oci 标准自己实现一个自己 runtime,这是题外话了.

pouch 调用 containerd 的 NewTask 接口发起一个创建容器命令,这个函数的第二个参数是初始化 IO 的函数指针,看一下代码,https://github.com/alibaba/pouch/blob/master/ctrd/container.go#L677-L685


网络异常,图片无法展示
|


初始化 IO,也就是创建 fifo 并打开 fifo 读端的函数在 NewTask 执行的第一行就会被调用


网络异常,图片无法展示
|


pouch 也是调用 containerd 中的 cio 包去打开 fifo 的,pouch 指定了 fifo 路径,最终调用 containerd 中的 fifo 包去创建并打开 fifo, 用的图中的 fifo.OpenFifo 函数


网络异常,图片无法展示
|

 

看一下图中的代码,cio 包里代码是只读阻塞的模式(虽然 flags 传了 NONBLOCK,但是在 fifo 包里会被去掉)打开 stdout 和 stderr2 个 fifo 的,pouch 打开 2 个 fifo 后,会开始拷贝 2 个容器 IO 流,io.Copy 的读端是 fifo 的输入,写端是可以自定义的,写端可以是 json 文件,syslog 或其他.换个说法,这里的写端就是容器引擎配置的 log-driver

这里打开 fifo 的部分要注意一下,containerd fifo 包封装了整个流程,和直接调用是不一样的,最直接看出不同的地方就是打开文件的个数,重新放一张上面案例中发过的示例图,代码里对 stdout 和 stderr2 个 fifo 文件只打开了一次,但是这个 fd 显示文件被打开了 2 次,这是因为 fifo 包里对 fifo 的处理加了一层,打开了 2 次,第一次打开的是 fifo 文件,即下面的路径,第二次按参数指定的 flag 打开了第一次打开的 fd 文件, 即/proc/self/fd/22.



之所以打开 2 次是为了 fifo 文件在物理上被删除后,内存中打开的 fd 也可以被关闭

 

containerd 创建容器与初始化 IO

 

还是先介绍一下 containerd 创建容器的大致原理,其实这里还有一个 shim 进程,准确来说,shim 是实际管理容器进程,也就是说 shim 是容器 1 号进程的父进程,containerd 和 shim 之间通过 ttrpc 交互(ttrpc 是 containerd 社区实现的低内存占用的 grpc 版本),containerd 收到创建容器请求时,会创建一个 shim 进程,然后通过 ttrpc 发送后续的相关请求.

shim 创建容器的同时会初始化容器 IO,相关代码可以看一下这几个文件,https://github.com/containerd/containerd/tree/master/pkg/process

shim 先创建 os.Pipe,因为这个容器只需要 stdout 和 stderr,所以这里只会创建 stdout 和 stderr 的 2 个 pipe,作用是其中一端用来作为容器 1 号进程的输入和输出,另一端输出到 pouch 创建的 fifo 里, 这样 pouch 就读到了容器进程的标准输出

看一下下面这张图,cmd 封装了 shim 调用 runccreate, cmd 的 stdio 就是容器进程的 stdio, 这里的原因在第 3 步 runc 创建容器里细讲


网络异常,图片无法展示
|


调用 runc create 返回后,shim 开始拷贝容器 IO 到 pouch 创建的 fifo 里,代码在这里,https://github.com/containerd/containerd/blob/master/pkg/process/io.go#L135-L232

下面这张图是拷贝 stdout 的 IO 流的逻辑, 拷贝 stderr 也类似,rio.Stdout() 是上面 shim 创建的 pipe 的另外一端


网络异常,图片无法展示
|


看一下 rio.Stdout() 函数就知道了


网络异常,图片无法展示
|


i.out.w 作为 cmd 的 STDOUT,就是说容器进程输出到 i.out.w,pipe 的另一端 i.out.r 读到数据,再把数据拷贝到 fifo 里,wc 是只写方式打开的 pouch fifo 文件,结合第一步里的过程, pouch 读方式打开的 fifo 读到这里的输入数据,就拿到了容器进程输出


网络异常,图片无法展示
|


看一下 shim 进程打开的 fd,发现 stdout 和 stderr fifo 都打开了 2 次,这是因为不打开 fifo 读端或多次重新打开读端, 只写方式打开 fifo 写端, 若写入 fifo 里的数据超过缓冲区,fifo 写端报 EPIPE (Broken pipe)错误退出,所以这里分别用读写方式打开了 2 次 fifo


runc 创建容器与初始化 IO

这里是最后一个创建容器的步骤, containerd 调用实际的容器运行时创建容器,我以大家最常用的 runc 来做介绍

这里插一句,案例里的 kata 安全容器也是一种 OCI 标准的运行时,简单来说安全容器就是有自己的内核,不和宿主机共享内核,这样才是安全可靠的.kata 是基于 qemu 来做, 可以理解他有 2 层,第一层在宿主机上,和 qemu 以及 qemu 里的进程交互,第二层在 qemu 里,接收第一层发来的请求,实际完成的代码就是封装了 runc 的 libcontainer.所以 kata 的 stdio 相比于 runc 多转发了一次

同样我先简单概括一下 runc 创建容器的流程,shim 创建容器需要调用 2 次 runc,第一次是 runccreate,这个命令完成后,容器的用户进程还没有被拉起,runc 启动了一个 init 进程,这个 init 进程把容器启动的所有准备都做完, 包括切换 ns,cgroup 隔离,挂载镜像 rootfs, volume 等,runc init 进程最后会向一个 fifo(和 pouch fifo 没有关系, runc 自己用的一个 fifo 文件)写 0,在 0 被读取出来之前 runc init 会一直 hang 着


网络异常,图片无法展示
|


shim 的第二次调用是 runcstart,runc start 做的工作很简单,从 fifo 中读出数据,这时 hang 住的 runc init 会往下执行,调用 execve 加载用户进程, 这时容器的用户进程才开始运行

在介绍 runc 创建容器 IO 之前,我们先看一下容器进程的 stdio 的 fd 指向吧,因为没有标准输入,所以进程 0 号 fd 是指向/dev/null 的,1 号和 2 号 fd 分别指向了一个 pipe,这个 pipe 就是第二步里 shim 创建的 pipe


网络异常,图片无法展示
|


可以打开 shim 进程的 proc 文件确认一下, 13 和 15 号 fd 打开的 fd 号是和容器进程打开的 2 个 pipe 是一样的, 说明 2 个进程打开的是同样的 pipe


上面说到 runc 启动的第一个进程是 runc init, 启动进程的流程同样也是封装了一个 cmd 命令,cmd 的 stdio 是指向 process 的 stdio


网络异常,图片无法展示
|


这个 process 在 runc 中代表了 init 或 exec 的进程,当容器不需要 tty 时,runc 把 process 的 stdio 设置为继承自身的 stdio,图中的 os.Stdin/os.Stdout/os.Stderr 指的是本进程的 0,1,2 fd


网络异常,图片无法展示
|


所以当真正的容器进程启动的时候自然也继承了 runc init 的 stdio


合(后记)


看起来是个网络问题,最后发现是一个 fifo 的问题,但是循序渐进的分析下来,感觉一切都是合情合理的

 

类似排查网络问题的套路一样,问题排查一样也有套路(抽象方法)可循,也看过这方面的总结,但还是写下自己的理解,当套路被压缩到极致之后,就变成了高大上的逻辑思维方式

 

1.   详细分析问题出现的现象,问题进程的大致工作流程,问题触发的条件

2.   不要凭经验判断哪些组件不会出问题,详细分析组件日志和代码,尤其不要对任何代码有敬畏之心(不敬畏,但是尊重所有代码),尤其不要认为内核,系统库都是基本稳定的

3.   问题链路上涉及的原理最好都去学习熟悉

希望上述排查思路可以给其他同学定位问题时带来灵感


相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
1月前
|
Web App开发 前端开发 JavaScript
Web开发者必收藏的10个实用网站,你还没收藏吗?
将这些网站收藏起来,定期访问,使它们成为您日常工作的一部分,助您在快速发展的 Web 开发领域保持领先。
107 2
Web开发者必收藏的10个实用网站,你还没收藏吗?
|
24天前
|
人工智能 搜索推荐 PHP
PHP在Web开发中的璀璨星辰:构建动态网站的幕后英雄###
【10月更文挑战第25天】 本文将带您穿越至PHP的宇宙,揭示其作为Web开发常青树的奥秘。通过生动实例与深入解析,展现PHP如何以简便、高效、灵活的姿态,赋能开发者打造动态交互式网站,同时不忘探讨其在新时代技术浪潮中面临的挑战与机遇,激发对技术创新与应用的无限思考。 ###
32 1
|
25天前
【Azure App Service】PowerShell脚本批量添加IP地址到Web App允许访问IP列表中
Web App取消公网访问后,只允许特定IP能访问Web App。需要写一下段PowerShell脚本,批量添加IP到Web App的允许访问IP列表里!
|
1月前
|
Kubernetes 安全 Cloud Native
云上攻防-云原生篇&K8s安全-Kubelet未授权访问、API Server未授权访问
本文介绍了云原生环境下Kubernetes集群的安全问题及攻击方法。首先概述了云环境下的新型攻击路径,如通过虚拟机攻击云管理平台、容器逃逸控制宿主机等。接着详细解释了Kubernetes集群架构,并列举了常见组件的默认端口及其安全隐患。文章通过具体案例演示了API Server 8080和6443端口未授权访问的攻击过程,以及Kubelet 10250端口未授权访问的利用方法,展示了如何通过这些漏洞实现权限提升和横向渗透。
159 0
云上攻防-云原生篇&K8s安全-Kubelet未授权访问、API Server未授权访问
WK
|
24天前
|
安全 Java 编译器
C++和Java哪个更适合开发web网站
在Web开发领域,C++和Java各具优势。C++以其高性能、低级控制和跨平台性著称,适用于需要高吞吐量和低延迟的场景,如实时交易系统和在线游戏服务器。Java则凭借其跨平台性、丰富的生态系统和强大的安全性,广泛应用于企业级Web开发,如企业管理系统和电子商务平台。选择时需根据项目需求和技术储备综合考虑。
WK
38 0
|
1月前
|
存储 Kubernetes 负载均衡
基于Ubuntu-22.04安装K8s-v1.28.2实验(四)使用域名访问网站应用
基于Ubuntu-22.04安装K8s-v1.28.2实验(四)使用域名访问网站应用
30 1
|
1月前
|
负载均衡 应用服务中间件 nginx
基于Ubuntu-22.04安装K8s-v1.28.2实验(二)使用kube-vip实现集群VIP访问
基于Ubuntu-22.04安装K8s-v1.28.2实验(二)使用kube-vip实现集群VIP访问
57 1
|
3月前
|
Java 数据库连接 数据库
强强联手!JSF 与 Hibernate 打造高效数据访问层,让你的应用如虎添翼,性能飙升!
【8月更文挑战第31天】本文通过具体示例详细介绍了如何在 JavaServer Faces (JSF) 应用程序中集成 Hibernate,实现数据访问层的最佳实践。首先,创建一个 JSF 项目并在 Eclipse 中配置支持 JSF 的服务器版本。接着,添加 JSF 和 Hibernate 依赖,并配置数据库连接池和 Hibernate 配置文件。然后,定义实体类 `User` 和 DAO 类 `UserDAO` 处理数据库操作。
61 0
|
3月前
|
API UED 开发者
Vaadin路由魔法:导航之舟,带你穿越页面迷宫!驾驭神奇URL,解锁无限可能!
【8月更文挑战第31天】Vaadin是一款现代Java Web开发框架,其路由机制结合前后端路由,确保流畅的用户体验和高效服务器资源利用。通过`@Route`注解和`Router`类,开发者可以轻松定义和管理页面路径。例如,`@Route("home")`可指定视图路径,而参数化路由如`@Route("user/:userId")`则允许URL传参。此外,Vaadin还提供了丰富的导航API和自定义路由事件监听器,助力开发者构建结构清晰且体验优秀的Web应用。
48 0
|
3月前
|
前端开发 API 数据处理
构建高效现代Web应用:深入探讨Entity Framework Core与GraphQL在数据访问中的结合使用
【8月更文挑战第31天】随着Web应用的发展,传统的RESTful API逐渐显现出局限性,现代应用开始转向GraphQL。与此同时,Entity Framework Core(EF Core)作为强大的ORM工具,在数据访问方面表现出色,支持异步操作和自动变更跟踪,简化了数据处理。GraphQL作为一种灵活的查询语言,允许客户端精确获取所需数据,减少不必要的传输。将EF Core与GraphQL结合使用,可实现高效的数据访问和灵活的数据查询,优化数据流并提升应用性能。这种技术组合不仅提高了开发效率,还优化了用户体验,有望成为未来Web开发的重要方向。
30 0
下一篇
无影云桌面