Repository navigation
[Store] Delete the mooncake master's mass client expiry, cause and outcome - #13
Conversation
master_bench mounts every segment before the load starts, so the one control-plane event a production master actually has to absorb while it is loaded -- a store pod joining and mounting its segment -- cannot be expressed. A master that stops answering Ping for longer than client_live_ttl_sec expires every client it has, unmounts their segments and erases the keys those segments held, so how long a mount holds the segment lock on a loaded master decides whether the whole cache survives. Add the event and the two views needed to read it: - --mount_extra_at_sec / --extra_segment_size: at that second of the load phase a further client connects and mounts one more segment. It is constructed outside segment_clients_mutex, because holding the mutex the ping thread walks would stop this benchmark's own pings for the mount's duration -- an observer manufacturing the symptom it is measuring. - --metrics_server / --metrics_csv / --metrics_names: a once-per-second scrape of the master's own metrics endpoint into a long-format CSV, plus the scrape's own latency, which is itself a reading when the master is stalled. --metrics_names defaults to empty and keeps every metric exported; a guessed name that the master does not export reads exactly like a metric that never moved. - --ping_csv: each client's Ping outcome and latency, and each remount's latency. The master-side scrape cannot show a Ping that never reached the master, which is the observation this is for. Ping and remount failures are now recorded and returned from rather than thrown out of the ping thread. Throwing loses the only client observing the master at exactly the moment of interest, and the master answering again afterwards -- the recovery -- is part of what has to be measured. Every default keeps the previous behaviour: no extra mount, no sampler, no ping log. Upstream: none (not submitted yet)
Two gaps that stop master_bench from standing in for a serving cell's master. A client only enters the master's ok_client_ set by remounting, and a remount of a segment that already holds replicas fails: with HA off, standby_accounted_memory_bytes_ is empty, so ReMountSegment returns INTERNAL_ERROR for every segment whose descriptors it had to restore. The existing bench mounts and starts load in the same breath, so its clients never register, master_active_clients stays 0 for the whole run, and the master has nothing it could expire -- the state a real cell is in for its entire life is unreachable. --wait_register_sec waits for the first Ping to come back OK on every client before the load barrier, which is the order a store fleet starts in. It defaults to 0, the previous behaviour. BatchExistKey is the request a hicache client actually makes -- one lookup per prefix-cache probe -- and it dominates master traffic on a serving cell, but the bench could only Put and Get. Add it as an --operation, prefilling like the other read operations do. Upstream: none (not submitted yet)
One thread pinging every client in turn cannot tell a master that stopped answering from a pinger blocked on the first client in its list: when the master holds an exclusive lock, the shared pinger stalls inside client 0's Ping and never issues client 1..N's at all, so their silence is the benchmark's own and not the master's. A store pod pings on its own timer, and whether every client independently observes the stall is exactly the question when the master's response to that silence is to expire all of them. --independent_ping gives each client its own 1 Hz ping thread. Off by default; the shared pinger is unchanged and still the one that runs. Upstream: none (not submitted yet)
…tored replicas pending (kvcache-ai#3517) (kvcache-ai#3533) Backport of upstream c5635ff. ReMountSegment walked all 1024 metadata shards while holding client_mutex_ and snapshot_mutex_ both exclusively, and Ping needs client_mutex_ shared, so no client could heartbeat for the duration of the scan. The scan only has work to do for segments whose standby-restored replicas are still accounted, which never happens without HA -- so on a non-HA master it consumed 0.72 s at 1M keys and 13.64 s at 18M keys to produce nothing, against a 10 s client_ttl. Co-authored-by: jiayuzailiu <jiayuzailiu@tencent.com> Upstream: kvcache-ai#3533 (cherry picked from commit c5635ff)
…n lock
A master expires a client by unmounting its segments and erasing its keys, and
with no snapshot and no HA that is unrecoverable -- one bad tick costs a cell
its whole L3 cache. ClientMonitorFunc had no way to tell "my clients died" from
"I failed to process their heartbeats", and the second is what actually
happens: an exclusive lock held across a key-count-proportional scan stops the
master servicing Ping at all, so client after client lapses with nothing wrong
on their side.
The absence of heartbeats cannot separate those two, because it is the same
observation either way. So measure the cause. Ping needs client_mutex_ shared,
which makes an exclusive holder of that lock the one thing that can stop the
handler running -- and the master knows when it holds its own lock. Every site
that takes client_mutex_ exclusively now records its hold window into atomics
(ReMountSegment, UpdateClientHostId, ResetStateAfterFailedRestoreAttempt), and
the monitor holds an expiry back when a recorded hold overlapped
[newest_expired_deadline - client_ttl, now], the window in which the newest
candidate's heartbeat could have been refreshed and was not. ClientMonitorFunc's
own expiry branch takes the lock exclusively too and is deliberately not
instrumented: a previous tick's expiry work must not come back as evidence that
the master was stalling.
The master's silence stays as weaker, secondary evidence, because an exclusive
hold is not the only way to starve heartbeats -- io threads all blocked on
snapshot_mutex_ or on shard locks starve Ping too, and record no hold. It is
gated on the monitor tracking at least two clients. Silence says something
about the master only when there was somebody else who could have spoken for
it; with one tracked client, "nobody pinged" and "that client died" are the
same sentence, and reading it as a stall would hold back every store pod
restart in a single-client cell for the whole grace.
The two are OR-ed, so the breaker errs towards holding back: being wrong that
way costs client_mass_expiry_grace_sec of stale segments, being wrong the other
way costs the cell its cache. The cases, all of them covered by tests:
1 client, it dies, master healthy expire now
1 client, a ReMountSegment stall hold back
13 clients, one stall split 6/7 across ticks hold back
13 clients, one stall, split 1/12 hold back
13 clients, healthy master, a majority die expire now
13 clients, healthy master, whole fleet dies hold back, then expire at
the grace bound
The last row is the price of keeping the silence evidence, and it is not
avoidable: a fleet that has entirely gone and a master that has entirely
stalled produce the identical heartbeat stream. The cost falls on segments
whose owners are all gone, and it is bounded by the grace.
The residual gap is a single-client cell starved by something other than a
client_mutex_ hold -- all io threads busy on shard locks, say. No hold is
recorded and one tracked client is not evidence, so that cell's keys go.
Closing it needs a liveness signal off the data path, which this does not
build.
The share of clients expiring in one tick, which an earlier version of this
used, is gone. It fires only when the master has positive evidence it served
heartbeats after those deadlines lapsed -- which is exactly when the expiring
clients really are dead -- so it held back a genuine rack loss while the master
was demonstrably healthy, and it was defeated in the other direction by where a
tick boundary fell. That also cuts the tunables to two.
Holding back is doing nothing at all: client_ttl keeps its entry, no unmount is
prepared and no stale-handle sweep runs, and no deadline is refreshed by hand.
Recovery is the monitor's own queue drain, so a client whose heartbeat lands
during the deferral stops being a candidate on its own, and the grace stays a
bound on the whole episode rather than on the interval between refreshes.
The judgement itself is a pure function of one tick's readings
(EvaluateClientMassExpiry), so its boundary cases are a table-driven test with
no sleeping, and the grace is compared on raw durations rather than on whole
seconds.
client_mass_expiry_guard, default true, is the named off switch; it and a zero
client_mass_expiry_grace_sec both log a startup warning, because they are
silent-disable values. The grace's validator predicate is shared with the
config-file path, which the DEFINE_validator does not reach.
master_client_expiry_deferred_total counts the ticks held back;
master_client_expiry_deferred_clients is how many clients are held back right
now, published as a delta so two MasterService instances in one process
compose, and taken back on every exit from the monitor loop.
Upstream: none (not submitted yet)
4a5b3e2 to
7f5e4ca
Compare
…r owes The breaker deferred an expiry when a recorded exclusive client_mutex_ hold overlapped [newest_expired_deadline - client_ttl, now]. That left edge is pinned to one instant -- the candidate's own last heartbeat pop -- and never advances while now does, so any hold that ever fell inside a dying client's last ttl window stayed evidence for the whole grace. A routine few-millisecond mount by another client was enough: a client that genuinely died had its segments kept for 60 s while every other client's heartbeats were flowing and proving the master healthy. The window is now [last_ping_batch_at, now], the stretch this master has no heartbeat of its own to show for. A completed hold counts only while no heartbeat has been popped since it released; once one has, the master is demonstrably serving Ping again, that hold is refuted as an explanation, and a candidate still overdue is genuinely gone. A hold in progress still counts unconditionally, because it is stopping heartbeats at this instant. The narrowing cannot lose keys. It never widens the window and never narrows below the old one where the old one was doing real work: a candidate's last pop is by definition at or before last_ping_batch_at, and the two coincide exactly when the newest candidate is the last client the master heard from -- the single-client cell and the whole fleet, which keep the protection they had. It is the heartbeat stream and not the wall clock for the same reason: after a hold releases the master can stay starved for many more ticks, and through that no heartbeat is popped, so last_ping_batch_at does not advance and the released hold stays evidence. A window of "holds within the last tick or two" would have aged out mid-stall and erased the cache. client_live_ttl is no longer an input to the judgement, so it is gone from ClientMassExpiryInputs. No flag, metric or log field changes. Two wall-clock cases had encoded the pinned edge and now say what the rule does: the split-across-ticks case stalls the master with a real exclusive hold instead of leaning on another client's released registration hold, and the growing-candidate-set case gets a ttl longer than its own stagger, because under the new rule each group's registration heartbeat is proof the master is serving and would expire the group before it. The pure decision table gains the stale-hold row this change is about, the recovery-gap row that must still defer, and per-row expectations for exclusive_hold_overlap and master_silent so a row says which condition carried it.
|
(B) 를 upstream 에 제출했습니다: kvcache-ai#3936 (draft). upstream/main 리베이스에서 실제로 손이 간 곳:
upstream 기준 검증: 빌드 무경고, upstream 제출용으로 뺀 것: 사내 이슈 키·모델·클러스터·노드 이름 전부, 그리고 upstream 템플릿이 500 LOC 초과 변경에 RFC 이슈를 먼저 요구하는데, 이 변경은 1637 insertions (테스트·fixture 985, 프로덕션 코드 약 660) 이라 해당됩니다. 체크박스를 비워 두고 "원하면 RFC 를 먼저 열고 이 PR 을 그 뒤에 세우겠다"고 본문에 적었습니다. |
|
upstream 템플릿의 500 LOC 규칙에 따라 RFC 이슈를 먼저 열었습니다: kvcache-ai#3937. upstream PR kvcache-ai#3936 본문은 두괄식으로 다시 써서 RFC 를 참조하도록 교체했고, RFC 에 문제 정의·대안 비교·행동 변화·메인테이너 질문 4개를 실었습니다. |
2026-09-05
solar-pro4-prod사고 — store 파드 한 대가 붙어 480 GB 세그먼트를remount 하는 동안 마스터가 멈추고, 그 침묵이
client_ttl(기본 10초)을 넘겨멀쩡한 클라이언트 12개가 한꺼번에 만료되고 키 13.9M 개가 지워진 것 — 의
원인과 결과를 각각 하나씩 지운다. 스냅샷도 HA도 없으니 만료는 되돌릴 수 없다.
INF-461 P2.
커밋 두 개는 일부러 분리해 뒀다. (A) 는 upstream 백포트라 sync merge 때
자동으로 빠지고, (B) 는 우리 고유 수정이라 나중에 upstream 에 따로 낼 수
있어야 한다.
[Store] Skip full metadata scan in ReMountSegment…c5635ff3(#3533) 백포트.ReMountSegment의 1024-shard 순회를any_standby_kept_alive뒤로 가둔다[Store] Hold back a client mass expiry whose cause is the master's own lockClientMonitorFunc의 mass-expiry 서킷 브레이커 + 플래그 2개 + 지표 2개 + 유닛 테스트앞의 세 커밋(
[Bench] …)은 재현 하네스이고 PR #12 에 있던 것을 그대로 옮겨온것이다. #12 는 이 PR 로 대체하고 닫는다.
(A) — 원인을 지운다
enable_ha=0인 우리 셀에서standby_accounted_memory_bytes_는 항상 비어있으므로, 그 순회는 전량 낭비였다. cherry-pick 은 충돌 없이 붙었고, 코드는
upstream 과 바이트 단위로 같다(주석 한 줄만 (B) 에서 붙였다).
측정(리그, 프로덕션과 같은 8 CPU / 16 rpc 스레드 형상):
(B) — 결과를 지운다
만료는 세그먼트를 언마운트하고 키를 지운다. 그러니 마스터는 "내 클라이언트가
죽었다" 와 "내가 하트비트를 처리하지 못했다" 를 구분해야 한다. 이 판단
규칙은 이번이 세 번째다. 앞의 두 개는 모두 증상 — 하트비트가 오지 않는다는
사실 — 을 근거로 삼았고, 그래서 둘 다 반증됐다.
달려 있다. 한 번의 stall 이 만료를 연속된 tick 에 나눠 놓으면 각 tick 은
소수만 들고 있어 임계값을 못 넘는다. 게다가 이 조건은 마스터가 "그 deadline
이후에도 하트비트를 처리했다" 는 적극적 증거를 가진 tick 에서만 발동하는데,
그건 정확히 만료 대상이 진짜로 죽은 상황이다. 즉 진짜 rack 단위 소실을
유예하고, 진짜 stall 은 놓친다.
클라이언트" 가 포함되면 구조적으로 항상 참이다(deadline = 그 tick 의
now + ttl,
last_ping_batch_at= 같은 tick 의 now). 클라이언트가 하나인셀에서는 언제나 그렇고, 그래서 평범한 store 파드 재시작이 grace 60초
내내 유예됐다.
solar-pro4-bf16gqa가 지금 1-클라이언트 셀이다.증상으로는 "내가 처리하지 못했다" 와 "아무도 보내지 않았다" 를 가를 수 없다.
같은 관측이기 때문이다. 그래서 원인을 측정한다.
Ping핸들러가 하는 일은ok_client_의 shared-lock 읽기와 lock-free 큐push 뿐이다. 따라서
client_mutex_를 exclusive 로 잡은 쪽만이 이 핸들러의실행을 막을 수 있고, 마스터는 자기가 그 락을 잡고 있는 시점을 안다.
1차 조건 — exclusive
client_mutex_hold 가[newest_expired_deadline - client_ttl, now]와 겹쳤는가. 그 구간이 "가장늦게 만료된 후보의 하트비트가 갱신될 수 있었는데 갱신되지 않은 창" 이다.
client_mutex_를 exclusive 로 잡는 모든 지점이 hold 구간을 relaxed atomic세 개에 기록한다(
ReMountSegment,UpdateClientHostId,ResetStateAfterFailedRestoreAttempt). 진행 중인 hold 도 센다 — 지금 잡고있으면 지금 하트비트를 막고 있는 것이다. 최소 hold 시간 같은 상수는 두지
않았다.
ClientMonitorFunc자신의 만료 분기는 일부러 계측하지 않는다:같은 락을 exclusive 로 잡으므로, 계측하면 지난 tick 의 만료 작업이 "마스터가
stall 했다" 는 증거로 되돌아온다.
이건 클라이언트가 아니라 마스터에 대한 진술이라서, tick 경계도 후보
개수도 흔들 수 없다.
2차 조건 — 마스터가 그 이후 ping 을 하나도 처리하지 못했고, 추적 중인
클라이언트가 2개 이상인가. exclusive hold 만이 하트비트를 굶기는 길은
아니다 — io 스레드가 전부
snapshot_mutex_나 shard 락에 걸리면Ping도같이 굶고 그때는 hold 가 기록되지 않는다. 그래서 침묵은 약한 증거로
남긴다. 클라이언트 수 조건이 이걸 증거로 만들어 주는 부분이다: 침묵이 마스터에
대해 뭔가 말해 주는 건 마스터를 대신해 말해 줄 다른 누군가가 있었을 때
뿐이고, 추적 클라이언트가 하나면 "아무도 ping 하지 않았다" 와 "그 하나가
죽었다" 는 같은 문장이다.
둘은 OR 다. 유예 쪽으로 기울지만 그게 유계인 방향이다 — 틀렸을 때 비용이
client_mass_expiry_grace_sec만큼의 stale 세그먼트인 반면, 반대로 틀리면셀의 L3 캐시 전체다.
케이스 표 (코드의 주석과 같은 표이고, 각 행이 테스트다)
ReMountSegmentstall마지막 행은 2차 조건을 남긴 대가이고 회피할 수 없다. 전원이 사라진 플릿과
전면 stall 한 마스터는 동일한 하트비트 스트림을 만든다(pop 없음, 추적
클라이언트 전원 overdue, hold 없음). 비용은 이미 전부 사라진 소유자들의
세그먼트에만 떨어지고, grace 로 유계다.
남은 구멍 (residual gap)
client_mutex_hold 가 아닌 원인으로 굶는 1-클라이언트 셀은 키를 잃는다.io 스레드가 전부 shard 락에 걸린 경우가 그 형태다 — hold 가 기록되지 않고,
추적 클라이언트 하나는 증거가 되지 못한다. 막으려면 데이터 경로와 분리된
liveness 신호가 필요하고, 이 PR 은 그것을 만들지 않는다. 숨기지 않고 코드
주석과 커밋 메시지에 명시했다.
의도적으로 하지 않은 것 — 유예 중 세그먼트 격리
리뷰에서 "유예하는 동안 해당 세그먼트를
DRAINING으로 내려 쓰기를 막자" 는제안이 있었지만 일부러 넣지 않았다. 원인 기반 규칙에서 유예되는 케이스는
진짜 stall 이고, 그때 클라이언트들은 살아 있어서 그쪽으로 가는 쓰기가 정상
성공한다. 그러니 얻는 것이 없고, 대신 브레이커를 운영자가 몰던 drain 상태
기계와 엮어 버린다. 뒤집으려면 "유예된 클라이언트가 실제로 죽어 있고 쓰기를
받고 있는" 케이스가 필요하다.
삭제한 것
--mass_expiry_fraction과 그 validator·기본값 상수·config 키·buildersetter·문서, 바닥값
kMinMassExpiryClients, 판정과 로그의bulk/threshold.튜너블이 둘로 줄었다.
구조
판정 자체가 한 tick 의 관측값에 대한 순수 함수
EvaluateClientMassExpiry(
ClientMassExpiryInputs→ClientMassExpiryDecision)다. 그래서 경계 케이스가sleep 없는 테이블 테스트가 되고, grace 비교도 초 단위로 절단하지 않고 raw
duration 으로 한다.
유예는 아무것도 하지 않는 것이다. TTL 엔트리가 그대로 남고, 언마운트도
준비되지 않고, stale-handle sweep 도 돌지 않는다. deadline 을 손으로 갱신하지도
않는다 — 회복은 모니터가 매 tick 하는 큐 drain 이 알아서 하고, 그래야 grace 가
에피소드 전체의 상한으로 남는다.
--client_mass_expiry_guardtrue--client_mass_expiry_grace_sec60둘 다 "조용히 끄는 값" 이라서,
guard=false든grace=0든 시작 시WARNING을 한 줄 남긴다. grace validator 의 술어는 config 파일 경로와공유한다 —
DEFINE_validator는 커맨드라인 플래그에만 돌기 때문에, yaml 로들어온 음수 값은 이전에는 검사를 통째로 건너뛰고 브레이커를 조용히 껐다.
master_client_expiry_deferred_totalmaster_client_expiry_deferred_clientsgauge 는 프로세스 전역 싱글턴인데 쓰는 쪽은 인스턴스별 모니터 스레드라서,
절대값
set대신active_clients와 같은 inc/dec 델타로 발행한다. 한 프로세스안의 두
MasterService가 서로를 덮어쓰지 않고, 모니터 루프가 유예 중에끝나도 소멸자가 자기 몫을 되돌린다.
검증
유닛 테스트는 리그에서 실행했다.
master_service_test(162개)master_service_config_test(9개)테스트가 옳은 이유로 통과하는지도 확인했다. 두 번째 설계가 리뷰까지
살아남은 이유가 "엉뚱한 이유로 통과하는 테스트" 였으므로, 두 조건을 차례로
빌드에서 지우고 다시 돌렸다.
exclusive_hold_overlap = false강제)SingleClientFleetStallIsDeferredFAILED,ClientMassExpirySplitAcrossTicksIsDeferredFAILED. 나머지 둘은 그대로 통과 — 1차 조건에 기대지 않는 케이스라 맞다bulk재삽입MajorityGenuineDeathOnHealthyMasterExpiresAtOnceFAILED,CaseTableFAILED리그 게이트
프로덕션과 같은 8 CPU / 60 Gi / 16 rpc 스레드 형상의 마스터 파드에서,
12개 클라이언트 + 480 GB 세그먼트 +
BatchExistKey폐루프 +--independent_ping.게이트 1 — (B) 단독, 트리거를 살려둔 상태. (A) 를 빌드 시점에 되돌리고
18.0M 키. 합류 클라이언트의
ReMountSegment가 13,170,621 µs (10초client_ttl대비 13.2초) — 트리거가 확실히 임계값을 넘었다.만료 0건, 키 18,000,000개 그대로,
master_active_clients12 미만으로 내려간적 없음. 로그가 어느 조건이 발동했는지 (
exclusive_hold_overlap=1) 밝힌다.세 tick 만으로 끝났다 — stall 이 풀리고 다음 drain 이 13개 deadline 을 전부
갱신하자 에피소드가 스스로 닫혔다.
같은 형상을 15.0M 키에서 먼저 돌렸는데, 그때는 스캔이 9,831,669 µs 로
10초 TTL 을 0.17초 차로 넘지 못해 만료가 아예 발생하지 않았다. 프로그램 문서의
제약("만료는 14.2-14.4M 근처의 임계 사건") 그대로이고, 트리거가 임계값 통과에
달린 게이트는 게이트가 아니라서 18M 으로 다시 돌렸다.
게이트 1 의 teardown 이 두 행을 공짜로 줬다 (15M·18M 두 번 모두 동일):
12개 중 10개가 한 tick 에 유예 없이 만료된 것이 defect 2 의 수정을 마스터가
직접 진술한 것이다 — 옛
bulk는 10 ≥ceil(0.5 x 12) = 6로 읽어 살아 있는클라이언트들의 하트비트가 마스터의 건강을 증명하는 동안 열 개를 60초 붙잡았다.
마지막 2개는 전원 소실 행 그대로: grace 만큼 유예 후 만료.
게이트 2 — 1-클라이언트 셀의 두 케이스. 세그먼트 1개(추적 클라이언트 1개),
--client_ttl=2, 3.2M 키.2b — 정상 마스터에서 단독 사망: 즉시 만료. graceful exit 은 명시적 release 라
만료 경로가 아니므로 SIGKILL 로 죽였다.
kill 부터 만료까지 정확히 TTL 하나(2초), 유예 카운터는 한 번도 안 움직였다.
옛 규칙에서는
master_silent이 구조적으로 참이라 60초 grace 를 통째로기다렸을 자리다. defect 1 을 닫는 측정이다.
2a — 같은 셀의
ReMountSegmentstall 중: 유예, 키 보존. 에피소드 첫 tick:master_silent=0— 그 순간 마스터는 하트비트를 처리하고 있었으므로 침묵 조건은아무 기여도 하지 않았고, 기록된 hold 가 판정 전부다. 이 런의 유예 tick 116개 중
13개가 이 형태이고, hold 없이 침묵만으로 유예된 tick 은 0개다. 그동안
master_active_clients는 1,master_key_count는 3,200,000 에서움직이지 않았다.
게이트 3 — 마감 게이트, (A)+(B) 를 사고와 같은 형상에서. 14.0M 키,
클라이언트 12개, 부하 중 13번째 합류, 그리고 손으로 8번 더 합류(30초 간격 4번 +
연속 3번).
ReMountSegment298,598 µs (P1 의 무보정 9,760,564 µs 대비 세자릿수 개선). ping gap 없음, 유예 0.
각 합류자의 클라이언트는 프로세스 종료 후 단독으로 즉시 만료(유예 없음).
ClearStaleHandlessweep 을 재현: 9,563,521 /8,833,041 / 8,539,128 µs. 그리고 이 세션에서 가장 강한 로그 한 줄을 줬다:
15개 중 1개, 그리고
master_silent=0. 이번에 교체한 두 규칙 어느 쪽도 이tick 을 유예하지 못한다 — 비율 조건은 8개가 필요한데 1개였고, 침묵 조건은
마스터가 방금 하트비트를 처리했으니 거짓이다. hold 조건 단독으로 유예했고,
이 에피소드 26 tick 중 25개가 같은 형태다. tick-split 유닛 테스트와 케이스 표의
"1/12 로 갈림" 행의 라이브 대응물이고, 첫 설계가 깨진 바로 그 케이스이며 두
번째 설계도 놓쳤을 케이스다.
에피소드는 26 tick, grace 초과 0. sweep 이 끝나고 마스터가 그 deadline
이후의 하트비트를 처리하자 합류자 3개를 만료시켰다 — 스스로를 보증할 수 없는
동안 유예하고, 보증할 수 있게 된 순간 만료.
아무것도 잃지 않았다. 부하 구간 600 샘플 전체에서:
master_key_countmaster_active_clientsgrace_exceededclient_expired상태 정정 (2026-09-21, INF-522)
7faef38b동기화 때 upstream 코드로 흡수된다.7f5e4ca2,8f7f8b5b): [Store] Remove the mass-expiry circuit breaker ahead of the upstream liveness rework #16 에서 revert 로 제거했다. upstream [Store] Add recoverable client liveness and asynchronous offboarding kvcache-ai/Mooncake#2991 이 breaker 의 입력(ping queue, client 별 TTL map)을 없애고, 근본 원인은 upstream [Store] Centralize client session lifecycle and resource ownership kvcache-ai/Mooncake#4180 / [Store] Serve Ping on a separate RPC server that clients discover on Connect kvcache-ai/Mooncake#4183 이 다룬다. upstream 제출분 [Store] Defer a client mass expiry the master cannot rule out as its own stall kvcache-ai/Mooncake#3936 은 닫혔다.[Bench]커밋 3개 (master_bench): 계속 carry 한다. 그래서 [Store] Skip full metadata scan in ReMountSegment when unneeded (#3517) kvcache-ai/Mooncake#3533 이 흡수된 뒤에도 이 PR 은 Absorbed 로 옮기지 않는다.Upstream: kvcache-ai#3533
Absorbed: no (upstream took only the kvcache-ai#3533 part; the breaker was removed in #16 and the three [Bench] master_bench commits stay carried)