본문으로 건너뛰기
Folly Code Review · 83/89

folly::RecordIO — append-only 로그 파일 포맷

· Hawk · 4분 읽기

한 줄 요약: RecordIO는 가변 길이 record를 append-only로 쓰고 mid-file부터도 안전하게 읽을 수 있는 파일 포맷이다. 각 record에 magic, length, checksum이 붙어 truncation/corruption을 견딘다.

#동기

append-only log 파일은 흔하다. event log, audit log, write-ahead log, message queue spool. 다음을 만족해야 한다.

  1. write가 atomic — partial write가 다음 read를 망가뜨리면 안 됨.
  2. mid-file부터 읽기 가능 — process crash 후 임의 offset에서 다음 valid record를 찾을 수 있어야.
  3. 각 record가 self-describing — length, checksum 포함.
  4. schema 유연 — record가 임의 binary blob.

이걸 매번 직접 짜면 endian, alignment, frame sync에서 실수가 나온다. RecordIO가 표준화한다.

#Frame 포맷

+----------------+----------------+----------------+----------------+
| magic (4) | length (4) | fileId (4) | checksum (4) |
+----------------+----------------+----------------+----------------+
| payload (length bytes) |
+-------------------------------------------------------------------+
총 헤더: 16 bytes
magic : 0xFADE0001 (sync word)
length : payload 길이 (big endian)
fileId : 같은 파일을 식별하는 32-bit (random per file)
checksum : SpookyHashV2 결과 32-bit (header + payload)

핵심은 두 가지.

  • magic — 임의 offset에서 검색 시작점. magic이 발견되면 candidate record.
  • checksum — magic이 우연히 데이터 안에 나올 수 있으므로 false positive 걸러냄.
  • fileId — 두 RecordIO 파일이 어쩌다 concat되면 다른 fileId로 구분.

#API

#include <folly/io/RecordIO.h>
// write
{
folly::File f("log.recordio", O_WRONLY | O_CREAT | O_APPEND);
folly::RecordIOWriter writer(std::move(f));
writer.write(folly::IOBuf::wrapBuffer(data1, len1));
writer.write(folly::IOBuf::wrapBuffer(data2, len2));
}
// read
{
folly::File f("log.recordio", O_RDONLY);
folly::RecordIOReader reader(std::move(f));
for (auto rec : reader) {
folly::ByteRange payload = rec.first;
off_t offset = rec.second;
Process(payload);
}
}

Writer는 단순 append, Reader는 iterator로 record를 streaming.

#Random-access seek + 복구

folly::RecordIOReader reader(std::move(f));
// 파일 중간 임의 offset에서 다음 valid record 찾기
auto it = reader.seek(offset);
for (; it != reader.end(); ++it) {
Process(it->first);
}

seek(offset)은 offset 이후의 첫 valid record를 찾는다. magic을 forward 검색 → checksum 검증 → 통과한 record를 반환.

partial write/corruption 영역은 자동으로 skip. log file이 crash로 중간이 잘려도 다음 valid frame부터 복원.

#내부 구현

// folly/io/RecordIO.cpp 약식
struct Header {
uint32_t magic;
uint32_t length;
uint32_t fileId;
uint32_t checksum;
};
void RecordIOWriter::write(std::unique_ptr<folly::IOBuf> buf) {
Header h;
h.magic = kMagic;
h.length = buf->computeChainDataLength();
h.fileId = fileId_;
h.checksum = computeChecksum(h, *buf); // header + payload
pwrite(fd_, &h, sizeof(h), offset_);
pwriteIOBuf(fd_, *buf, offset_ + sizeof(h));
offset_ += sizeof(h) + h.length;
}
static uint32_t computeChecksum(const Header& h, const IOBuf& buf) {
folly::hash::SpookyHashV2 sp;
sp.Init(0, 0);
// header without checksum field
Header partial = h;
partial.checksum = 0;
sp.Update(&partial, sizeof(partial));
// payload
for (auto& range : buf) {
sp.Update(range.data(), range.size());
}
uint64_t h1, h2;
sp.Final(&h1, &h2);
return static_cast<uint32_t>(h1);
}

writer는 단순 append. 한 record가 atomic하게 보이도록 한 번의 write로 (가능하면 writev/iovec) 보낸다.

// reader iterator
auto RecordIOReader::Iterator::operator++() -> Iterator& {
for (;;) {
if (offset_ >= fileSize_) { /* end */ return *this; }
Header h;
pread(fd_, &h, sizeof(h), offset_);
if (h.magic != kMagic) {
offset_ = findNextMagic(offset_ + 1); // forward search
continue;
}
if (h.fileId != fileId_) {
offset_ = findNextMagic(offset_ + 1);
continue;
}
// 길이 sanity
if (offset_ + sizeof(h) + h.length > fileSize_) {
offset_ = findNextMagic(offset_ + 1);
continue;
}
auto buf = readPayload(offset_ + sizeof(h), h.length);
if (computeChecksum(h, *buf) != h.checksum) {
offset_ = findNextMagic(offset_ + 1);
continue;
}
current_ = {buf->coalesce(), offset_};
offset_ += sizeof(h) + h.length;
return *this;
}
}

