docs(keycloak-session-store): import the session-storage lab as a new project
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
+111
@@ -0,0 +1,111 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: deadline-propagation
|
||||
title: 호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파
|
||||
topic: transaction-deadline-and-pool
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:deadline-propagation
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: deadline-propagation
|
||||
file: ../../../final/evidence/rendered/deadline-propagation.svg
|
||||
- key: deadline-propagation-diagram
|
||||
file: ../../../final/assets/diagrams/deadline-propagation.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/deadline-propagation.txt
|
||||
source:
|
||||
- 원본 분석 절은 final/document.md#3-2 · analysis/05 §3.3 이다.
|
||||
---
|
||||
|
||||
# 호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파
|
||||
|
||||
호출자가 가진 시간 예산이 트랜잭션 타임아웃으로, 다시 데이터베이스의 로컬 타임아웃으로 좁혀진다. 각 단계가 앞 단계보다 작아야 상위 호출자가 포기한 뒤에도 하위가 계속 도는 상황이 생기지 않는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **데드라인은 호출 예산에서 시작해 세 단계로 좁힌다**
|
||||
이 개념을 규칙으로 옮긴 것이다.
|
||||
- **쓰기 트랜잭션에는 유한 타임아웃이 필수다**
|
||||
이 전파의 마지막 단계가 없을 때의 문제를 다룬 규칙이다.
|
||||
- **세션 스코프 설정은 풀로 돌아간 커넥션에 남는다**
|
||||
로컬 타임아웃을 설정할 때의 함정이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
호출자의 남은 예산이 세 단계로 좁혀져 DB 세션 설정에 도달하는 구조의 설명이다.
|
||||
|
||||
## 마감이 좁혀지는 세 단계
|
||||
|
||||
:::evidence key="deadline-propagation-diagram" alt="호출 예산과 트랜잭션 마감과 DB 로컬 타임아웃이 위에서 아래로 쌓여 있고 오른쪽에 좁아지는 방향 화살표가 있다" caption="마감이 좁혀지는 세 단계" zoom="false"
|
||||
:::
|
||||
|
||||
## 획득 전에 요구하는 것
|
||||
|
||||
`connectionTimeout + beginBudget + minimumActionWindow + completionMargin`을 요구하고, Spring의 초 단위 타임아웃이 1초 미만이면 시작하지 않는다. begin 이후에는 statement/lock/idle 셋을 각각 유도하고 하나라도 1ms 미만이면 거부한다.
|
||||
|
||||
## 예산이 좁혀지는 경로
|
||||
|
||||
:::evidence key="deadline-propagation" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true"
|
||||
:::
|
||||
|
||||
## SET 이 아니라 set_config 인 이유
|
||||
|
||||
둘이다 — `SET`은 파라미터 바인딩 전에 파싱되어 syntax error가 나고, 함수 호출은 값이 statement text에서 빠진다. 세 번째 인자 `true`가 transaction-local을 뜻한다.
|
||||
|
||||
## H2가 두 가지에서 다르다
|
||||
|
||||
세션 스코프이고 idle 가드가 없다. 그것이 H2의 성질이지 선택이 아니라는 점도 함께 적혀 있다.
|
||||
|
||||
:::note
|
||||
|
||||
실제 세션에서 SHOW statement_timeout으로 적용을 확인하지 않았다
|
||||
|
||||
:::
|
||||
|
||||
## 왜 전파해야 하는가
|
||||
|
||||
상위 호출자가 30 초 예산을 갖고 있는데 데이터베이스 쿼리에 타임아웃이 없으면, 호출자가 포기한 뒤에도 쿼리는 계속 돈다. 그 커넥션은 반납되지 않고 풀에서 빠져 있다.
|
||||
|
||||
부하가 걸리면 그 상태가 누적된다. 아무도 기다리지 않는 작업이 풀을 점유한다.
|
||||
|
||||
## 계산이 자기 타입을 갖는다
|
||||
|
||||
데드라인 계산기가 별도 타입이다. 획득 봉투를 포함하는 형태와 포함하지 않는 형태를 나눠 갖는다.
|
||||
|
||||
획득 봉투는 커넥션을 얻는 데 드는 시간이다. 그것을 예산에서 빼지 않으면, 커넥션을 기다리다가 남은 시간이 없는 채로 쿼리를 시작하게 된다.
|
||||
|
||||
## 데이터베이스마다 다른 설정기
|
||||
|
||||
로컬 타임아웃을 실제로 거는 방법은 데이터베이스마다 다르다. PostgreSQL 용 설정기와 H2 용 설정기가 따로 있다.
|
||||
|
||||
이 분리가 필요한 이유는 두 가지다. 설정 문법이 다르고, 세션 스코프 설정이 커넥션에 남는 방식도 다르다.
|
||||
|
||||
## 세 단계
|
||||
|
||||
```text
|
||||
호출 예산 상위 호출자가 기다릴 수 있는 시간
|
||||
↓ 획득 봉투를 뺀다
|
||||
트랜잭션 타임아웃 스프링 트랜잭션 템플릿에 설정
|
||||
↓ 여유를 남긴다
|
||||
DB 로컬 타임아웃 데이터베이스가 스스로 끊는 시간
|
||||
```
|
||||
|
||||
각 단계가 앞 단계보다 작다. 마지막이 가장 작아야 데이터베이스가 먼저 끊고, 그래야 애플리케이션이 그 실패를 분류할 기회를 갖는다.
|
||||
|
||||
:::note
|
||||
|
||||
순서가 반대가 되면 애플리케이션이 먼저 타임아웃되고 데이터베이스는 계속 돈다. 그 쿼리는 아무도 결과를 받지 않은 채 자원을 쓴다.
|
||||
|
||||
:::
|
||||
|
||||
## 템플릿을 미리 만드는 것과의 관계
|
||||
|
||||
트랜잭션 템플릿은 모드마다 미리 만들어져 있고 전부 같은 격리 수준에 고정되어 있다. 템플릿을 호출마다 고쳐 쓰면 경합이 생기기 때문이다.
|
||||
|
||||
그래서 데드라인은 템플릿의 필드가 아니라 실행 시점에 계산되어 전달된다.
|
||||
|
||||
<!-- body:end -->
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: requires-new-connection-cost
|
||||
title: REQUIRES_NEW의 커넥션 비용과 풀 사이징 제약
|
||||
topic: transaction-deadline-and-pool
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:requires-new-connection-cost
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: requires-new-connection-cost
|
||||
file: ../../../final/evidence/rendered/requires-new-connection-cost.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/requires-new-connection-cost.txt
|
||||
source:
|
||||
- 원본 분석 절은 final/document.md#3-2 · analysis/05 §3.1, §13.2 이다.
|
||||
---
|
||||
|
||||
# REQUIRES_NEW의 커넥션 비용과 풀 사이징 제약
|
||||
|
||||
새 트랜잭션은 새 커넥션을 요구하고 바깥 커넥션은 반납되지 않는다. 그래서 풀 크기 하한이 동시 스레드 수와 중첩 깊이의 곱에 묶인다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **REQUIRES_NEW가 바깥 커넥션을 핀한 채 새 커넥션을 딴다**
|
||||
이 제약이 실제로 나타나는 사례다.
|
||||
- **호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파**
|
||||
풀 획득이 예산의 일부라는 점에서 연결된다.
|
||||
- **풀 계약 레인이 실행되지 않아 포화 동작이 확인되지 않았다**
|
||||
이 제약의 검증에 대한 미해결 질문이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`REQUIRES_NEW`는 바깥 트랜잭션의 커넥션을 **핀한 채로** 새 물리 JDBC 커넥션을 딴다. 그래서 풀 사이징 제약이 곱셈이 된다 — `maximumPoolSize >= (concurrent_threads × (1 + max_inNew_depth)) + 1`.
|
||||
|
||||
## 커넥션 비용이 곱셈이 되는 이유
|
||||
|
||||
:::evidence key="requires-new-connection-cost" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 레코드마다 inNew 를 도는 루프가 금지인 이유
|
||||
|
||||
풀 고갈과 데드락이다.
|
||||
|
||||
## 같은 곱셈 함정이 database-per-tenant에서 반복된다
|
||||
|
||||
각 tenant 풀은 개별적으로 합리적이고 그 합이 아니다 — 50 tenant × 10 = 서버 `max_connections` 100에 500 커넥션. 실패는 idle이던 것 포함 모든 tenant에 동시에 도착한다.
|
||||
|
||||
:::note
|
||||
|
||||
풀 계약 레인 미실행
|
||||
|
||||
:::
|
||||
|
||||
## 일시 중단은 커넥션을 놓지 않는다
|
||||
|
||||
새 트랜잭션 모드는 바깥 트랜잭션을 일시 중단한다. 그 일시 중단은 트랜잭션 경계에 대한 것이다.
|
||||
|
||||
바깥 커넥션은 유지된다. 나중에 재개해야 하기 때문이다.
|
||||
|
||||
## 그래서 하한이 생긴다
|
||||
|
||||
설정 파일이 그 제약을 수식으로 적는다.
|
||||
|
||||
```text
|
||||
maxPoolSize >= concurrent_threads * (1 + max_inNew_depth) + 1
|
||||
```
|
||||
|
||||
중첩 깊이 1 이면 스레드당 두 커넥션이다. 동시 스레드가 100 이면 최소 201 이 필요하다.
|
||||
|
||||
## 지켜지지 않으면 데드락이다
|
||||
|
||||
모든 스레드가 바깥 커넥션을 잡고 안쪽 커넥션을 기다린다. 아무도 반납하지 않으므로 전부 풀 획득 타임아웃까지 대기한다.
|
||||
|
||||
이 상태는 부하가 임계를 넘는 순간 한꺼번에 나타난다. 그 전까지는 아무 증상이 없다.
|
||||
|
||||
## 풀 사이징의 다른 근거와 충돌한다
|
||||
|
||||
같은 주석 블록이 반대 방향의 근거도 적는다.
|
||||
|
||||
```text
|
||||
D1 (feature-database-connection-pool-contract): small-pool axiom + PostgreSQL formula
|
||||
starting point (maximumPoolSize = cores * 2 + effective_spindle_count, adjust via load
|
||||
test). Fixed-size pool recommended (minimumIdle = maximumPoolSize).
|
||||
```
|
||||
|
||||
작은 풀이 낫다는 공리와 코어 수 기반 시작점이다. 그 값은 대개 동시 스레드 수보다 훨씬 작다.
|
||||
|
||||
:::warning
|
||||
|
||||
두 근거가 같은 손잡이를 반대 방향으로 민다. 작은 풀 공리는 값을 줄이라 하고 중첩 하한은 늘리라 한다. 해소하는 방법은 풀을 키우는 것이 아니라 중첩 깊이를 줄이는 것이다.
|
||||
|
||||
:::
|
||||
|
||||
## 고정 크기 풀
|
||||
|
||||
최소 유휴를 최대 크기와 같게 두는 것을 권장한다. 풀이 줄었다가 늘어나는 동안 중첩 하한이 일시적으로 깨지는 것을 막는다.
|
||||
|
||||
## 멀티테넌시에서의 확장
|
||||
|
||||
테넌트별 풀 예산 타입이 이 제약을 테넌트 단위로 다시 적용한다. 테넌트 하나가 풀 전체를 소진하는 것을 막으면서도 각 테넌트의 중첩 하한을 만족해야 한다.
|
||||
|
||||
<!-- body:end -->
|
||||
Reference in New Issue
Block a user