이코에코(Eco²)/Observability

이코에코(Eco²) Observability #0: 로깅 파이프라인 아키텍처 설계

mango_fr 2025. 12. 18. 12:12

개요

마이크로서비스 아키텍처에서 로깅은 단순한 디버깅 도구를 넘어 시스템 투명성을 확보하는 핵심 인프라입니다.
이 글에서는 15개 노드, 7개 API 서비스로 구성된 Eco² 백엔드의 로깅 파이프라인 아키텍처 설계 과정을 공유합니다.

배경

  • 도입 전 상태: 각 Pod의 stdout/stderr 로그가 노드에 분산 저장, 구조화된 로깅 정책 부재
  • 문제점: 장애 발생 시 여러 노드를 직접 접속해 로그 확인 필요
  • 목표: 중앙 집중화된 로그 수집, 저장, 검색, 시각화 환경 구축

기능 요구사항

중앙 집중화 모든 노드의 로그를 한 곳에서 조회 P0
실시간 수집 로그 발생 후 5초 이내 검색 가능 P0
구조화된 로깅 JSON 포맷으로 필드별 검색 P1
트레이스 연동 trace_id로 분산 트레이싱과 연결 P1
대시보드 에러율, 응답시간 등 시각화 P2

비기능 요구사항

가용성 99% 로깅 장애가 서비스에 영향 없어야 함
지연 시간 < 5초 로그 발생 → 검색 가능
저장 용량 50GB (7일) 개발 환경 기준
리소스 격리 전용 노드 API 서비스와 분리

EFK 선택 이유: EDA 전환 효율성

1. Logstash CRD 통합 (ECK Operator)
EDA 도입 시 Kafka를 통한 로그 버퍼링과 복잡한 로그 변환(Saga trace_id 상관관계, CDC 이벤트 파싱)이 필요하다.
ECK Operator를 사용하면:

  • Logstash CRD 추가 시 ES 연결 자동 설정
  • TLS/인증 자동 구성
  • 무중단 전환 가능 (Fluent Bit output만 변경)

2. PLG 선택 시 전환 비용
고밀도 로깅 / 분산 환경에서 Logstash가 유리하기에 별도 Consumer 개발 또는 ES로 마이그레이션 필요
3. 로그량 증가 대응
EDA 로드맵에서 분석한 대로, EDA 도입 후 로그량이 폭증함

현재: 1 API 요청 → 1~3개 로그
EDA 후: 1 API 요청 → 10~30개 로그/이벤트
- Kafka Producer/Consumer 로그
- Saga 체인 로그 (시작/완료/실패)
- CDC 이벤트 로그 (Debezium)
- Celery 작업 로그
- 재시도/DLQ 로그

Elasticsearch의 전문 검색은 이러한 복잡한 분산 트랜잭션 로그에서 trace_id 기반 검색에 필수적이다.


ELK vs EFK: Logstash 도입 반려

EDA 전환 백서에서 분석한 대로, 향후 EDA 전환 시 로그 파이프라인에 Logstash가 더 유용할 여지가 있다.
그러나 현재 단계에서는 ELK가 아닌 EFK를 선택했다.

ELK vs EFK 비교

메모리 1~4GB ~5MB/노드
배포 방식 중앙 집중 DaemonSet (노드별)
Pod 친화성 ❌ 낮음 ✅ 높음
로그 변환 ✅ 강력함 (Grok, Ruby) ⚠️ 기본 (Regex, Lua)
Kafka 연동 ✅ 네이티브 Input ⚠️ Output만 지원
현재 필요성 낮음 (동기식 로그) 높음 (수집 특화)

🚫 Logstash 반려 이유: 현재 용도에 비해 과도한 리소스

1. 현재 동기식 로그만 발생
현재 Eco² 백엔드는 HTTP 1.1/gRPC 동기식 아키텍처다:

현재: Client → API → DB → Response
      └── 로그: 요청/응답/에러 (단순)

이 단계에서 Logstash의 강력한 변환 기능(Grok 파싱, Ruby 필터, 다중 파이프라인)은 용도에 비해 과도
2. 리소스 효율성 계산

# ELK (Logstash 포함)
Elasticsearch: 5GB + Kibana: 1GB + Logstash: 1GB = 7GB
└── 8GB 노드에서 시스템 여유 1GB (위험)

