Skip to content

Commit 8f3983e

Browse files
authored
Merge pull request #2129 from wswsmao/monitor
analyzer: add pre-container monitor to capture early file access
2 parents 3eb8ee2 + 584a3dc commit 8f3983e

File tree

4 files changed

+165
-0
lines changed

4 files changed

+165
-0
lines changed

analyzer/analyzer.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
"github.com/containerd/log"
4242
"github.com/containerd/platforms"
4343
"github.com/containerd/stargz-snapshotter/analyzer/fanotify"
44+
"github.com/containerd/stargz-snapshotter/analyzer/fanotify/service"
4445
"github.com/containerd/stargz-snapshotter/analyzer/recorder"
4546
"github.com/opencontainers/go-digest"
4647
"github.com/opencontainers/image-spec/identity"
@@ -96,6 +97,26 @@ func Analyze(ctx context.Context, client *containerd.Client, ref string, opts ..
9697
}
9798
defer cleanup()
9899

100+
var preMonitorEnabled bool
101+
var preMonitor *service.PreContainerMonitor
102+
if aOpts.preMonitor {
103+
preMonitor = service.NewPreContainerMonitor(target)
104+
if err := preMonitor.Start(); err != nil {
105+
return "", fmt.Errorf("failed to start pre-container monitor: %w", err)
106+
}
107+
preMonitorEnabled = true
108+
defer func() {
109+
if err := preMonitor.Close(); err != nil {
110+
log.G(ctx).WithError(err).Warnf("failed to close pre-container monitor")
111+
}
112+
}()
113+
go func() {
114+
if err := preMonitor.Monitor(); err != nil {
115+
log.G(ctx).WithError(err).Warnf("pre-container monitor failed")
116+
}
117+
}()
118+
}
119+
99120
// Spawn a fanotifier process in a new mount namespace and setup recorder.
100121
fanotifier, err := fanotify.SpawnFanotifier("/proc/self/exe")
101122
if err != nil {
@@ -195,6 +216,21 @@ func Analyze(ctx context.Context, client *containerd.Client, ref string, opts ..
195216
return "", err
196217
}
197218
defer rc.Close()
219+
220+
if preMonitorEnabled {
221+
prePaths := preMonitor.GetPaths()
222+
for _, path := range prePaths {
223+
cleanPath := path
224+
if strings.HasPrefix(path, target) {
225+
cleanPath = strings.TrimPrefix(path, target)
226+
}
227+
if err := rc.Record(cleanPath); err != nil {
228+
log.G(ctx).WithError(err).Debugf("failed to record pre-container path %q", cleanPath)
229+
}
230+
}
231+
log.G(ctx).Infof("recorded %d paths from pre-container monitor", len(prePaths))
232+
}
233+
198234
if err := fanotifier.Start(); err != nil {
199235
return "", fmt.Errorf("failed to start fanotifier: %w", err)
200236
}

analyzer/fanotify/service/service.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"fmt"
2323
"io"
2424
"os"
25+
"sync"
2526
"time"
2627

2728
"github.com/containerd/stargz-snapshotter/analyzer/fanotify/conn"
@@ -95,3 +96,120 @@ func Serve(target string, r io.Reader, w io.Writer) error {
9596

9697
return nil
9798
}
99+
100+
type PreContainerMonitor struct {
101+
targetDir string
102+
file *os.File
103+
paths map[string]struct{}
104+
mu sync.RWMutex
105+
done chan struct{}
106+
closed bool
107+
closeMu sync.Mutex
108+
}
109+
110+
func NewPreContainerMonitor(targetDir string) *PreContainerMonitor {
111+
return &PreContainerMonitor{
112+
targetDir: targetDir,
113+
paths: make(map[string]struct{}),
114+
done: make(chan struct{}),
115+
}
116+
}
117+
118+
func (m *PreContainerMonitor) Start() error {
119+
fd, err := unix.FanotifyInit(unix.FAN_CLASS_NOTIF, unix.O_RDONLY)
120+
if err != nil {
121+
return fmt.Errorf("fanotify_init: %w", err)
122+
}
123+
m.file = os.NewFile(uintptr(fd), "fanotify")
124+
125+
if err := unix.FanotifyMark(fd,
126+
unix.FAN_MARK_ADD|unix.FAN_MARK_FILESYSTEM,
127+
unix.FAN_ACCESS|unix.FAN_OPEN,
128+
unix.AT_FDCWD,
129+
m.targetDir,
130+
); err != nil {
131+
m.file.Close()
132+
return fmt.Errorf("fanotify_mark: %w", err)
133+
}
134+
135+
return nil
136+
}
137+
138+
func (m *PreContainerMonitor) Monitor() error {
139+
if m.file == nil {
140+
return fmt.Errorf("monitor not started")
141+
}
142+
143+
nr := bufio.NewReader(m.file)
144+
145+
for {
146+
event := &unix.FanotifyEventMetadata{}
147+
if err := binary.Read(nr, binary.LittleEndian, event); err != nil {
148+
if err == io.EOF {
149+
return nil
150+
}
151+
select {
152+
case <-m.done:
153+
return nil
154+
default:
155+
}
156+
return fmt.Errorf("read fanotify fd: %w", err)
157+
}
158+
159+
if event.Vers != unix.FANOTIFY_METADATA_VERSION {
160+
return fmt.Errorf("fanotify version mismatch %d(got) != %d(want)",
161+
event.Vers, unix.FANOTIFY_METADATA_VERSION)
162+
}
163+
164+
if event.Fd < 0 {
165+
fmt.Fprintf(os.Stderr, "Warn: queue overflow")
166+
continue
167+
}
168+
169+
path, err := os.Readlink(fmt.Sprintf("/proc/self/fd/%d", event.Fd))
170+
if err != nil {
171+
unix.Close(int(event.Fd))
172+
continue
173+
}
174+
175+
m.mu.Lock()
176+
m.paths[path] = struct{}{}
177+
m.mu.Unlock()
178+
179+
if err := unix.Close(int(event.Fd)); err != nil {
180+
fmt.Fprintf(os.Stderr, "Warn: failed to close fd %d: %v", event.Fd, err)
181+
}
182+
}
183+
}
184+
185+
func (m *PreContainerMonitor) GetPaths() []string {
186+
m.mu.RLock()
187+
defer m.mu.RUnlock()
188+
189+
paths := make([]string, 0, len(m.paths))
190+
for path := range m.paths {
191+
paths = append(paths, path)
192+
}
193+
return paths
194+
}
195+
196+
func (m *PreContainerMonitor) Close() error {
197+
m.closeMu.Lock()
198+
defer m.closeMu.Unlock()
199+
200+
if m.closed {
201+
return nil
202+
}
203+
m.closed = true
204+
205+
close(m.done)
206+
207+
if m.file != nil {
208+
if err := m.file.Close(); err != nil {
209+
return fmt.Errorf("failed to close fanotify fd: %w", err)
210+
}
211+
m.file = nil
212+
}
213+
214+
return nil
215+
}

analyzer/option.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type analyzerOpts struct {
3131
terminal bool
3232
stdin bool
3333
waitLineOut string
34+
preMonitor bool
3435
}
3536

3637
// Option is runtime configuration of analyzer container
@@ -88,3 +89,10 @@ func WithWaitLineOut(s string) Option {
8889
opts.waitLineOut = s
8990
}
9091
}
92+
93+
// WithPreMonitor enables pre-container monitoring phase.
94+
func WithPreMonitor() Option {
95+
return func(opts *analyzerOpts) {
96+
opts.preMonitor = true
97+
}
98+
}

cmd/ctr-remote/commands/optimize.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,9 @@ func analyze(ctx context.Context, clicontext *cli.Context, client *containerd.Cl
441441

442442
// Analyze layers and get prioritized files
443443
aOpts := []analyzer.Option{analyzer.WithSpecOpts(getSpecOpts(clicontext))}
444+
if clicontext.IsSet("gpus") {
445+
aOpts = append(aOpts, analyzer.WithPreMonitor())
446+
}
444447
if clicontext.Bool("wait-on-signal") && clicontext.Bool("terminal") {
445448
return "", nil, nil, fmt.Errorf("wait-on-signal can't be used with terminal flag")
446449
}

0 commit comments

Comments
 (0)