一行超长日志引发的 “血案” - Containerd 频繁 OOM 背后的真相

2024-06-19 16:12

本文主要是介绍一行超长日志引发的 “血案” - Containerd 频繁 OOM 背后的真相,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

案发现场:混沌初现

2024年6月10日,本应是平静的一天。但从上午 9 点开始,Sealos 公有云的运维监控告警就开始不停地响。北京可用区服务器节点突然出现大量 “not ready” 告警,紧接着,系统自动触发 004 节点重启,让服务暂时恢复了正常。

就在我以为这只是个小插曲的时候,7分钟后,广州可用区服务器也沦陷了!001 节点不得不重启以求自保。事情似乎并没有那么简单。

“发生什么事了?!” 运维同学们迅速登录服务器排查。原本稳定运行在 30%左右的内存使用率,在几分钟内飙升到 100%。“看起来像是有新应用大量占用内存?”

问题排查:真相难明

“难道是底层机器的内存不足以支撑业务的增长?” 抱着姑且一试的态度,我们紧急升级了北京可用区服务器的配置,将内存容量直接翻倍。观察一段时间后,服务基本恢复稳定。“看来还是资源预留不足,3 倍底线仍然扛不住啊。” 大家暂时松了口气。

广州可用区的扩容也在40分钟后完成。

正当大家松了一口气,准备撤离 “战场” 时,北京可用区又开始卷土重来:16:22,接连不断的 not ready 告警又回来了!003 和 004 节点在资源耗尽的边缘反复横跳。人工重启维持一会儿还好,可 10 几分钟后,崩溃又卷土重来,内存曲线坚定地向右上方奔去...

更让人无语的是,计划赶不上变化,17:46,广州可用区也开始出问题,两个集群再次陷入节点轮番崩溃的怪圈

揭示真相:内存炸弹,原来是你!

局势愈发紧张,我们只能通过不断重启节点,维持业务的运转。但内存增长的势头依然强劲,系统每隔 15-30分钟就会崩溃

内存泄露?

面对诡异的内存曲线,有人提出会不会是内存泄露?大家决定从系统内核和容器两个方向深挖。

首先怀疑是不是有容器在疯狂打印日志。统计对比发现整体集群的 Pod 数量上升并不异常,排除了 Pod 数量暴增导致的资源问题。但在统计各节点内存使用时,发现所有 Pod 的实际内存使用总和,远小于宿主机的内存占用。

进一步确认是 Containerd 进程的内存占用在飙升,最高达到了恐怖的 1G/s 速率!而与此同时,Pod 本身的资源使用一直很平稳。

定位罪魁祸首

进一步怀疑可能是 Containerd 的 Bug 导致内存泄露。我们决定先将整个集群的 Containerd 升级到最新的 1.7.18 版本再继续观察。升级命令如下:

#!/usr/bin/env bashset -euxo pipefail
wget https://github.com/containerd/containerd/releases/download/v1.7.18/containerd-1.7.18-linux-amd64.tar.gz
tar xfz containerd-1.7.18-linux-amd64.tar.gz -C /usr
systemctl restart containerd && sleep 3
systemctl is-active containerd

结果负载降低后,Containerd 的内存占用依然在缓慢上涨。也就是说,更新后的 Containerd 还是在快速泄露内存

大家开始把注意力放到 Containerd 身上,从多个角度展开排查:

1。通过 pprof 采样分析 containerd 内存分配情况

首先修改 containerd 配置,使其输出 debug 日志信息:

[debug]address = "/run/containerd/containerd-debug.sock"uid = 0gid = 0level = "debug"format = "json"

然后开始采样,输出 pprof 文件:

ctr pprof --debug-socket /run/containerd/containerd-debug.sock heap >> containerd.pprof

查看 pprof 文件内容,怀疑与容器日志有关。

于是开始通过节点保存的容器日志查找是否有 pod 输出了大量日志。

2。观察集群节点互相重启的情况,尝试通过黑盒方式定位出问题的容器/镜像

