고처리량 IoT 로그 집계기

발행: (2025년 12월 26일 오전 04:47 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

Cover image for High-Throughput IoT Log Aggregator

수천 개의 센서가 매초 텔레메트리 패킷을 전송하는 산업 모니터링 시스템을 상상해 보세요. 시스템은 다음을 수행해야 합니다:

  • 원시 데이터 패킷 배치를 수집한다.
  • 비활성 센서를 필터링한다.
  • 디바이스 ID별로 온도 값을 집계한다.
  • 대시보드를 위한 텍스트 요약 로그를 생성한다.

문제는 이 프로세스가 지속적으로 실행된다는 점이며, 작은 비효율(예: 불필요한 메모리 할당)도 가비지 컬렉션 일시 중지와 데이터 손실을 초래할 수 있습니다. 해결책은 Go에서 메모리 효율적인 패턴을 사용하는 것입니다.

시스템 흐름도

flowchart LR
    A[Raw Data Ingestion] -->|Slice Pre‑allocation| B[Batch Processing]
    B -->|Value Semantics| C{Filter Inactive}
    C -->|Map Pre‑allocation| D[Aggregation]
    D -->|strings.Builder| E[Log Generation]
    E --> F[Final Report]

Source:

최적화 기법

구조체 정렬

type SensorPacket struct {
    Timestamp int64   // 8 bytes
    Value     float64 // 8 bytes
    DeviceID  int32   // 4 bytes
    Active    bool    // 1 byte
    // 3 bytes padding added automatically
}

패킷당 8바이트를 절약하면 백만 레코드당 약 8 MB의 RAM을 절감할 수 있습니다.

슬라이스 사전 할당

packets := make([]SensorPacket, 0, n) // n = expected batch size

사전 할당 없이 100 000개의 항목을 로드하면 약 18번의 리사이즈와 많은 메모리 복사가 발생합니다.

맵 크기 힌트

agg := make(map[int32]float64, 100) // anticipate ~100 devices

버킷을 미리 할당하면 맵이 성장할 때 발생하는 비용이 큰 재해싱을 방지할 수 있습니다.

strings.Builder

var sb strings.Builder
sb.WriteString("Device ")
sb.WriteString(strconv.Itoa(id))
sb.WriteString(": Avg Temp ")
sb.WriteString(fmt.Sprintf("%.2f", avg))

빌더를 사용하면 수백 개의 임시 문자열 생성을 피할 수 있습니다.

값 vs 포인터

func processBatch(cfg Config, data []SensorPacket) *Report {
    // cfg passed by value (fast stack access)
    // Report returned as a pointer to avoid copying the large map
}

메모리 레이아웃 비교

최적화된 (현재 코드)

[ Timestamp (8) ] [ Value (8) ] [ DeviceID (4) | Active (1) | Pad (3) ]
Total: 24 Bytes / Block

비최적화된 (혼합)

[ Active (1) | Pad (7) ] [ Timestamp (8) ] [ DeviceID (4) | Pad (4) ] [ Value (8) ]
Total: 32 Bytes / Block (33% wasted memory!)

예시 결과

--- Processing Complete in 6.5627ms ---
--- BATCH REPORT ---
Batch ID: 1766689634
Device 79: Avg Temp 44.52
Device 46: Avg Temp 46.42
Device 57: Avg Temp 45.37
Device 11: Avg Temp 44.54
Device 15: Avg Temp 46.43
... (truncated)

벤치마크 결과

연산구현시간 (ns/op)메모리 (B/op)할당 (op)성능 향상
Slice Append비효율적66,035357,62619
효율적 (Pre‑alloc)15,87381,9201약 4.1× 빠름
String Build비효율적 (+)8,72721,08099
효율적 (Builder)244.74161약 35.6× 빠름
Map Insert비효율적571,279591,48579
효율적 (Size hint)206,910295,55433약 2.7× 빠름
Struct Pass값 전달 (복사)0.2600
포인터 전달 (참조)0.2500비슷함

구조체에 대한 참고: 마이크로 벤치마크에서는 Go 컴파일러가 호출을 인라인화하기 때문에 값 전달과 포인터 전달 사이의 차이가 거의 없습니다. 실제 코드에서 호출 스택이 깊어질 경우, 큰 구조체를 포인터로 전달하면 CPU 사용량을 눈에 띄게 줄일 수 있습니다.

주요 내용

  • 문자열 연결: 루프에서 + 사용을 피하세요. strings.Builder는 중간에 생성되는 가비지 문자열을 없애서 35배 이상 빠르고 메모리를 98 % 절감합니다.
  • 메모리 사전 할당: 슬라이스와 맵에 미리 용량을 지정하면 반복적인 크기 조정 및 재해시 비용을 없앨 수 있습니다.
  • 할당 횟수 중요: 할당(allocs/op)이 적을수록 가비지 컬렉터의 작업량이 줄어들어 애플리케이션이 더 안정적이고 반응성이 높아집니다.
  • 슬라이스: 할당 수가 19 → 1로 감소했습니다.
  • 맵: 할당 수가 79 → 33으로 감소했습니다.

이러한 패턴들을 종합하면 IoT 로그 집계기가 높은 처리량 상황에서도 성능을 유지할 수 있습니다.

Back to Blog

관련 글

더 보기 »