Skip to content

Commit

Permalink
psi-collector: Introduce standalone version of PSI Collector.
Browse files Browse the repository at this point in the history
This commit adds a standalone version of the PSI Collector, allowing it
to be built and used independently on older versions of EVE where the
collector is not integrated into the root filesystem. The new component
includes a Makefile for building the binary, a README with instructions,
and a basic main.go implementation that handles the PSI collection
process.

This ensures compatibility and provides memory pressure monitoring
capabilities for legacy systems.

Signed-off-by: Nikolay Martyanov <nikolay@zededa.com>
  • Loading branch information
OhmSpectator committed Aug 12, 2024
1 parent cdd9d80 commit 83eb7ed
Show file tree
Hide file tree
Showing 4 changed files with 319 additions and 0 deletions.
2 changes: 2 additions & 0 deletions pkg/pillar/agentlog/cmd/psi-collector/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin
psi.txt
32 changes: 32 additions & 0 deletions pkg/pillar/agentlog/cmd/psi-collector/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
BUILD_ARGS = CGO_ENABLED=0 GOOS=linux
BUILD_FLAGS = -a -ldflags '-extldflags "-static"'

build:
GOARCH=amd64 $(BUILD_ARGS) go build -o bin/psi-collector $(BUILD_FLAGS) main.go

build-arm:
GOARCH=arm64 $(BUILD_ARGS) go build -o bin/psi-collector $(BUILD_FLAGS) main.go

local-check-dir:
ssh local_eve "mkdir -p /persist/memory-monitor/psi-collector"

local-install: local-check-dir
scp -O bin/psi-collector local_eve:/persist/memory-monitor/psi-collector

local-run:
ssh local_eve /persist/memory-monitor/psi-collector/psi-collector

local-get-results:
scp -O local_eve:/persist/memory-monitor/output/psi.txt .

local-view-results:
make -C ../../../../../tools/psi-visualizer prepare-env
source ../../../../../tools/psi-visualizer/venv/bin/activate && python ../../../../../tools/psi-visualizer/visualize.py psi.txt

help:
@echo "build - build the binary"
@echo "local-install - install the binary on local_eve"
@echo local-run - run the binary on local_eve"
@echo "local-get-results - get the results from local_eve"
@echo "local-view-results - view the results, using psi-visualizer"
@echo "help - show this help message"
100 changes: 100 additions & 0 deletions pkg/pillar/agentlog/cmd/psi-collector/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Pressure Stall Information (PSI) Collector

The PSI is a kernel feature that provides information about the pressure on the
memory, CPU, and IO subsystems. In our case, we are interested in the memory
pressure.

The PSI collector is a tool that collects PSI metrics from the kernel and
outputs them in a format that can be consumed by a visualization tool.

## Requirements

The PSI collector requires the kernel to have the PSI feature enabled. The PSI
feature is available in the Linux kernel starting from version 4.20, but it is
disabled by default. To enable the PSI feature, the kernel must be compiled with
the `CONFIG_PSI` option enabled.

## Output

The output can be found in the `/persist/memory-monitor/output/psi.txt` file.

The output is a series of lines, where each line represents a single snapshot
of the PSI metrics. They are formatted as follows:

```text
date time someAvg10 someAvg60 someAvg300 someTotal fullAvg10 fullAvg60 fullAvg300 fullTotal
```

### Visualization

The PSI collector output can be visualized using the PSI visualizer tool. The
tool is available in [psi-visualizer](../../../../../tools/psi-visualizer).
For more information on how to use the PSI visualizer, see the tool's
[README](../../../../../tools/psi-visualizer/README.md).

## EVE Integration

The PSI collector is integrated with the Pillar agentlog component. The PSI
collector can be started and stopped by sending corresponding commands to the
Pillar. The command to start the PSI collector are integrated as a part of the
eve script.

### Start

To start the PSI collector, run the following command:

```sh
eve psi-collector-start
```

### Stop

To stop the PSI collector, run the following command:

```sh
eve psi-collector-stop
```

## Standalone Usage

For the older versions of EVE, the PSI collector can be run as a standalone
tool. For that one needs to build the PSI collector binary and copy it to the
target device.

Worth noting that in this case, EVE Kernel should have the PSI feature enabled.
Most probably, the kernel should be recompiled with the `CONFIG_PSI` option.

### Building

To build the PSI collector, run the following command:

```sh
make build
```

To build the binary for ARM architecture, run:

```sh
make build-arm
```

The binary will be placed in the `bin` directory.

### Running

After building the binary, copy it to the target device, preferably to the
`/persist/memory-monitor` directory. Then run the binary:

```sh
/persist/memory-monitor/psi-collector
```

## Local make targets

In the case of running EVE on a local machine, in QEMU, with SSH access enabled,
and available as `local_eve`, the following make targets can be used:

