Repository navigation
[Store] Add HealthCheck RPC and /readyz probe for master RPC readiness - #4140
Colors-111 wants to merge 5 commits into
Conversation
|
FYI, @Aionw already has a PR that removes the per-shard locking granularity. This may make the shard-level health check here unnecessary. |
Thanks for the pointer. I took a look at #3593 (bounded write-lock batches for ClearStaleHandles). It does address the core of #3940 — the sweep no longer holds the per-shard write lock across an entire shard; it now picks candidates under a shared lock and takes the exclusive lock in batches of 64 ( But the
So the probe framework is orthogonal to the sweep-locking work. Happy to adjust Open to the maintainers' call on sequencing. |
Description
Background
A production incident (#3940) observed a window where the master's RPC plane became fully unresponsive: when a store client's lease expired, the master's
ClientMonitorThreadFuncsynchronously invokedClearInvalidHandles→ClearStaleHandlesto sweep all keys/replicas owned by that client. During the sweep,ClearStaleHandlesheld the metadata shard write lock, while data-plane RPCs such asPutStart/Getneed the same shard's read lock — blocking all data-plane RPCs and driving inference-engine get/put throughput to zero for minutes.In a K8s deployment, operators want a readinessProbe to automatically remove a blocked master from Service endpoints and shift traffic to a healthy standby, plus Prometheus alerts to surface the condition. But every existing liveness/readiness mechanism fails against this "process alive, port listening, RPCs fully blocked" scenario:
tcpSocket 50051httpGet /health(admin port 9003)PingRPCclient_mutex_shared lock, not a shard lock, returns normallyServiceReadyRPCRoot cause:
Pingtakes only theclient_mutex_shared lock (MasterService::Ping),ServiceReadytakes no lock, andClearInvalidHandlesreleasesclient_mutex_before callingClearStaleHandles— none of these contend with the shard write lock, so none can be blocked by the sweep. Reliably detecting this condition requires a probe RPC that contends for the shard read lock.Design
Add a new public RPC
HealthCheckthat walks all metadata shards (kNumShards = 1024) probing read-lock availability, paired with an in-process self-probe thread, Prometheus metrics, and a/readyzendpoint. No existing RPC interface is modified —Ping/ServiceReady/ExistKeykeep their signatures and behavior;HealthCheckis purely additive.1. New RPC
HealthCheckSignature (
WrappedMasterService/MasterService, no parameters):Algorithm:
try_lock_shared()(non-blocking) on each shard'smutex.try_lock_sharedfails → retry with ~10ms backoff until success or global budget exhaustion.kShardLockBudget(default 2s, below the probe timeout of 3s).ok=true, shards_checked=1024; any shard exceeds budget → returnok=false, shards_blocked=<first blocked shard index>.try_lockcalls complete in < 5ms; only a shard held for seconds triggers failure.Register with one line in
RegisterRpcService:server.register_handler<&mooncake::WrappedMasterService::HealthCheck>( &wrapped_master_service);2. Self-probe thread (RPC probe)
Add an independent
rpc_probe_thread_toMasterAdminServer(which already runs a 10s metrics-reporting thread):rpc_probe_interval_seconds(default 5s); runs only whenservice_available_is true.HealthCheckagainstrpc_address:<rpc_port>with timeoutrpc_probe_timeout_seconds(default 3s). The probe target uses the RPC server's actual bind address:0.0.0.0/::is normalized to127.0.0.1; otherwiserpc_addressis used directly (when bound to a specific IP,127.0.0.1is unreachable).rpc_porteven when the RPC server enables RDMA).rpc_probe_ok_,rpc_probe_latency_ms_.service_available_=false(RPC server not started) → the probe thread skips →/readyzreturns 503 directly, which is correct.3. Prometheus metrics +
/readyzNew metrics in
MasterMetricManager:mooncake_master_rpc_responsivemooncake_master_rpc_probe_latency_msmooncake_master_rpc_probe_failures_totalNew
GET /readyzonMasterAdminServer:service_available_ && rpc_probe_ok_→ HTTP 200; otherwise HTTP 503. It only reads cached state — the admin thread takes no shard lock, so it returns instantly. The existingGET /healthis unchanged (process liveness, always 200).4. Configuration (new gflags, all tunable)
Design Rationale
Ping/ServiceReadyhave callers relying on their current semantics (no shard lock, always fast). Changing their lock behavior would break client contracts.HealthCheckhas a clear purpose ("all-shard read-lock liveness") and is probe-only — purely additive, zero regression risk.service_->HealthCheck()in-process saves the RPC client but bypasses the dispatch path, so it cannot detect "RPC worker pool exhausted by blocked RPCs" — a secondary symptom. Going over RPC exercises the full accept → dispatch → handler path, giving the most faithful readiness verdict — under pool exhaustionHealthCheckitself is delayed and judged unavailable, which is the desired behavior.kNumShards = 1024; a single-key probe hits ~1/1024, unable to reliably catch a specific shard's long-held write lock./readyz(drives readinessProbe; failure only removes traffic, no restart) answers "can it serve";/health(drives livenessProbe; failure restarts) answers "is the process alive". Do not use "RPC timeout" to trigger liveness restarts — it would interrupt a legitimate sweep, re-scan after restart, and likely recur.Known Limitations
HealthCheckhas no Python binding. The self-probe is an in-process C++ behavior needing no Python; a Python client wrapper for the publicHealthCheckRPC is left as follow-up.ClearStaleHandles) is Approach A in [Bug][Store]: Lease-expired client cleanup blocks all master RPC for minutes under HA + SSD Offload #3940 and should proceed separately./readyzitself only reads cached state and takes no shard lock, so it never delays under blocking.Testing
New unit tests:
HealthCheckReturnsOkWhenNoLockHeld: with no shard write lock held,HealthCheckreturnsok=true, shards_checked=1024, shards_blocked=0, completing in milliseconds.HealthCheckDetectsShardWriteLockHeld: the test fixture takes a write lock on shard 0 and holds it past the budget (2s);HealthCheckreturnsok=falsewithshards_blocked=0(the first blocked shard); after releasing the lock,HealthCheckrecovers took=true, shards_checked=1024.Module
mooncake-transfer-engine)mooncake-store)mooncake-conductor)mooncake-reshard)mooncake-ep)mooncake-pg)mooncake-integration)mooncake-p2p-store)mooncake-wheel)mooncake-common)mooncake-rl)Type of Change
How Has This Been Tested?
Test commands:
Test results:
Checklist
./scripts/code_format.shAI Assistance Disclosure
AI assisted with root-cause analysis (reading the
Ping/ServiceReady/ClearStaleHandleslock paths and thekNumShardsconstraint) and design drafting. The human submitter has reviewed every changed line and can explain end-to-end: why existing probes fail against the "process alive, port listening, RPCs fully blocked" scenario, whyHealthCheckwalks all 1024 shards rather than a single key, and why an RPC probe (vs. an in-process call) additionally covers the RPC worker-pool exhaustion symptom.