一行超长日志引发的 “血案” - 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 = 0
  gid = 0
  level = "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 <= maxLen
    if maxLen > 0 && maxLen < bufSize {
            bufSize = maxLen
    }
    r := bufio.NewReaderSize(rc, bufSize)
...
    for {            
        newLine, isPrefix, err := readLine(r)
...
        if maxLen > 0 && length > maxLen {
            exceedLen := length - maxLen
            last := 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. 复盘总结很重要,要举一反三,从根本上解决类似问题,并形成规范和知识库,提升整个团队的效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/723169.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

重学java 75.JDK新特性 ① Lambda表达式

你所做的事情&#xff0c;也许暂时看不到成果&#xff0c;你不是没有成长&#xff0c;而是在扎根 —— 24.6.19 一、函数式编程思想和Lambda表达式定义格式 1.面向对象思想:是Java的核心编程思想 强调的是找对象,帮我们做事儿 比如:去北京 -> 强调的是怎么去,火车,高铁,飞机…

为什么人们对即将推出的 Go 1.23 迭代器感到愤怒

原文&#xff1a;gingerBill - 2024.06.17 TL;DR 它让 Go 变得太“函数式”&#xff0c;而不再是不折不扣的命令式语言。 最近&#xff0c;我在 Twitter 上看到一篇帖子&#xff0c;展示了 Go 1.23&#xff08;2024 年 8 月&#xff09;即将推出的 Go 迭代器设计。据我所知&a…

35 Debian如何配置Postfix+Dovecot

作者:网络傅老师 特别提示:未经作者允许,不得转载任何内容。违者必究! Debian如何配置Postfix+Dovecot 《傅老师Debian知识库系列之35》——原创 ==前言== 傅老师Debian知识库特点: 1、拆解Debian实用技能; 2、所有操作在VMware虚拟机实测完成; 3、致力于最终形成Deb…

网安人必备!开源网络安全工具TOP 10(附下载地址)

工欲善其事&#xff0c;必先利其器。对于广大的网络安全从业者&#xff0c;以及未来想要从事网络安全的人来说&#xff0c;选择并善用合适的网络安全工具&#xff0c;能有效提升工作效率。 开源网络安全工具之所以能够在众多安全解决方案中脱颖而出&#xff0c;不仅是因为它们…

1.22 LeetCode总结(基本算法)_位运算

进制的概念 进制即进位计数制&#xff0c;是利用固定的数字符号和统一的规则的带进位的计数方法。 任何一种进位计数制都有一个基数&#xff0c;基数为 X 的进位计数制称为 X 进制&#xff0c;表示每一个数位上的数运算时都是逢 X 进一。 504. 七进制数 手法1&#xff1a;当…

小林图解系统-二.硬件结构 2.4CPU缓存一致性

CPU Cache的数据写入 CPU和内存的访问性能越差越大&#xff0c;于是在CPU内部嵌入CPU Cache(高速缓存)。 CPU Cache由Cache Line组成&#xff0c;Cache Line由头标志Tag数据块Data Block组成。 如果数据写入Cache&#xff0c;内存和Cache相对应的数据将不同&#xff0c;需要…

排序(3)【归并排序】【计数排序】【排序算法度及其稳定性分析】

一.归并排序 归并排序&#xff08;MERGE-SORT&#xff09;是建立在归并操作上的一种有效的排序算法,该算法是采用分治法&#xff08;Divide and Conquer&#xff09;的一个非常典型的应用。将已有序的子序列合并&#xff0c;得到完全有序的序列&#xff1b;即先使每个子序列有…

CCAA质量管理【学习笔记】​​ 备考知识点笔记(六)质量改进系统方法与工具

第七节 质量改进系统方法与工具 1 质 量 改 进 方 法 概 述 可以说几乎每种质量管理领域的方法与工具都可以用于质量改进&#xff0c;但是一个组织在改进的整体推进中&#xff0c;往往不是采用单一的方法&#xff0c;会涉及多种改进的工具和手段&#xff0c;并依据一定的模式…

虹科免拆诊断案例 | 2022款问界M5增程式混合动力车充电口盖指示灯不工作

故障现象 一辆2022款问界M5增程式混合动力车&#xff0c;搭载1.5T发动机和发电机作为增程器&#xff0c;累计行驶里程约为3.6万km。该车因尾部受到碰撞进厂维修&#xff0c;维修后进行慢充&#xff0c;发现充电口盖指示灯不点亮&#xff08;图1&#xff09;&#xff0c;但仪表…

开放式运动耳机哪个品牌音质好用又实惠?五款2024高口碑产品精选力荐!

​开放式运动耳机在如今社会中已经迅速成为大家购买耳机的新趋势&#xff0c;深受喜欢听歌和热爱运动的人群欢迎。当大家谈到佩戴的稳固性时&#xff0c;开放式耳机都会收到一致好评。对于热爱运动的人士而言&#xff0c;高品质的开放式运动耳机无疑是理想之选。特别是在近年来…

要颜值有颜值,有性价比有性价比,华硕天选键、鼠组合分享

作为ROG产品的忠实粉丝&#xff0c;用过不少ROG 相关的产品&#xff0c;近期华硕天选TX98和天选MINI 鼠标的发布&#xff0c;独特配色令我眼前一亮。 华硕天选TX98键盘&#xff0c;作为新品&#xff0c;从看上的第一眼就觉得这款键盘是非常值得推荐。 它完美地诠释了潮玩新次元…

联想618收官:以69亿销售额勇夺15冠王

随着6月19日零点钟声的响起&#xff0c;今年的618年中大促正式落下帷幕。在这个备受瞩目的购物狂欢节里&#xff0c;联想凭借出色的产品表现和市场策略&#xff0c;再次展现了其强大的品牌实力和市场号召力。 数据显示&#xff0c;在今年618活动期间&#xff0c;联想全网销售额…

关于微信小程序(必看)

前言 为规范开发者的用户个人信息处理行为&#xff0c;保障用户的合法权益&#xff0c;自2023年9月15日起&#xff0c;对于涉及处理用户个人信息的小程序开发者&#xff0c;微信要求&#xff0c;仅当开发者主动向平台同步用户已阅读并同意了小程序的隐私保护指引等信息处理规则…

elasticsearch的安装和配置

单节点安装与部署 我们通过docker进行安装 1.docker的安装 如果以及安装了docker就可以跳过这个步骤。 首先更新yum: yum update安装docker: yum install docker查看docker的版本&#xff1a; docker -v此时我们的docker就安装成功了。 2.创建网络 我们还需要部署kiban…

计算机专业毕设-springboot论坛系统

1 项目介绍 基于SSM的论坛网站&#xff1a;后端 SpringBoot、Mybatis&#xff0c;前端thymeleaf&#xff0c;具体功能如下&#xff1a; 基本功能&#xff1a;登录注册、修改个人信息、修改密码、修改头像查看帖子列表&#xff1a;按热度排序、按更新时间排序、查看周榜月榜查…

SSMP整合案例

黑马程序员Spring Boot2 文章目录 1、创建项目1.1 新建项目1.2 整合 MyBatis Plus 2、创建表以及对应的实体类2.1 创建表2.2 创建实体类2.2.1 引入lombok&#xff0c;简化实体类开发2.2.2 开发实体类 3、数据层开发3.1 手动导入两个坐标3.2 配置数据源与MyBatisPlus对应的配置3…

创建第一个Springboot项目(环境准备、环境存在的问题、启动时存在的问题、启动的方式)

一、环境准备 专业版创建springboot&#xff0c;直接有一个选项可以选择 社区版&#xff0c;需要下载一个spring的插件 不要直接点 install 因为这个插件是付费的&#xff0c;直接点安装只有30天使用期限 在里面找免费版本的下载 然后安装 安装完成后&#xff0c;这个插件名会变…

记一次生产事故,来来回回搞了一个月

&#x1f345;我是小宋&#xff0c;关注我&#xff0c;带你轻松过面试 读源码。提升简历亮点&#xff08;14个demo&#xff09; &#x1f30f;号&#xff1a;tutou123com。拉你进专属群。 记一次生产事故&#xff0c;来来回回搞了一个月 背景 我们的主要业务是台湾省的一个小…

免费分享:2014-2021年OSM中国POI数据(附下载方法)

OpenStreetMap&#xff08;OSM&#xff09;是一个全球性的开源协作地图项目&#xff0c;允许任何人编辑和分享地理信息&#xff0c;旨在创建自由、准确且可广泛使用的世界地图。POI是“Point of Interest”的缩写&#xff0c;意为“兴趣点”。 OSM POI矢量数据是OpenStreetMap项…

通过MATLAB实现PID控制器,积分分离控制器以及滑模控制器

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 5.完整工程文件 1.课题概述 通过MATLAB实现PID控制器,积分分离控制器以及滑模控制器。通过对比三个算法可知&#xff0c;采用滑模控制算法&#xff0c;其具有最快的收敛性能&#xff0c;较强的鲁棒性&…