mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-07-11 18:55:46 -04:00
The kubelet used cadvisor's lib/utils/oomparser to watch the kernel log for OOM kills and to produce the container_oom_events_total metric. That made the kubelet depend on the cadvisor lib module for OOM handling. This moves OOM handling in-tree: - The OOM kill parser is forked from cadvisor into third_party/forked/cadvisor/oomparser. It reads /dev/kmsg and extracts OOM events, adapted to take a context.Context and log through klog. - The kmsg reader is forked from github.com/euank/go-kmsg-parser into third_party/forked/go-kmsg-parser/kmsgparser. - The kubelet OOM watcher reads OOM events through the forked parser and records the container_oom_events_total metric in-tree. - cadvisor/lib is bumped to v0.60.3, which no longer ships the OOM parser. This removes the transitive github.com/euank/go-kmsg-parser v2.0.0+incompatible dependency and the vendored cadvisor lib/utils/oomparser package. Behavior is unchanged. OOM events are still emitted and the metric still reports the same counts.
261 lines
7.4 KiB
Go
261 lines
7.4 KiB
Go
/*
|
|
Copyright 2016 Euan Kemp
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
// Package kmsgparser implements a parser for the Linux `/dev/kmsg` format.
|
|
// More information about this format may be found here:
|
|
// https://www.kernel.org/doc/Documentation/ABI/testing/dev-kmsg
|
|
// Some parts of it are slightly inspired by rsyslog's contrib module:
|
|
// https://github.com/rsyslog/rsyslog/blob/v8.22.0/contrib/imkmsg/kmsg.c
|
|
//
|
|
// This package is forked from github.com/euank/go-kmsg-parser/v3 at v3.0.0:
|
|
// https://github.com/euank/go-kmsg-parser/blob/v3.0.0/kmsgparser/kmsgparser.go
|
|
// The upstream Logger abstraction (log.go) is dropped; the parser logs through
|
|
// klog instead.
|
|
package kmsgparser
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"k8s.io/klog/v2"
|
|
)
|
|
|
|
// Parser is a parser for the kernel ring buffer found at /dev/kmsg
|
|
type Parser interface {
|
|
// SeekEnd moves the parser to the end of the kmsg queue.
|
|
SeekEnd() error
|
|
// Parse reads from kmsg and provides a channel of messages.
|
|
// Parse will always close the provided channel before returning.
|
|
// Parse may be canceled by calling 'Close' on the parser.
|
|
//
|
|
// The caller should drain the channel after calling [Close]. The caller must
|
|
// not close the channel passed in.
|
|
Parse(chan<- Message) error
|
|
|
|
Close() error
|
|
}
|
|
|
|
// Message represents a given kmsg logline, including its timestamp (as
|
|
// calculated based on offset from boot time), its possibly multi-line body,
|
|
// and so on. More information about these mssages may be found here:
|
|
// https://www.kernel.org/doc/Documentation/ABI/testing/dev-kmsg
|
|
type Message struct {
|
|
Priority int
|
|
SequenceNumber int
|
|
Timestamp time.Time
|
|
Message string
|
|
}
|
|
|
|
// NewParser constructs a new parser with the given Options.
|
|
func NewParser(opts ...Option) (Parser, error) {
|
|
f, err := os.Open("/dev/kmsg")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bootTime, err := getBootTime()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p := &parser{
|
|
kmsgReader: f,
|
|
bootTime: bootTime,
|
|
follow: true,
|
|
}
|
|
for _, opt := range opts {
|
|
opt(p)
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// Option is a configuration option for [NewParser]
|
|
type Option func(p *parser)
|
|
|
|
// WithNoFollow configures [Parser] to stop reading and close the channel when
|
|
// it reaches the end of the kmsg buffer.
|
|
func WithNoFollow() func(p *parser) {
|
|
return func(p *parser) {
|
|
p.follow = false
|
|
}
|
|
}
|
|
|
|
type parser struct {
|
|
kmsgReader *os.File
|
|
bootTime time.Time
|
|
// follow indicates whether we should stop when we hit the end of the kmsg
|
|
// buffer, or keep reading. Similar to dmesg --follow.
|
|
// Default true
|
|
follow bool
|
|
}
|
|
|
|
func getBootTime() (time.Time, error) {
|
|
var sysinfo syscall.Sysinfo_t
|
|
err := syscall.Sysinfo(&sysinfo)
|
|
if err != nil {
|
|
return time.Time{}, fmt.Errorf("could not get boot time: %v", err)
|
|
}
|
|
// sysinfo only has seconds
|
|
return time.Now().Add(-1 * (time.Duration(sysinfo.Uptime) * time.Second)), nil
|
|
}
|
|
|
|
func (p *parser) SeekEnd() error {
|
|
_, err := p.kmsgReader.Seek(0, io.SeekEnd)
|
|
return err
|
|
}
|
|
|
|
func (p *parser) Close() error {
|
|
return p.kmsgReader.Close()
|
|
}
|
|
|
|
// Parse reads from the provided reader and provide a channel of messages
|
|
// parsed.
|
|
// If the provided reader *is not* a proper Linux kmsg device, Parse might not
|
|
// behave correctly since it relies on specific behavior of `/dev/kmsg`
|
|
//
|
|
// The caller should typically run 'Parse' in a goroutine.
|
|
func (p *parser) Parse(msgs chan<- Message) error {
|
|
// with follow (dmesg --follow), we can use go's usual way of reading thing (i.e. epoll + nonblocking IO).
|
|
if p.follow {
|
|
return p.readFollow(msgs)
|
|
}
|
|
return p.readNofollow(msgs)
|
|
}
|
|
|
|
func (p *parser) readFollow(msgs chan<- Message) error {
|
|
defer close(msgs)
|
|
msg := make([]byte, 8192)
|
|
for {
|
|
// Each read call gives us one full message.
|
|
// https://www.kernel.org/doc/Documentation/ABI/testing/dev-kmsg
|
|
n, err := p.kmsgReader.Read(msg)
|
|
switch {
|
|
case err == nil:
|
|
case errors.Is(err, syscall.EPIPE):
|
|
klog.Warningf("short read from kmsg; skipping")
|
|
continue
|
|
case errors.Is(err, io.EOF), errors.Is(err, os.ErrClosed):
|
|
// someone closed us
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unexpected kmsg reading error: %w", err)
|
|
}
|
|
msgStr := string(msg[:n])
|
|
message, err := p.parseMessage(msgStr)
|
|
if err != nil {
|
|
return fmt.Errorf("malformed kmsg message: %w", err)
|
|
}
|
|
msgs <- message
|
|
}
|
|
}
|
|
|
|
func (p *parser) readNofollow(msgs chan<- Message) error {
|
|
defer close(msgs)
|
|
|
|
rawReader, err := p.kmsgReader.SyscallConn()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// we're forced to put the fd into non-blocking mode to be able to detect the
|
|
// end of the buffer, but to not use go's built-in epoll
|
|
if ctrlErr := rawReader.Control(func(fd uintptr) {
|
|
err = syscall.SetNonblock(int(fd), true)
|
|
}); ctrlErr != nil {
|
|
return fmt.Errorf("error calling control on kmsg reader: %w", ctrlErr)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("unable to set nonblocking on fd: %w", err)
|
|
}
|
|
msg := make([]byte, 8192)
|
|
for {
|
|
// Each read call gives us one full message.
|
|
// https://www.kernel.org/doc/Documentation/ABI/testing/dev-kmsg
|
|
var err error
|
|
var n int
|
|
readErr := rawReader.Read(func(fd uintptr) bool {
|
|
n, err = syscall.Read(int(fd), msg)
|
|
return true
|
|
})
|
|
if readErr != nil && err == nil {
|
|
err = readErr
|
|
}
|
|
switch {
|
|
case err == nil:
|
|
case errors.Is(err, syscall.EPIPE):
|
|
klog.Warningf("short read from kmsg; skipping")
|
|
continue
|
|
case errors.Is(err, syscall.EAGAIN):
|
|
// end of ring buffer in nofollow mode, we're done
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unexpected kmsg reading error: %w", err)
|
|
}
|
|
msgStr := string(msg[:n])
|
|
|
|
message, err := p.parseMessage(msgStr)
|
|
if err != nil {
|
|
return fmt.Errorf("malformed kmsg message: %w", err)
|
|
}
|
|
msgs <- message
|
|
}
|
|
}
|
|
|
|
func (p *parser) parseMessage(input string) (Message, error) {
|
|
// Format:
|
|
// PRIORITY,SEQUENCE_NUM,TIMESTAMP,-;MESSAGE
|
|
parts := strings.SplitN(input, ";", 2)
|
|
if len(parts) != 2 {
|
|
return Message{}, fmt.Errorf("invalid kmsg; must contain a ';'")
|
|
}
|
|
|
|
metadata, message := parts[0], parts[1]
|
|
|
|
metadataParts := strings.Split(metadata, ",")
|
|
if len(metadataParts) < 3 {
|
|
return Message{}, fmt.Errorf("invalid kmsg: must contain at least 3 ',' separated pieces at the start")
|
|
}
|
|
|
|
priority, sequence, timestamp := metadataParts[0], metadataParts[1], metadataParts[2]
|
|
|
|
prioNum, err := strconv.Atoi(priority)
|
|
if err != nil {
|
|
return Message{}, fmt.Errorf("could not parse %q as priority: %v", priority, err)
|
|
}
|
|
|
|
sequenceNum, err := strconv.Atoi(sequence)
|
|
if err != nil {
|
|
return Message{}, fmt.Errorf("could not parse %q as sequence number: %v", priority, err)
|
|
}
|
|
|
|
timestampUsFromBoot, err := strconv.ParseInt(timestamp, 10, 64)
|
|
if err != nil {
|
|
return Message{}, fmt.Errorf("could not parse %q as timestamp: %v", priority, err)
|
|
}
|
|
// timestamp is offset in microsecond from boottime.
|
|
msgTime := p.bootTime.Add(time.Duration(timestampUsFromBoot) * time.Microsecond)
|
|
|
|
return Message{
|
|
Priority: prioNum,
|
|
SequenceNumber: sequenceNum,
|
|
Timestamp: msgTime,
|
|
Message: message,
|
|
}, nil
|
|
}
|