7. 멀티스레드 안전성

7.1 락 전략

컴포넌트

락 타입

읽기

쓰기

UBMS::CacheManager

shared_mutex

shared_lock (동시 다중)

unique_lock (배타적)

UBMS::Chain

mutex

-

addBlock/rollBack 직렬화

UBMS::BlockCurrent

atomic shared_ptr + mutex

atomic load (락 없음)

mutex 보호

UBMS::BlockMetaFullCache

atomic shared_ptr + mutex

atomic load (락 없음)

COW 교체

UBMS::BlockPool

atomic shared_ptr + mutex

atomic load (락 없음)

COW 교체

UBMS::VolatileCache

shared_mutex

shared_lock

unique_lock

UBMS::ConfirmedCache

shared_mutex

shared_lock

unique_lock

UBMS::TxCurrent

shared_mutex

shared_lock

unique_lock

DB

mutex

모든 연산 직렬화

모든 연산 직렬화

7.2 TOCTOU 회피 설계

취약한 설계 (분리 호출):

Thread A: isSpentKey(key) → false (미소비)
  --- 이 사이에 Thread B가 addBlock()으로 key 소비 ---
Thread A: addTx(key) → 이미 소비된 UTXO로 TX 생성 (이중 지불!)

UBMS의 해결: 원자적 검사 함수

checkDoubleSpendWithPool(address, key):
    // CacheManager의 shared_lock이 이미 걸린 상태

    // 1단계: 확정 원장 소비 확인
    //        TX풀 잔돈이면 통과, 아니면 거부

    // 2단계: TX풀 소비 확인 (같은 주소)
    //        이미 소비 예약되었으면 거부

    // 3단계: 전역 크로스-주소 이중소비 확인
    //        다른 주소의 TX가 이미 사용했으면 거부

    // 전부 통과하면 미소비 확인 완료

원자적 검사 함수 전체 목록:

함수

검사 범위

용도

checkDoubleSpendWithPool()

확정 + TX풀 + 전역

이중 지불 다단 검사

isNotSpentKeyWithPool()

확정 + TX풀 잔돈

미소비 확인

getUTXOWithPool()

확정 + TX풀

UTXO 통합 조회

getInputWithPool()

DB + volatile + TX풀

입력 수집

getBalanceAvailableWithPool()

DB + volatile + TX풀

사용 가능 잔액

getStakeWithPool()

DB + volatile + TX풀

스테이크 통합 조회

락 계층 설계 (교착 방지):

UBMS::Chain::m_mutex (std::mutex, 배타적)     ← addBlock/rollBack 직렬화
  └→ UBMS::CacheManager::m_mutex (shared_mutex) ← 읽기/쓰기 분리
       └→ UBMS::VolatileCache::m_mutex          ← 내부 보호
       └→ UBMS::ConfirmedCache::m_mutex         ← 내부 보호
       └→ UBMS::TxCurrent::m_mutex              ← 내부 보호

규칙: 외부 → 내부 순서로만 취득, 역순 불가 (교착 원천 차단)

7.3 락프리 읽기 패턴

현재 블록, 블록 메타 전체캐시, 블록풀은 읽기 경로에서 잠금 없이 현재 상태의 스냅샷을 즉시 가져올 수 있도록 설계됨. 쓰기는 단일 경로로 제한하여 데이터 경합을 원천 차단하면서도, 읽기 쪽에 대기나 차단이 발생하지 않음.

7.4 TX풀 이중 소비 방지 — 4단 충돌 검사

isConflict_(reserved, tx):

  검사 1: TX 내부 입력 중복
    같은 TX 안에서 동일 UTXO를 두 번 참조하면 거부

  검사 2: 주소별 예약 풀 이중 소비
    같은 주소의 이전 TX가 이미 소비 예약한 UTXO면 거부

  검사 3: 전역 크로스-주소 이중 소비 
    다른 주소의 TX가 이미 사용한 UTXO면 거부
    → m_globalSpentKeys (unordered_set, O(1) 조회)

  검사 4: TX 내부 출력 인덱스 중복
    같은 출력 인덱스를 두 번 생성하면 거부

TX풀 예약 풀 (UtxoPool) 동작:

applyReserveAdd_(tx):
  output 중 자기 주소로 돌아오는 잔돈 → available에 추가
  input → spent_keys + m_globalSpentKeys에 등록

효과:
  A가 5 UBMS UTXO로 3 UBMS 전송 시
  → spent_keys: {원본 5 UBMS key}
  → available: {잔돈 2 UBMS key}   ← 다음 TX에서 즉시 사용 가능

블록 확정 후 TX풀 정리 (3단계):

1) syncCurrentTx(confirmedTxList)         → txid로 확정된 TX 제거
2) removeConflictingCurrentTx(spentKeys)  → 충돌하는 미확정 TX 제거
3) cleanupCurrentTxByHeight(minHeight)    → 너무 오래된 TX 제거

8. 성능 분석

