← 기사 목록으로
March 27, 2026
5분 소요

ZigBolt: 우리가 Zig로 자체 Aeron을 구축해 메시지당 20나노초를 달성한 이유

ZigBolt: 우리가 Zig로 자체 Aeron을 구축해 메시지당 20나노초를 달성한 이유
#zigbolt
#zig
#고빈도매매
#초저지연
#메시징
#aeron
#ipc
#오픈소스

ZigBolt — 초저지연 메시징 Lock-free 링 버퍼, 제로 카피 코덱, Raft 클러스터 — 모두 순수 Zig로, 모두 오픈소스.

알고리즘 트레이딩이나 마켓 메이킹을 하고 있다면, 1마이크로초의 가치를 잘 알 것입니다. 컨텍스트 스위치 하나만 추가되어도 주문이 한 발 늦게 도착합니다. JVM 가비지 컬렉션이 한 번 멈추면, 반대편의 마켓 메이커는 이미 호가를 갱신했습니다. 나노초 단위로 수익이 결정되는 세계에서, 메시징 인프라는 서비스 간의 지루한 파이프가 아니라 핵심 경쟁력입니다.

우리는 ZigBolt를 만들었습니다 — Zig 언어로 처음부터 구축한 고빈도 매매용 메시징 시스템입니다. JVM 없이, 가비지 컬렉터 없이, Media Driver 없이, XML 설정 없이. 그리고 SPSC 링 버퍼에서 p50 지연 20나노초, 공유 메모리 IPC에서 30나노초를 달성했습니다.

이 글에서는 왜 이것이 필요했는지, 내부 동작 원리, 그리고 왜 Zig를 선택했는지를 설명합니다.


요약

  • ZigBolt — 순수 Zig로 작성된 HFT용 오픈소스(MIT) 메시징 시스템
  • SPSC p50: 20ns, IPC p50: 30ns — Aeron의 공식 성능 수치를 상회
  • 제로 카피 코덱은 0ns로 동작 (컴파일 타임 코드 생성, 런타임은 단순 포인터 캐스트)
  • GC 없음, JVM 없음, Media Driver 없음 — 라이브러리를 앱에 직접 임베딩
  • Raft 클러스터, 아카이브, 시퀀서 — 모두 내장
  • FFI 바인딩: Rust, Python, Go, TypeScript, C 지원 — 편한 언어로 사용

문제점: Aeron은 훌륭하지만 충분하지 않다

Real Logic의 Aeron은 자본시장 저지연 메시징의 사실상 표준입니다. 수십 개의 HFT 회사가 사용하고 있으며, 실전에서 검증되었고, 우수한 아키텍처를 갖추고 있습니다. 그러나 Aeron에는 근본적인 문제가 있습니다 — JVM입니다.

JVM safepoints: 보이지 않는 적

모든 데이터를 신중하게 오프힙 메모리에 배치하고, GC 에르고노믹스를 끄고, GuaranteedSafepointInterval=300000을 설정해도, JVM은 여전히 때때로 safepoint에서 모든 스레드를 중지시킵니다. 이것은 버그가 아니라 아키텍처 설계입니다. JVM은 디옵티마이제이션, 바이어스드 락킹, 스택 워킹을 위해 safepoint가 필요합니다.

실제로는 이런 일이 벌어집니다: 스레드가 p50 = 200ns로 메시지를 보내고 있는데, 갑자기 p99.9가 50μs로 치솟습니다. 뚜렷한 원인 없이. JVM의 어떤 스레드가 safepoint 시간이라고 판단했기 때문입니다.

Media Driver: 불필요한 홉

Aeron은 Media Driver를 통해 작동합니다 — 공유 메모리를 통해 퍼블리셔와 서브스크라이버 간에 메시지를 라우팅하는 별도의 프로세스(또는 임베디드 JVM)입니다. 깔끔한 격리를 제공하지만, 최소 한 번의 추가 홉이 발생합니다:

Aeron:    App → shm → Media Driver → shm → socket → NIC
ZigBolt:  App → ring buffer → io_uring → NIC

매 홉마다 추가 나노초, 추가 캐시 미스, 추가 불확실성이 더해집니다.

SBE: 별도의 빌드 단계

Simple Binary Encoding은 금융 메시지를 위한 표준 FIX 코덱입니다. Aeron 생태계에서는 XML 스키마로부터 코드를 생성하는 별도의 Java 유틸리티입니다. 별도의 의존성, 별도의 빌드 단계, 별도의 문제들.


해결책: ZigBolt

우리는 스스로에게 물었습니다: Aeron의 최고의 아이디어들 — 트리플 버퍼 로그, lock-free 링 버퍼, Raft 클러스터 — 을 다음과 같은 특성을 가진 언어로 구현하면 어떨까:

  1. 런타임 오버헤드 없음 (GC 없음, safepoints 없음)
  2. 컴파일 타임에 코드 생성 가능 (comptime)
  3. C 라이브러리(DPDK, io_uring)와의 통합이 간단
  4. 바이너리 크기 약 100KB