* local-install - install the binary on local_eve
* local-run - run the binary on local_eve
* local-get-results - get the results from local_eve
* local-view-results - view the results, using psi-visualizer
185 changes: 185 additions & 0 deletions pkg/pillar/agentlog/cmd/psi-collector/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package main

import (
"context"
"fmt"
"log/syslog"
"os"
"os/signal"
"syscall"

"github.com/sirupsen/logrus"

"github.com/lf-edge/eve/pkg/pillar/agentlog"
"github.com/lf-edge/eve/pkg/pillar/base"
"github.com/lf-edge/eve/pkg/pillar/types"
)

const (
//PIDFile is the file to store the PID
PIDFile = types.MemoryMonitorDir + "/psi-collector/psi-collector.pid"
)

var log *base.LogObject

func createPIDFile() error {
f, err := os.Create(PIDFile)
if err != nil {
log.Errorf("Failed to create PID file: %v", err)
return err
}
defer f.Close()
_, err = f.WriteString(fmt.Sprintf("%d", os.Getpid()))
if err != nil {
log.Errorf("Failed to write PID to file: %v", err)
return err
}
return nil
}

func getPIDFromFile() (int, error) {
f, err := os.Open(PIDFile)
if err != nil {
log.Errorf("Failed to open PID file: %v", err)
return 0, err
}
defer f.Close()
var pid int
_, err = fmt.Fscanf(f, "%d", &pid)
if err != nil {
log.Errorf("Failed to read PID from file: %v", err)
return 0, err
}
return pid, nil
}

func finishDaemonize() error {

// Change the file mode mask
syscall.Umask(0)

// Redirect standard file descriptors to /dev/null
f, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil {
log.Errorf("Failed to open /dev/null: %v", err)
return err
}
// Use syscall.Dup3 instead of syscall.Dup2 as it is available in all platforms
err = syscall.Dup3(int(f.Fd()), int(os.Stdin.Fd()), 0)
if err != nil {
return err
}
err = syscall.Dup3(int(f.Fd()), int(os.Stdout.Fd()), 0)
if err != nil {
return err
}
err = syscall.Dup3(int(f.Fd()), int(os.Stderr.Fd()), 0)
if err != nil {
return err
}

return nil
}

func daemonize() error {

// Check if the process is already daemonized by checking the environment variable
if os.Getenv("DAEMONIZED") == "1" {
if err := finishDaemonize(); err != nil {
log.Errorf("Failed to finish daemonize: %v", err)
return err
}
// The process is already daemonized, return
return nil
}

// If it's not daemonized, daemonize it

log.Noticef("Starting Memory PSI Collector")

filePath, err := os.Executable()
if err != nil {
log.Errorf("Failed to get executable path: %v", err)
return err
}
args := os.Args
env := os.Environ()
// Add the daemon env variable to differentiate the daemon process
env = append(env, "DAEMONIZED=1")
forkAttr := &syscall.ProcAttr{
// Files is the set of file descriptors to be duped into the child's
Files: []uintptr{uintptr(syscall.Stdin), uintptr(syscall.Stdout), uintptr(syscall.Stderr)},
Sys: &syscall.SysProcAttr{
Setsid: true, // Create a new session to detach from the terminal
},
Env: env,
}

// Fork off the parent process
_, err = syscall.ForkExec(filePath, args, forkAttr)
if err != nil {
log.Errorf("Failed to fork: %v", err)
return err
}
os.Exit(0)
return nil
}

func main() {

// Create a logger
logger := logrus.New()

// Create a syslog writer
syslogWriter, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_DAEMON, "psi-collector")
if err != nil {
fmt.Println("Failed to create syslog writer: ", err)
os.Exit(1)
}
// Set the output of the logger to the syslog writer
logger.SetOutput(syslogWriter)

// Create a log object
log = base.NewSourceLogObject(logger, "psi-collector", os.Getpid())

// Check if the collector is already running
if _, err := os.Stat(PIDFile); err == nil {
savedPid, err := getPIDFromFile()
if err != nil {
log.Errorf("Failed to get PID from file: %v", err)
return
}
if savedPid != os.Getpid() {
log.Errorf("Memory PSI Collector is already running with PID: %d", savedPid)
return
}
}

err = daemonize()
if err != nil {
log.Errorf("Failed to daemonize: %v", err)
return
}

// Create a PID file
err = createPIDFile()
if err != nil {
log.Errorf("Failed to create PID file: %v", err)
return
}
defer os.Remove(PIDFile)

signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
<-signalChan
os.Remove(PIDFile)
log.Noticef("Memory PSI Collector stopped")
os.Exit(0)
}()

err = agentlog.MemoryPSICollector(context.Background(), log)
if err != nil {
log.Errorf("MemoryPSICollector failed: %v", err)
}
}

0 comments on commit 83eb7ed

Please sign in to comment.