# EFK (Fluent Bit)
Elasticsearch: 5GB + Kibana: 1GB + Fluent Bit: ~50MB(전체) = 6GB
└── 8GB 노드에서 시스템 여유 2GB (안정)

3. Logstash 없이 가능한 현재 워크로드

로그 수집 (tail)
JSON 파싱
K8s 메타데이터 ⚠️ 플러그인 ✅ 네이티브
Grok 패턴 파싱 ⚠️ Regex ❌ (JSON 출력)
Kafka Consumer ❌ (현재 MQ 없음)
복잡한 라우팅 ⚠️ 제한적

로그 수집기: Fluent Bit 선택 근거

Pod 친화적 DaemonSet 아키텍처

ELK: 중앙 집중형 (네트워크 병목)

⚠️ 문제점

  • t3.large 단일 노드 기준 EK 만으로 메모리 70% 점유
  • 로그 노드 장애 시 파이프라인 전체 다운 (SOF)

EFK: 분산 수집형 (노드별 처리)

장점:

  • 각 노드에서 로컬 파일 직접 tail (네트워크 비용 최소화)
  • 병렬 처리로 네트워크 병목 분산
  • 노드 장애 시 해당 노드 로그만 영향 (Blast Radius 최소화)

수집기 비교표

언어 C Ruby JRuby
메모리 ~5MB/노드 ~40MB/노드 ~1-4GB
CPU 매우 낮음 낮음 중간
플러그인 기본 제공 풍부함 풍부함
Kubernetes DaemonSet 최적화 가능 비권장
역할 수집/전달 특화 수집/변환 변환/라우팅 특화

선택: Fluent Bit - 경량 수집 에이전트로 리소스 절약, Pod 친화성 확보


향후 확장: EFK → EFKL 전환 계획

현재 EFK를 선택한 것은 Logstash를 영구 배제한 것이 아니다.
EDA 전환 백서에서 예측한 대로, MQ 도입 시 로그가 5~10배 폭증하면 Logstash가 필수가 된다.

Phase 2에서 Logstash 변환이 필요한 사례

1. ECK Operator 기반으로 구축했기 때문에, Logstash CRD 추가만으로 전환 가능.
2. CDC 전환 시 Fluent Bit은 수집기로 유지하고, Logstash는 중앙 파이프라인으로 추가.
3. Fluent Bit의 output만 Kafka로 변경하면 되므로 코드 재사용성 증가.

저장소: Elasticsearch vs Loki vs ClickHouse

쿼리 방식 전문 검색 라벨 기반 SQL
인덱싱 전체 텍스트 라벨만 컬럼
메모리 높음 (4GB+) 낮음 중간
에코시스템 Kibana, APM Grafana Grafana
복잡한 쿼리 ✅ 우수 ⚠️ 제한적 ✅ 우수

선택: Elasticsearch - 전문 검색, Kibana 시각화, APM 확장성

배포 방식: Helm vs ECK Operator

관리 복잡도 수동 자동화
TLS/인증 수동 설정 자동 생성
업그레이드 수동 Rolling Update
Logstash 통합 별도 설정 CRD로 통합
리소스 오버헤드 없음 ~200MB

선택: ECK Operator - 운영 자동화, 향후 Logstash 확장 용이


아키텍처 설계

Observability 3 Pillars 상세

1. 로깅 (Logs) - 분산 로그 중앙화

 

25.12.28 기준 개발 및 고도화 완료.

 
전 시스템, 서비스에 걸쳐 실시간으로 로그를 수집하며 색인 및 시각화를 수행한다.
로깅 정책 및 ES JSON 표준화를 통해 trace_id, http_method/status, service_name 등 풍부한 Data View를 제공하도록 구성했다.
마이크로서비스 환경에선 로그가 각 노드에 흩어져 있어서, 장애 발생하면 여러 파드를 돌아다니며 로그 찾아야 한다.
EFK 파이프라인으로 Fluent Bit가 각 노드의 로그를 수집, Elasticsearch에 보관 및 색인, Kibana로 시각화했다.

Fluent Bit 로그 수집 에이전트 각 노드에 DaemonSet으로 배포, ~5MB 초경량
Elasticsearch 로그 저장/인덱싱 전문 검색, ECS 스키마 기반
Kibana 시각화/대시보드 KQL로 로그 검색, Lens 시각화

 

