Skip to content

06. Storage Design

목적

Trace 저장 방식과, 향후 다른 저장소(PostgreSQL, ClickHouse 등)로 교체하기 위한 구조를 설명한다.

범위

collector 모듈 내 io.apmx.collector.trace 패키지의 저장소 구현.

설계

MVP 기본값: H2 (파일 모드)

외부 DB 설치 없이 바로 실행해 볼 수 있도록 H2를 파일 모드(jdbc:h2:file:./data/apmx-collector)로 기본 설정했다. 데이터는 저장소 루트의 data/ 디렉터리에 생성되며 .gitignore에 포함되어 커밋되지 않는다.

Repository/Adapter 분리

text
[Controller] → TraceRepository (interface, port)

                     │ 구현
              TraceRepositoryImpl (adapter)
                     │ 사용
              TraceJpaRepository (Spring Data JPA)

                   H2 / PostgreSQL

TraceRepository 인터페이스는 도메인 모델(Trace, ServiceSummary, UriStatistic)만 다루고 JPA 관련 타입을 노출하지 않는다. 따라서 저장소를 교체하더라도(JPA → 다른 방식) 이 인터페이스의 구현체만 바꾸면 되고, Controller/API 코드는 변경할 필요가 없다.

PostgreSQL로 전환하는 방법

코드 변경 없이 환경변수만 바꾸면 된다 (collector/src/main/resources/application.yml 참고).

bash
export SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/apmx
export SPRING_DATASOURCE_USERNAME=apmx
export SPRING_DATASOURCE_PASSWORD=apmx

org.postgresql:postgresql 드라이버는 이미 collector/build.gradle.ktsruntimeOnly로 포함되어 있다.

테이블 구조

trace 테이블 (JPA ddl-auto: update로 자동 생성, MVP 한정 - 운영 전환 시 마이그레이션 도구(Flyway 등) 도입 필요):

컬럼타입설명
idbigint (PK)내부 식별자
trace_idvarchar(64), uniqueAgent가 생성한 Trace ID
service_namevarchar(128)서비스명
host_namevarchar(128)호스트명
languagevarchar(32)예: java
runtimevarchar(32)예: spring-boot
methodvarchar(16)HTTP Method
urivarchar(512)요청 URI
remote_addrvarchar(64), nullable요청 클라이언트 IP (X-Forwarded-For 우선)
status_codeintHTTP 상태 코드
start_time / end_timebigintepoch millis
duration_msbigint처리 시간(ms)
errorboolean에러 여부
error_messagevarchar(1024)예외 메시지 (nullable)
request_headers_jsonclob, nullable요청 헤더 (JSON 문자열, 민감 헤더는 Agent에서 이미 제외됨)
response_headers_jsonclob, nullable응답 헤더 (JSON 문자열, 민감 헤더는 Agent에서 이미 제외됨)

헤더는 조회 패턴이 Trace 상세 화면 1건 표시로 한정되어 있어 별도 정규화 테이블 없이 JSON 문자열 컬럼으로 저장한다 (과설계 방지). apmx.header.capture.enabled=false(기본값)인 Agent가 보낸 Trace는 두 컬럼 모두 null이다.

(service_name, start_time), (trace_id)에 인덱스를 두어 서비스별 조회/Trace 단건 조회 성능을 확보했다.

span 테이블 (@ApmxTrace 메서드 호출 스택, 하나의 Trace에 0개 이상 매핑):

컬럼타입설명
idbigint (PK)내부 식별자
trace_idvarchar(64)소속된 Trace의 ID (trace.trace_id, FK 제약 없이 값만 참조)
span_idvarchar(64), uniqueAgent가 생성한 Span ID
parent_span_idvarchar(64), nullable부모 Span ID. 최상위 Span은 trace_id와 동일한 값
class_namevarchar(256)계측된 메서드의 클래스명
method_namevarchar(128)계측된 메서드명
argsvarchar(1024), nullable인자값 (안전 타입만 값, 그 외는 클래스명만)
start_time / end_timebigintepoch millis
duration_msbigint처리 시간(ms)
errorboolean예외 발생 여부
error_messagevarchar(1024), nullable예외 메시지

Trace와 마찬가지로 저장/조회는 SpanRepository(port) → SpanRepositoryImpl(adapter) → SpanJpaRepository(Spring Data JPA) 3계층 구조를 그대로 따른다. trace_id에 인덱스를 두어 Trace 상세 조회 시 해당 Trace의 Span 목록을 빠르게 가져온다.

resource_metrics 테이블 (Agent가 10초마다 수집해 heartbeat에 동봉한 JVM 리소스 메트릭 스냅샷, docs/04-agent-design.md 참고 - 조회 API는 아직 없고 저장만 한다):

컬럼타입설명
idbigint (PK)내부 식별자
agent_idvarchar(256)소속된 Agent의 ID (agent.agent_id, FK 제약 없이 값만 참조)
timestampbigint스냅샷 수집 시각(epoch millis, heartbeat 전송 시각이 아님)
cpu_usagedouble, nullable프로세스 CPU 사용률(0.0~1.0). 측정 불가 시 -1
heap_used / heap_maxbigint, nullableHeap 사용량/최대치(byte)
thread_countint, nullable스레드 총 개수
gc_count / gc_timebigint, nullable전체 GC 누적 실행 횟수 / 누적 시간(ms)

(agent_id, timestamp)에 인덱스를 두어 Agent별 시계열 조회 성능을 확보했다(아직 조회 API는 없지만 이후 추가할 때를 대비함). SpanRepository와 동일하게 ResourceMetricRepository(port) → ResourceMetricRepositoryImpl(adapter) → ResourceMetricJpaRepository(Spring Data JPA) 3계층 구조를 따른다.

예시

로컬 H2 콘솔은 MVP에서 별도로 열지 않았다. 필요하면 spring.h2.console.enabled=trueapplication.yml에 추가해 확인할 수 있다.

TODO

  • ClickHouse/Elasticsearch 어댑터 추가
  • 대용량 트래픽을 고려한 파티셔닝/보존 기간(retention) 정책
  • Flyway 등 스키마 마이그레이션 도구 도입