The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
4.8 KiB
Plaintext
72 lines
4.8 KiB
Plaintext
# 주제: messaging-outbox-jdbc-postgresql 조립 탐침과, 백오프 지터가 목적을 달성하지 못하는 지점
|
|
# revision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
|
|
# ---- 조립 탐침 (new <Type>( 정규식, 완전수식 포함) ----
|
|
JdbcOutboxRepository src/main=0 src/test=2 <-- 스타터가 만들지 않는다
|
|
OutboxRelay src/main=1 src/test=3
|
|
OutboxRelayWorker src/main=1 src/test=1
|
|
OutboxRetryScheduler src/main=2 src/test=6
|
|
OutboxCleanupJob src/main=1 src/test=3
|
|
OutboxEnvelopeFactory src/main=0 src/test=5 <-- 스타터가 만들지 않는다
|
|
JdbcAdminOperationJournal src/main=0 src/test=2 <-- 스타터는 InMemory 를 쓴다
|
|
DebeziumOutboxEventRouter src/main=1 src/test=2 (DebeziumOutboxRecordMapper 내부 필드)
|
|
DebeziumOutboxRecordMapper src/main=0 src/test=1 <-- 아무도 만들지 않는다
|
|
DebeziumOutboxProfile src/main=1 src/test=3 (자기 polling() 팩토리)
|
|
|
|
# src/main 생성 상세:
|
|
DebeziumOutboxProfile.java:44 new DebeziumOutboxProfile(RelayMode.POLLING, "", false) (자기 팩토리)
|
|
DebeziumOutboxRecordMapper.java:32 new DebeziumOutboxEventRouter() (자기 필드)
|
|
DebeziumOutboxRecordMapper.java:63 new DebeziumMappedRecord(...) (자기 반환)
|
|
OutboxRetryScheduler.java:84 new OutboxRetryScheduler(OutboxProperties.defaults(), 5분) (자기 standard())
|
|
MessagingReliabilityAutoConfiguration:63 new OutboxRetryScheduler(properties, Duration.ofMinutes(1))
|
|
MessagingReliabilityAutoConfiguration:89 new OutboxRelay(...)
|
|
MessagingReliabilityAutoConfiguration:109 new OutboxRelayWorker(relay, scheduler)
|
|
MessagingReliabilityAutoConfiguration:141 new OutboxCleanupJob(outbox, properties, 20)
|
|
|
|
# => 스타터 밖 생성은 전부 이 리프 내부의 자기 참조다.
|
|
# JdbcOutboxRepository / OutboxEnvelopeFactory / JdbcAdminOperationJournal 은
|
|
# 애플리케이션이 직접 빈으로 등록해야 한다.
|
|
|
|
# ---- 릴레이는 실제로 기동된다 (대조군) ----
|
|
# command: git grep -n "\.start()" -- src/messaging
|
|
# MessagingOutboxRelayLifecycle.java:42 worker.start();
|
|
# => OutboxRelayWorker javadoc:18-21 이 기록한 과거 결함
|
|
# ("The relay, its retry scheduler and the attempt budget all existed and nothing ever called
|
|
# runOnce")이 실제로 수정되어 배선까지 완료되어 있다.
|
|
# 이 문서에서 확인한 "선언되고 배선되지 않은" 항목들과 대비되는 사례다.
|
|
|
|
# ---- CDC 경로 전체가 소비되지 않는다 ----
|
|
# command: git grep -n "requireExactlyOneRelay|DebeziumOutboxProfile.polling|RelayMode" -- src
|
|
# 전부 DebeziumOutboxProfile.java 자기 자신 + DebeziumOutboxRecordMapperTest
|
|
# => requireExactlyOneRelay 프로덕션 호출부 0건.
|
|
# DebeziumOutboxProfile 클래스 javadoc:9-13 은
|
|
# "the incompatibility is therefore enforced at startup instead of documented"
|
|
# 라고 쓰지만, 기동 시 이것을 부르는 코드가 없다.
|
|
# 두 릴레이가 동시에 켜지는 구성을 막는 주체가 없다.
|
|
|
|
# ---- 백오프 지터가 복제본을 분산시키지 못한다 ----
|
|
# OutboxRetryScheduler.java:18-20 클래스 javadoc:
|
|
# "Jitter is applied deterministically from the attempt count rather than randomly. Several relay
|
|
# instances that all started at deployment time would otherwise synchronise their retries into a
|
|
# thundering herd, and a random source would make the schedule impossible to test."
|
|
#
|
|
# OutboxRetryScheduler.java:98-109
|
|
# public Duration backoff(int consecutiveEmptyOrFailedPasses) {
|
|
# if (consecutiveEmptyOrFailedPasses <= 0) return baseInterval;
|
|
# int exponent = Math.min(consecutiveEmptyOrFailedPasses, 20);
|
|
# long scaled = baseInterval.toMillis() << exponent;
|
|
# long capped = Math.min(scaled, maxInterval.toMillis());
|
|
# long jittered = capped - (capped / 8) * (exponent % 3);
|
|
# return Duration.ofMillis(Math.max(baseInterval.toMillis(), jittered));
|
|
# }
|
|
#
|
|
# jittered 는 exponent 만의 함수다. exponent 는 워커의 unproductivePasses 카운터에서 온다
|
|
# (OutboxRelayWorker.java:183, 192).
|
|
# 동시에 배포되어 같은 브로커 장애를 겪는 복제본들은 같은 카운터 값을 갖게 되므로
|
|
# 같은 backoff 를 계산한다. 즉 지터가 인스턴스 간 위상차를 만들지 않는다.
|
|
# 지터는 시도 횟수에 따라 값을 바꿀 뿐(0, 1/8, 2/8 감산), 인스턴스에 따라 바꾸지 않는다.
|
|
#
|
|
# 주: 행(row) 단위 백오프(nextAttemptAt)는 DB 컬럼 next_attempt_at 에 기록되므로
|
|
# 이 문제와 무관하다. 문제는 pass 단위 백오프에만 해당한다.
|
|
# javadoc 이 말하는 "several relay instances … synchronise their retries" 는 pass 단위 얘기다.
|