超长单行日志使 Containerd 内存占用过高

某应用部署后造成 Containerd 内存持续飙升,透过 ctr pprof 查询后发现函数 redirectLogs 占用了绝大部分的内存,且有 Containerd 配置如下:

  [plugins."io.containerd.grpc.v1.cri"]
    max_container_log_line_size = -1

如何找到容器日志

  1. 程序将报错等日志写入 stdoutstderr
  2. CRI 运行时 (如 Containerd )将日志重定向到系统上的一份日志文件中,文件日志路径由调用 CRI 运行时接口的程序(如 Kubernetes )指定,比如在 Kubernetes 中容器日志默认保存在 /var/log/pods 下。

kubectl logs

这个 kubectl 命令可以调用 kubelet 的接口来读取存在 /var/log/pods 下对应的容器日志。

容器日志重定向的代码实现

每创建一个容器,Containerd 都会创建一个 Logger ( NewCRILogger ) ,里面的 redirectLogs 函数负责重定向日志,这个函数有几个比较重要的变量:

var (
// ...
                buf       [][]byte 
                length    int
                bufSize   = defaultBufSize
// ...
        )
  • buf :用于在内存中暂存日志内容
  • length :当前读取的日志长度
  • bufSize :一次可读取的最大日志片段长度

另外此函数有一限制 maxLen ,代表最大实际写入文件的行长度。

读取日志以行为单位,并将这行存于 buf 中:

newLine, isPrefix, err := readLine(r)
if len(newLine) > 0 {
                        inputEntries.Inc()
                        inputBytes.Inc(float64(len(newLine)))
                        // Buffer returned by ReadLine will change after
                        // next read, copy it.
                        l := make([]byte, len(newLine))
                        copy(l, newLine)
                        buf = append(buf, l)
                        length += len(l)
                }

当没有日志可读取时,此函数将返回,并写入剩下的日志内容:

                if err != nil {
                        // ...
                        // Stop after writing the content left in buffer.
                        stop = true
                }
                // ...
                if stop {
                        // readLine only returns error when the message doesn't
                        // end with a newline, in that case it should be treated
                        // as a partial line.
                        writeLineBuffer(partial, buf)
                } else {
                        writeLineBuffer(full, buf)
                }
                // ...
                if stop {
                        break
                }

检查是否超过 maxLen ,如果超过代表需写入:

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
                }

也就代表,如果 maxLen 是一个非常大的值,那么日志将会长时间留在内存中。

max_container_log_line_size

在 Containerd 的配置文件中, max_container_log_line_size 这个参数对应代码中的 maxLen ,如果此参数值为 -1 ,写入日志的一行大小可视为无限大。

总结

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

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