mirror of
https://github.com/kvcache-ai/Mooncake.git
synced 2026-11-05 01:17:55 +00:00
[GH-ISSUE #4856] [Bug]: [Store] Standalone master spends 20–48 s destroying its index after SIGTERM (12M–25M objects) #1235
Labels
No labels
ascend
auto-closed
bug
bug
CI/Build
Common
documentation
enhancement
feature request
good first issue
help wanted
high priority
MetaX
nightly-failure
pull-request
PyTorch Backend
question
RFC
Roadmap
stale
Store
Tests
Transfer Engine
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
kvcache-ai/Mooncake#1235
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @Juhyun-Kim-Memphis on GitHub (Oct 22, 2026).
Original GitHub issue: https://github.com/kvcache-ai/Mooncake/issues/4856
Bug Report
Summary
On SIGTERM, a standalone (non-HA)
mooncake_masterstops its RPC server within about 0.1 s and returns frommain(). The destructors then tear down the object index one object at a time on the main thread. With 12M objects this took 20.7 s from the signal to process exit, and with 25M objects 48.2 s (median of 3 runs each, currentmain). More than 90% of that time is spent destroying the index. For comparison, after SIGKILL the kernel needed 1.1 s (12M) and 2.4 s (25M) to tear down the same process.The master stops serving RPCs at the start of this window. A standalone master does not keep its index across a restart (the legacy snapshot was removed in #4406), so the result of this work is thrown away. On Kubernetes, the pod stays in Terminating for this time, and a
Recreaterollout starts the new master only after the old pod is gone, so the time adds to the period without a master. At 25M objects all three runs also took longer than the defaultterminationGracePeriodSeconds(30 s), so in a pod with the default the kubelet would end the exit with SIGKILL.The HA branch does not run this teardown: since #4745, a shutdown ends the process with
_exitafter the drain, before the service is destroyed.Related: #4666 (the planned-restart RFC; its appendix listed this item as a separate report).
Where in the code
Line numbers are for
mainat8c19b728(2026-10-22), the commit we measured.The non-HA branch waits for the signal flag, stops the RPC server and returns 0 from
main(). The objects on the stack, including theWrappedMasterServicethat owns theMasterService, are destroyed on the way out:github.com/kvcache-ai/Mooncake@8c19b7286c/mooncake-store/src/master.cpp (L1742-L1766)~MasterServicestops and joins its threads (master_service.cppL550-L612). After that, its members are destroyed, includingmetadata_shards_: 1,024 shards, each anunordered_map<TenantId, TenantState>whoseTenantStateholds anunordered_map<std::string, ObjectRecord>:github.com/kvcache-ai/Mooncake@8c19b7286c/mooncake-store/include/master_service.h (L1101-L1136)For every object this destroys the key, the
ObjectMetadata(its destructor decrements the key-count gauge,object_metadata.hL61-L66), thevector<ObjectPart>(object_metadata.hL503), each part'svector<Replica>, and for a memory replica theAllocatedBufferand itsOffsetAllocationHandle. Each of those two locks aweak_ptrto its allocator (allocator.cppL66-L75,offset_allocator.cppL531-L536). NeitherOffsetBufferAllocator::deallocatenorOffsetAllocator::freeAllocationappears in our profile. We think the allocators are already gone at that point:segment_manager_is declared aftermetadata_shards_(master_service.hL2328), so it is destroyed first. If so, the time goes to walking and freeing the nodes, not to returning space to the allocators.For comparison, in the HA branch a shutdown ends the process in
release_service()beforewrapped_master_service.reset(), throughshutdown_exitandExitAfterShutdown(_exit, "no destructors run"):github.com/kvcache-ai/Mooncake@8c19b7286c/mooncake-store/src/ha/leadership/master_service_supervisor.cpp (L733-L742)(
shutdown_exitis at L437-L444,ExitAfterShutdownininclude/shutdown_request.hL39-L42.)The non-HA signal handling came from #2359. That PR's goal was to stop the RPC server and the HTTP metadata server cleanly and to join the metrics thread. It did not discuss the index teardown.
Observed
Median of 3 runs; the range is in parentheses. Times are seconds after the signal.
Shutdown signal received)~MasterServicethreads joined (log lineClient offboarding worker stopped)HTTP metadata server stopped, which runs after the service is destroyedThe exiting process uses one core the whole time. In the 12M run 1 it used 13.5 s of user time and 5.8 s of system time. In the 25M run 1 it used 28.3 s and 15.8 s. Pinned to one NUMA node (
numactl --cpunodebind=0 --membind=0), system time dropped to 0.6 s (12M) and 1.2 s (25M). The user time stayed at about 1 µs per object.Profile (12M objects;
perf record --call-graph dwarfat 499 Hz from 1 s before the signal to exit). Themooncake_mastertarget is linked with-s, so for this run we relinked the same object files without-s; the.textsection is identical. 89.5% of the process's on-CPU samples are under:Self time: the hash-node deallocation loop 34.0%,
~AllocatedBuffer7.0%,shared_ptrcontrol-block release 6.9%, libc (freeand unresolved internals) 17.7%,~OffsetAllocationHandle3.0%, the key-count gauge 0.6%. The kernel had 30% of all samples: 24% in page faults, of which 15% are automatic NUMA balancing faults (do_numa_page, with 12.5% inmigrate_misplaced_folio) as the single exiting thread walks memory that the RPC threads allocated on both sockets, and 5% in the address-space teardown at exit (exit_mmap).For reference, we had measured 21.9 s for the same graceful exit on an older build based on upstream
245e7106, with 12.17M objects (most with a local-disk replica, 7.0M of them disk-only), real stores and a 4-CPU limit. The setups differ, but the currentmainis not faster.Setup
mainat8c19b728,CMAKE_BUILD_TYPE=Release(-O3 -DNDEBUG),-DUSE_HTTP=ON, no etcd, default glibc malloc.--rpc_port=50051 --metrics_port=9003 --enable_http_metadata_server=true --http_metadata_server_host=0.0.0.0 --http_metadata_server_port=8080 --client_ttl=60 -enable_offload=true. All other flags were defaults (standalone, offset allocator, 16 RPC threads).MountSegmentby a test client that keeps sendingPinguntil the end (metadata only, no data, no transfer engine). Objects are put withBatchPutStart/BatchPutEnd: 256 KiB each, 96-byte keys, one memory replica, no disk replicas. 12.0M and 25.0M objects use 36% and 75% of the capacity, so no eviction ran. We used the load tool attached to #4818 (--mode=segments, then--mode=fill --threads=48 --put_batch=64 --value_size=262144). Filling took 35–40 s (12M) and 80–100 s (25M).kernel.numa_balancing=1. The master ran in a container that was not CPU-limited and was not pinned, except in the two pinned runs.waiton the master's PID. The shell was the master's parent. Kubernetes was not in the loop, so kubelet and runtime overheads are not included.Limits
perf). We did not find the cause. The 12M runs stayed within 0.8 s of each other.Possible directions (suggestions only)
_exit(0). With the measurements above, the exit would then take about as long as the thread joins plus the SIGKILL time (about 2–2.5 s in these runs). We have not checked whether any destructor further down has an effect outside the process that would need to run first.MasterServiceleavemetadata_shards_undestroyed (for example, move the maps into a heap object that is intentionally not freed before process exit), and keep the rest of the destructors.Before submitting...
@github-actions[bot] commented on GitHub (Oct 22, 2026):
Thanks for opening this issue, @Juhyun-Kim-Memphis!
32131411A maintainer will triage this when possible. To help us respond faster, please include:
Useful links: Documentation · Contributing guide
@huangxiaobai-ydmy commented on GitHub (Oct 22, 2026):
Hi, I'd like to take this one, following direction 1 (end the non-HA process without destroying the index once the RPC server, the HTTP metadata server and the service threads have stopped, as the HA branch does since #4745).
I confirmed on current main that the non-HA branch returns 0 from main() after stopping the server, which destroys the full metadata_shards_ index on the way out. I'll audit the destructors that get skipped for any effect outside the process (non-HA has no etcd leases to revoke; the HTTP metadata server is stopped explicitly first) before choosing the exact exit point, and keep the explicit stop/join steps from #2359 in place so shutdown stays observable in the logs.