reader는 각 record를 전부 검증. magic mismatch, fileId mismatch, length impossible, checksum mismatch 중 하나면 forward search로 다음 magic을 찾는다.

#사용 패턴

#Streaming write

folly::RecordIOWriter writer(folly::File("events.log", O_WRONLY | O_APPEND | O_CREAT));
void OnEvent(const Event& e) {
auto buf = SerializeToIOBuf(e);
writer.write(std::move(buf));
}

매 event마다 append. 순서 보존.

#Crash 복구

folly::RecordIOReader reader(folly::File("events.log", O_RDONLY));
size_t valid = 0, recovered = 0;
for (auto rec : reader) {
if (rec.first.empty()) continue;
Replay(rec.first);
++valid;
}
LOG(INFO) << "valid records: " << valid;

crash 후 read 시 corrupt 영역은 자동으로 skip. fsync 안 한 write가 사라지더라도 valid 데이터는 모두 복원.

#std와의 비교

항목표준 (없음)folly::RecordIOApache Hadoop SequenceFileProtobuf delimited
frameN/Amagic+length+checksumlength+sync blocklength-prefixed
복구N/Amid-file seeksync marker전체 재처리
checksumN/ASpookyHashV2optional없음
append-safeN/AOOpartial 가능
schemaN/Abinary blobtypedprotobuf

RecordIO프레임 자체에 집중. payload 안의 schema(JSON, Thrift, Protobuf 등)는 호출자 책임.

비슷한 포맷: Kafka log segment, Apache Avro container file, RocksDB WAL — 모두 magic+length+checksum 패턴이 표준.

#코드 리뷰 포인트

  • write가 O_APPEND 없이 열림 → 동시 write 시 truncation. 항상 O_APPEND.
  • fsync 정책 — RecordIO는 fsync 안 함 (호출자 결정). durability 필요하면 명시적 fsync.
  • record가 큼 (수십 MB) → IOBuf 체인이 한 번에 write되도록. memory pressure 확인.
  • corruption 영역이 전체 file의 큰 비율이면 forward search가 매우 느림. 적절히 archive/rotate.
  • fileId가 우연히 다른 파일과 같으면 concat된 파일에서 잘못 인식 — random 32-bit이라 확률 낮으나 zero가 의도된 값이면 충돌.

#자주 보는 안티패턴

// 1. fsync 없이 곧바로 process 종료 가능한 코드
writer.write(buf);
exit(0); // page cache가 disk에 안 갔을 수 있음
// → fsync(fd) 또는 graceful shutdown
// 2. record가 너무 작음 (수 byte)
for (int i = 0; i < 1e6; ++i) {
writer.write(folly::IOBuf::wrapBuffer(&i, sizeof(i)));
}
// → 16-byte header overhead가 80%. 묶어서 write 또는 다른 포맷.
// 3. reader가 매 read마다 새 File 열기
for (auto& path : files) {
RecordIOReader r(File(path, O_RDONLY));
for (auto rec : r) { ... }
}
// → 정상 사용. 단 동일 파일을 반복 열면 page cache는 유지되지만 syscall은 누적.
// 4. checksum 무시
for (auto rec : reader) {
if (rec.first.empty()) continue; // forward search 결과는 OK
// 하지만 reader가 그저 raw read만 한다면 checksum 검증 skipping 위험
}

#정리

  • RecordIO는 append-only log 파일의 표준 frame format.
  • magic + length + fileId + checksum (16 byte header).
  • mid-file seek로 corruption 영역 자동 skip 후 복원.
  • write는 단순 append, fsync는 호출자 결정.
  • payload schema는 호출자가 결정 (binary blob).

#다음 편

Part 20-02: Compression에서 zstd/lz4/snappy wrapper를 본다.

#관련 항목