그 언어는 바로 Zig입니다.


아키텍처

┌─────────────────────────────────────────────────────────┐
│  Publisher/Subscriber API (타입화된 래퍼)                  │
├─────────────────────────────────────────────────────────┤
│  Transport Layer (채널 팩토리, 라이프사이클)                │
├─────────────────────────────────────────────────────────┤
│  IPC Channel (공유 메모리)    │ UDP Channel (네트워크)     │
├─────────────────────────────────────────────────────────┤
│  WireCodec (comptime 제로 카피) │ SBE Encoder/Decoder    │
├─────────────────────────────────────────────────────────┤
│  Ring Buffers (SPSC/MPSC) │ LogBuffer (트리플 버퍼)       │
├─────────────────────────────────────────────────────────┤
│  Archive (리플레이) │ Sequencer (전체 순서) │ Raft (HA)    │
└─────────────────────────────────────────────────────────┘

7개의 계층으로 구성되며, 각 계층은 독립적으로 사용할 수 있습니다. 두 프로세스 간 IPC용 SPSC 링 버퍼만 필요한가요? 그대로 사용하면 됩니다. Raft 합의와 아카이브가 포함된 완전한 클러스터가 필요한가요? 그것도 있습니다.


벤치마크: 말이 아닌 숫자로

ZigBolt 벤치마크 결과

다음은 1,000만 회 반복(Apple Silicon / macOS)에서의 실측 결과입니다:

SPSC 링 버퍼

메시지 크기 p50 p99 p99.9 처리량
8바이트 20ns 30ns 120ns 42.8M msg/s
32바이트 30ns 50ns 150ns 28.5M msg/s
64바이트 50ns 60ns 320ns 17.6M msg/s
256바이트 30ns 50ns 50ns 29.5M msg/s

IPC Channel (공유 메모리)

메시지 크기 p50 p99 p99.9 처리량
64바이트 30ns 40ns 40ns 35.7M msg/s
256바이트 40ns 40ns 170ns 27.4M msg/s
1024바이트 90ns 260ns 900ns 9.9M msg/s

LogBuffer (Aeron 스타일 트리플 버퍼)

메시지 크기 p50 p99 p99.9 처리량
32바이트 30ns 40ns 320ns 33.6M msg/s
64바이트 30ns 30ns 160ns 38.0M msg/s
256바이트 30ns 40ns 60ns 31.1M msg/s

WireCodec (comptime 제로 카피)

연산 지연 처리량
인코딩 (32바이트) 0ns ∞ (인라인 memcpy)
디코딩 (32바이트) ~0.4ns 27억 msg/s

네, 제대로 읽으셨습니다: 인코딩은 0나노초입니다. WireCodec(T)이 컴파일 타임에 구조체를 검증하고 encode/decode를 일반적인 @memcpy 또는 포인터 캐스트로 변환하기 때문입니다. 런타임 오버헤드 = 제로.

비교를 위해: Aeron은 IPC RTT(왕복 지연)를 약 250ns로 공표합니다. 우리의 단방향 지연은 30ns입니다. 왕복으로 계산해도 4배 더 빠릅니다.


내부 동작 원리

Lock-free SPSC: 단순함이라는 미덕

SPSC 링 버퍼

단일 생산자-단일 소비자 링 버퍼는 가장 단순하고 빠른 lock-free 자료구조입니다. 쓰기 측이 head를 이동시키고, 읽기 측이 tail을 이동시킵니다. CAS가 필요 없이 acquire/release 아토믹만으로 충분합니다.

핵심 기법은 캐시 라인 패딩입니다. headtail이 같은 캐시 라인에 있으면, 한쪽 카운터의 업데이트가 다른 코어의 캐시를 무효화합니다(false sharing). 해결책:

// Head (쓰기 위치) — 자체 캐시 라인
head: std.atomic.Value(usize) align(128) = .init(0),

// 128바이트 패딩 — 격리 보장
_pad0: [128 - @sizeOf(std.atomic.Value(usize))]u8 = .{0} ** ...,

// Tail (읽기 위치) — 자체 캐시 라인
tail: std.atomic.Value(usize) align(128) = .init(0),

64바이트가 아닌 128바이트입니다 — Apple Silicon(및 많은 ARM)에서 하드웨어 프리페처가 캐시 라인 쌍 단위로 작동할 수 있기 때문입니다. 안전한 선택을 했습니다.

WireCodec: 코드 생성 대신 comptime

WireCodec — 컴파일 타임 코덱

