Skip to content

Suspending actors no longer OOM-kills tight worker pods - #3353

Merged
Tim Hockin (thockin) merged 4 commits into
agent-substrate:mainfrom
BenTheElder:snapshot-write-cap
Oct 28, 2026
Merged

Tim Hockin (thockin) merged 4 commits into
agent-substrate:mainfrom
BenTheElder:snapshot-write-cap

Conversation

@BenTheElder

Copy link
Copy Markdown
Collaborator

A suspend writes about as much page cache as the actor has in memory, and it's charged to the worker pod. In a pod sized close to its actors that runs the pod out of memory, and the kernel OOM kills the worker along with every actor on it.

Part of #3092.

  • While a microVM snapshot or gVisor checkpoint is written, its cgroup gets memory.high, so the page cache can only grow into what the actor booked and isn't using, plus what the pod has spare to lend. If the pod can lend all of it there's no limit, so pods with room still suspend at full speed. Tight pods slow down to disk speed instead.
  • A worker writes at most 2 snapshots at once, and a newly placed actor waits for lent memory to be written back before it boots
  • The rootfs upper and durable-dir tars write out and drop their page cache every 8MiB, so they can't pile up dirty pages either. This waits on the disk, so it gives back some of the speed tarutil: drop fsync of rootfs-upper and durable-dir tars (#1954) #3026 got by dropping the fsync. Happy to tune it.
  • microVM actor overhead goes from 128MiB back to 4MiB + 8MiB/GiB, the 128MiB in MicroVM guests get their whole memory limit, and placement books each actor's runtime overhead #3104 was headroom for these writes. Both workers keep 32MiB plus 16MiB per write slot (64MiB) for themselves.

Tested on GKE with a fresh install:

  • A 1GiB microVM actor filled to 893MiB in a 1132Mi pod survived 4/4 suspend/resume cycles
  • four 256Mi gVisor actors filled to 225MiB in a 1088Mi pod survived 6/6 concurrent suspend waves.
  • Without the cap that gVisor case OOM-killed the worker. Neither worker restarted.

AI use: AI-assisted, I reviewed and tested it.

  • Tests pass
  • Docs updated

while CH writes a snapshot the actor's cgroup gets memory.high at what it booked plus what the pod can lend, or no limit when the pod can lend the whole write. a placement that would grow into lent memory waits for writeback. also write out tar archives as we go.

Signed-off-by: Benjamin Elder <[email protected]>
the checkpoint cgroup's page cache gets the same limit and lending as microVM snapshot writes, and at most two checkpoints write at once.

Signed-off-by: Benjamin Elder <[email protected]>

@thockin Tim Hockin (thockin) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to approve because it seems better than the alternative (doing nothing). But I think we need a more holistic approach to this.

Broadly speaking, actors have a lifecycle, and the nodes and edges of that lifecycle need varying amounts of resources. I think we can find a cleaner way to manage resources that feels like like whack-a-mole and is still "safe" in the context of cgroups.

return io.CopyBuffer(writerOnly{dst}, readerOnly{src}, *bp)
}

// writeBehindChunk is how much of an archive may be dirty in the page cache

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FTR I am somewhat concerned that we don't FULLY understand the consequences of trying to manage this so tightly. I feel like there SHOULD BE more holistic approaches to this, rahter than piecemeal.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but we're already doing this for snapshot/resume. I think we do need bursting for performance and limiting for safety but we have to get the right balance between these. They were added incrementally late in the cycle. I think we can carefully evolve these without a totally breaking change (in particular it may be slightly breaking in that the same actor : worker pod mix may not pack as densely)

}

// An archive bigger than writeBehindChunk is written out a chunk at a time.
func TestRoundTripLargeFile(t *testing.T) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but we really need some focused benchmarks to measure the impact of these sorts of changes? We need to identify and isolate variables and see how manipulating them changes the throughput and the footprint.

E.g. convince me that using O_DIRECT is a worse answer.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was talking to our scalability / density team about this PR stack a bit, ACK to follow-up on that.

