背景
某应用部署后造成 Containerd 内存持续飙升,透过 ctr pprof
查询后发现函数 redirectLogs
占用了绝大部分的内存,且有 Containerd 配置如下:
[plugins."io.containerd.grpc.v1.cri"]
max_container_log_line_size = -1
如何找到容器日志
- 程序将报错等日志写入
stdout
或stderr
- 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
无法返回日志 )。