Java/C++ 세계에서 바이너리 코덱을 사용하려면 별도의 단계가 필요합니다: 스키마 작성 -> 코드 생성기 실행 -> 코드 획득 -> 컴파일. Zig에서는 이 모든 것이 컴파일 타임에 완료됩니다:

const TickMsg = packed struct {
    symbol_id: u32,
    price: i64,
    quantity: u32,
    side: u8,
    _reserved: [3]u8,
    timestamp: u64,
};

const Codec = WireCodec(TickMsg);

// 인코딩 — 32바이트 memcpy에 불과. 1-2개 명령어로 인라인화.
Codec.encode(&msg, buf[0..Codec.wire_size]);

// 디코딩 — 포인터 캐스트. 복사 제로.
const tick = Codec.decode(buf[0..Codec.wire_size]);

Zig 컴파일러는 comptime 단계에서 다음을 검증합니다:

  • 구조체가 packed인지 (패딩 홀 없음)
  • 크기가 8바이트의 배수인지 (SIMD 정렬)
  • 모든 필드가 프리미티브 타입인지

문제가 있으면 — 컴파일 에러가 발생합니다. 새벽 3시 프로덕션 환경의 런타임 예외가 아닙니다.

공유 메모리를 통한 IPC

두 프로세스가 /dev/shm의 동일 파일을 매핑합니다. 퍼블리셔가 링 버퍼에 쓰고, 서브스크라이버가 읽습니다. 핫 패스에 소켓도 시스템 콜도 없습니다:

// Publisher
const channel = try IpcChannel.create("/market-data", .{
    .term_length = 1024 * 1024, // 1 MB
});
channel.publish(&msg_bytes, msg_type_id);

// Subscriber (다른 프로세스)
const channel = try IpcChannel.open("/market-data", .{
    .term_length = 1024 * 1024,
});
const count = channel.poll(handler_fn, 10);

publish()에서 서브스크라이버의 handler_fn 호출까지의 전체 경로 — 64바이트 메시지 기준 30나노초.

NAK 기반 UDP 신뢰성

네트워크 전송에서 ZigBolt는 수신 측 주도 재전송 메커니즘을 사용합니다. 수신 측이 비트맵으로 시퀀스 번호의 갭을 추적하고, 송신 측에 NAK(부정 확인 응답)을 보냅니다. 여기에 AIMD 혼잡 제어 — TCP와 유사한 슬로우 스타트 및 혼잡 회피 — 를 적용하여 네트워크 과부하를 방지합니다.

Raft 클러스터: 일관성이 필수일 때

메시지 손실이 허용되지 않는 경우(예: 매칭 엔진), ZigBolt는 완전한 Raft 합의를 탑재하고 있습니다:

  • 리더 선출: 설정 가능한 타임아웃(150-300ms)
  • 로그 복제 — 리더가 각 메시지를 팔로워에 복제
  • WAL(선행 기록 로그): CRC32 검증 및 장애 복구
  • 스냅샷 — WAL의 무한 증가 방지

아카이브: 기록과 리플레이

모든 메시지를 디스크의 세그먼트 아카이브에 기록할 수 있습니다. 이후 시간 또는 시퀀스 번호로 임의 위치에서 리플레이 가능합니다. 외부 의존성 없는 내장 LZ4 스타일 압축. 세그먼트 내 빠른 검색을 위한 희소 인덱스.

전체 순서 시퀀서

여러 거래소에서 마켓 메이킹을 할 때, 모든 이벤트에 글로벌 순서가 필요합니다. 시퀀서는 N개의 입력 스트림을 받아 단조 증가하는 시퀀스 번호를 부여하며 하나로 병합합니다. 모든 참여자가 동일한 이벤트 시퀀스를 보게 됩니다.


왜 Rust/C/C++가 아닌 Zig인가?

네 가지 후보 언어를 비교했습니다. 아래는 공정한 비교표입니다:

기준 Zig C/C++ Rust Java (Aeron)
GC / 런타임 오버헤드 없음 없음 없음 JVM safepoints, GC
Comptime 코드 생성 네이티브 매크로/템플릿 proc macros 없음
C 상호운용(DPDK, io_uring) 간단한 @cImport 네이티브 FFI/bindgen JNI 오버헤드
SIMD @Vector, 내장 Intrinsics packed_simd (불안정) 벡터화 힌트
크로스 컴파일 내장 CMake 지옥 cargo target N/A
빌드 시간 초 단위 분 단위 (C++) 분 단위 초 단위 + JVM 시작
숨겨진 제어 흐름 없음 예외, 암시적 캐스팅 unwrap의 패닉 예외

Zig는 독보적인 조합을 제공합니다: C의 성능 + 개발 시 안전성 + comptime 메타프로그래밍(코덱, 룩업 테이블, 프로토콜 상태 머신 — 모두 컴파일 타임에 생성) + @cImport를 통한 DPDK, liburing, ef_vi와의 원활한 통합.