Folly Code Review · 84 of 89

  1. 1 Folly Code Review — Meta의 production-grade C++ 라이브러리 코드 분석
  2. 2 Folly 개요 — Meta가 production에서 검증한 utility 모음 분석
  3. 3 Folly vs Abseil 철학 비교 — performance-first vs std-compatible
  4. 4 Folly 빌드와 fbcode 환경 — monorepo의 그림자
  5. 5 Folly API stability 정책 — 어떤 보장도 없다는 솔직함
  6. 6 Folly production validation 문화 — peta-scale에서 단련된 코드
  7. 7 folly::Future 분석 — std::future의 한계를 넘는 composable async
  8. 8 folly::Promise·makeFuture — Future를 만드는 두 길
  9. 9 folly::SemiFuture vs Future — executor binding의 명시화
  10. 10 folly::Future thenValue·thenError·thenTry — continuation 체인 분석
  11. 11 folly::collect·collectAll·collectAny — fan-in 패턴 분석
  12. 12 folly::Future retry·window·via — 제어 흐름 조합자
  13. 13 folly::fibers 분석 — M:N stackful coroutine
  14. 14 folly::InlineExecutor — 호출자 thread에서 즉시 실행
  15. 15 folly::CPUThreadPoolExecutor — CPU-bound 작업의 표준 thread pool
  16. 16 folly::IOThreadPoolExecutor — libevent 기반 I/O pool
  17. 17 folly::ManualExecutor — 결정적 테스트를 위한 수동 진행
  18. 18 folly::EventBase 분석 — libevent 이벤트 루프의 핵심
  19. 19 folly::IOBuf 분석 — zero-copy buffer chain의 기본 단위
  20. 20 folly::IOBufQueue — chain의 push/pull 추상화
  21. 21 folly::io::Cursor·RWCursor — chain 위의 stream
  22. 22 folly Zero-copy 패턴 — IOBuf로 ScatterGather I/O 표현
  23. 23 folly::IOBuf shared semantics — clone·unshare·takeOwnership
  24. 24 folly::FBString 분석 — SSO + COW 구현
  25. 25 folly의 fmt::format 통합 — 모던 포맷팅 채택
  26. 26 folly::StringPiece — string_view 호환 분석
  27. 27 folly Join·Split utilities — 문자열 분해와 결합
  28. 28 folly::to·tryTo — text↔num 변환 분석
  29. 29 folly Conv Customization — 사용자 타입 지원
  30. 30 folly Conv 성능 비교 — sprintf·stringstream 대비
  31. 31 folly::F14ValueMap vs std::unordered_map
  32. 32 folly::F14NodeMap — stable pointer가 필요할 때
  33. 33 folly::F14VectorMap — cache-friendly iteration
  34. 34 folly::F14FastMap — auto-select 동작
  35. 35 folly F14 internals — SIMD probing 메커니즘
  36. 36 folly::small_vector — inline storage 분석
  37. 37 folly::FixedString — compile-time string
  38. 38 folly::AtomicHashMap — lock-free read 분석
  39. 39 folly::ConcurrentHashMap — sharded 동시 해시 맵
  40. 40 folly::EvictingCacheMap — LRU 구현 분석
  41. 41 folly::Synchronized — lock wrapper 패턴
  42. 42 folly::SharedMutex 분석
  43. 43 folly::Baton — one-shot wait 동기화
  44. 44 folly::RWSpinLock 분석
  45. 45 folly::PicoSpinLock — 1-byte spinlock
  46. 46 folly::ProducerConsumerQueue — SPSC 큐 분석
  47. 47 folly::MPMCQueue — multi-producer multi-consumer
  48. 48 folly::UnboundedQueue — 동적 크기 lock-free
  49. 49 folly::fibers::Channel — Go-like channel
  50. 50 folly::dynamic — JSON-like dynamic type 분석
  51. 51 folly JSON conversion — toJson·parseJson
  52. 52 folly dynamic ↔ struct — manual marshaling
  53. 53 folly dynamic Visitor pattern — type별 분기
  54. 54 folly::Singleton vs Meyers/static — 왜 Folly의 Singleton인가
  55. 55 folly::SingletonVault 분석 — 등록·소멸·의존성
  56. 56 folly::Singleton try_get·try_get_fast — TLS-cached 접근
  57. 57 folly::ExceptionWrapper — type-erased exception holder
  58. 58 folly::ScopeGuard·SCOPE_EXIT — RAII cleanup
  59. 59 folly::Optional vs std::optional
  60. 60 folly::Function vs std::function
  61. 61 folly::Lazy — 지연 초기화 wrapper
  62. 62 folly Meta 스타일 code review 패턴
  63. 63 folly anti-patterns — 잘못 쓰면 std보다 느림
  64. 64 folly vs std 선택 기준 분석
  65. 65 folly::coro 개요 — production C++20 코루틴 어댑터
  66. 66 folly::coro::Task — lazy single-shot 코루틴
  67. 67 folly::coro::AsyncGenerator — 비동기 스트림
  68. 68 folly coro blockingWait·collectAll — 동기 경계와 fan-in
  69. 69 folly::coro::Baton·Mutex — 코루틴-aware 동기화
  70. 70 folly::Expected — 결과 또는 오류
  71. 71 folly::Try — Future 결과 wrapper
  72. 72 folly::Try vs Expected 선택 기준
  73. 73 folly::Range — 일반 iterator pair
  74. 74 folly::Uri — URL 파서
  75. 75 folly Fingerprint64·128 — 분산 hash
  76. 76 folly SpookyHashV2 — fast non-crypto hash
  77. 77 folly::Init — main() 부트스트랩
  78. 78 folly::Indestructible — global lifetime 패턴
  79. 79 folly::MicroLock — 1-byte 락
  80. 80 folly::MicroSpinLock — 가장 좁은 spin lock
  81. 81 folly::format — legacy formatter 분석
  82. 82 folly::demangle — typeid 디망글링
  83. 83 folly::DynamicConverter — dynamic ↔ struct
  84. 84 folly::RecordIO — append-only 로그 파일 포맷
  85. 85 folly::io::Compression — zstd·lz4·snappy wrapper
  86. 86 folly::AsyncIO — io_uring·Linux AIO
  87. 87 folly::CancellationToken — 코루틴·Future 취소 전파
  88. 88 folly::observer — hot config의 atomic refresh
  89. 89 fbcode 패턴 모음 — folly 사용의 실전