Fluent Bit이 각 노드의 /var/log/containers/*.log 파일을 tail해서 ES로 전송.
앱은 stdout으로 구조화된 JSON만 하면 자동 수집.


🔍 2. 트레이싱 (Traces) - 분산 트레이스 중앙화

25.12.18 기준 개발 완료 및 고도화 진행 중

 

Envoy Sidecar 트레이스 자동 수집 Istio가 모든 Pod에 주입, Zipkin 프로토콜
OTEL SDK 앱 내부 트레이스 FastAPI 자동 계측, B3 propagator
Jaeger 트레이스 저장/시각화 Service Map, Dependencies 그래프

 
현재 trace.id 커버리지 100%, trace span은 모든 HTTP 1.1 프로토콜, 일부 gRPC를 전방위로 추적한다.
하나의 API 요청이 여러 서비스를 거칠 때, 어디서 병목이 생기는지 추적하려면 분산 트레이싱의 중요도가 급부상한다.
Istio Service Mesh의 Envoy Sidecar가 자동으로 트레이스 정보를 수집한다.
현 이코에코 클러스터는 Istio Envoy 중심으로 트레이싱이 이뤄진다.(Pod 커버리지 100%) 
Istio를 SSOT로 선정한 후 trace.id를 발급, 해당 trace.id를 각 Observability 툴에 전파해 트레이스 현황을 자동 추적 및 시각화한다.

OpenTelegraphy와 호환되도록 B3 프로토콜 및 헤더(x-b3-traceid)로 공유하도록해 E2E 추적 커버리지를 높였으며 분산 시스템 전반의 E2E 추적이 가능해졌다.

 
트레이스 연결 구조

istio-ingressgateway (Span 1)
└── auth-api.auth (Sidecar Span 2)
    └── auth-api (App OTEL Span 3)
        └── asyncpg: SELECT... (DB Span 4)

3. 메트릭 (Metrics) - Prometheus Pull 모델

 

작성일 기준 메트릭, 대시보드 고도화 완료. Golden Signal(p99, avg latency, rps, usage)를 포함한 풍부한 시각화를 제공한다.


 

메트릭은 Prometheus가 각 서비스의 /metrics 엔드포인트를 Pull 방식으로 수집한다,
HTTP 1.1 기반이라 Service Mesh와 호환성이 높다.

컴포넌트 역할 특징
Prometheus Exporter 메트릭 노출 /metrics 엔드포인트, HTTP 1.1
Prometheus 메트릭 수집/저장 Pull 모델, PromQL, TSDB
Grafana 시각화/알림 대시보드, 알림 룰

ServiceMonitor로 자동 발견:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: api-services
spec:
  selector:
    matchLabels:
      tier: business-logic
  endpoints:
  - port: http
    path: /metrics

3 Pillars 통합: trace_id로 연결

로그, 트레이스, 메트릭은 trace_id를 공유해서 세 지표를 연결할 수 있다.

연결 방법 용도
로그 → 트레이스 Kibana에서 trace.id 클릭 → Jaeger 이동 에러 로그의 전체 호출 흐름 확인
트레이스 → 로그 Jaeger span에서 로그 링크 특정 span의 상세 로그 확인
메트릭 → 트레이스 Grafana Exemplar 느린 요청의 샘플 트레이스 확인

배경

  • 현재 상태: 각 Pod의 stdout/stderr 로그가 노드에 분산 저장
  • 문제점: 장애 발생 시 여러 노드를 직접 접속해 로그 확인 필요
  • 목표: 중앙 집중화된 로그 수집, 저장, 검색, 시각화 환경 구축

로그 데이터 파이프라인

단계 컴포넌트 역할
1️⃣ FastAPI App ECS JSON 로그 출력 (stdout)
2️⃣ containerd CRI 포맷으로 래핑 (timestamp stream flag log)
3️⃣ Fluent Bit CRI 파싱 → K8s 메타 추가 → 노이즈 필터
4️⃣ Elasticsearch logs-YYYY.MM.DD 인덱스 저장
5️⃣ Kibana DataView logs-*로 검색/시각화

Fluent Bit 필터 체인

파싱 단계별 데이터 변환:

단계 입력 출력
Raw 2025-12-18T02:42:48.982Z stdout F {"@timestamp":...} -
CRI Parser - time, stream, logtag, log 분리
K8s Filter log (JSON) log_processed.* 필드로 병합
ES 문서 - + k8s_namespace_name, k8s_pod_name, cluster

클러스터 노드 토폴로지

노드 격리 전략

로깅 시스템은 메모리/디스크 I/O 집약적이므로 API 서비스와 물리적으로 격리한다.

# 로깅 노드 구성
Node: k8s-logging
├── Label: workload=logging
├── Taint: domain=observability:NoSchedule
├── Instance: t3.large (2 vCPU, 8GB RAM)
└── Storage: 100GB gp3

리소스 배분 상세

Elasticsearch 5GB (heap) 1 core 로그 저장/검색
Kibana 1GB 0.5 core 시각화/대시보드
ECK Operator 200MB 0.1 core ES/Kibana 관리
System 1.8GB 0.4 core OS/kubelet

Fluent Bit 리소스 (노드당)

항목 비고
기본 메모리 ~5MB C 언어 경량 구현
버퍼 50MB Mem_Buf_Limit 설정
총계 ~55MB 15개 노드 = 825MB, 경량 에이전트

🔄 점진적 확장 계획

Phase 1 vs Phase 2 아키텍처 비교

Phase 1 (현재): EFK 직접 연결

현재 구성의 장점:

  • ✅ 단순한 구조 (컴포넌트 3개)
  • ✅ 빠른 구축 (1일)
  • ✅ 리소스 효율적 (Fluent Bit ~5MB/노드)
  • ✅ 개발 환경 적합

Phase 2 (EDA 도입 시): EFKL + Kafka 버퍼

Phase 2에서 Logstash가 필요한 이유:

변환 작업 설명 Fluent Bit Logstash
Saga 체인 파싱 saga_id 추출 및 상관관계 ✅ Grok
CDC 이벤트 보강 Debezium 메타데이터 추가 ⚠️ 제한적 ✅ Ruby
조건부 라우팅 에러→별도 인덱스 ⚠️ 기본 ✅ 강력함
DLQ 라벨링 재시도 횟수 태깅

Phase 1 → Phase 2 전환 작업

Kafka 배포 (별도 노드) 30분 없음 -
Logstash CRD 추가 15분 없음 ✅ ES 연결 자동
ES 메모리 조정 (5GB → 3GB) 15분 Rolling
Fluent Bit output 변경 15분 없음 -
총계 ~1.5시간 ~5분  

로그량 예측

현재 (동기) 10,000 70,000 - ~5
EDA 도입 후 10,000 350,000 5-10x ~50

디렉토리 구조

backend/
├── workloads/
│   └── logging/
│       ├── base/
│       │   ├── kustomization.yaml
│       │   ├── elasticsearch.yaml    # ECK ES CR
│       │   ├── kibana.yaml           # ECK Kibana CR
│       │   ├── fluent-bit.yaml       # DaemonSet
│       └── dev/
│           └── kustomization.yaml
├── clusters/
│   └── dev/
│       └── apps/
│           ├── 62-eck-operator.yaml  # ECK Operator
│           └── 63-efk-stack.yaml     # EFK 스택
└── docs/
    └── decisions/
        └── ADR-001-logging-architecture.md

핵심 설계 결정 (ADR)

ADR-001: ECK Operator 사용

결정: StatefulSet 직접 관리 대신 ECK Operator 사용
이유:

  1. Elasticsearch ↔ Kibana ↔ Logstash 간 인증/TLS 자동 구성
  2. Rolling Upgrade 자동화
  3. Phase 2에서 Logstash CRD 추가만으로 확장 가능

트레이드오프:

  • Operator Pod 추가 리소스 (~200MB)
  • ECK 버전 종속성

Reference

Service

 

Elastic

Please upgrade your browser This Elastic installation has strict security requirements enabled that your current browser does not meet.

kibana.dev.growbin.app

 

Grafana

If you're seeing this Grafana has failed to load its application files 1. This could be caused by your reverse proxy settings. 2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath. If not using a reverse proxy m

snapshots.raintank.io

 

Jaeger UI

 

jaeger.dev.growbin.app