22 KiB
같은 Redis 장애가 DEGRADED와 DOWN으로 갈리는 코드
Redis 코드 상세 시리즈 18/20 · 전체 지도 · 이전: Redis Session 요청은 어디에서 멈추는가: Web 설정과 미완성 Repository · 다음: Redis 테스트가 증명하는 것과 증명하지 않는 것
이 글이 답하는 코드 질문
Redis가 응답하지 않을 때 cache-only deployment는 왜 DEGRADED이고 session·idempotency·rate-limit·lease deployment는 왜 DOWN일까요? health contributor의 status만 다르게 만들면 readiness group이 안전하게 따라올까요? startup/capability probe와 command observation은 실제 production에 어디까지 조립됐을까요? 이 글은 probe 호출부터 Actuator group membership, low-cardinality tag까지 추적합니다.
코드 지도
| 코드 | 입력 | 출력 | production 상태 |
|---|---|---|---|
RedisHealthContributor |
runtime owner + fast timeout | reachable + bounded detail | 두 HealthIndicator가 사용 |
RedisSdkAutoConfiguration.redisOptional() |
health probe 결과 | UP 또는 DEGRADED |
Redis-on이면 항상 bean |
RedisSdkAutoConfiguration.redisRequired() |
correctness role predicate | UP 또는 DOWN |
correctness role에서만 bean |
RedisCorrectnessRoles |
Environment selectors | required contributor 생성 여부 | health/readiness 공통 predicate |
RedisReadinessGroupPostProcessor |
config data + same predicate | readiness include property source | spring.factories 등록됨 |
RedisStartupProbe |
server facts + required capability | confirmed RedisCapabilities |
production bean/collector 없음 |
RedisObservation |
descriptor, lane, mode, slot, outcome | closed tag map | 실행기가 생성, exporter bean 없음 |
NoThrowObservationSink |
observation consumer | telemetry failure 격리 + drop count | 실행기 constructor에서 wrapping 가능 |
request-time health probe
두 Actuator contributor는 같은 RedisHealthContributor.probe()를 호출합니다. probe 순서는 다음과 같습니다.
sequenceDiagram
participant A as Actuator HealthIndicator
participant H as RedisHealthContributor
participant O as RedisRuntimeOwner
participant R as Redis
A->>H: probe()
alt owner != OPEN
H-->>A: unreachable / shutting-down
else owner OPEN
H->>O: borrow(REGULAR)
O-->>H: lease
H->>R: PING
alt timeout 안에 reply
R-->>H: PONG 또는 reply
H-->>A: reachable=true
else interrupt/failure/timeout
H-->>A: reachable=false
end
H->>O: lease close
end
owner state가 OPEN이 아니면 connection을 빌리지 않고 shutting-down을 반환합니다. OPEN이면 REGULAR lane을 빌려 PING completion을 timeout.toNanos() 안에서 기다립니다. 단순 connection.isOpen() flag가 아니라 round trip을 검사합니다.
interrupt가 발생하면 thread interrupted flag를 복원하고 unreachable을 반환합니다. 다른 Exception도 health endpoint에 throw하지 않고 unreachable로 바꿉니다. health detail에는 다음 세 field만 있습니다.
mode:STANDALONE,SENTINEL,CLUSTERstate:reachable,unreachable,interrupted,shutting-downreason: PING reply, owner state, 또는 exception class simple name
endpoint, username, key, driver message는 detail에 넣지 않습니다. 다만 reachable의 reason에 String.valueOf(reply)를 쓰므로 보통 PONG이 들어갑니다.
owner borrow가 lane ceiling 때문에 거절되어도 catch에서 unreachable로 바뀝니다. Redis server가 살아 있어도 REGULAR lane saturation 때문에 health가 실패할 수 있습니다. health는 “별도 우선순위 connection으로 server만 검사”가 아니라 실제 application lane을 포함한 가용성을 봅니다.
같은 probe, 다른 status
optional contributor의 custom status는 DEGRADED입니다. reachable이면 UP, unreachable이면 DEGRADED입니다.
required contributor는 reachable이면 UP, unreachable이면 DOWN입니다. 차이는 probe 구현이 아니라 adapter가 health result를 Actuator status로 투영하는 한 줄입니다.
이 taxonomy의 기준은 role의 correctness 영향입니다.
| role | Redis 장애 의미 | status/readiness |
|---|---|---|
| cache | 원본 조회로 우회하면 느려짐 | redisOptional=DEGRADED, readiness 밖 |
| session | 인증 상태를 올바르게 판정할 수 없음 | redisRequired=DOWN, readiness 포함 |
| idempotency | 중복 실행 방지/재생 상태를 보장할 수 없음 | DOWN |
| rate limit | quota enforcement를 보장할 수 없음 | DOWN |
| lease | 단일 holder 가정을 보장할 수 없음 | DOWN |
cache가 RedisCorrectnessRoles.SELECTORS에 없는 것은 의도적입니다. SELECTORS는 session/idempotency/rate-limit/lease 네 개만 포함합니다.
Redis-on이면 optional contributor는 cache selector와 무관하게 항상 생깁니다. 즉 lease-only deployment에도 redisOptional과 redisRequired가 둘 다 존재합니다. readiness에는 required만 들어갑니다.
required bean과 readiness membership을 같은 predicate로 묶기
Actuator는 management.endpoint.health.validate-group-membership=true일 때 group include에 없는 contributor name이 들어가면 startup을 거절합니다. 반대로 validation을 끄면 오타나 absent contributor를 조용히 빼고 readiness가 false green이 될 수 있습니다.
애플리케이션의 shipped group은 application.yml health 구간에서 다음을 선언합니다.
- liveness:
livenessState - readiness:
readinessState,db - startup:
readinessState
redisRequired를 정적으로 쓰지 않습니다. 대신 RedisReadinessGroupPostProcessor가 config data 뒤에 실행되어 조건이 맞을 때만 append합니다. 이 class는 spring.factories에 등록되어 있습니다.
호출 순서는 다음과 같습니다.
- config data가
app.redis.enabled, role selector, 기존 readiness include를 해석합니다. - post-processor가 Redis-on인지 확인합니다.
RedisCorrectnessRoles.anySelected(environment)를 호출합니다.- 기존 comma-separated member를 순서 보존 set으로 만듭니다.
redisRequired를 중복 없이 append한 property source를 가장 앞에 둡니다.- context refresh 때
RedisCorrectnessRoleBoundcondition도 같은anySelected()를 호출해 bean을 만듭니다.
post-processor의 order는 ConfigDataEnvironmentPostProcessor.ORDER + 1입니다. config data 전에 실행되면 shipped base group을 읽지 못해 readinessState, db를 잃을 수 있기 때문입니다.
global switch가 off이거나 correctness role이 없으면 post-processor는 아무것도 하지 않습니다. cache-only일 때 optional contributor는 생겨도 readiness group에는 들어가지 않습니다.
startup/capability probe가 검사하도록 설계된 것
health PING은 지금 응답하는지만 봅니다. RedisStartupProbe.confirm()은 deployment 선언과 server fact가 일치하는지 확인하는 별도 type입니다.
입력 ServerFacts는 다음 네 값을 가집니다.
INFO server에서 파싱한RedisVersionCOMMAND LIST에서 얻은 lowercase command name setmin-replicas-to-writemin-replicas-max-lag
ServerFacts.from()은 version이 없으면 추측하지 않고 실패합니다. durability config 값이 없으면 0으로 간주하지 않고 admin account에 +config|get grant가 필요하다고 실패합니다.
RedisCapabilityProbe.probe()는 다음을 확인합니다.
- server version이 minimum supported 7.2.0 이상인지
- Cluster database가 0인지
- version상 가능한 capability의 witness command가 실제 server에 있는지
- deployment가 required로 선언한 capability가 available set에 있는지
version은 가능성 filter일 뿐 proof가 아닙니다. JSON/SEARCH/TIME_SERIES/PROBABILISTIC 같은 module capability는 해당 witness command가 실제 보고되어야 합니다.
requireWriteDurability()는 replicated mode에서 두 durability setting이 모두 양수인지 요구합니다. acknowledgedWriteLossAccepted=true이면 이 guard를 명시적으로 waive합니다.
그러나 production source에는 RedisStartupProbe나 RedisCapabilityProbe bean을 만드는 코드, INFO/COMMAND/CONFIG GET으로 ServerFacts를 수집하는 호출자가 확인되지 않습니다. 단위 계약은 구현됐지만 실제 startup에서 실행된다고 말할 수 없습니다.
command observation의 bounded cardinality
RedisObservation.starting()은 descriptor, lane, deployment mode, optional Cluster slot으로 observation을 만듭니다. 결과는 started, success, failure, ambiguous, rejected 중 하나입니다.
metric/span 이름 상수는 다음과 같습니다.
- span:
redis.command - duration:
backend.redis.command.duration - request bytes:
backend.redis.command.request.bytes - reply bytes:
backend.redis.command.reply.bytes - rejection:
backend.redis.policy.rejections - retry:
backend.redis.retry.count
lowCardinalityTags()은 정확히 열 개 key를 반환합니다.
family, risk, access, operation, mode, connection.kind, outcome, retries, ambiguous, slot.bucket입니다. raw key, field, member, value, user id는 없습니다. 16,384개 Cluster slot은 1,024로 나눠 b0~b15 bucket으로 축소합니다. slot이 없으면 none입니다.
Sync/Reactive/Queueing executor와 batch 실행 source는 observation을 생성하고 sink에 전달합니다. 다만 aggregate executor/operations의 production DI가 확인되지 않고, app-bootstrap에 MeterRegistry나 tracer로 연결하는 Consumer<RedisObservation> bean도 없습니다. 상수와 tag model이 있다는 사실은 실제 metric이 export된다는 뜻이 아닙니다.
NoThrowObservationSink는 telemetry failure가 command result를 바꾸지 않게 합니다. delegate가 RuntimeException 또는 LinkageError를 던지면 observation을 drop하고 LongAdder를 올립니다. 첫 drop은 warning, 이후는 debug입니다. drop metric 이름은 backend.redis.observation.drops이지만 이 counter를 metric backend에 bind하는 production 코드 역시 확인되지 않습니다.
정상·실패·degraded 분기
| 상황 | optional health | required health | readiness 영향 |
|---|---|---|---|
| PING 성공 | UP | UP | required role이면 정상 |
| PING timeout/driver failure | DEGRADED | DOWN | required role이면 unready |
| owner DRAINING/CLOSED | DEGRADED | DOWN | shutdown 중 새 traffic 차단 가능 |
| REGULAR lane saturation | DEGRADED | DOWN | server 생존과 무관하게 실제 lane unavailable |
| cache-only outage | DEGRADED | bean 없음 | readiness 유지 |
| correctness role outage | DEGRADED도 존재 | DOWN | readiness DOWN |
startup probe가 production에 조립된다면 version/capability/durability mismatch는 startup failure여야 합니다. 현재는 이 branch가 unit-tested type에 머뭅니다.
observation sink 실패는 command 성공/실패와 분리되어 observation drop으로 끝납니다. executor timeout 뒤 write가 적용됐는지는 ambiguous outcome으로 표현할 수 있지만, production exporter가 없으므로 운영 backend에서 이 tag를 볼 수 있다고 보장할 수 없습니다.
테스트가 고정하는 계약
LiveRedisCompositionTest.theOptionalContributorReportsUp()는 real server에서 optional contributor가 UP임을 확인합니다. unreachable에서 DEGRADED/DOWN을 직접 검증하는 전용 test는 현재 config test package에서 확인되지 않았습니다.
RedisReadinessGroupPostProcessorTest는 ApplicationContextRunner가 아니라 실제 SpringApplication을 띄웁니다. runner는 EnvironmentPostProcessor를 실행하지 않기 때문입니다.
redisOffStartsAndDoesNotNameTheContributor(): Redis-off context와 group membership 검증cacheOnlyDoesNotGateReadiness(): optional bean은 존재하지만 readiness 밖correctnessRoleGatesReadiness(): required bean과 group membership 동시 존재, base member 보존eachCorrectnessRoleGatesReadiness(): 네 correctness selector 전부 확인
RedisStartupProbeTest는 matching standalone, absent capability, replicated durability 두 조건, unreadable setting, missing version, explicit waiver를 고정합니다. 모두 pure unit test입니다.
RedisObservationTest는 raw key 부재, closed tag key set, slot bucket, outcome, 이름 상수를 고정합니다.
기본 module test는 이전 root 세션에서 성공했다는 공통 기록이 있지만, 이번 문서 작업에서는 real-server standalone/Sentinel/Cluster/TLS lane을 실행하지 않았습니다.
현재 구현 공백과 잘못 읽기 쉬운 지점
RedisStartupProbe와RedisCapabilityProbe는 production 미조립입니다. server capability/durability fail-fast는 현재 runtime 보장이 아닙니다.- health PING은 request-time Actuator 호출이며 application startup의 endpoint validation을 대신하지 않습니다.
- optional contributor는 Redis-on이면 cache 선택 여부와 무관하게 생깁니다.
redisOptional이라는 이름은 “cache bean만의 health”가 아니라 degradation-only 투영입니다. - correctness predicate에는 미완성 Redis Session selector도 포함됩니다. readiness가 Redis를 gate한다고 session repository request path가 완성되는 것은 아닙니다.
- observation model과 no-throw sink는 있으나 Micrometer/OTel exporter production 조립은 확인되지 않습니다.
- metric name 상수 중 duration/request/reply/rejection/retry를 실제 backend에 record하는 adapter도 확인되지 않습니다.
- unreachable optional=
DEGRADED, required=DOWN분기의 직접 단위 테스트가 부족합니다. 구현은 명확하지만 test contract 강도는 readiness membership보다 낮습니다.
다음에 열어볼 source와 관련 글
RedisHealthContributor.probe()redisOptional()과redisRequired()RedisCorrectnessRoles.anySelected()RedisReadinessGroupPostProcessorRedisStartupProbe.confirm()RedisObservation.lowCardinalityTags()
관련 시리즈 주제는 command executor의 timeout·ambiguous execution과 semantic capability별 failure policy입니다.