게다가 Zig 바이너리는 약 100KB에 불과합니다. JVM 기반 솔루션의 20MB 이상과 비교해 보세요. 엣지 배포와 컨테이너에 있어 이는 중요합니다.


바인딩: 익숙한 언어로

ZigBolt는 C-ABI 공유 라이브러리로 컴파일되며, 다섯 가지 언어의 기성 바인딩을 제공합니다:

TypeScript / Node.js

import { IpcChannel } from "@zigbolt/node";

const channel = IpcChannel.create({
  name: "/my-market-data",
  termLength: 1024 * 1024,
});

const msg = Buffer.from("BTC/USDT 42000.50", "utf-8");
channel.publish(msg, 1);

Rust

use zigbolt::IpcChannel;

let ch = IpcChannel::create("/my-channel", 64 * 1024).unwrap();
ch.publish(b"hello", 1).unwrap();

// Subscriber
let sub = IpcChannel::open("/my-channel", 64 * 1024).unwrap();
sub.poll(|data, msg_type_id| {
    println!("got {} bytes, type={}", data.len(), msg_type_id);
}, 10);

Python

from zigbolt import IpcChannel

ch = IpcChannel.create("/market-data", term_length=1024*1024)
ch.publish(b"tick data here", msg_type_id=1)

Go와 순수 C 바인딩도 있습니다. 동일한 공유 메모리 채널에 모든 언어에서 동시에 접근 가능합니다 — 퍼블리셔는 Zig, 서브스크라이버는 Python, 모니터링은 Go. 모두가 같은 mmap 영역을 읽습니다.


SBE 코덱: FIX 호환 메시지

금융 프로토콜을 위해 ZigBolt는 컴파일 타임 스키마를 갖춘 완전한 SBE(Simple Binary Encoding) 코덱을 탑재하고 있습니다. 내장 메시지 타입:

  • NewOrderSingle — 주문 발주
  • ExecutionReport — 체결 보고
  • MarketDataIncrementalRefresh — 증분 시장 데이터 업데이트
  • MassQuote — 대량 호가
  • Heartbeat — 연결 확인
  • Logon — 인증

외부 코드 생성기 불필요, XML 불필요. 모든 것을 Zig 구조체로 기술하며 컴파일 타임에 검증됩니다.


Wire Protocol: Aeron 호환

ZigBolt는 Aeron 호환 wire protocol flyweights를 구현합니다:

  • DataHeaderFlyweight — 데이터 프레임
  • StatusMessage — 흐름 제어
  • NAK — 부정 확인 응답
  • Setup, RTT, Error — 제어 프레임

이는 ZigBolt가 기존 Aeron 인프라와 공존할 수 있음을 의미합니다. 마이그레이션을 한 번에 할 필요가 없습니다.


향후 계획

ZigBolt는 현재 버전 0.2.1입니다. 코어는 안정적이고, 벤치마크는 재현 가능하며, 바인딩은 정상 작동합니다. 향후 계획:

  • io_uring 백엔드 — Linux 6.0+에서 제로 카피 네트워크 전송 (IORING_OP_SEND_ZC)
  • DPDK / AF_XDP — 1마이크로초가 승패를 가르는 상황을 위한 커널 바이패스
  • Multi-Raft — 종목/전략별 샤딩
  • 컬럼나 아카이브 — 분석을 위한 Apache Arrow/Parquet 통합
  • Hugepage 지원 — TLB 미스 최소화를 위한 사전 할당 2MB/1GB 대형 페이지

직접 사용해 보세요

소스에서 빌드(zig build)하고, 벤치마크를 실행(zig build bench)하고, FFI로 어떤 언어에서든 연결하세요. Zig 0.15.1과 몇 분의 시간이 있다면 — ping-pong 벤치마크를 시도해서 현재 솔루션과 비교해 보세요.


링크:


인용

@software{soloviov2026zigbolt,
  author = {Soloviov, Eugen},
  title = {ZigBolt: 우리가 Zig로 자체 Aeron을 구축해 메시지당 20나노초를 달성한 이유},
  year = {2026},
  url = {https://marketmaker.cc/ko/blog/post/zigbolt-zig-messaging-hft},
  version = {0.2.1},
  description = {HFT를 위한 초저지연 메시징 시스템을 Zig 언어로 처음부터 구축한 과정과 이유. JVM 없이, GC 없이, 예상치 못한 지연 없이.}
}
blog.disclaimer

MarketMaker.cc Team

퀀트 리서치 및 전략

Telegram에서 토론하기
Newsletter

시장에서 앞서 나가세요

뉴스레터를 구독하여 독점적인 AI 트레이딩 통찰력, 시장 분석 및 플랫폼 업데이트를 받아보세요.

귀하의 개인정보를 존중합니다. 언제든지 구독을 취소할 수 있습니다.