首先通过命令获取节点的所有正在运行的 Pod,获取其中的所有 image 信息,减去其他正常节点的所有 image 信息,得到只在出问题节点运行的 image 镜像列表。

通过北京可用区节点和广州可用区节点的上述操作求交集,进一步减少独立出现的镜像列表,得到如下的列表:

aibotk/wechat-assistant
aibotk/wechat-assistant:v4.6.28
calciumion/new-api:latest
cloudreve/cloudreve
cloudtogouser/pageplug-ce
csjiangyj/filesys
docker.io/semitechnologies/weaviate:1.19.6
ghcr.io/songquanpeng/one-api:latest
harbor.service.xiaoyangedu.net/zadig/confluence-crack:7.13.20
infracreate-registry.cn-zhangjiakou.cr.aliyuncs.com/apecloud/csi-attacher:v3.4.0
jupyter/scipy-notebook
prom/alertmanager:v0.25.0
quay.io/cilium/hubble-ui-backend:v0.13.0@sha256:1e7657d997c5a48253bb8dc91ecee75b63018d16ff5e5797e5af367336bc8803
quay.io/cilium/hubble-ui:v0.13.0@sha256:7d663dc16538dd6e29061abd1047013a645e6e69c115e008bee9ea9fef9a6666
quay.io/cilium/operator-generic:v1.15.5@sha256:f5d3d19754074ca052be6aac5d1ffb1de1eb5f2d947222b5f10f6d97ad4383e8
registry.cn-chengdu.aliyuncs.com/wyxz_test/wyxz:go-man-2024-05-22-15-04-30-2
registry.cn-hangzhou.aliyuncs.com/fastgpt_docker/m3e-large-api:latest
registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:latest
registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.8.3
registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.7
registry.cn-hangzhou.aliyuncs.com/gxtatu/guet-server:latest
registry.cn-hangzhou.aliyuncs.com/pandak/ssh:latest
registry.cn-hongkong.aliyuncs.com/manyun2024/subconverter:clashv2
registry.k8s.io/coredns/coredns:v1.10.1

排除掉最常见的集群内部组件的镜像,也还是有十多个应用镜像,于是我们尝试逐个进入应用终端内排查,可是每个容器资源占用都很正常。

排查过程中,出现问题的节点仍然因为 OOM 不得不反复重启 Containerd。

继续研究 containerd 的内存使用和日志处理流程,终于在 NewCRILogger 函数的 redirectLogs 中,发现端倪:

redirectLogs 会根据传入的 maxLen,也就是 max_container_log_line_size 参数,来决定内存缓冲区的大小。如果 maxLen 是-1 (无限制),那么任何超长的日志,都会一次性载入内存,直到把内存吃光!

顺藤摸瓜,发现罪魁祸首正是 Sealos 集群默认配置里的 max_container_log_line_size=-1。而 Containerd 官方默认值应该是 16384

为了验证,我们设置广州可用区的 node004 节点的 Containerd 将 maxLen 改为 16384,重启后案情峰回路转,内存终于不再失控了!

为了确保这就是根本原因,我们编写了一个测试程序,不断打印超长日志。在有问题的默认配置下,成功稳定复现了节点 Containerd 内存飙升的现象。而限制了长度的新配置,即使打印再多日志也不会有问题。

究其根本,只是之前没遇到打印特别长日志的应用,一直 “藏而不发” 罢了。

问题复现

最终我们可以确定,在出问题的默认配置的 Containerd 环境上,如果有容器在一行打印大量日志,必定会出现大量占用内存的情形。

可以参考我们 mock 程序源码地址,其中的核心就一个 fmt.Printf 函数。

私有化部署的 Sealos 环境可以直接在 Launchpad 上部署我们打包好的镜像,可以稳定复现。测试镜像地址:docker.io/lingdie/containerd-log-debug:dev

刨根问底

Sealos 的 runtime 将 Containerd 的 max_container_log_line_size 参数设置为默认值 -1,这块代码在创建时就没有再动过,而排查 Containerd 的源码发现其最新版的默认值是 16384

出问题的源码在这里:https://github.com/labring-actions/runtime/blob/443cc6f4625a6a505586dba5e90a71adec275639/containerd/etc/config.toml.tmpl#L28

