3센터 A-A-A RTGS 실시간총액결제 프로토타입 최초 버전관리 시작. - backend: Kotlin/Gradle 멀티모듈(sequencer, common, 채널/센터 모듈) - frontend: Vite + TS - infra: docker-compose, prometheus/grafana, ELK 네이티브 스크립트 - loadtest(k6), sim(장애/순서/정합성 시나리오), docs(PoC 보고서/복원력방안)
12 KiB
RTGS 3센터 삼중화 설계 (B1·B2·B3) — 구현 착수 확정본
| 항목 | 내용 |
|---|---|
| 목적 | 2센터 PoC → 3센터 Active-Active-Active 전환. 코드 구현은 현 PC, 전체 3센터 실측은 고사양 PC 이관 후 |
| 범위 | B1 순번기 이중화·무손실, B2 정족수(2/3) 완결, B3 순서결번 처리 + 멀티센터 런타임·설정 공통화 |
| 전제 | 코드는 이미 CENTER_ID/CENTER_COUNT 파라미터화. §9 결정사항 확정 완료(단일 인스턴스 3DB·인메모리 ack·순번기 3인스턴스) |
| 실행 환경 | 실측 전제 = 고사양 PC(40코어/128GB) 3터미널. 단, 현 개발 PC는 8코어 → 이 PC는 코드/빌드/단위테스트/경량 스모크(1~2센터)까지, 전체 3센터 21 JVM 실측은 이관 후 |
| 상태 | 설계 확정 + 코드 구현·빌드·단위테스트 완료(2026-07-13, 8코어 PC) · 3센터 실측은 고사양 PC |
| 구현 산출물 | B1/B2/B3 코드 + V4__seq_outbox.sql + 단위테스트(QuorumAggregator·GapBuffer) + run-dc2/dc3.cmd·init-3centers-db.cmd. 기동·검증은 docs/RTGS 3센터 이관·기동 체크리스트.md 참조 |
0. 핵심 원리 (재확인)
- 결정론적 재생: 3센터가 같은 저널을 같은 순서로 재생하면 각자 동일 상태 도달 → 원장 데이터 동기화 불필요(양팀장님 이론 맞음).
- 센터 간 조율이 필요한 건 딱 둘: (a) 하나의 전역 순서 생성(순번기) + (b) 완결 선언 시 과반 확인(정족수).
- 재해 규율: 1센터 down = 서비스 지속(복구 후 밀린 저널 진도처리=따라잡기) / 2센터 down = 서비스 중단(안전정지) / 복구 후 3센터 정상 재개.
1. 현재 상태 (전환 대상)
| 구분 | 현재(1센터) | 삼중화 전환 |
|---|---|---|
| Sequencer | 단일 인스턴스, seq=인메모리→DB max복원 | durable-before-publish + 리더선출(3인스턴스) + 펜싱 |
| 완결(Prada) | confirmations=1 하드코딩 → 즉시 ACCC |
센터별 applied ack 집계 → 과반(2/3) 후 ACCC |
| 순서(Hermes) | gap 버퍼링(무한대기 가능) | gap 타임아웃 시 durable(Kafka/journal_log)에서 결번 pull |
| 원장 DB | 단일 rtgs |
센터별 rtgs_dc1/dc2/dc3 |
| 서비스 | 1세트(7) | 3세트(센터별) + 단일 순번기 그룹 |
| 설정 | 서비스별 yml 중복 | 공통 yml + env 파라미터화(A안) |
2. 멀티센터 런타임 구성 (단일 호스트, 3터미널)
flowchart TB
subgraph SHARED["공유 인프라 (단일)"]
K["Kafka :9092<br/>rtgs.inbound / rtgs.journal(단일파티션) / rtgs.result(=applied-ack) / rtgs.notify"]
PG[("PostgreSQL :5433<br/>rtgs_dc1 · rtgs_dc2 · rtgs_dc3")]
OBS["Prometheus:9090 · Grafana:3000 · ELK"]
end
subgraph SEQ["전역 순번기 (그룹=sequencer, 1 active=리더)"]
S1["seq@DC1"]:::a --- S2["seq@DC2"]:::s --- S3["seq@DC3"]:::s
end
DC1["DC1 스택 809x<br/>Chanel/Dior/Hermes/Prada/Gucci/LV"]
DC2["DC2 스택 819x<br/>동일"]
DC3["DC3 스택 829x<br/>동일"]
K --- SEQ
K --- DC1 & DC2 & DC3
DC1 --> PG
DC2 --> PG
DC3 --> PG
classDef a fill:#cfe8cf; classDef s fill:#eee
포트 오프셋 (센터별 +100): DC1 80908099·5174 / DC2 81908199·5175 / DC3 8290~8299·5176.
공유: Kafka 9092 · PostgreSQL 5433(3 DB) · Prometheus 9090 · Grafana 3000 · ELK 9200/5000/5601.
컨슈머 그룹: 원장서비스는 센터별 접미사(예: hermes-DC1/DC2/DC3) → 각 센터가 저널 전량 독립 재생.
순번기는 단일 그룹 sequencer(3인스턴스, 1파티션 → 1개만 활성 = 리더).
구현지점(현 코드 상태):
@KafkaListener(groupId="hermes")등 groupId가 하드코딩되어 있음 (dior/hermes/prada/chanel/sequencer 각 서비스). 센터별 독립 재생을 위해 원장서비스 groupId를CENTER_ID접미사로 파라미터화 필요 → SpELgroupId = "hermes-#{@centerId}"또는groupId = "\${rtgs.group.hermes}"(프로퍼티 주입). 순번기만 예외(접미사 없이 단일 그룹sequencer유지).
3. B1 — 순번기 이중화 + 무손실 (durable-before-publish)
현 코드 상태(전환 대상):
SequencerService.kt는 인메모리AtomicLong+ 부팅 시@PostConstruct로MAX(journal_log.global_seq, transfer.global_seq)복원. 순번기 자신은journal_log/outbox를 쓰지 않고 바로rtgs.journal발행(발행-후 크래시 시 무손실 미보장). 따라서 아래 SEQUENCE·outbox·재발행은 전량 신설이다(현 코드에 없음).
리더선출(간결·견고): 순번기 3인스턴스를 같은 컨슈머 그룹 sequencer 로 rtgs.inbound(단일 파티션) 구독 →
Kafka가 파티션을 1개 인스턴스에만 배정 = 자동 단일 리더. 리더 死 시 Kafka 리밸런스로 자동 승계(별도 선출 로직 불필요).
durable-before-publish (무손실·무결번):
- 리더가 접수 수신 → Postgres SEQUENCE
global_seq_seq.nextval(원자적·durable 순번) - 저널 엔트리를 outbox 테이블에 저장(commit) → 그 후
rtgs.journal발행 - 크래시로 발행 전 죽어도, 재기동/승계 리더가 outbox의 미발행분을 재발행(at-least-once)
- 소비측은 seq 기준 멱등(이미 있음) → 중복 무해
펜싱(split-brain): nextval이 원자적이라 중복 순번 불가. 순간적 이중리더가 있어도 서로 다른 seq만 발급 → 저널 오염 없음. (엄격 fencing이 필요하면 outbox에 leader epoch 컬럼 추가—후순위)
작업: 새 서비스/모듈 대신 SequencerService 개편 + global_seq_seq·journal_outbox + 재발행 스케줄러.
- Flyway 위치: 마이그레이션은
backend/louisvuitton/src/main/resources/db/migration/에 있고 현재V1__baseline·V2__admin_login·V3__service_heartbeat까지 존재 → SEQUENCE+outbox는V4__seq_outbox.sql로 추가. - 재발행:
journal_outbox에published플래그 컬럼 → 발행 성공 시 true. 스케줄러가 미발행분 주기 재발행(at-least-once).
4. B2 — 정족수(2/3) 완결
흐름: 각 센터 Hermes가 seq 적용(선저널 commit) 후 applied ack 발행 → rtgs.applied(bmi, seq, center).
각 센터 Prada가 bmi별 서로 다른 center 수 집계 → ≥ quorum(=CENTER_COUNT/2+1) 도달 시 ACCC 확정.
결과통보는 origin 센터만 → 과반 도달 후 pacs.002 송부.
구현 결정 — rtgs.result를 applied-ack으로 재활용(별도 토픽 불필요):
- 당초
rtgs.applied신설을 검토했으나,rtgs.result가 이미 커밋 후에만(publish-after-commit) 발행되고processedCenter를 담으므로 "그 센터가 durable 적용했다"는 ack와 정확히 동일하다. → 토픽/Hermes 변경·마이그레이션 없이 더 단순·안전.Topics에 APPLIED 추가하지 않음. - 구현 완료(2026-07-13):
prada/QuorumAggregator.kt(신규, 순수로직+단위테스트 6): bmi별 서로 다른processedCenter집계, 과반 최초 도달 판정, 완결 후 멱등 가드.PradaService.finalize():confirmations=1제거 →aggregator.record()기반. 과반 미달=ACSP, 도달=ACCC(반려면 RJCT).@KafkaListener(groupId="prada-${rtgs.center-id}")로 전 센터분 소비.- 중복 통보 방지: origin 센터 Prada만 notify 발행(
notice.originCenter == centerId).NotificationService의 origin 필터는 그대로 유지(이중 안전).
ack 집계 저장(§9.4 확정): 인메모리 맵(bmi→집계한 center 집합). finality_ack 테이블은 불채택(간단성 우선).
재기동 경계조건(인메모리 손실 대응):
- 각 센터 Prada는 자기 컨슈머그룹으로
rtgs.applied를 소비 → 재기동 시 커밋된 오프셋에서 재개. - 인메모리 집계 맵이 재기동으로 비어도: 이미 ACCC 확정분은
transfer.status에 영속되어 안전(재판정해도 멱등). 아직 과반 미달(ACSP)인 건만 이후 도착하는 ack로 재집계 → 최종 수렴. 이중완결/누락 없음.
가용성 매핑:
- 1센터 down → 남은 2센터가 ack 2개 → 과반 충족 → 완결 지속.
- 2센터 down → ack 1개 → 과반 미달 → 완결 보류(안전정지). 복구 후 자동 재개.
5. B3 — 순서 결번(gap) 처리
- B1으로 영구 결번 소멸(발급=durable). 지연으로 인한 일시 gap은 Hermes 버퍼가 재정렬(S4 검증됨).
- 방어책:
expected순번이 T초 이상 미도착 시 → 경보 + Kafka에서 해당 seq 오프셋 재읽기(pull) (durable 저널에서 직접 가져옴). 스킵 금지(실제 엔트리는 반드시 적용). - 지표:
rtgs.journal.gap(이미 추가) + 타임아웃 카운터 → Grafana 경보.
6. 재해·복구 시나리오 (검증 항목)
| 시나리오 | 기대 | 검증(7/13) |
|---|---|---|
| 평상시 3센터 | 동일 순서·동일 잔액, 완결 즉시 과반 | 3 DB 대사 일치, 총액 보존 |
| 1센터 강제종료 | 서비스 지속(과반 2/3), 이중지급 0 | 나머지 2센터 완결 지속 |
| 재해센터 복구 | 밀린 저널 진도처리(따라잡기) 후 3센터 수렴 | 오프셋 재생→잔액 일치 |
| 2센터 종료 | 완결 보류(안전정지), 오처리 0 | 과반 미달로 ACCC 정지 |
| 2센터 복구 | 따라잡기 후 3센터 정상 재개 | 완결 재개·정합성 |
| 순서 역전/gap | 재정렬·pull로 정답 수렴 | S4 확장(3센터) |
7. 설정 공통화 (A안, 삼중화 전제작업)
common에application-common.yml(datasource·kafka·management 공통) → 각 서비스spring.config.import.- 센터·포트·DB·그룹접미사는 env 파라미터:
CENTER_ID,CENTER_COUNT=3,POSTGRES_DB=rtgs_dc1,PORT_OFFSET,GROUP_SUFFIX. - 실행 스크립트 3종:
run-dc1.cmd(현행) +run-dc2.cmd·run-dc3.cmd(env만 다름) + 공유 인프라/ELK/Prometheus는 1회 기동.
8. 착수 체크리스트 (순서)
A. 현 개발 PC(8코어)에서 가능 — 코드·빌드·경량 검증
2. 설정 공통화(A안) + env 파라미터화(CENTER_ID/CENTER_COUNT/PORT_OFFSET/GROUP_SUFFIX) + run-dc2/dc3.cmd.
3. DB 분리: 단일 인스턴스(5433)에 rtgs_dc1/dc2/dc3 생성(Flyway가 각 DB 스키마 생성).
4. B1: V4 SEQUENCE+outbox+재발행 스케줄러, 순번기 3인스턴스(그룹 리더선출) → 단일 순번 검증.
5. B2: rtgs.applied 토픽 + Hermes ack 발행 + Prada 과반 집계(인메모리) → 완결 규율.
6. B3: gap 타임아웃·pull.
→ 각 단계마다 **./gradlew build + 단위테스트 + 경량 스모크(관자 1~2센터 기동)**로 확인.
B. 고사양 PC 이관 후 — 전체 3센터 실측
- 환경 이관 검증: C:\ai-dev 복사본으로 빌드·기동 확인(포터블 무설치 철학이라 경로만 동일하면 OK).
- 재해/복구 시나리오(§6) + 성능 실측(터미널3, 21 JVM, k6/테스트화면, 순번·지연 병목).
- sim S3(정족수/펜싱)·S5(스파이크) 추가.
9. 결정사항 — 확정 완료 (2026-07-13, 양팀장님)
| # | 항목 | 확정 | 비고 |
|---|---|---|---|
| 1 | PostgreSQL | 단일 인스턴스 · 3 DB(5433, rtgs_dc1/dc2/dc3) |
자원 절약, 8코어 PC 적합. Flyway가 각 DB 스키마 생성 |
| 2 | 프론트 콘솔 | DC1 콘솔 하나로 3센터 조회(제안) | 간단. 센터별 3콘솔(5174~5176)은 필요 시 후속 |
| 3 | 순번기 배치 | 3인스턴스(센터당 1, 그룹 sequencer 리더선출) |
Kafka 리밸런스 자동 승계 |
| 4 | applied ack 저장 | 인메모리 맵 | finality_ack 테이블 불채택. 재기동 경계조건은 §4 참조 |
10. 리스크·주의
- 21개 서비스 JVM 동시 구동은 고사양 PC 전용(여유). **현 8코어 PC에선 경량 스모크(1~2센터, 일부 서비스)**로 코드 정상동작만 확인하고, 전체 동시기동 실측은 이관 후. 개별 명령 기동(bash for-loop 동시기동 실패 이슈).
- Kafka 저널은 반드시 단일 파티션(전역 순서). 3센터가 같은 토픽 독립 그룹으로 소비.
- 정합성 대사: 3 DB의
transfer/account가 동일해야(총액·상태·순번). 자동 대사 스크립트 준비. - 참조: 전문 보강(BAH/pacs.002 검증)은
docs/pacs.7z기준(B7과 함께).
본 설계는 구현 착수 기준 확정본(리뷰·보완 2026-07-13). §9 결정 반영 완료 — 이후 구현 중 세부만 조정.