一行超长日志引发的 “血案” - 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

相关文章

flume系列之:查看flume系统日志、查看统计flume日志类型、查看flume日志

遍历指定目录下多个文件查找指定内容 服务器系统日志会记录flume相关日志 cat /var/log/messages |grep -i oom 查找系统日志中关于flume的指定日志 import osdef search_string_in_files(directory, search_string):count = 0

我在移动打工的日志

客户:给我搞一下录音 我:不会。不在服务范围。 客户:是不想吧 我:笑嘻嘻(气笑) 客户:小姑娘明明会,却欺负老人 我:笑嘻嘻 客户:那我交话费 我:手机号 客户:给我搞录音 我:不会。不懂。没搞过。 客户:那我交话费 我:手机号。这是电信的啊!!我这是中国移动!! 客户:我不管,我要充话费,充话费是你们的 我:可是这是移动!!中国移动!! 客户:我这是手机号 我:那又如何,这是移动!你是电信!!

Detectorn2预训练模型复现:数据准备、训练命令、日志分析与输出目录

Detectorn2预训练模型复现:数据准备、训练命令、日志分析与输出目录 在深度学习项目中,目标检测是一项重要的任务。本文将详细介绍如何使用Detectron2进行目标检测模型的复现训练,涵盖训练数据准备、训练命令、训练日志分析、训练指标以及训练输出目录的各个文件及其作用。特别地,我们将演示在训练过程中出现中断后,如何使用 resume 功能继续训练,并将我们复现的模型与Model Zoo中的

SSM项目使用AOP技术进行日志记录

本步骤只记录完成切面所需的必要代码 本人开发中遇到的问题: 切面一直切不进去,最后发现需要在springMVC的核心配置文件中中开启注解驱动才可以,只在spring的核心配置文件中开启是不会在web项目中生效的。 之后按照下面的代码进行配置,然后前端在访问controller层中的路径时即可观察到日志已经被正常记录到数据库,代码中有部分注释,看不懂的可以参照注释。接下来进入正题 1、导入m

多数据源的事务处理总是打印很多无用的log日志

之前做了一个项目,需要用到多数据源以及事务处理,在使用事务处理,服务器总是打印很多关于事务处理的log日志(com.atomikos.logging.Slf4jLogger),但是我们根本不会用到这些log日志,反而使得查询一些有用的log日志变得困难。那要如何屏蔽这些log日志呢? 之前的项目是提高项目打印log日志的级别,后来觉得这样治标不治本。 现在有一个更好的方法: 我使用的是log

flume系列之:记录一次flume agent进程被异常oom kill -9的原因定位

flume系列之:记录一次flume agent进程被异常oom kill -9的原因定位 一、背景二、定位问题三、解决方法 一、背景 flume系列之:定位flume没有关闭某个时间点生成的tmp文件的原因,并制定解决方案在博主上面这篇文章的基础上,在机器内存、cpu资源、flume agent资源都足够的情况下,flume agent又出现了tmp文件无法关闭的情况 二、

android两种日志获取log4j

android   log4j 加载日志使用方法; 先上图: 有两种方式: 1:直接使用架包 加载(两个都要使用); 架包:android-logging-log4j-1.0.3.jar 、log4j-1.2.15.jar  (说明:也可以使用架包:log4j-1.2.17.jar)  2:对架包输入日志的二次封装使用; 1:直接使用 log4j 日志框架获取日志信息: A:配置 日志 文

Unity Post Process Unity后处理学习日志

Unity Post Process Unity后处理学习日志 在现代游戏开发中,后处理(Post Processing)技术已经成为提升游戏画面质量的关键工具。Unity的后处理栈(Post Processing Stack)是一个强大的插件,它允许开发者为游戏场景添加各种视觉效果,如景深、色彩校正、辉光、模糊等。这些效果不仅能够增强游戏的视觉吸引力,还能帮助传达特定的情感和氛围。 文档

机械学习—零基础学习日志(概率论总笔记5)

引言——“黑天鹅” 要获得95%以上置信度的统计结果,需要被统计的对象出现上千次,但是如果整个样本只有几千字,被统计的对象能出现几次就不错了。这样得到的数据可能和真实的概率相差很远。怎么避免“黑天鹅”? 古德-图灵折扣估计法 在词语统计中,有点词语虽然是出现0次,但是实际的出现概率并不是永远不可能的零。 那需要把一些概率转移给到这些词语。 古德的做法实际上就是把出现1次的单词的总量,给了

如何使用Selenium捕获控制台日志

Selenium是一个流行的开源工具,用于自动化Web浏览器。其中一个关键功能是能够与浏览器的开发者控制台交互。本文将向您展示如何在Selenium中使用Java获取控制台日志。这些日志对于调试和解决Selenium脚本的问题非常有用。 如何查看任何网页的控制台日志 首先,打开浏览器的开发者控制台。在大多数浏览器中,您可以通过右键点击页面并选择“检查”来做到这一点。我们将在我们的测试网站——h