이하 O() 표기는 시간 복잡도(Time Complexity)를 나타낸다. O(1)은 데이터 양과 무관하게 일정한 시간, O(N)은 데이터 양에 비례하는 시간을 의미.

8.1 조회 성능

연산

경로

비고

잔액 조회

DB(owner 인덱스) + volatile 필터링

O(UTXO 개수)

TX 존재 확인

volatile(10블록) -> DB

최신 TX는 메모리에서 즉시

UTXO 소비 확인

volatile spentKeys -> DB

O(10) 최대

최근 TX 조회

volatile 역순 -> DB

txid 타임스탬프 정렬

referral 조회

ConfirmedCache 메모리

O(1) 해시맵

8.2 쓰기 성능

연산

비용

비고

블록 추가

O(TX수 + UTXO수)

volatile pushBack

DB 커밋

O(차분 크기)

10블록마다 한 번, 배치 쓰기

롤백

O(1)

volatile popBack, DB 건드리지 않음

8.3 I/O 성능 — 실측 벤치마크

측정 환경: AMD Ryzen 7 2700X (2018년 출시), 싱글스레드 (1 core), 3초 측정, localhost
8년 전 CPU의 단일 코어만으로 측정한 수치. 동일 세대 Ryzen 9 3900X에서는 싱글코어 기준 30% 이상 향상 확인

처리량 — 텍스트 프로토콜

메시지 크기

클라이언트

처리량 (msg/s)

송신

수신

32B

1

1,195,757

3,587,433

3,587,271

32B

10

10,132,692

60,851,192

30,398,077

256B

1

883,132

2,649,581

2,649,396

256B

10

4,284,165

27,230,564

12,852,496

1KB

1

801,805

2,405,416

2,405,416

1KB

10

1,628,376

8,246,630

4,885,129

4KB

1

320,268

1,299,084

960,806

4KB

10

205,166

1,706,216

615,498

처리량 — 바이너리 프로토콜

메시지 크기

클라이언트

처리량 (msg/s)

송신

수신

32B

1

1,431,783

4,295,670

4,295,349

32B

10

9,755,885

74,314,915

29,267,656

256B

1

1,231,194

3,693,694

3,693,584

256B

10

5,260,272

31,169,940

15,780,817

1KB

1

627,774

1,883,322

1,883,322

1KB

10

1,709,745

8,647,578

5,129,235

4KB

1

509,816

1,687,959

1,529,449

4KB

10

447,800

2,172,084

1,343,400

레이턴시 (64B echo, 1,000회 반복)

지표

평균

32.0 us

p50

32.0 us

p99

38.1 us

min

28.1 us

max

40.6 us

연결 속도

지표

연결 속도

12,419 conn/s

총 연결

1,000건 / 0.08초

실측 수치 요약

위 벤치마크는 libubmsio_uring.so 자체 실측값이며, 타 라이브러리와의 동일 조건 비교 측정은 수행하지 않았다. 다만 싱글스레드에서 1KB 메시지 170만 msg/s, 32B 메시지 1,013만 msg/s, p99 레이턴시 38us는 io_uring의 커널 수준 최적화를 실용적으로 잘 활용한 결과이며, 공개된 epoll 기반 라이브러리(libevent, libuv, Boost.Asio 등)의 벤치마크 자료와 대조해도 상당히 높은 수치.

주요 실측 수치:

- 바이너리 1KB, 10클라이언트: 1,709,745 msg/s (싱글스레드)
- 텍스트 1KB, 10클라이언트: 1,628,376 msg/s (싱글스레드)
- 32B 소형 메시지, 10클라이언트: 10,132,692 msg/s
- p99 레이턴시: 38.1us (64B echo RTT)
- 연결 속도: 12,419 conn/s (1,000건/0.08초)
- 멀티코어 활용 시 추가 향상 확인 (사무실 장비에서 20%+ 향상)

io_uring 자체의 커널 수준 최적화(SQE 배치 제출, 커널 측 폴링, 시스템콜 감소)가 처리량의 주된 원인이며, libubmsio_uring.so는 이 위에 multishot, zero-copy, provided buffers, backpressure를 조합하여 블록체인 노드에 적합한 네트워크 계층을 구성.

벤치마크 바이너리(ubmsio_uring_benchmark)가 빌드 산출물에 포함되어 있으므로, 위 수치는 누구든 동일 환경에서 직접 재현할 수 있다.

8.4 메모리 사용 추정

VolatileCache     ~50MB  (10블록 x 5MB/블록)
ConfirmedCache    ~10MB  (referral tree + nodeInfo)
TxCurrent         ~100MB (pending TX + 예약 풀)
BlockPool         ~100MB (미확정 분기)
합계              ~260MB (정상 상태)

DB           수 GB ~ 수백 GB (블록 데이터)

8.5 네트워크 최적화

메시지 압축       zlib (평문 메시지)
메시지 중복 제거  메시지 ID 해싱 + 윈도우 기반 중복 추적
공개키 캐싱       LRU 방식 (PEM 파싱 비용 제거)
배치 전송         sendmsg 일괄 전송