通过深入源码发现,这里的参数会影响系统申请的 buf 的实现,参考:https://github.com/containerd/containerd/blob/main/internal/cri/io/logger.go

这个 max_container_log_line_size 会作为 NewCRILoggermaxLen 参数传递进来:

// 这里的 maxLen,sealos 默认值是-1, containerd 最新版默认值是 16384
func NewCRILogger(path string, w io.Writer, stream StreamType, maxLen int) (io.WriteCloser, <-chan struct{}) {log.L.Debugf("Start writing stream %q to log file %q", stream, path)prc, pwc := io.Pipe()stop := make(chan struct{})go func() {redirectLogs(path, prc, w, stream, maxLen)close(stop)}()return pwc, stop
}

这里会启动一个 channel 去重定向日志,而其中最关键的 redirectLogs 函数里面,会根据 maxLen 决定 buf 的大小:

const (// defaultBufSize is the default size of the read buffer in bytes.defaultBufSize = 4096
)
...
func redirectLogs(path string, rc io.ReadCloser, w io.Writer, s StreamType, maxLen int) {var (
...bufSize   = defaultBufSize
...)
...// Make sure bufSize <= maxLenif maxLen > 0 && maxLen < bufSize {bufSize = maxLen}r := bufio.NewReaderSize(rc, bufSize)
...for {            newLine, isPrefix, err := readLine(r)
...if maxLen > 0 && length > maxLen {exceedLen := length - maxLenlast := buf[len(buf)-1]if exceedLen > len(last) {// exceedLen must <= len(last), or else the buffer// should have be written in the previous iteration.panic("exceed length should <= last buffer size")}buf[len(buf)-1] = last[:len(last)-exceedLen]writeLineBuffer(partial, buf)splitEntries.Inc()buf = [][]byte{last[len(last)-exceedLen:]}length = exceedLen
...}}
}

由于 Containerd 将日志写入文件的方式以行为单位,因此若某程序产生一行超长日志,且 max_container_log_line_size 不限制,那么将造成其日志永久占用内存资源,也无法在节点上查询这段日志 (比如 kubectl logs 无法返回日志)。

解决方案

明白病灶所在,治疗就轻而易举了。我们为所有集群的 containerd 配置都加上了合理的 maxLen 限制,观察一段时间后,问题不再出现。

同时修复了 Sealos 源码里的默认配置,重新打包了镜像。还对有问题的应用日志长度做了切分限制。

相关 PR:https://github.com/labring-actions/runtime/pull/15

相关 Issue:https://github.com/labring-actions/runtime/issues/14

私有化部署修复方案

sealos exec 'sed -i "s/max_container_log_line_size = -1/max_container_log_line_size = 16384/" /etc/containerd/config.toml'
sealos exec 'systemctl restart containerd'

经验总结

回顾这次事件,虽然过程一波三折,但庆幸我们没有被 “温水煮青蛙”,及时止损。感谢团队的通力协作,不眠不休地排除故障,力保业务平稳。这次的惨痛教训也让我们更加深刻地认识到,在复杂的容器环境中,Kernel 的每一个细微参数都可能带来致命影响。有几点经验在这里总结一下:

  1. 默认配置一定要设置合理,不能存有侥幸。无限制的内存很可能就是定时炸弹;
  2. 要警惕打印超长日志的情况,在应用侧做好长度截断。但底层程序也要有自我保护;
  3. 监控警报、资源曲线、节点指标,是异常的第一发现者。再结合运维同学的经验直觉,能快速圈定排查方向;
  4. 源码面前,了无秘密。当反复试错无果时,要学会深入源码一探究竟。这需要一定的技术积累,平时多读优秀开源项目的代码就很有帮助;
  5. 复盘总结很重要,要举一反三,从根本上解决类似问题,并形成规范和知识库,提升整个团队的效率。

这篇关于一行超长日志引发的 “血案” - Containerd 频繁 OOM 背后的真相的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1075515

相关文章

Linux系统稳定性的奥秘:探究其背后的机制与哲学

在计算机操作系统的世界里,Linux以其卓越的稳定性和可靠性著称,成为服务器、嵌入式系统乃至个人电脑用户的首选。那么,是什么造就了Linux如此之高的稳定性呢?本文将深入解析Linux系统稳定性的几个关键因素,揭示其背后的技术哲学与实践。 1. 开源协作的力量Linux是一个开源项目,意味着任何人都可以查看、修改和贡献其源代码。这种开放性吸引了全球成千上万的开发者参与到内核的维护与优化中,形成了

【文末附gpt升级秘笈】腾讯元宝AI搜索解析能力升级:千万字超长文处理的新里程碑

腾讯元宝AI搜索解析能力升级:千万字超长文处理的新里程碑 一、引言 随着人工智能技术的飞速发展,自然语言处理(NLP)和机器学习(ML)在各行各业的应用日益广泛。其中,AI搜索解析能力作为信息检索和知识抽取的核心技术,受到了广泛的关注和研究。腾讯作为互联网行业的领军企业,其在AI领域的探索和创新一直走在前列。近日,腾讯旗下的AI大模型应用——腾讯元宝,迎来了1.1.7版本的升级,新版本在AI搜

Sapphire开发日志 (十) 关于页面

关于页面 任务介绍 关于页面用户对我组工作量的展示。 实现效果 代码解释 首先封装一个子组件用于展示用户头像和名称。 const UserGrid = ({src,name,size,link,}: {src: any;name: any;size?: any;link?: any;}) => (<Box sx={{ display: "flex", flexDirecti

TableView 当前选中的行号。 默认会使哪一行选中 加入导航条后contentInset向下偏移的64

1.得到当前选中的行号     NSLog(@"%ld %s",  [self.tableView indexPathForSelectedRow].row,__func__); 2.默认选中表格的那一行     [self.tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0 ]

linux匹配Nginx日志,某个字符开头和结尾的字符串

匹配 os=1 开头, &ip结尾的字符串 cat 2018-06-07.log | egrep -o ‘os=1.*.&ip’ 存入日志。然后使用submit 前面和后面的值去掉,剩下就是需要的字符串。 cat 2018-06-07.log | egrep -o ‘os=1.*.&ip’ >log.log

springboot学习02-[热部署和日志]

热部署和日志 热部署 热部署

C# 日志框架Serilog使用

1、框架和说明        C#日志框架Serilog支持多种场景输出,简单验证了一下,比较方便        包的安装,推荐直接使用“推荐NuGet包管理器”安装Serilog.AspNetCore,常见的组件都已经集成在一个包中,使用比较方便 2、配置文件        Serilog可以由配置文件来定义行为,而且配置文件的修改即时生效。参考配置文件如下: {"Serilog":

Android Log日志 - 打印不全问题

AndroidStudio在打印Log的时候目前支持4*1024长度,超出部分不能打印。当你在各种百度之后有对应的解决办法,但是每次都是部分代码,看着都忧伤。索性此次项目调试的数据也是比较多滴,目前就准备对Log开刀来写一个Log类,还是如以往的性格直接写完整的类,方便需要的人用。反正又不是什么高深的东西,为了给被方便同时也是给自己方便。 /*** Relin* 2019-07-10 10:40

Containerd命令行工具nerdctl

Containerd 客户端工具 nerdctl 相比Containerd自带的ctr工具,nerdctl操作方式更接近之前的docker命令。nerdctl 是一个与 docker cli 风格兼容的 containerd 客户端工具,而且直接兼容docker compose的语法的。 仓库:https://github.com/containerd/nerdctl 1. 安装 二进制文

Sublime Text 快速折叠CSS代码到一行

快速折叠CSS代码到一行 1.使用HTML/CSS/JS Prettify或者CSScomb美化代码 2.使用Alt+F3特征选取,删除下图中所有的特征空格 3.使用Ctrl+Shift+M选取括号里的内容,再使用一次Ctrl+Shift+M将大括号也一起选中。 4.使用Ctr+J折叠代码 5.Home键 6.Backspace键