// writeBehindChunk is how much of an archive may be dirty in the page cache
// before we write it out and drop it. Dirty pages can't be reclaimed, and
// piling up a whole archive of them can run a worker pod out of memory.
const writeBehindChunk = 8 << 20

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like it should be a field in type writeBehind, even if we don't plumb it all the way down from the caller yet

if l.PodMemory != nil {
return l.PodMemory()
}
return cgroup.PodMemory()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, for later -- this deeply rooted knowledge of Kubernetes may turn out to be a liability. Kube stuff evolves. We should (followup) to make this a bit more abstract to figure out more generally what resources are available and manage those without knowing "it's a Pod"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is internal to ateworker, do we expect ateworker to run outside of pods?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably not, but why couple if we don't need to? E.g. would it be WRONG to run an ateworker on a non-kube VM? I don't think it would be wrong (even if it lacked some nice things)


mu sync.Mutex
bytes int64
// returned is closed and replaced whenever bytes drops

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a very odd contract - why not just send a message (e.g. the delta)?

type Ledger struct {
// Unbooked is the worker's reported memory capacity, for a pod memory
// limit, less what its hosted actors booked.
Unbooked func(podLimit int64) int64

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen agents choosing this pattern of using function pointers rather than defining an interface. I wonder why? I would have written a 2-method interface and injected it here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what we should do is add an agents.md rule for this, we'll be playing whack-a-mole otherwise. I agree on the interface, but wasn't super worried about this WRT correctness / relatively readable.

@thockin

Copy link
Copy Markdown
Collaborator

We need to find a way to get some pre-merge scale testing on critical PRs like this. Max Smythe (@maxsmythe) Aditya Shantanu (@aditya-shantanu)

@thockin
Tim Hockin (thockin) added this pull request to the merge queue Oct 28, 2026
@BenTheElder

Copy link
Copy Markdown
Collaborator Author

Agreed. I did some testing but it's all adhoc inbetween everything else. I think these values will have to be tuned in follow-ups.

I would still really like something easier to use than our benchmark harness that can be used on PRs with a personal developer cluster reasonably, so at minimum when I'm submitting or reviewing these I can run a stable test to A/B across PRs. We've been evolving the benchmarks rapidly still and the tooling we have is not easy to just point at an existing small test cluster.

cc Walter Fender (@cheftako) we've talked about this for the uVM team as well.

Merged via the queue into agent-substrate:main with commit e8cdf13 Oct 28, 2026
5 checks passed
@BenTheElder
Benjamin Elder (BenTheElder) deleted the snapshot-write-cap branch October 28, 2026 19:03
Mourad (md-mq) pushed a commit to polyaxon/substrate that referenced this pull request Oct 29, 2026
… down its worker (agent-substrate#3429)

A microVM actor's cloud-hypervisor and virtiofsd run in the worker pod
with no memory limit of their own.

If they grow past what the actor booked, they take the pod to its limit,
and the OOM kill takes out the worker and every actor on it.

agent-substrate#3106 fixed this for gVisor, this PR fixes it for microVM.

Part of agent-substrate#3092.

- each actor's cgroup gets `memory.high` at what it books (its memory
limit plus the runtime overhead from agent-substrate#3104). The guest can't use more
than its memory limit, so anything past that is cloud-hypervisor or
virtiofsd.
- the worker kills a cgroup stuck at its `memory.high` with
`cgroup.kill`, using the same rule as agent-substrate#3106, and reports the actor
crashed with "killed for exceeding its memory limit". Other actors keep
running.
- the watch pauses during snapshot writes. When the pod can lend the
whole write, the cgroup's `memory.high` is lifted for the write and
restored after, same as agent-substrate#3353.
- without PSI the worker sets no limit and warns at startup, like gVisor
- the gVisor watch from agent-substrate#3106 moves into a shared `cgroup.MemoryGuard`
that both workers use, gVisor's behavior is unchanged

Tested on GKE with a fresh install:
- a 1GiB actor filled to 925MiB peaked at 1007MiB against a
`memory.high` of 1036MiB, and survived 3/3 suspend/resume cycles with no
kills
- a 400MiB process added to one 512Mi actor's cgroup got that actor
killed and reported crashed. Its neighbour kept answering and the worker
didn't restart.

AI use: AI-assisted, I reviewed and tested it.

- [x] Tests pass
- [x] Docs updated

---------

Signed-off-by: Benjamin Elder <[email protected]>
Max Smythe (maxsmythe) pushed a commit to maxsmythe/substrate that referenced this pull request Nov 3, 2026
…upper back from disk (agent-substrate#3485)

Why: In the microVM burn-in (GKE downstream testing), wake p50 went from
~350ms to ~1.1s (!), almost all of it in upper_join. Reported to me by
@annapendleton though I'm not sure who originally found it.

agent-substrate#3353 started writing out snapshot tars as they're written, so their
dirty pages can't run a tight worker pod out of memory. It also dropped
the written pages from the page cache, so a resume on the same node read
the rootfs upper tar back from disk ... which is slow. It also made
every suspend write the tars at disk speed.

Fixes by commit order:
1. Pull summing memory.stat keys out of memoryUsed, no behavior change.
2. Clean pages are reclaimable, so dropping them is irrelevant for OOM
safety.
- The tars now stay cached. Write-out waits on the previous chunk
instead of the one just written, so dirty pages stay at about two chunks
without stalling on each one.
4. Start snapshot writeback after the OnDemand delta merge, which
replaces memory-ranges, so it tracks the merged file instead of the
unlinked delta.
5. Write snapshot files back in the background instead of with a
blocking fdatasync.
- Starting write-out can block for a long time on a busy disk
(https://github.com/torvalds/linux/blob/v6.12/fs/sync.c#L315-L317), and
holding deleted snapshot files open meanwhile leaked disk under load, so
it starts write-out once, 64MiB at a time, and closes a file as soon as
it's unlinked, which drops its dirty pages.
7. What snapshot writes can borrow leaves clean page cache out, so
cached tars don't make a pod look "full".
- A worker pod without a memory limit (e.g. the deploy_locust.sh
default) has nothing to protect, so it lends all of it instead of
capping every suspend as if the pod were full.
9. When the pod can lend memory for the whole archive, the tar skips the
write-behind and goes to the page cache at full speed, and the loan
comes back once it's on disk. Otherwise the write-behind still bounds
its dirty pages.

That said, with respect to workers without limits: We should probably
consider recommending memory limits on worker pods. I will think about
it overnight and propose docs updates tomorrow.

Tested with locust burn-in tests (see comments in the PR).

AI use: AI-assisted, I reviewed and tested it.

- [x] Tests pass
- [x] Docs updated

---------

Signed-off-by: Benjamin Elder <[email protected]>
Eitan Yarmush (EItanya) pushed a commit to kagent-dev/substrate that referenced this pull request Nov 3, 2026
…upper back from disk (agent-substrate#3485)

Why: In the microVM burn-in (GKE downstream testing), wake p50 went from
~350ms to ~1.1s (!), almost all of it in upper_join. Reported to me by
@annapendleton though I'm not sure who originally found it.

agent-substrate#3353 started writing out snapshot tars as they're written, so their
dirty pages can't run a tight worker pod out of memory. It also dropped
the written pages from the page cache, so a resume on the same node read
the rootfs upper tar back from disk ... which is slow. It also made
every suspend write the tars at disk speed.

Fixes by commit order:
1. Pull summing memory.stat keys out of memoryUsed, no behavior change.
2. Clean pages are reclaimable, so dropping them is irrelevant for OOM
safety.
- The tars now stay cached. Write-out waits on the previous chunk
instead of the one just written, so dirty pages stay at about two chunks
without stalling on each one.
4. Start snapshot writeback after the OnDemand delta merge, which
replaces memory-ranges, so it tracks the merged file instead of the
unlinked delta.
5. Write snapshot files back in the background instead of with a
blocking fdatasync.
- Starting write-out can block for a long time on a busy disk
(https://github.com/torvalds/linux/blob/v6.12/fs/sync.c#L315-L317), and
holding deleted snapshot files open meanwhile leaked disk under load, so
it starts write-out once, 64MiB at a time, and closes a file as soon as
it's unlinked, which drops its dirty pages.
7. What snapshot writes can borrow leaves clean page cache out, so
cached tars don't make a pod look "full".
- A worker pod without a memory limit (e.g. the deploy_locust.sh
default) has nothing to protect, so it lends all of it instead of
capping every suspend as if the pod were full.
9. When the pod can lend memory for the whole archive, the tar skips the
write-behind and goes to the page cache at full speed, and the loan
comes back once it's on disk. Otherwise the write-behind still bounds
its dirty pages.

That said, with respect to workers without limits: We should probably
consider recommending memory limits on worker pods. I will think about
it overnight and propose docs updates tomorrow.

Tested with locust burn-in tests (see comments in the PR).

AI use: AI-assisted, I reviewed and tested it.

- [x] Tests pass
- [x] Docs updated

---------

Signed-off-by: Benjamin Elder <[email protected]>
(cherry picked from commit a6442f9)
Signed-off-by: Eitan Yarmush <[email protected]>
Dmitry Berkovich (dberkov) pushed a commit that referenced this pull request Nov 3, 2026
…upper back from disk (#3485)

Why: In the microVM burn-in (GKE downstream testing), wake p50 went from
~350ms to ~1.1s (!), almost all of it in upper_join. Reported to me by
@annapendleton though I'm not sure who originally found it.

#3353 started writing out snapshot tars as they're written, so their
dirty pages can't run a tight worker pod out of memory. It also dropped
the written pages from the page cache, so a resume on the same node read
the rootfs upper tar back from disk ... which is slow. It also made
every suspend write the tars at disk speed.

Fixes by commit order:
1. Pull summing memory.stat keys out of memoryUsed, no behavior change.
2. Clean pages are reclaimable, so dropping them is irrelevant for OOM
safety.
- The tars now stay cached. Write-out waits on the previous chunk
instead of the one just written, so dirty pages stay at about two chunks
without stalling on each one.
4. Start snapshot writeback after the OnDemand delta merge, which
replaces memory-ranges, so it tracks the merged file instead of the
unlinked delta.
5. Write snapshot files back in the background instead of with a
blocking fdatasync.
- Starting write-out can block for a long time on a busy disk
(https://github.com/torvalds/linux/blob/v6.12/fs/sync.c#L315-L317), and
holding deleted snapshot files open meanwhile leaked disk under load, so
it starts write-out once, 64MiB at a time, and closes a file as soon as
it's unlinked, which drops its dirty pages.
7. What snapshot writes can borrow leaves clean page cache out, so
cached tars don't make a pod look "full".
- A worker pod without a memory limit (e.g. the deploy_locust.sh
default) has nothing to protect, so it lends all of it instead of
capping every suspend as if the pod were full.
9. When the pod can lend memory for the whole archive, the tar skips the
write-behind and goes to the page cache at full speed, and the loan
comes back once it's on disk. Otherwise the write-behind still bounds
its dirty pages.

That said, with respect to workers without limits: We should probably
consider recommending memory limits on worker pods. I will think about
it overnight and propose docs updates tomorrow.

Tested with locust burn-in tests (see comments in the PR).

AI use: AI-assisted, I reviewed and tested it.

- [x] Tests pass
- [x] Docs updated

---------

Signed-off-by: Benjamin Elder <[email protected]>
(cherry picked from commit a6442f9)
Signed-off-by: Eitan Yarmush <[email protected]>
Anna Pendleton (annapendleton) added a commit to annapendleton/substrate that referenced this pull request Nov 4, 2026
After agent-substrate#3485 (a6442f9), every snapshot checkpoint called snapshotcache.StartWriteback unconditionally, even on worker pods without a memory limit.

In workloads that suspend and resume frequently (such as glutton_mem_512mi_1_user_gvisor_pause), atelet deletes the previous local snapshot as soon as the next checkpoint completes (~1.9s later). Without forced writeback, deleting that short-lived snapshot drops its unwritten pages in memory without touching the disk. Calling StartWriteback unconditionally forced all 520 MiB of every checkpoint onto the 175 MB/s disk and held the files open until writeback finished, saturating the disk and stalling gvisor_sentry for ~750ms per suspend (SuspendActorAsync p50 ~1,400ms vs ~410ms before agent-substrate#3353).

Move the StartWriteback call into Ledger.Writeback(paths, n), which returns n immediately when the pod has no memory limit (or n <= 0) and only flushes to disk when a memory-limited pod has borrowed memory to reclaim.
Anna Pendleton (annapendleton) added a commit to annapendleton/substrate that referenced this pull request Nov 4, 2026
In the gVisor pause benchmark (glutton_mem_512mi_1_user_gvisor_pause),
SuspendActorAsync p50 regressed from ~390-430 ms (before agent-substrate#3353) to
~1300-1500 ms (after agent-substrate#3353 / agent-substrate#3485).

Each 512 MiB checkpoint writes ~520 MiB into the kernel page cache on a
175 MB/s persistent disk. Because atelet only deletes the previous
checkpoint after the next checkpoint finishes (~1.8 s later), calling
sync_file_range(SYNC_FILE_RANGE_WRITE) immediately on every checkpoint
causes a 3.0 s disk flush that overlaps the next checkpoint at t = 1.4 s
and stalls gvisor_sentry for ~750-1,000 ms in balance_dirty_pages.

Move post-checkpoint writeback behind Ledger.Writeback(paths, n):
- Without a pod memory limit, return n immediately without starting
  writeback.
- With a pod memory limit, poll paths (every 25ms..500ms) so unlinked
  checkpoints close their FDs and drop dirty pages in RAM promptly, and
  only force write-out via sync_file_range when the pod has less than n
  of unbooked or free memory left.
Anna Pendleton (annapendleton) added a commit to annapendleton/substrate that referenced this pull request Nov 4, 2026
In the gVisor pause benchmark (glutton_mem_512mi_1_user_gvisor_pause),
SuspendActorAsync p50 regressed from ~390-430 ms (before agent-substrate#3353) to
~1300-1500 ms (after agent-substrate#3353 / agent-substrate#3485).

Each 512 MiB checkpoint writes ~520 MiB into the kernel page cache on a
175 MB/s persistent disk. Because atelet only deletes the previous
checkpoint after the next checkpoint finishes (~1.8 s later), calling
sync_file_range(SYNC_FILE_RANGE_WRITE) immediately on every checkpoint
causes a 3.0 s disk flush that overlaps the next checkpoint at t = 1.4 s
and stalls gvisor_sentry for ~750-1,000 ms in balance_dirty_pages.

Move post-checkpoint writeback behind Ledger.Writeback(paths, n) so
write-out is only started when the pod has less than n of unbooked or
free memory left.
Anna Pendleton (annapendleton) added a commit to annapendleton/substrate that referenced this pull request Nov 4, 2026
In the gVisor pause benchmark (glutton_mem_512mi_1_user_gvisor_pause),
SuspendActorAsync p50 regressed from ~390-430 ms (before agent-substrate#3353) to
~1300-1500 ms (after agent-substrate#3353 / agent-substrate#3485).

Each 512 MiB checkpoint writes ~520 MiB into the page cache. atelet only
deletes the previous checkpoint after the next one finishes, so starting
write-out of every checkpoint right away keeps the disk busy flushing
files that are about to be deleted, and the next checkpoint stalls behind
it in balance_dirty_pages.

StartWriteback now takes a flush func, and only starts write-out on a
pass when it returns true. The callers pass Ledger.Tight, which reports
whether the pod has less than the borrowed amount of unbooked or free
memory left. While the pod has room, dirty pages stay cached and are
dropped when the file is unlinked; once it's tight, they're written out
as before so placements waiting on the loan can boot.
Anna Pendleton (annapendleton) added a commit to annapendleton/substrate that referenced this pull request Nov 4, 2026
In the gVisor pause benchmark (glutton_mem_512mi_1_user_gvisor_pause),
SuspendActorAsync p50 regressed from ~390-430 ms (before agent-substrate#3353) to
~1300-1500 ms (after agent-substrate#3353 / agent-substrate#3485).

Each 512 MiB checkpoint writes ~520 MiB into the page cache. atelet only
deletes the previous checkpoint after the next one finishes, so starting
write-out of every checkpoint right away keeps the disk busy flushing
files that are about to be deleted, and the next checkpoint stalls behind
it in balance_dirty_pages.

StartWriteback now takes a flush func, and only starts write-out on a
pass when it returns true. The callers pass Ledger.Tight, which reports
whether the pod has less than the borrowed amount of unbooked or free
memory left. While the pod has room, dirty pages stay cached and are
dropped when the file is unlinked; once it's tight, they're written out
as before so placements waiting on the loan can boot.
Anna Pendleton (annapendleton) added a commit to annapendleton/substrate that referenced this pull request Nov 4, 2026
In the gVisor pause benchmark (glutton_mem_512mi_1_user_gvisor_pause),
SuspendActorAsync p50 regressed from ~390-430 ms (before agent-substrate#3353) to
~1300-1500 ms (after agent-substrate#3353 / agent-substrate#3485).

Each 512 MiB checkpoint writes ~520 MiB into the page cache. atelet only
deletes the previous checkpoint after the next one finishes, so starting
write-out of every checkpoint right away keeps the disk busy flushing
files that are about to be deleted, and the next checkpoint stalls behind
it in balance_dirty_pages.

StartWriteback now takes a flush func, and only starts write-out on a
pass when it returns true. The callers pass Ledger.Tight, which reports
whether the pod has less than the borrowed amount of unbooked or free
memory left. While the pod has room, dirty pages stay cached and are
dropped when the file is unlinked; once it's tight, they're written out
as before so placements waiting on the loan can boot.
Anna Pendleton (annapendleton) added a commit to annapendleton/substrate that referenced this pull request Nov 4, 2026
In the gVisor pause benchmark (glutton_mem_512mi_1_user_gvisor_pause),
SuspendActorAsync p50 regressed from ~390-430 ms (before agent-substrate#3353) to
~1300-1500 ms (after agent-substrate#3353 / agent-substrate#3485).

Each 512 MiB checkpoint writes ~520 MiB into the page cache. atelet only
deletes the previous checkpoint after the next one finishes, so starting
write-out of every checkpoint right away keeps the disk busy flushing
files that are about to be deleted, and the next checkpoint stalls behind
it in balance_dirty_pages.

StartWriteback now takes a flush func, and only starts write-out on a
pass when it returns true. The callers pass Ledger.Tight, which reports
whether the pod has less than the borrowed amount of unbooked or free
memory left. While the pod has room, dirty pages stay cached and are
dropped when the file is unlinked; once it's tight, they're written out
as before so placements waiting on the loan can boot.
Anna Pendleton (annapendleton) added a commit to annapendleton/substrate that referenced this pull request Nov 4, 2026
atelet only deletes the previous checkpoint after the next one finishes, so starting write-out of every checkpoint right away (agent-substrate#3353 / agent-substrate#3485) keeps the disk busy writing ~520 MiB that is about to be unlinked, and the next gVisor checkpoint stalls behind it in balance_dirty_pages.

StartWriteback now takes a flush func and only starts write-out on a pass when it returns true. Callers pass Ledger.Tight, which reports whether the pod has less than the borrowed amount of unbooked or free memory left.

Signed-off-by: Anna Pendleton <[email protected]>
Anna Pendleton (annapendleton) added a commit to annapendleton/substrate that referenced this pull request Nov 4, 2026
atelet only deletes the previous checkpoint after the next one finishes, so starting write-out of every checkpoint right away (agent-substrate#3353 / agent-substrate#3485) keeps the disk busy writing ~520 MiB that is about to be unlinked, and the next gVisor checkpoint stalls behind it in balance_dirty_pages.

StartWriteback now takes a flush func and only starts write-out on a pass when it returns true. Callers pass Ledger.Tight, which reports whether the pod has less than the borrowed amount of unbooked or free memory left.

Signed-off-by: Anna Pendleton <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/gvisor area/microVM prio/P0 Highest priority / required for next milestone

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants