TL;DR — Double Buffer는 여러 Session Actor가 비동기적으로 게시한 event를 World Server의 60Hz tick 입력으로 확정하고, producer와 consumer의 소유권을 분리하기 위해 도입했습니다. 그러나 worker와 A/B slot을 나눈 것만으로 pipeline이 병렬화되지는 않았습니다. Ingress는 Queue 수집과 deadline swap으로 분리하고, Outbound는 완성된 batch를 Publisher가 독립적으로 처리하도록 바꾼 뒤 세 번째 slot을 추가했습니다.
Table of contents
Open Table of contents
- 들어가며
- 1. Double Buffer가 필요한 이유를 정리한다
- 2. Slot을 ownership protocol로 만든다
- 3. 첫 구현은 worker를 나누고도 직렬화되었다
- 4. Ingress를 coordinator의 tick plan에서 분리한다
- 5. 초기 Outbound는 publication 완료 뒤 다음 simulation으로 넘어갔다
- 6. 완성된 Outbound를 Publisher가 독립적으로 처리한다
- 7. 동일한 4 MiB 조건에서 실행 결과를 비교한다
- 8. 4 MiB는 100-client benchmark의 용량 경계다
- 정리하며
- 참고 자료
들어가며
앞선 Fixed-Step과 Bounded Catch-Up 글에서는 World Coordinator가 60Hz의 absolute deadline과 authoritative tick을 유지하는 과정을 살펴봤습니다. 고정된 시간 간격으로 simulation을 실행하고, deadline을 놓치면 catch-up batch로 밀린 tick을 따라잡는 구조입니다.
하지만 시간 규칙을 정한 것만으로 여러 클라이언트의 입력이 같은 기준에서 처리되지는 않습니다. Client packet은 서로 다른 시점에 도착하고, IOCP completion과 Session Actor의 실행 순서도 고정되어 있지 않습니다. 계속 들어오는 event 중 어디까지를 현재 tick의 입력으로 확정할지 별도의 경계가 필요했습니다.
이 문제를 해결하기 위해 Ingress와 Outbound에 Double Buffer를 도입했습니다. 구현 과정에서는 자료구조에 A/B slot이 있다는 사실과 실제 pipeline 병렬성은 서로 다르다는 점도 확인했습니다. 이 글은 초기 구조에서 발견한 직렬화 지점과 이를 claim 기반 Ingress, 독립적인 Publisher와 Outbound Triple Buffer로 바꾼 과정을 다룹니다.
이 글에서 다루는 내용:
- 비동기 event를 authoritative tick 입력으로 확정하기 위해 Double Buffer가 필요한 이유
- slot state, claim, epoch와 generation으로 ownership을 표현한 방법
- worker를 분리하고도 coordinator 중심으로 실행이 직렬화된 초기 구조
- Ingress를 continuous pump와 swap 신호 중심으로 바꾼 과정
- Outbound publication 완료가 다음 simulation을 막았던 초기 구조
- 완성된 outbound batch를 Publisher의 책임으로 분리한 과정
- 세 번째 slot과 tick 시작 전 준비 단계로 publication 대기를 분리한 방법
- 동일한 4 MiB 조건에서 측정한 Double/Triple Buffer 결과와 해석 범위
사전 지식: server-authoritative simulation, fixed-step game loop, mutex와 condition variable에 대한 기본 이해를 전제로 합니다.
1. Double Buffer가 필요한 이유를 정리한다
NetworkRuntime과 World Server는 서로 다른 상태와 수명을 소유합니다.
NetworkRuntime
socket accept / recv / send
TCP packet framing
connection과 I/O lifetime
Session Actor scheduling
To-World event 게시
↓
World Server
client identity와 shared state 관리
simulation과 rule evaluation
authoritative state 반영
응답 대상과 payload 결정
NetworkRuntime은 packet type과 payload를 안전하게 복원하지만, packet이 요청한 World State 변경을 직접 수행하지 않습니다. Session Actor도 session-local I/O state만 직렬화하고 World State를 변경하지 않습니다. 여러 session이 공유하는 판정 순서와 mutation은 World Coordinator가 담당합니다.
Session Actor
→ application event 게시
→ shared World State를 변경하지 않음
World Coordinator
→ 확정된 event batch 소비
→ simulation rule 실행
→ authoritative state 변경
프로젝트에서 이 경계를 통과하는 event가 NrToWorldEvent입니다. 연결 수명이나 client packet을 Runtime 소유 payload와 함께 World로 넘기지만, Actor에게 World State 수정 권한을 제공하지는 않습니다.
1.1 Fixed-Step만으로는 입력의 끝을 정할 수 없다
Fixed-Step은 World를 언제, 얼마만큼 전진시킬지 결정합니다. 그러나 계속 추가되는 event 중 어디까지를 현재 tick에 포함할지는 결정하지 않습니다.
Fixed-Step
→ World를 언제 전진시키는가
Double Buffer
→ 현재 tick에서 어떤 입력까지 소비하는가
Bounded Catch-Up
→ deadline을 놓쳤을 때 시간축을 어떻게 복구하는가
Ingress Double Buffer의 swap은 비동기 event stream에 epoch cutoff를 만듭니다. Swap 이전까지 write된 event는 현재 sealed batch에 포함되고, 이후 도착한 event는 반대편 write slot에 적재되어 다음 epoch에서 처리됩니다.
flowchart LR
Client["Client"] -->|"request packet"| Runtime["Network Runtime"]
Runtime -->|"application event"| Ingress["Ingress write slot"]
Ingress -->|"sealed input batch"| Coordinator["World Coordinator"]
Coordinator -->|"authoritative mutation"| State["World State"]
Coordinator -->|"sealed output batch"| Outbound["Outbound Publisher"]
Outbound -->|"response packet"| Client
Double Buffer가 모든 packet의 도착 순서를 결정적으로 만드는 것은 아닙니다. 비동기 network에서 실제 도착 순서는 달라질 수 있습니다. 대신 Actor Worker가 event를 즉시 World에 반영하지 않고, 명시적인 epoch cutoff와 Coordinator의 commit 순서로 처리하게 합니다.
2. Slot을 ownership protocol로 만든다
Double Buffer를 단순히 배열 두 개를 번갈아 사용하는 최적화로 보지 않았습니다. 중요한 것은 각 slot을 어느 thread가 사용할 수 있는지입니다.
Empty → Writing → Sealed → Reading → Empty
| 상태 | 소유자 | 허용되는 작업 |
|---|---|---|
Empty | 없음 | 다음 writer가 획득할 수 있습니다. |
Writing | Producer | event 또는 outbound record를 씁니다. |
Sealed | Buffer | 완성된 batch를 reader에게 게시합니다. |
Reading | Consumer | payload를 읽지만 수정하지 않습니다. |
한 slot이 Writing인 동안 consumer는 해당 slot을 읽을 수 없습니다. Reading인 동안 producer는 payload를 reset하거나 덮어쓸 수 없습니다. Consumer가 read claim을 반납한 뒤에만 slot이 다시 Empty가 됩니다.
2.1 Claim이 긴 작업에서 mutex를 분리한다
Mutex는 event 전체를 적재하거나 simulation을 실행하는 동안 유지하지 않습니다. 다음과 같은 짧은 상태 전환만 보호합니다.
- write 또는 read claim을 획득합니다.
- slot state와 epoch를 변경합니다.
- Ready slot을 게시하거나 소비합니다.
- claim 반납을 확인하고 slot을
Empty로 전환합니다. - close 상태를 게시하고 대기 중인 worker를 깨웁니다.
Claim을 획득한 뒤 payload 접근은 해당 thread의 단독 소유권으로 처리합니다. Batch의 각 element를 읽고 쓸 때마다 mutex를 획득하지 않습니다.
2.2 Epoch와 generation이 stale release를 막는다
Slot index만 비교하면 이전 역할에서 얻은 claim이 재사용된 slot을 잘못 해제할 수 있습니다. Ingress claim은 slot index와 role generation을 함께 보관하고, read batch는 epoch까지 연결합니다.
Slot A / Generation 4 / Epoch 100
Slot A / Generation 6 / Epoch 102
두 claim은 같은 물리 slot을 가리키지만 같은 ownership이 아닙니다. 현재 generation과 일치하지 않는 claim은 stale claim으로 거부합니다.
Ingress와 Outbound는 같은 ownership 원칙을 사용하지만 실행 조건은 다릅니다.
| 구간 | Producer | Consumer | 경계 |
|---|---|---|---|
| Ingress | WorldIngressPump | World Coordinator | Coordinator의 WaitSwap() |
| Outbound | World Coordinator | Outbound Publisher | 완성된 batch 게시와 FIFO 소비 |
Ingress는 Coordinator가 tick 경계를 결정합니다. Outbound는 Coordinator가 생성한 결과를 Publisher가 나중에 소비하므로, producer seal과 consumer dequeue가 서로 독립적인 계약이 필요했습니다.
3. 첫 구현은 worker를 나누고도 직렬화되었다
첫 구조에서도 Ingress Pump, Coordinator와 Outbound Publisher는 각각 별도 thread를 사용했습니다. 그러나 worker 협업은 Coordinator가 plan을 보내고 같은 epoch의 completion을 기다리는 방식이었습니다.
Coordinator
→ Ingress Plan N 게시
Ingress Pump
→ deadline까지 event drain
→ ingress N seal
→ Ingress Completion N 게시
Coordinator
→ Completion N 대기
→ ingress N 처리
→ simulation N 실행
→ outbound N seal
→ Outbound Plan N 게시
Outbound Publisher
→ outbound N 제출
→ read slot release
→ Outbound Completion N 게시
Coordinator
→ Completion N 확인 후 다음 outbound batch 진행
자료구조에는 A/B slot이 있었지만 Coordinator가 worker의 작업 시작과 완료 순서를 모두 조정했습니다. 서로 다른 slot을 사용할 수 있는 구간에도 다음 단계가 completion을 기다렸기 때문에 실행은 사실상 single-thread pipeline처럼 이어졌습니다.
이 구조도 correctness 측면에서는 의미가 있었습니다.
- Runtime payload와 World 처리 lifetime을 분리했습니다.
- epoch 단위 입력 경계를 만들었습니다.
- Pump, Coordinator와 Publisher의 책임을 별도 객체로 분리했습니다.
- 읽는 slot과 쓰는 slot의 ownership을 명시했습니다.
하지만 worker의 존재와 pipeline 병렬성은 같은 의미가 아니었습니다. A/B storage가 있어도 Coordinator가 매 단계의 completion을 기다리면 두 slot은 lifetime을 분리할 뿐, 동시에 작업을 진행할 수는 없게 됩니다.
4. Ingress를 coordinator의 tick plan에서 분리한다
먼저 Ingress에서 tick용 plan/completion을 제거했습니다. Pump는 Coordinator가 매 tick 보내는 plan을 기다리지 않고, NetworkRuntime의 ToWorldEvent Queue에서 event를 가져와 현재 write slot을 채웁니다. Coordinator는 자신의 absolute deadline에서 swap만 요청합니다.
Ingress Pump
write claim 획득
→ ToWorldEvent Queue에서 bounded batch pop
→ write slot에 event commit
→ write claim 반납
Queue가 비어 있으면
→ write claim 반납
→ ToWorldEvent Queue의 event notification 대기
World Coordinator
absolute deadline 대기
→ WaitSwap(epoch)
→ sealed read batch 획득
→ event decode와 validation
→ fixed-step simulation
→ read claim 반납
Pump는 먼저 짧은 write claim을 획득해 ToWorldEvent Queue에서 bounded batch pop을 시도합니다. Queue가 비어 있으면 event를 0개 commit해 claim을 즉시 반납한 뒤, Queue에 새 event가 게시될 때까지 기다립니다. 즉 Queue notification을 기다리는 긴 구간에는 write slot을 점유하지 않습니다.
Coordinator가 deadline에 WaitSwap(epoch)을 요청하면 진행 중인 bounded transfer가 있을 때만 해당 transaction의 종료를 기다립니다. Swap을 위해 ToWorldEvent Queue를 빌 때까지 drain하지 않습니다. 현재 write slot에 옮기지 못한 event는 Queue에 남고, swap 이후 반대편 write slot으로 이동합니다.
sequenceDiagram
participant Runtime as Network Runtime
participant Queue as ToWorldEvent Queue
participant Pump as Ingress Pump
participant Buffer as A/B Buffer
participant World as World Coordinator
Runtime->>Queue: To-World event 게시
Pump->>Queue: Bounded batch pop
Pump->>Buffer: Write B에 event 적재
World->>Buffer: Swap epoch N 요청
Buffer-->>World: Sealed Read B 제공
par 현재 epoch 처리
World->>World: Tick N simulation과 commit
and 다음 epoch 입력 수집
Runtime->>Queue: 새 event 게시
Pump->>Queue: 다음 batch pop
Pump->>Buffer: Write A에 event 적재
end
Tick 수와 catch-up 판단은 계속 Coordinator가 소유합니다. Ingress Buffer는 Queue의 event를 저장하는 owner가 아니라, Queue에서 꺼낸 bounded batch의 write/read ownership과 epoch swap만 담당합니다.
Catch-up batch에서도 sealed ingress event는 한 번만 소비합니다. 현재 epoch에서 client별 최신 입력 상태를 확정한 뒤 밀린 simulation tick을 순서대로 전진시킵니다. Catch-up 도중 도착한 packet은 반대편 write slot 또는 ToWorldEvent Queue에 남고 다음 ingress epoch에서 처리됩니다.
5. 초기 Outbound는 publication 완료 뒤 다음 simulation으로 넘어갔다
첫 구조에서는 Coordinator가 tick simulation을 끝낸 뒤 outbound batch를 완성하고 Publisher에 넘겼습니다. Publisher는 완성된 batch를 NetworkRuntime에 제출한 뒤 read slot을 반납했습니다. Coordinator는 이 publication 완료와 outbound role swap을 확인한 뒤 다음 tick simulation을 시작했습니다.
Coordinator
ingress N 소비
→ simulation N
→ outbound N 생성과 seal
Outbound Publisher
outbound N 제출
→ read slot 반납
Coordinator
publication N 완료 확인
→ outbound role swap 완료
→ simulation N+1 시작
Ingress, Coordinator와 Outbound가 서로 다른 worker에서 실행되더라도, 다음 simulation의 시작 조건에 이전 publication 완료가 포함되어 있었습니다. 이미 authoritative 결과가 확정된 뒤의 전송 준비까지 Coordinator의 tick 진행 절차가 블로킹되는 구조였습니다.
5.1 Double Buffer로 publication과 다음 simulation을 겹친다
첫 개선에서는 Publisher가 outbound N을 처리하는 동안 Coordinator가 ingress N+1과 simulation N+1을 실행하도록 바꿨습니다.
Outbound Publisher
outbound N 제출 ───────────────┐
│
Coordinator │
ingress N+1 소비 │
→ simulation N+1 │
→ outbound N+1 생성 ──────────┘
tick 종료에서 합류
실제로 outbound를 제출하는 과정과 simulation N+1이 처리되는 과정이 병렬적으로 진행됐지만, 다음과 같은 상태가 다시 발생했습니다.
Slot A → Publisher가 Reading N
Slot B → Coordinator가 Writing N+1
Coordinator가 simulation을 처리하고 나서 outbound를 만들고 이를 Publisher에게 위임하는 과정에서 다음 outbound에 필요한 slot을 기다려야 하는 상황이 발생합니다.
tickStartedAt
→ ingress N+1 소비
→ simulation N+1
→ outbound N+1 생성
→ outbound N+1 seal과 role swap
└─ publication N 완료와 Slot A 반납 대기
→ tickCompletedAt
Publisher가 빠르면 이 대기는 거의 발생하지 않습니다. Simulation 처리 비용이 작고 Publisher가 tick 종료 전에 Slot A를 반납하면 Double Buffer도 publication을 임계 경로에서 제외할 수 있습니다. 반대로 두 작업이 함께 길어지면 publication 시간이 현재 tick의 execution tail에 합류합니다.
여기에서 책임을 다시 나눴습니다. Coordinator는 authoritative simulation과 outbound 결과 확정까지만 담당합니다.
이미 완성된 결과를 NetworkRuntime으로 제출하고 slot을 반납하는 과정은 Outbound Publisher가 독립적으로 진행하게 만드는 것입니다.
6. 완성된 Outbound를 Publisher가 독립적으로 처리한다
변경한 구조에서는 Coordinator가 완성된 outbound batch를 순서가 보장된 ready queue에 게시한 뒤 Publisher의 완료를 기다리지 않고 바로 다음 tick을 시작합니다.
Publisher는 가장 오래된 ready batch를 직접 가져와 NetworkRuntime에 제출하고, 처리가 끝난 slot을 Empty로 돌려놓습니다.
Coordinator
simulation N
→ outbound N 완성
→ ready batch로 게시
→ publication 완료를 기다리지 않음
Outbound Publisher
가장 오래된 ready batch 소비
→ NetworkRuntime에 제출
→ slot 반납
이 구조에서 publication의 순서와 backpressure는 Outbound가 책임집니다. Coordinator는 Publisher가 언제 batch를 가져갔는지 확인하지 않고 다음 fixed-step deadline과 simulation을 계속 관리합니다.
6.1 Tick 시작 전에 Ingress와 Outbound를 함께 준비한다
Publisher가 사용할 outbound batch는 여전히 Coordinator가 simulation을 끝낸 뒤 seal합니다.
이 상태에서 Coordinator는 다음 tick에서 사용할 outbound batch slot을 tick의 시작 시점으로 옮겨 확보하게 됩니다.
이전 tick 완료
∥ Publisher가 이전 outbound 처리
↓
다음 fixed-step deadline
→ Ingress read batch 준비
→ Outbound write slot 준비
→ tickStartedAt
→ simulation과 outbound 생성
→ outbound batch seal과 ready 게시
→ tickCompletedAt
이 순서는 이전 tick이 끝난 뒤 다음 tick의 deadline이 도착할 때까지의 간격을 Publisher가 사용할 수 있게 됩니다.
Coordinator는 새 tick을 측정하기 전에 Ingress input과 Outbound write 공간이 모두 준비됐는지 확인하고, 준비된 상태에서만 simulation을 시작합니다. Tick 시작 조건을 한곳으로 모으면서 publication 대기가 simulation 종료 시간에 붙는 것도 막을 수 있게 된 것입니다.
모든 outbound slot이 사용 중이면 Coordinator는 다음 tick 시작 전에 빈 slot을 기다립니다. 따라서 여전히 Coordinator는 outbound slot 획득을 위해 블로킹이 될 수 있습니다.
6.2 세 번째 slot이 한 batch의 publication 지연을 흡수한다
그래서 Publisher는 총 3개의 slot을 사용합니다. 두 slot만 사용하면 Reading N + Writing N+1 상태에서 다음 write 공간이 없습니다. 세 번째 slot을 추가하면 다음 상태까지 허용할 수 있습니다.
Slot A → Publisher가 Reading N
Slot B → Ready N+1
Slot C → Coordinator가 Writing N+2
Coordinator는 N+1을 ready batch로 게시한 뒤 Publisher의 N 완료를 기다리지 않고 N+2를 위한 write slot을 준비할 수 있습니다.
모든 slot이 점유된 상태에서는 Coordinator가 여전히 빈 slot이 생길 때까지 기다려야 하지만, Coordinator와 Publisher의 time-slice를 최대한 사용할 수 있다는 장점이 있습니다.
반면 Ingress에는 세 번째 slot을 추가하지 않았습니다. Coordinator가 deadline에 swap을 요청하면 진행 중인 bounded Queue-to-slot transfer만 끝낸 뒤 현재 write slot을 바로 read slot으로 넘기는 구조이기 때문입니다.
또한 아직 slot으로 옮기지 못한 event는 ToWorldEvent Queue에 남습니다. Outbound에서 사용할 세 번째 slot의 역할을 이 event queue에서 대신해주고 있다고 볼 수도 있겠습니다.
7. 동일한 4 MiB 조건에서 실행 결과를 비교한다
구조 변경의 목표는 모든 tick을 무조건 빠르게 만드는 것이 아니었습니다. Publisher가 이전 batch를 처리하는 동안 simulation이 다음 outbound batch를 만들었을 때, publication 지연이 현재 tick의 execution tail에 합류하는 빈도를 줄이는 것이었습니다.
다음 조건에서 publication 완료가 tick 종료에 합류하는 2-slot 구조와 Publisher가 독립적으로 처리하는 3-slot 구조를 비교했습니다.
- Windows x64 Release, debugger 미부착
- World Server 2개를 동시에 실행
- 서버당 실제 client connection 100개
- 60Hz, 3분 workload
maxCatchUpSteps = 4- Outbound payload capacity는 slot당 4 MiB로 동일
- 각 구현을 3회 실행하고 순서를 교차
- 각 회차의 두 channel 평균을 구한 뒤 repeat3 중앙값 사용
| Phase | Execution p99: Double | Execution p99: Triple | Overrun: Double | Overrun: Triple | Start lag p99: Double | Start lag p99: Triple |
|---|---|---|---|---|---|---|
| Early | 14.534 ms | 14.753 ms | 0.285% | 0.352% | 16.511 ms | 16.583 ms |
| Mid | 16.607 ms | 15.648 ms | 0.954% | 0.629% | 16.625 ms | 16.485 ms |
| Late | 11.341 ms | 12.353 ms | 0.126% | 0.182% | 16.631 ms | 16.507 ms |
두 구현을 합친 6회의 반복 실행, 즉 총 12개의 server run은 모두 client 100/100이 완료됐고 worker failure 없이 정상 종료했습니다. Overrun은 두 구현 모두에서 발생했으며 일부 run은 Performance Gate를 넘었습니다.
Overrun은 한 번의 tick batch 실행 시간이 60Hz의 tick budget인 16.67ms를 초과한 경우입니다. 표의 Overrun 값은 각 phase의 전체 sample 중 이 기준을 초과한 sample의 비율입니다.Performance Gate는 프로젝트에서 사용하는 내부 측정 기준입니다. Early, Mid와 Late 각 phase에서 execution p99가 16.67ms 미만이고 Overrun 비율이 1% 미만일 때 통과합니다. 이 기준의 초과는 서버나 클라이언트의 기능 실패를 의미하지 않습니다.- 같은 구현도 repeat마다 다른 결과를 보였습니다. Gate 통과 여부에는 측정 당시의 하드웨어 상태와 OS의 다른 부하가 영향을 주므로, 절대적인 성공 보장보다 해당 device와 실행 시점의 baseline으로 참고하는 것이 적절합니다.
Mid 구간에서는 Triple Buffer의 execution p99가 5.78% 낮고 overrun ratio가 0.325%p 낮았습니다. Simulation과 publication이 동시에 길어지는 pressure 구간에서 세 번째 slot이 한 batch의 지연을 흡수하고, publication 완료를 tick 종료 경로에서 분리한다는 설계 의도와 일치하는 결과입니다.
반면 Early execution p99는 1.51%, Late는 8.92% 높았습니다. Start lag 차이도 모든 phase에서 1% 미만이었습니다. Publisher가 tick 종료 전에 slot을 반납하는 낮은 pressure 구간에서는 기존 Double Buffer도 기다릴 이유가 없으므로 Triple Buffer를 통해 얻는 이익이 거의 없었던 것입니다.
저부하 구간의 차이를 buffer lock, worker wake-up, cache locality, CPU scheduling 중 하나로 단정할 수는 없습니다. 프로세스 CPU 평균은 Double 10.47%, Triple 10.33%로 비슷했습니다. Working set peak 중앙값은 Double 58.25 MiB, Triple 63.11 MiB로 세 번째 slot을 추가한 Triple Buffer가 약 4.86 MiB 높았습니다.
이 결과에서 확인한 범위는 다음과 같습니다.
- Triple Buffer는 publication 지연이 tick 종료 경로와 충돌하는 pressure 구간에서 여유를 제공합니다.
- Publisher가 제때 slot을 비우는 구간에서는 Double Buffer도 동일한 병목을 만들지 않습니다.
- 세 번째 slot은 전 구간의 일관된 성능 향상을 보장하지 않습니다.
- Overrun gate는 기능 성공 여부와 구분해야 하며, repeat3 수치는 해당 device와 실행 시점의 baseline입니다.
8. 4 MiB는 100-client benchmark의 용량 경계다
Benchmark의 outboundPayloadByteCapacityPerSlot은 4 MiB로 고정했습니다. 이 값은 100-client workload의 batch 상한을 보수적으로 잡기 위한 설정입니다.
가장 큰 application payload는 최대 8,186 bytes이고 catch-up batch는 최대 4 tick을 처리합니다. Client마다 processed tick당 최대 크기 payload record 하나가 만들어지는 경우를 기준으로 다음과 같이 계산했습니다.
100 clients × 4 catch-up ticks × 8,186 bytes
= 3,274,400 bytes
≈ 3.12 MiB
여기에 같은 epoch의 control packet과 chunking 여유를 포함해 payload storage를 slot당 4 MiB로 올려 잡았습니다. Recipient와 record 배열은 이 payload byte capacity와 별도로 고정 용량을 가집니다. Triple Buffer에서는 payload storage만 4 MiB × 3 slots = 12 MiB를 예약합니다.
Peak memory를 구조적으로 낮추려면 publication 단위를 바꿔야 합니다.
- Outbound record를 bounded chunk로 seal하고 Publisher가 chunk를 순차적으로 제출합니다.
- Session 또는 recipient shard로 독립적인 batch를 나누고 여러 Publisher가 병렬로 제출합니다.
첫 방식은 peak payload storage를 줄이지만 chunk 사이의 순서를 관리해야 합니다. 두 번째 방식은 병렬성을 늘릴 수 있지만 session별 packet FIFO, replication snapshot의 chunk 일관성과 failure 처리가 더 복잡해집니다. 현재 4 MiB 설정은 이 설계를 도입하기 전, 100-client benchmark의 입력 조건을 고정하기 위한 용량 경계입니다.
정리하며
첫 구현은 worker와 A/B slot을 분리했지만 Coordinator가 각 단계의 완료를 기다려 실행이 직렬화되었습니다.
그래서 Ingress는 ToWorldEvent Queue 수집과 deadline swap으로 분리했습니다. Outbound는 publication과 다음 simulation을 병렬로 실행했지만, 두 slot만으로는 tick 종료 시점에 이전 Publisher를 다시 기다리게 됐습니다.
최종적으로 Coordinator는 완성된 outbound batch를 게시한 뒤 publication 완료를 기다리지 않습니다. Publisher가 게시된 batch를 순서대로 처리하고, 세 번째 slot이 한 batch의 publication 지연을 흡수합니다. Coordinator는 다음 tick을 시작하기 전에 Ingress input과 Outbound write 공간을 함께 준비하며, 모든 slot이 사용 중이면 빈 slot을 기다려 읽지 않은 batch를 덮어쓰지 않습니다.
핵심 요약:
- Fixed-Step은 authoritative 시간 기준을, Double Buffer는 tick 입력의 batch와 ownership 기준을 만듭니다.
- Session Actor는 session-local I/O state만 처리하고 World State를 직접 변경하지 않습니다.
- Ingress Pump는
ToWorldEvent Queue를 계속 소비하고 Coordinator는 tick deadline에 swap만 요청합니다. - Outbound Double Buffer는 publication과 simulation을 겹쳤지만 두 slot이 모두 사용되면 tick 종료에서 다시 합류했습니다.
- Publisher의 독립적인 batch 처리와 세 번째 slot은 한 batch의 publication 지연을 흡수합니다.
- Triple Buffer는 pressure 구간의 tail을 줄였지만 전 구간의 일관된 성능 향상을 보장하지 않았습니다.
- 4 MiB는 100-client benchmark의 용량 경계이며, one-shot outbound batch의 근본적인 해결책은 아닙니다.
참고 자료
- World Server 1: Fixed-Step과 Bounded Catch-Up — Authoritative tick, deadline과 bounded catch-up 정책을 정리한 이전 글입니다.
- World Server 실행 ownership과 tick pipeline — Ingress swap과 Outbound 2/3-slot 협업 구조입니다.
- WorldIngressDoubleBuffer.h — Runtime event를 보관하는 Ingress slot입니다.
- WorldOutboundDoubleBuffer.h — 2/3-slot storage와 순서가 보장된 batch 게시를 제공하는 Outbound Buffer입니다.
- WorldDoubleBufferedTickCoordinator.h — Sealed input, fixed-step simulation, canonical commit과 Outbound seal을 조정합니다.
이 게시물은 학습한 내용을 바탕으로 초안을 작성한 뒤, LLM의 도움을 받아 내용을 검수하고 다듬어 완성되었습니다.