728x90
반응형
728x90
반응형
반응형

고려사항

ASIS 고려

  • 각 서버에서 요청 시 수집 서버를 호출하는 방식 말고 쌓인 로그 파일을 수집하여 수집 서버에서 로그를 분석하는 방식을 우선적으로 고려
  • 기존에 그라파나, 프로메테우스 설정이 되어 있으니 필요 시 이를 활용할 수 있는 방안을 고려
  • 기존 PIS 알림 방식이 가능한지 고민(특정 에러가 1분 안에 5번 이상 호출 시 알람 발생 등)

요청의 흐름에 대한 모니터링이 쉽게 되었으면 좋겠다고 생각함

  • trace id는 프론트에서 생성해서 헤더에 심어 백엔드로 전파하는게 제일 좋을 것 같음
  • 백엔드는 헤더에 trace id가 있으면 이걸 다음 컴포넌트에게 전파, 없으면 생성하여 헤더에 심어서 전파
    • 추가적인 의미있는 정보: global trace id, span id, user key...
  • 이걸 모듈화(기존 log module을 활용하여)하면 좋겠다는 생각..
  • was, ws 간 로그 포맷 정형화 필요
    • springboot 로그에서도 심지만 nginx 등 웹서버, 디비 호출, 인프라(loadbalancer) 등 에서도 trace id, 유저 구분자 등 활용 필요

Observability

로그나 실시간으로 수집되고 있는 모니터링 지표와 같은 출력을 통해 시스템의 상태를 이해할 수 있는 능력

  • 시스템/어플리케이션의 내부 상태를 이해 -> 원인/문제를 진단(디버깅) -> 성능을 최적화하는 능력

측정 데이터

  1. 메트릭 (Metrics)
    • 설명: 성능 지표. 시간에 따른 수치 데이터를 측정하여 시스템의 성능을 모니터링하기 위한 데이터
      • CPU 사용량, 메모리 소비, 요청 수 등의 지표를 포함
    • 도구 예시: Prometheus, Grafana
  2. 로그 (Logs)
    • 설명: 시간 기반 텍스트, 애플리케이션과 시스템의 이벤트에 대한 기록. 구조화된 로그 필요
    • 도구 예시: Elasticsearch, Loki
  3. 트레이스 (Traces)
    • 설명: 데이터가 흘러가는 전체적인 경로(큰그림)
      • 많은 시스템을 거쳐가는 분산 시스템에서 요청의 흐름을 추적하여 성능 병목 현상을 식별할 수 있음
      • Trace ID 기반으로 로그-트레이스 연결 가능
    • 도구 예시: Jaeger, Zipkin, Tempo

OpenTelemetry(OTel)


OpenTelemetry은 Traces, Metrics, Logs 같은 데이터를 instrumenting, generating, collecting, exporting 할 수 있는 Observability Framework

  • 오픈소스, 클라우드 네이티브 컴퓨팅 재단(CNCF, Cloud Native Computing Foundation) 프로젝트
  • 분산 추적(Distributed Tracing) 및 모니터링을 위한 표준을 제공
  • 벤더 종속적이지 않음, 큰 틀을 제공
  • OpenTelemetry는 Spring Boot와 잘 호환되는 APM(Application Performance Monitoring) 솔루션, 자동 계측 가능

위 프래임워크와 함께 선택한 기술 스택

  • LGT(M)

제안하는 아키텍쳐

OpenTelemetry Collector

설치 방식: 바이너리 다운로드 / Docker / Kubernetes(Helm Chart) 중 선택

Collector는 꼭 필요한가?

  • 애플리케이션이 많을 때 → 모든 서비스가 개별적으로 Tempo랑 연결하는 것보다 효율적
  • 샘플링, 필터링이 필요할 때 → Collector에서 간편하게 설정 가능. 오류 발생한 Trace만 Tempo로 보낼 수 있음
  • 다른 백엔드로도 보내야 할 때 → Tempo뿐만 아니라 Zipkin, Jaeger, Loki 등에도 동시에 전송 가능


Collector는 필수가 아님, 하지만 확장성을 고려하면 강력한 도구!
대규모 MSA 환경에서는 Collector가 필수!

직접 구현해도 되나?

Java로 OpenTelemetry Collector 구현 가능

  • OpenTelemetry가 공식적으로 제공하는 proto 정의 파일을 기반으로 Java 코드를 생성해야 함

하지만… 일반적인 방식은 아니며 비효율적일 수도 있음

  • 기존 Go 기반 OpenTelemetry Collector보다 성능 저하 가능성 있음.
  • 기능 추가 및 유지보수가 어려움 (기본 OpenTelemetry Collector는 이미 다양한 Exporter 제공).
  • 프로토버프 버전 관리 및 업데이트 부담.

그래도 직접 Java로 Collector를 만들고 싶다면?

  • ProtoBuf를 이용해 OTLP 데이터 처리
  • gRPC 서버로 수신 후 필요한 백엔드로 Export
  • 필요한 Receiver, Processor 및 Exporter를 추가 개발

결론: 가능하지만 OpenTelemetry 공식 Collector를 사용하는 것이 더 현실적!

내부 데이터 흐름 (Receiver → Processor → Exporter)

  • Receiver(수집기): 외부 시스템(애플리케이션, 에이전트, 다른 Collector 등)에서 데이터를 수신예시: OTLP, Jaeger, Zipkin, Prometheus, Loki 등 다양한 수집기 지원
  • Processor(처리기): 데이터를 필터링, 배치 처리, 속성 추가 등의 변환 작업 수행예시: batch, filter, transform 등 다양한 프로세서 사용 가능
  • Exporter(전송기): 데이터를 최종 모니터링 시스템(Grafana Tempo, Prometheus, Loki 등)으로 전송
    • 예시: Tempo, Zipkin, Jaeger, Loki, Prometheus 등 다양한 Exporter 지원

 

collector 설정은 yaml로

log, trace, metric 각각은 파이프라인으로 연결

설정 예시

receivers:
  otlp:
    protocols:
      grpc: "0.0.0.0:4317"  # gRPC 기본 포트
      http: "0.0.0.0:55681"  # HTTP 기본 포트
  loki:
    endpoint: "http://loki:3100"
  prometheus:
    config:
      scrape_configs:
        - job_name: 'otel-metrics'
          static_configs:
            - targets: ['localhost:9090']

processors:
  batch:     # 배치로 전송
    timeout: 10s
  attributes:
    actions:
      - key: "http.status_code"
        value: "404"
        action: "drop"  # 404 응답 코드가 포함된 트레이스나 로그를 드롭
  filterlogs:
    match:
      log:
        severity: ERROR  # ERROR 로그만 필터링

exporters:
  otlp:
    endpoint: "http://tempo:4317"
  loki:
    endpoint: "http://loki:3100"
  prometheus:
    endpoint: "http://prometheus:9090"
  logging:
    verbosity: detailed

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [attributes, batch]
      exporters: [logging, otlp]
    logs:
      receivers: [loki]
      processors: [filterlogs, batch]
      exporters: [loki, logging]
    metrics:
      receivers: [prometheus]
      processors: [batch]
      exporters: [prometheus]
 



로그 전송기 - Promtail

중간에 Promtail 안 쓰고 바로 collector로 연결한다면?

Promtail을 사용하지 않으면, 애플리케이션이 직접 로그를 OpenTelemetry Collector로 전송해야 함.

  1. Collector가 로그 파일을 직접 읽어 Loki로 전송하는 방식
    • 로그 포맷(구조화 로그 등)을 사전에 맞춰야 함
  2. 애플리케이션에서 직접 Collector로 Push (OTLP 사용)
    • 파일 기반이 아니라 애플리케이션 내부에서 생성된 로그를 바로 전송 가능

기존 로그 파일을 그대로 활용하고 싶다면 Promtail → Collector → Loki

  • Promtail에서도 로그 포매팅 가능
  • Collector에서도 로그 변환 가능
  • Loki에서도 포맷 조정 가능

 

Log 저장소 - Loki

Loki는 로그 수집, 저장, 쿼리를 위한 오픈 소스 로그 집계 시스템으로 별도의 데이터베이스 없이 파일 시스템이나 클라우드 스토리지, 객체 저장소 등을 사용하여 로그를 저장

참고: 왜 Loki?

Metric 저장소 - Prometheus

Prometheus는 수집된 메트릭 데이터를 저장하고 이를 쿼리할 수 있는 중앙 데이터베이스 역할을 하기 위한 것

꼭 있어야 하나?

springboot actuator prometheus 사용 시..
콜랙터에서 바로 그라파나와 연결하여 실시간 metric 확인 가능

다만 사용 시 아래의 혜택을 얻을 수 있음

  • 메트릭 저장을 통한 장기적인 메트릭 분석 가능
  • 고급 쿼리 기능 활용
  • 알림 시스템 지원

 

Trace 저장소 - Tempo

Tempo 없이 Loki만으로 분산 추적이 가능할까?

  • Tempo 없이 완전한 분산 추적은 어려움
  • Trace ID를 로그에 남기면 Trace ID가 포함된 로그를 검색할 수는 있지만, 서비스 간 호출 관계(Span, Parent-Child 관계)는 분석 불가
  • Tempo는 Trace 간 시간 흐름을 시각화하여, 어느 서비스가 느린지, 어디에서 지연이 발생하는지 확인 가능. Loki는 단순한 텍스트 로그 검색이라 이런 분석 불가

 

Tempo는 기본적으로 Push 방식(Polling 지원 X) 관련하여 아래 설정 가능

  • Head-based sampling: 모든 요청을 추적하지 않고 일부만 추적(확률 설정) - 기본값
  • Tail-based sampling: 모든 요청을 수집하지만 collector에서 특정 조건을 만족하는 요청만 저장하도록 설정(필터링 사용) / 어플리케이션 성능에 영향 없음
    • 정상 요청은 버리고, 오류만 저장하도록 설정할 수도 있음
    • 배치 설정: 일정량 쌓이면 한번에 Push하도록 설정
  • Always on sampling: 모든 요청을 100% 저장, 데이터 저장 비용 증가 가능


참고: 다른 trace 저장소와 비교

알람 관련

Grafana 방식

  • Grafana는 Prometheus/Loki에서 데이터를 가져와 알람(Alert)을 설정 가능(템포 x)

장점

  • UI로 알람을 설정할 수 있어 편리
  • Alertmanager 없이 바로 메일/Slack/Webhook 전송 가능

단점

  • 중앙 집중형 관리 어려움
  • 코드 기반 관리 불가능
  • 확장성 없음

 

Prometheus Ruler + Alertmanager 방식이 가장 표준적인 방식

(Loki, Premetheus, Tempo) → Prometheus Ruler → Alertmanager → Email/Slack/Webhook
 


Prometheus Ruler?

  • Prometheus Ruler는 Prometheus 서버와 함께 동작(내장됨)하며, 알림 규칙(Alerting Rules)을 관리하는 기능을 제공
  • Prometheus Ruler는 알람 규칙(Alerting Rules)을 처리하고, 이 규칙이 Trigger되면 Alertmanager에 알림을 보냄
  • yaml 설정으로 관리

Alertmanager?

  • Prometheus 및 Loki, Tempo 등에서 발생한 알람을 관리하고, 이메일, Slack, PagerDuty 등의 채널로 알림을 전송하는 역할을 하는 도구
  • 알람 수신 및 라우팅, 집계, mute, 알람 중복 방지 등 기능이 있음
  • 프로메테우스 설정에서 ruler를 사용하도록 설정 후 별도 파일(yaml)로 설정 관리


Loki에서 직접 알람 가능?

  • Loki 자체적으로는 알람을 트리거할 기능이 제한적
  • logql 쿼리를 사용하여 Prometheus Ruler에서 감지 후 Alertmanager로 전송하는 방식이 일반적

Prometheus에서 직접 알람 가능?

  • Prometheus는 자체적으로 Prometheus Ruler를 통해 알람을 감지 가능
  • 하지만 Alertmanager 없이 직접 알람을 보낼 수 없음

Tempo에서 직접 알람 가능?

  • Tempo는 직접적인 알람 기능이 없음
  • Trace 기반으로 메트릭을 생성한 후 Prometheus Ruler를 통해 감지하는 방식 사용


Prometheus Ruler + Alertmanager를 사용 시 장단점

장점

1. 유연한 알림 라우팅

  • Alertmanager는 알림을 '라벨' 기반으로 라우팅할 수 있어서, 다양한 알림 조건에 대해 수신자를 유연하게 지정할 수 있음.
  • 예를 들어, severity, project, team과 같은 라벨을 기반으로 알림을 각기 다른 수신자 그룹(메일, 슬랙, 웹훅 등)으로 전달할 수 있음.
  • 라벨을 이용하여 프로젝트별로 혹은 환경 별로 다양한 알림 조건을 설정할 수 있음

2. 알림 집계 및 수집

  • Alertmanager는 동일한 경고에 대해 여러 번 알림을 보내지 않도록 알림을 집계하고 알림 그룹화 기능을 제공
  • 예를 들어, 여러 번 발생하는 동일한 경고를 하나의 알림으로 묶어서 처리할 수 있음

3. 알림 수신 채널 다채로움

  • 알림을 다양한 채널(이메일, 슬랙, 페이지듀티, SMS 등)로 전송할 수 있음.
  • Alertmanager는 알림을 설정한 대로 다양한 형식으로 전송할 수 있는 기능을 제공함

4. 정밀한 알림 조건 설정

  • Prometheus Ruler에서 제공하는 고급 알림 규칙 설정을 통해, 알림 조건을 세밀하게 정의할 수 있음.
  • 예를 들어, 특정 메트릭이 1분 동안 특정 값을 초과하거나, 특정 상황이 반복되는 경우에만 알림을 보내는 식으로 알림의 발생 조건을 세밀하게 조정할 수 있음.


단점

1. 설정이 복잡함

  • 설정하는 화면이 없고 yaml  파일을 작성하는 방식
  • 여러 팀이나 프로젝트별로 맞춤형 알림을 설정하는 경우, 설정 파일이 방대해질 수 있으며, 이를 관리하기 어려울 수 있다.

2. 리소스 요구사항

  • Prometheus Ruler와 Alertmanager는 각각 다른 시스템과 연동되어야 하므로, 시스템 자원의 관리가 필요함. Prometheus의 수집 데이터 양이 많아지면 알림 평가에 드는 시간과 리소스가 커질 수 있다.
  • 알림을 너무 많이 생성하거나 복잡한 계산을 수행하면 시스템에 부하를 줄 수 있음

도입 시 각 어플리케이션 수정 양은?

1. 아래 의존성 추가

// 애플리케이션에서 메트릭, 트레이스, 로그와 같은 관측 데이터를 수집하는 데 필요한 인터페이스를 제공; trace id 생성
implementation 'io.opentelemetry:opentelemetry-api:${version}'

// OpenTelemetry API의 구현체로, 실제 데이터를 수집하고 처리하는 기능을 제공; 데이터 수집
implementation 'io.opentelemetry:opentelemetry-sdk:${version}'

// Traces, Metrics, Logs 데이터를 OTLP(HTTP/gRPC) 프로토콜을 통해 Collector로 전송; 외부로 전송
implementation 'io.opentelemetry:opentelemetry-exporter-otlp:${version}'
 

2. tracer 빈 등록 - 콜렉터 정보 등록
3. 로그백 설정

  • logback.xml 파일에서 MDC(Mapped Diagnostic Context)를 사용하여 트래스 아이디와 스팬 아이디를 자동으로 포함시킬 수 있음
  • 받은 요청에 트래스(Trace) 아이디가 있으면, 이미 존재하는 트래스 아이디를 그대로 사용하고, 새로운 스팬(Span)을 생성한다. 만약 요청에 트래스 아이디가 없다면, 새로운 트래스 아이디를 생성하고 이를 기반으로 스팬을 생성한다.
  • 소스에서 수동으로도 트래이싱 정보 추가 가능(마킹 가능)

참고

오텔 설명
https://medium.com/@dudwls96/opentelemetry-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-18b6e4fe6e36
https://www.anyflow.net/sw-engineer/opensource-observability
위에서 제안한 아키텍쳐와 비슷한 구조로 세팅하는 과정 설명
https://blog.nashtechglobal.com/setup-observability-with-open-telemetry-prometheus-loki-tempo-grafana-on-kubernetes/

[토스] observability 시스템 구축 시 고려해야하는 사항들
https://youtu.be/Ifz0LsfAG94?si=cYAPtvm8eRy0Srk- 
[nhn forward] nhn cloud가 구축한 사용 예시
https://youtu.be/EZmUxMtx5Fc?si=_YtHU2mDayS2uxKr

728x90
반응형

'architecture > sw architecture' 카테고리의 다른 글

람다 아키텍처 vs 카파 아키텍처  (0) 2023.03.22
[design] proxy pattern 프록시 패턴  (0) 2022.04.25
반응형

환경: springboot3+

1. Spring Cloud Sleuth란?

Spring Cloud Sleuth는 Spring Boot 기반 애플리케이션에서 자동으로 분산 추적을 적용하는 라이브러리

  • Spring Boot 애플리케이션에서 HTTP 요청이 어떤 서비스에서 시작되어 어디까지 전달되었는지 추적 가능
  • Zipkin, Jaeger 등과 쉽게 연동 가능
  • 2022년부터 OpenTelemetry로 마이그레이션됨 (Spring Boot 3.0에서 Sleuth 제거됨)

장점

  • Spring Boot와 자연스럽게 통합됨
  • 자동으로 Trace ID/Span ID를 생성하고 로깅
  • Spring Cloud Gateway, Spring WebFlux와 연동 지원

단점

  • Spring Boot에 종속적 (Spring Cloud 기반 프로젝트가 아니면 사용 어려움)
  • Spring Boot 3부터 Sleuth가 공식적으로 제거됨 → OpenTelemetry로 전환 권장

잘 쓰던건데 deprecated 됨..ㅠㅠ

OpenTelemetry Tracing이란?

OpenTelemetry(OTel)는 벤더 중립적 표준 분산 추적 프레임워크

  • 기존 OpenTracing과 OpenCensus를 통합한 표준
  • Zipkin, Jaeger, Prometheus, Datadog 등 다양한 모니터링 시스템과 연동 가능
  • Java, Go, Python, JavaScript 등 다양한 언어 지원

장점

  • 특정 프레임워크(Spring Boot)에 종속되지 않음
  • 벤더 중립적 → 다양한 모니터링 백엔드 사용 가능
  • Spring Boot 3 이상에서 공식 지원됨

단점

  • Spring Cloud Sleuth에 비해 설정이 다소 복잡
  • 기존 Sleuth 기반 프로젝트라면 마이그레이션이 필요함

tracing 을 하기 위해서

<!-- Spring Boot 3 이상에서는 OpenTelemetry 사용 -->
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-api</artifactId>
    <version>1.23.1</version>
</dependency>

<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-otlp</artifactId>
    <version>1.23.1</version>
</dependency>

api 라이브러리 역할

  • OpenTelemetry의 핵심 API (Core API)
  • 개발자가 애플리케이션에서 추적(Tracing) 및 메트릭(Metrics)을 수집할 수 있도록 하는 기본 인터페이스 제공
  • Tracer, Span, Meter, Baggage 등의 개념을 정의
  • 실제 데이터 전송(Exporting) 기능은 포함되지 않음
  • opentelemetry-api를 사용하여 Span(추적 단위)을 생성하고, 애플리케이션에서 트랜잭션을 추적할 수 있음..
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.GlobalOpenTelemetry;

public class OpenTelemetryExample {
    public static void main(String[] args) {
        Tracer tracer = GlobalOpenTelemetry.getTracer("my-tracer");

        // 새 Span 시작
        Span span = tracer.spanBuilder("my-span").startSpan();
        try {
            System.out.println("OpenTelemetry Trace 시작");
        } finally {
            span.end(); // Span 종료
        }
    }
}

OTLP 라이브러리 역할

  • OpenTelemetry 데이터를 OTLP (OpenTelemetry Protocol) 포맷으로 변환 후 백엔드(예: Jaeger, Zipkin, Grafana, New Relic 등)에 전송
  • 기본적으로 4317(grpc) 포트를 통해 OpenTelemetry Collector로 데이터를 전송
  • Collector는 Jaeger, Zipkin 등으로 데이터를 전달하여 시각화 가능

OTLP란?

  • OpenTelemetry Protocol (OTLP)
  • OpenTelemetry에서 정의한 데이터 송수신 표준
  • gRPC 또는 HTTP/JSON을 통해 추적 데이터(Trace, Metrics, Logs)를 전송
  • push 전용 프로토콜

 

OTel framework

자세한건 다음 글에..

wlt


https://medium.com/@dudwls96/opentelemetry-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-18b6e4fe6e36

 

OpenTelemetry 란 무엇인가?

MSA기반으로 개발된 서비스가 많아지고, 서비스 간의 관계가 점점 복잡해지면서 장애 분석 및 버그 추적이 점점 어려워지고 있습니다. 서비스의 관측성(Observability) 확보를 위한 다양한 상용 서비

medium.com

https://www.anyflow.net/sw-engineer/opensource-observability

 

Open Source 기반 Observability via OpenTelemetry, Service mesh

Open source를 기반으로 한 Observability 환경 구축에 대한 논의이다. 본 논의 중심에는 OpenTelemetry 뿐 아니라 Service Mesh(Istio), Prometheus, Grafana가 위치한다.

www.anyflow.net

 

 

728x90
반응형

'architecture > micro service' 카테고리의 다른 글

zookeeper  (0) 2024.12.23
[캐시] 캐시 관련 문제들과 캐시웜업  (0) 2024.11.17
[Dead Letter] PDL, CDL  (0) 2024.11.14
E2E(end to end) 테스트  (0) 2024.11.13
대용량 데이터 처리 고민  (1) 2024.11.10
반응형
  • 주키퍼(Zookeeper)는 분산 시스템의 코디네이션 서비스
  • ZNode 기반 트리구조로 데이터를 관리
  • 고가용성 고성능

 

주요 기능

  1. 분산 설정 관리:
    • 서버간 동기화를 위한 분산 코디네이션 기능
    • 분산 시스템에서 여러 애플리케이션이 동일한 설정 데이터를 공유해야 할 때 중앙 저장소 역할을 함
  2. 리더 선출(Leader Election):
    • 분산 시스템에서 리더를 선출할 때, 투표 과정을 관리하고 리더를 선출. 이를 지속적으로 유지 및 관리
    • 카프카 브로커들
  3. 동기화(Synchronization):
    • 분산 환경에서 여러 노드(서버 또는 프로세스)가 동일한 상태를 유지하도록 지원, 데이터 일관성 유지
    • 로컬 캐시 동기화 시
  4. 이벤트 감시(Watches):
    • Zookeeper의 데이터가 변경되면 이벤트 알림을 애플리케이션에 전달하여 실시간 업데이트를 함
    • watch 기능을 이용한 변경 이벤트 감지
  5. 분산 락 관리:
    • 분산 환경에서의 데이터 접근을 제어하고 동기화를 위한 락관리

 

특징

  • 일관성 보장:
    • Zookeeper는 CAP 이론에서 CP(Consistency와 Partition Tolerance)를 보장
    • 항상 데이터의 일관성을 우선
  • 높은 가용성:
    • 클러스터를 통해 고가용성을 제공하며, 노드 장애 시에도 서비스가 중단되지 않음
  • 간단한 API:
    • 클라이언트가 쉽게 사용할 수 있도록 간단한 API를 제공
  • 쓰기 지연, 읽기 최적화:
    • 쓰기 연산은 느릴 수 있지만 읽기 연산은 매우 빠름



기본 아키텍처

  • ZNode: 주키퍼에서 데이터를 저장하는 단위로, 파일 시스템과 유사한 구조를 가짐
  • 클라이언트: 주키퍼에 연결하여 데이터를 읽고 쓰는 애플리케이션
  • 서버: 주키퍼의 데이터 저장 및 처리를 담당하는 노드

장점

  • 분산 환경에서 복잡한 작업을 단순화함
  • 높은 신뢰성과 일관성 제공
  • 트랜젝션을 원자적으로 처리
  • 다양한 분산 시스템과의 통합 용이

단점

  • 쓰기 연산의 성능이 낮음(읽기 최적화)
  • 클러스터 크기가 커질수록 성능 저하 가능
  • ZooKeeper 장애 시 의존적인 애플리케이션에 문제가 발생할 수 있음

ZNode란?

 

  • ZNode는 ZooKeeper가 관리하는 데이터 구조의 단위
  • 트리 구조(파일 시스템과 유사)로 구성되며, 각 노드가 ZNode에 해당
  • ZNode는 데이터와 상태 정보를 저장하고, ZooKeeper의 API를 통해 접근 가능

 

ZNode 기반 트리 구조의 특징

  1. 트리 형태:
    • ZooKeeper 데이터는 / 루트에서 시작하여 트리 형태
      • 예: /app/config/db, /app/config/cache.
  2. 데이터 크기 제한:
    • 각 ZNode는 최대 1MB 크기의 데이터 저장
    • ZNode는 주로 작은 상태 정보를 저장하며, 대용량 데이터는 다른 저장소를 사용해야 함
  3. 노드 유형:
    • Persistent ZNode:
      • 노드가 생성된 후 삭제 요청이 있을 때까지 유지
    • Ephemeral ZNode:
      • 클라이언트 세션이 종료되면 자동으로 삭제
    • Sequential ZNode:
      • 노드 이름에 고유한 순번을 추가하여 생성
  4. 원자적 연산:
    • ZNode의 데이터 변경은 원자적으로 이루어지며, 동시성이 보장
  1.  

로컬 캐시 동기화 예시

이벤트를 감지하는 timestamp를 변경하여 (값을 수정하고) 변경 이벤트를 각 서버로 전달
https://youtu.be/BUV4A2F9i7w?si=c04ObhVx1skzBmDR

  • 디비 갱신
  • 레디스 만료
  • 주키퍼가 만료되었다는 이벤트를 각 서버에 전달
  • 각 서버는 이벤트를 받고 로컬 캐시 만료처리
  • 그 이후 새 요청이 들어오면 레디스/로컬 캐시를 최신 데이터로 갱신

세팅

자바 필요

주키퍼 압축 풀고 conf/zoo.cfg 설정 해야 함

# 기본 설정
tickTime=2000
initLimit=10
syncLimit=5

# 데이터 디렉토리 경로
dataDir=/var/lib/zookeeper

# 클라이언트 연결 포트
clientPort=2181

# 서버 설정 (클러스터 구성 시 필요)
# server.1=zookeeper1:2888:3888
# server.2=zookeeper2:2888:3888

 

  • server.3=zookeeper3:2888:3888 설정은 클러스터 내에서
    • 서버 3이 호스트 이름 zookeeper3, 데이터 통신 포트 2888, 리더 선출 포트 3888을 사용한다는 것을 의미
  • myid파일
echo "3" > /var/lib/zookeeper/myid

 

  • Zookeeper는 실행될 때 데이터 디렉토리(dataDir)에 위치한 myid 파일을 읽음
    • dataDir=/var/lib/zookeeper
  • myid 파일에 기록된 숫자를 사용하여, 설정 파일(zoo.cfg)의 server.X 항목 중 자신에게 해당하는 항목을 찾음
  • 이를 통해 클러스터 내에서 자신의 역할과 통신할 포트를 결정

 

728x90
반응형

'architecture > micro service' 카테고리의 다른 글

[log tracing] sleuth? OpenTelemetry?  (0) 2025.02.17
[캐시] 캐시 관련 문제들과 캐시웜업  (0) 2024.11.17
[Dead Letter] PDL, CDL  (0) 2024.11.14
E2E(end to end) 테스트  (0) 2024.11.13
대용량 데이터 처리 고민  (1) 2024.11.10
반응형

캐시는 다음과 같은 이유로 주로 사용된다.

 

  • 성능 개선: 자주 요청되는 데이터를 빠르게 제공하여 응답 속도를 높임.
  • 부하 감소: 데이터베이스, API 서버 등 원본 소스에 대한 요청을 줄여 리소스 절약.
  • 비용 최적화: 외부 API 호출이나 고비용 데이터 처리 작업을 줄임.
  • 안정성 강화: 원본 데이터 소스 장애 시 캐시 데이터를 활용해 서비스 지속 가능.
  • 복잡한 연산 제거: 고비용 연산 결과를 캐싱하여 반복 작업 최소화.
  • 사용자 경험 향상: 빠르고 개인화된 데이터를 제공해 UX 개선.
  • 분산 시스템 효율성: 로컬 캐시와 CDN을 통해 네트워크 지연 감소.

 

따라서 캐시와 관련된 문제들은 애플리케이션 성능에 영향을 미칠 수 있다. 어떠한 문제들이 있을 수 있을까.

 

1. 캐시 미스(Cache Miss)

  • 정의: 애플리케이션이 필요한 데이터가 캐시에 없는 상황
  • 유형:
    • Cold Miss: 캐시가 초기화되거나 처음 사용될 때 발생하는 미스. 캐시에 아무 데이터가 없어서 모든 요청이 미스 처리
    • Capacity Miss: 캐시의 용량이 부족해 더 이상 데이터를 저장할 수 없는 상황에서 기존 데이터가 삭제되면서 발생하는 미스.
    • Conflict Miss: 특정 데이터가 저장될 수 있는 캐시 공간이 제한되어 있을 때 발생하는 미스입니다. 캐시에 이미 동일한 위치에 저장된 데이터가 있을 때 교체되면서 발생
  • 해결 방안: 캐시 크기 조절(키워서 미스 안 나게), 효율적인 캐싱 전략 설정, LRU(Least Recently Used)나 LFU(Least Frequently Used) 같은 캐시 교체 알고리즘 도입 등

2. 캐시 스탬피드(Cache Stampede)

  • 정의: 캐시가 만료된 경우 다수의 요청이 동시에 캐시 미스를 일으켜 데이터베이스나 원본 서버에 과부하를 유발
  • 예시: 특정 데이터의 캐시가 만료되었을 때, 많은 사용자나 프로세스가 동시에 캐시 된 데이터를 요청하며 원본 서버에 큰 부하가 걸림
  • 해결 방안:
    • 락(Locking): 캐시가 만료되었을 때 첫 번째 요청자만 원본 데이터에 접근하도록 락을 걸어 다른 요청자가 대기하도록.. 대기하고 나서는 캐시에서 가져가게
    • 백오프(Backoff): 캐시 업데이트를 요청하는 다수의 주체들이 동일한 시점에 갱신 요청을 보내는 것을 방지하기 위해, 재시도 전에 일정한 대기 시간을 설정하는 방식
      • API 호출 실패 후 1초 → 2초 → 4초로 대기 시간을 증가시키며 재요청.
      • 캐시가 만료된 데이터를 가져오기 위해 여러 클라이언트가 요청을 보낼 때, 요청 간의 간격을 두어 스탬피드를 방지.
    • 예상 만료 처리(Early Expiration): 캐시의 데이터가 만료되기 전에 미리 갱신 작업을 수행하여 캐시 미스 상황을 방지하는 방식
      • TTL(Time To Live) 설정 시, 실제 만료 시간(T)보다 짧은 시간(T-E)으로 갱신 작업을 예약.
      • 백그라운드 작업을 통해 갱신 작업 수행.
    • 탄력적 TTL: 캐시의 TTL을 랜덤하게 설정하여 여러 캐시 항목이 동시에 만료되지 않도록 조정(TTL skew)

3. 캐시 독(Cache Thundering Herd)

  • 정의: 특정 캐시 키에 대한 동시 접근이 일어나거나 캐시가 갱신되어야 할 때 한 번에 모든 스레드가 동일한 데이터를 요청하는 문제입니다. 캐시 스탬피드와 유사하지만 주로 특정 데이터에 집중된 대량 요청
  • 문제상황
    • 백엔드 부하 증가: 모든 요청이 원본 소스(DB, API 등)에 쏠려 장애를 유발.
    • 성능 저하: 응답 속도가 느려지고, 전체 시스템 성능이 저하.
    • 서비스 가용성 위협: 심한 경우 서비스 중단으로 이어질 수 있음.
  • 해결 방안: 분산 락, 지연 갱신(Lazy Update) 같은 접근을 통해 특정 리소스에 동시 접근하는 것을 방지

 

위와 같이 여러 해결책이 있지만 대체적으로 즉각적인 해결책이 되지 못한다.(응답 지연의 가능성)

캐시 웜업(Cache Warming)

캐시 웜업은 애플리케이션이나 서비스가 시작되거나 재시작될 때, 캐시를 미리 채워두는 작업. 이를 통해 서비스 초기 상태에서 캐시 미스(Cache Miss)로 인한 성능 저하를 방지하고, 원본 데이터 소스(DB, API 등)에 대한 부하를 줄일 수 있다.

캐시 웜업의 필요성

 

  • 초기 캐시 미스 문제 방지 -> 초기 성능 향상
    • 애플리케이션이 시작된 직후 캐시가 비어 있는 상태에서는 모든 요청이 원본 데이터 소스로 전달됨.
    • 갑작스러운 트래픽 증가로 원본 소스(DB/API)가 과부하 상태에 빠질 수 있음.
  • 서비스 안정성 향상, 사용자 경험 개선
    • 캐시 웜업으로 자주 사용되는 데이터를 미리 준비해 두면 초기 요청 처리 속도가 빨라지고, 사용자 경험이 개선됨.
  • 트래픽 분산
    • 캐시가 미리 채워져 있으면 요청이 분산되므로, 백엔드 부하가 줄어듦.

 

캐시 웜업의 일반적인 방법

  1. 프리로드(Preloading): 시스템 초기화나 캐시 시작 시점에 미리 정해진 데이터 집합을 캐시에 로드. 예를 들어, 자주 요청되는 데이터나 중요한 설정값을 미리 캐시에 넣어둔다.
  2. 배치 처리: 배치 작업을 통해 일정 시간 간격으로 특정 데이터를 캐시에 로드하여 캐시가 항상 최신 상태를 유지하도록 한다.
    • @Scheduled(fixedDelay = 300000) 혹은 별도의 배치 프로그램 
    • 만료 전 갱신으로 캐시만을 사용하여 응답하게 끔하여 트래픽 전이 되지 않고 응답 지연 없게 처리; 1분 주기 배치
      • 캐시 웜업 대상 누락 시 cache stampede 발생!
      • 캐시 웜업 대상 조회의 자동화 필요
  3. API 또는 관리자 도구 활용: 관리자 페이지나 별도의 API를 통해 수동으로 캐시를 웜업
  4. 온디맨드 웜업(On-Demand Warming): 실제 사용자가 특정 데이터를 요청하기 직전에, 예측 분석을 통해 해당 데이터가 자주 요청될 것으로 판단될 경우 이를 캐시에 미리 로드한다.
    1. 데이터 분석 시 "호출 횟수(view count) / 최근 호출 시간" 통계화 고려(날짜-id-view)
    2. 로컬 디비에 관련 데이터 쌓고(레디스 부하 발생 가능)
    3. 주기적으로 레디스에 올려
    4. 관련하여 레디스 자료구조 활용(분산 자료구조인 hash)
    5. 레디스에 올리고 로컬 삭제
    6. 해당 데이터 기반으로 캐시를 갱신한다

프리로드 예시(ApplicationRunner 이용)

@Component
public class CacheWarmupRunner implements ApplicationRunner {
    @Autowired
    private CacheService cacheService;
    @Autowired
    private DataService dataService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        List<Data> dataList = dataService.getFrequentlyAccessedData();
        dataList.forEach(data -> cacheService.put(data.getKey(), data));
    }
}

캐시 웜업의 장단점

  • 장점: 캐시 미스로 인한 부하를 줄이고, 사용자에게 보다 빠른 응답 시간을 제공
  • 단점: 데이터 신선도 이, 필요성이 낮은 데이터의 경우 불필요한 리소스 사용이 될 수 있음

 

하이브리드 캐시

: 로컬 캐시 + 리모트 캐시

  • 로컬 캐시로: 빈번하게 조회되고/공통 데이터/데이터 크기가 작고/업데이트 빈도가 낮은 데이터
  • 리모트 캐시: 최신 상태 데이터/중앙 관리(마스터 캐시)
  • 디비 -> 리모트 -> 로컬 순으로 갱신

주의사항

  • 로컬 캐시마다 TTL이 다 다르기 때문에 데이터 일관성을 유지하기 힘듦
  • 데이터 변경 시 누가 old로 내리는지 찾기 힘듦
  • 즉시 반영 시에는 별도의 만료 프로세스 필요
    • 주키퍼 활용해서 데이터 변경 시 이벤트 발행하여 동기화 가능
    • 변경 이벤트 ->리모트 캐시 만료 처리 -> 로컬 캐시 만료 처리

 

참고

초단위 캐시 웜업(준 실시간)

로컬 캐시 TTL 다 달라.. 초단위 웜업 시 rabbit 의 DLQ 활용 가능

1~60초 각각의 만료시간을 가진 메세지 발행

https://youtu.be/BUV4A2F9i7w?si=eVdHEBzJ3h8bZOIu


캐시 갱신 정책

캐시와 원본 데이터 간의 동기화 방법.

  1. Write-Through
    • 캐시에 쓰는 동시에 원본 데이터도 즉시 갱신.
    • 장점: 데이터 일관성 보장.
    • 단점: 쓰기 성능 저하.
  2. Write-Behind (Write-Back)
    • 캐시에 먼저 쓰고 나중에 원본 데이터 갱신.
    • 장점: 쓰기 성능 향상.
    • 단점: 데이터 손실 가능성.
  3. Read-Through
    • 캐시에 없을 때 원본 데이터를 읽고 캐시에 저장.
    • 장점: 자동 갱신.
    • 단점: 초기 요청 지연.
  4. Manual Update
    • 명시적으로 캐시를 갱신.
    • 장점: 제어 용이.
    • 단점: 관리 복잡.

 

캐시 만료 정책

캐시 데이터의 유효 기간을 설정하는 방법.

  1. TTL (Time-To-Live)
    • 데이터가 캐시에 저장된 후 유지 시간 설정.
    • 예: 30초 후 만료.
  2. TTI (Time-To-Idle)
    • 마지막 접근 후 일정 시간 지나면 만료.
    • 예: 10분 동안 미사용 시 삭제.
  3. Fixed Expiry
    • 특정 시간에 모든 데이터 만료.
    • 예: 매일 자정 초기화.
  4. Forever Cache
    • 만료 없이 지속적으로 유지.
    • 데이터 변경이 거의 없을 때 사용.
728x90
반응형

'architecture > micro service' 카테고리의 다른 글

[log tracing] sleuth? OpenTelemetry?  (0) 2025.02.17
zookeeper  (0) 2024.12.23
[Dead Letter] PDL, CDL  (0) 2024.11.14
E2E(end to end) 테스트  (0) 2024.11.13
대용량 데이터 처리 고민  (1) 2024.11.10
반응형

Dead Letter (데드 레터)

"Dead Letter"란, 메시지 큐에서 소비자(consumer)에게 전달할 수 없거나 처리 중에 문제가 발생하여 처리가 불가능한 메시지를 말합니다. 보통 메시지를 큐에 전달하려 했으나 여러 번 재시도해도 실패하거나 타임아웃이 발생한 경우, 해당 메시지를 일반 큐에서 제거하고 "Dead Letter Queue (DLQ)"라고 불리는 별도의 큐로 이동시킵니다.

이 과정은 메시지 처리 실패 시 메시지를 손실하지 않고 안전하게 보관하여 나중에 점검하거나 문제를 해결할 수 있도록 하기 위함입니다.

Producer Dead Letter (생산자 데드 레터)

"Producer Dead Letter"는 메시지를 큐로 전송하는 생산자(producer) 측에서 발생한 문제로 인해 큐에 정상적으로 전달되지 못한 메시지를 의미합니다. 프로듀서가 메시지를 발행할 때 네트워크 오류, 메시지 큐 자체의 장애, 또는 메시지 크기 제한 등으로 인해 큐에 메시지가 도달하지 못하는 경우, 이 메시지를 Dead Letter로 처리할 수 있습니다.

이러한 상황에서는 메시지를 버리거나 다른 DLQ에 저장하여 생산자 측 문제를 추적하고 나중에 재전송할 수 있습니다.

Consumer Dead Letter (소비자 데드 레터)

"Consumer Dead Letter"는 소비자가 특정 메시지를 처리하는 도중 실패하거나 오류가 발생하여 메시지를 정상적으로 처리하지 못한 경우를 의미합니다. 일반적으로 메시지 큐는 메시지가 소비자에게 여러 번 재시도된 후에도 실패하는 경우, 해당 메시지를 "Dead Letter Queue"로 이동시킵니다.

이러한 "Consumer Dead Letter"는 일반적으로 소비자가 처리할 수 없는 메시지(잘못된 형식, 누락된 데이터, 또는 예상치 못한 내용)를 담고 있어, 이를 DLQ에 넣고 이후 개발자나 운영자가 수동으로 처리하거나 점검할 수 있습니다.


Kafka에서 Dead Letter Queue 설정 방법

Kafka 자체에는 기본적으로 DLQ 기능이 없지만, 컨슈머 애플리케이션 쪽에서 구현 가능하다.

  1. DLQ 토픽 생성:
    • Kafka에 별도의 DLQ 토픽을 생성.
    • 예를 들어, 원본 토픽이 my-topic이라면, DLQ 토픽은 my-topic-dlq와 같은 방식으로 이름을 지정
  2. 컨슈머 예외 처리 및 DLQ 전송:
    • 메시지 처리 중 오류가 발생할 때 예외 처리를 통해 실패한 메시지를 DLQ 토픽으로 전송
    • 예를 들어, 메시지 처리 로직에서 예외가 발생하면 KafkaProducer를 사용하여 DLQ 토픽에 메시지를 전송
public void processMessage(String message) {
    try {
        // 메시지 처리 로직
    } catch (Exception e) {
        // DLQ로 메시지 전송
        ProducerRecord<String, String> record = new ProducerRecord<>("my-topic-dlq", message);
        kafkaProducer.send(record);
        log.error("Failed to process message, sent to DLQ", e);
    }
}

 

3. Kafka Connect 사용:

Kafka Connect는 Kafka와 다른 시스템 간에 데이터를 쉽게 전송(스트리밍)할 수 있게 해주는 프레임워크이다. 이를 통해 다양한 데이터베이스, 파일 시스템, 클라우드 서비스, 로그 시스템 등과 Kafka를 통합할 수 있음.

  • Source Connectors: 외부 시스템에서 Kafka로 데이터를 전송하는 커넥터
    • 예를 들어, 데이터베이스에서 데이터를 Kafka로 보내거나, 로그 파일에서 Kafka로 스트리밍
  • Sink Connectors: Kafka에서 외부 시스템으로 데이터를 전송하는 커넥터
    • 예를 들어, Kafka에서 받은 데이터를 데이터베이스나 HDFS, Elasticsearch 등으로 보냄

Kafka Connect는 standalone modedistributed mode로 실행할 수 있으며, connector configuration을 통해 여러 외부 시스템과의 통합을 자동화함.

Dead Letter Queue는 메시지 처리 중 오류가 발생한 메시지를 별도의 큐에 저장해두고, 후속 처리를 통해 문제를 해결할 수 있게 해주는 시스템이다. Kafka Connect에서 DLQ는 주로 메시지 처리 실패 시 데이터를 안전하게 보관하고, 문제가 해결된 후 재처리를 할 수 있도록 도와줌.

Kafka Connect DLQ 기능은 주로 dead-letter-policy와 관련된 설정을 통해 구성된다. 예를 들어, 메시지 처리에 실패할 경우 해당 메시지를 DLQ로 이동시켜 추가적인 검토 및 처리가 가능하게 함.

  • Kafka Connect 프레임워크와 Dead Letter Queue 기능을 지원하는 커넥터를 사용하고(ex. Debezium) 커넥터 설정에서 errors.deadletterqueue.topic.name을 지정하여 특정 토픽을 DLQ로 사용 가능.

 

RabbitMQ에서 Dead Letter Queue 설정 방법

RabbitMQ는 기본적으로 DLQ 기능을 지원함

  1. DLQ용 큐 생성:
    • Dead Letter를 위한 별도의 큐를 생성합니다.
    • 예를 들어 my-queue-dlq라는 이름의 큐를 생성
  2. Dead Letter Exchange 생성:
    • x-dead-letter-exchange와 같은 DLX(Dead Letter Exchange)를 생성하여 메시지를 전송할 곳을 설정. DLX는 메시지 처리 실패 시, 또는 메시지가 만료된 경우 메시지를 다른 큐로 라우팅하는 역할을 함.
  3. 원본 큐 설정에 DLX 추가:
    • 원본 큐를 설정할 때 x-dead-letter-exchange 속성을 추가하여 DLX를 지정.
    • 예를 들어, my-queue라는 원본 큐가 있을 때 해당 큐의 메시지가 실패하면 DLX를 통해 DLQ 큐로 메시지가 전달
    • 메시지 만료 시간(x-message-ttl)을 설정하여 특정 시간이 지나면 DLQ로 이동하도록 설정 가능
// Dead Letter Exchange 및 Queue 설정
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "my-dlx");  // 익스채인지 설정
args.put("x-dead-letter-routing-key", "my-queue-dlkey"); // 라우팅 키 설정
// 메시지 만료 시간 설정 (TTL)
args.put("x-message-ttl", 60000);  // 60초 후 메시지 만료

// 원본 큐 선언
channel.queueDeclare("my-queue", true, false, false, args); // 큐에 엮음

// Dead Letter Queue 선언
channel.queueDeclare("my-queue-dlq", true, false, false, null);
channel.queueBind("my-queue-dlq", "my-dlx", "my-queue-dlkey"); // 큐, 익스채인지, 라우팅 키

 

  • "my-queue"라는 이름의 원본 큐를 선언
  • args는 위에서 설정한 Dead Letter Exchange 및 라우팅 키를 포함하는 속성. 이 속성에 따라 메시지가 만료되거나 처리 실패 시 DLX를 통해 my-queue-dlq로 전달됨
  • "my-queue-dlq"라는 이름의 Dead Letter Queue(DLQ)를 선언. 이 큐는 Dead Letter로 전달된 메시지를 저장하는 역할을 함
  • channel.queueBind는 "my-dlx" DLX와 "my-queue-dlq" 큐를 지정한 라우팅 키 "my-queue-dlkey"를 통해 연결하여, DLX에서 발생한 Dead Letter 메시지가 이 큐에 전달되도록 설정

Dead Letter가 Dead Letter Queue 에 쌓이면 그 다음 단계는?

1. Dead Letter 모니터링 및 알림

  • DLQ에 메시지가 쌓이면 이를 모니터링하고, 일정 수 이상이 되거나 특정 조건을 만족하면 자동으로 알림이 오도록 설정
  • 알림 시스템을 통해 개발자나 운영팀이 문제를 인식하고 원인을 분석

2. 원인 분석

  • DLQ에 쌓인 메시지를 검토하여 처리 실패 원인을 파악.
    • 메시지 형식 오류 또는 잘못된 데이터
    • 처리 로직의 버그 또는 예외
    • 메시지 크기 초과나 타임아웃
  • 로그를 통해 오류 상황을 구체적으로 분석하거나, DLQ에 쌓인 메시지 내용 자체를 검토하여 어떤 부분에서 문제가 발생했는지 확인

3. 수동 재처리 또는 자동 재처리

  • 문제가 해결된 후 DLQ의 메시지를 다시 원래 큐에 넣어 재처리 가능. 이를 자동화할 수도 있지만, 데이터 일관성을 위해 보통 수동으로 처리
  • 자동화 시 재처리 시나리오를 신중하게 정의해야 하며, 동일한 실패가 발생하지 않도록 로직을 수정

 

728x90
반응형
반응형

E2E 테스트?

여러 구성 요소가 함께 예상대로 작동하는지 확인하는 테스트

  • 환경 유사성: E2E 서버는 실제 운영 환경을 최대한 유사하게 설정하여, 실제 서비스에서 발생할 수 있는 문제를 미리 발견할 수 있도록 합니다.
  • 자동화 테스트: Selenium, Cypress, Playwright 같은 도구를 사용하여 사용자의 인터랙션을 자동으로 시뮬레이션하고, 여러 계층 간의 흐름을 확인합니다.
  • 데이터 일관성: 일부 E2E 서버는 테스트 데이터베이스나 샌드박스 환경을 사용하여 실제 운영 데이터와 분리된 상태에서 테스트를 수행합니다.
  • CI/CD 통합: E2E 테스트는 CI/CD 파이프라인에 통합되어 배포 과정에서 발생할 수 있는 문제를 조기에 발견할 수 있도록 자동으로 실행됩니다.

 

E2E 서버 = E2E 테스트를 위한 서버

1. 테스트 환경 구성

E2E 서버는 운영 환경과 유사한 환경을 제공하는 것이 중요합니다. 이를 위해 다음과 같은 환경을 구성합니다.

  • 테스트용 데이터베이스: 실제 데이터베이스와 분리된 테스트용 데이터베이스를 설정하여 데이터 충돌을 방지하고 테스트 데이터를 자유롭게 사용할 수 있게 합니다.
  • 테스트 API 엔드포인트: E2E 서버는 운영 API와 구분된 테스트용 API 엔드포인트를 설정해 두는 것이 좋습니다.
    • url을 또 따는건 아닌 것 같음, 같은 소스에 profile 만 stage 이런식으로 주는게 좋을 것 같음. 매번 주소 바꾸고 관리하는거 불편
  • 샌드박스 모드: 외부 API를 사용하는 경우, 실제 외부 API와 통신하지 않고 샌드박스 모드(테스트 모드)로 설정하여 과금 등의 위험을 줄입니다.
    • 테스트 용 api를 제공해주는게 제일 좋은 것 같음.. 그렇지 않다면.. mock server를 띄워서 상황별로 일정한 값을 내리도록 세팅해야 할 듯
  • 인프라 설정: 실제 환경을 모방하여 Docker 등을 활용해 마이크로서비스 아키텍처를 구성하거나 CI/CD 파이프라인에서 테스트 환경을 동적으로 띄우는 방법을 사용할 수 있습니다.

2. 자동화 도구 설정

자동화 도구는 사용자의 행동을 시뮬레이션하고, 전체 흐름을 테스트하는 데 중요한 역할을 합니다. 몇 가지 인기 있는 도구는 다음과 같습니다.

  • Selenium: 주로 웹 애플리케이션의 UI 테스트를 자동화하는 데 사용되며, 다양한 브라우저를 지원합니다.
  • Cypress: 현대 웹 애플리케이션 테스트에 적합한 빠르고 간편한 도구로, 백엔드와의 상호작용을 테스트하는 데 강점을 가집니다.
  • Playwright: 다양한 브라우저와 모바일 디바이스까지 지원하며, 최신 웹 애플리케이션에 적합한 자동화 도구입니다.
  • soup ui: 사용해서 api 순차 호출하고 전 결과를 후 api 에 사용 가능. 시나리오별로 관리 가능

이 도구들을 CI/CD 파이프라인과 통합하여 특정 조건에서 E2E 테스트가 자동으로 실행되도록 설정할 수 있습니다.

3. 테스트 코드 작성

테스트 코드는 사용자가 실제로 애플리케이션을 사용하는 흐름을 최대한 비슷하게 재현해야 합니다.

  • 시나리오 설계: 로그인, 상품 조회, 결제 등 사용자의 주요 작업 시나리오를 정리하고, 순서대로 테스트가 진행되도록 합니다.
  • 상태 관리: 테스트 중 서버의 상태(예: 로그인 상태, 장바구니에 담긴 상품 등)를 일관되게 관리하기 위해 각 테스트 케이스를 독립적으로 작성하거나, 필요한 경우 테스트 실행 전후에 초기화 과정을 둡니다.
  • 에러 및 예외 처리 테스트: 정상적인 흐름뿐 아니라, 잘못된 입력이나 서버 오류 등 다양한 예외 상황을 테스트하여 애플리케이션이 안정적으
  • 로 작동하는지 확인합니다.

4. CI/CD 통합

테스트가 성공적으로 작성되었다면, 이를 CI/CD 파이프라인에 통합하여 배포 전 자동으로 실행되도록 설정합니다.

  • GitHub Actions, Jenkins, GitLab CI 등과 같은 CI/CD 도구를 사용하여 코드가 변경될 때마다 자동으로 E2E 테스트를 실행할 수 있습니다.
  • 테스트 결과가 실패하면 알림을 받도록 하여, 문제를 빠르게 확인하고 수정할 수 있도록 합니다.

cypress 예시

npm install cypress --save-dev

위 명령어로 설치하고 루트에 cypress.json 파일을 생성하고 아래처럼 api서버 url 설정

{
  "baseUrl": "http://localhost:8080"
}

테스트 파일 생성: cypress/integration/user_spec.js 파일을 생성하고 아래와 같이 작성

describe('User API E2E Test', () => {
  it('should create a new user and retrieve the user list', () => {
    const user = { name: 'John Doe', email: 'john.doe@example.com' };

    // Create a new user
    cy.request('POST', '/users', user)
      .its('status')
      .should('eq', 200);

    // Retrieve the user list and verify the new user exists
    cy.request('/users').then((response) => {
      expect(response.status).to.eq(200);
      expect(response.body).to.have.length.greaterThan(0);
      expect(response.body.some(u => u.email === user.email)).to.be.true;
    });
  });
});

 

  1. POST /users 엔드포인트에 사용자 데이터를 보내 사용자를 생성
  2. GET /users 엔드포인트로 모든 사용자를 조회하고, 방금 생성한 사용자가 포함되어 있는지 확인

아래 명령어로 cypress 실행, Cypress 창이 열리면 user_spec.js 테스트 파일을 선택

npx cypress open

 

github actions와 연동

테스트를 해야하는 서버의 코드에 .github/workflows/e2e-test.yml 파일생성하고 다음 설정을 추가

(gitHub는 .github/workflows/ 폴더에 있는 YAML 파일을 모두 인식하여 워크플로우로 처리)

name: E2E Tests

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  e2e-test:
    runs-on: ubuntu-latest

    services:
      db:
        image: mysql:5.7
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: e2e_test_db
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping --silent"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=3

    steps:
      - name: Check out the code
        uses: actions/checkout@v2

      - name: Set up JDK 11
        uses: actions/setup-java@v2
        with:
          java-version: '11'

      - name: Install dependencies
        run: ./gradlew build -x test

      - name: Start Spring Boot application with E2E profile
        run: ./gradlew bootRun -Dspring.profiles.active=e2e &
        env:
          SPRING_DATASOURCE_URL: jdbc:mysql://localhost:3306/e2e_test_db
          SPRING_DATASOURCE_USERNAME: root
          SPRING_DATASOURCE_PASSWORD: password
        timeout-minutes: 2

      - name: Wait for Spring Boot to start
        run: |
          timeout 60s bash -c "until echo > /dev/tcp/localhost/8080; do sleep 5; done"

      - name: Run Cypress tests
        uses: cypress-io/github-action@v2
        with:
          start: npm start
        env:
          CYPRESS_BASE_URL: http://localhost:8080

main 브랜치에 머지하거나 pr 날리면 cypress 실행하도록 작성

728x90
반응형

'architecture > micro service' 카테고리의 다른 글

[캐시] 캐시 관련 문제들과 캐시웜업  (0) 2024.11.17
[Dead Letter] PDL, CDL  (0) 2024.11.14
대용량 데이터 처리 고민  (1) 2024.11.10
transaction outbox pattern + polling publisher pattern  (0) 2024.11.07
2PC vs 2PL  (1) 2024.11.06
반응형

1. 대용량 데이터를 처리할 때의 주요 고려사항은 무엇인가요?

  • 대용량 데이터 처리에서는 확장성, 성능, 데이터 일관성 등을 고려해야 합니다.
    • 데이터를 효율적으로 저장하고 처리하기 위해 분산 시스템을 활용하고(확장성)
    • 캐싱 전략을 사용하여 읽기 성능을 높입니다.
  • 데이터 중복 방지와 실패 시 복구 전략이 필요합니다.
    • 중복방지: 멱등성 API(PUT); 디비에 UNIQUE KEY잡아서 중복 데이터 삽입 안되게
      • Redis 또는 Memcached를 사용해 데이터 처리 전 고유 키를 캐싱하여 중복 여부를 빠르게 확인할 수 있습니다. 예를 들어, 특정 ID가 Redis에 존재하면 중복으로 간주하고 처리하지 않는 방식입니다.
    • 복구 전략
      • 트랜잭션과 롤백 (Transaction & Rollback)
        • RDBMS의 트랜잭션 기능을 사용해 작업 단위로 처리하고, 실패 시 모든 작업을 롤백하여 데이터 일관성을 유지합니다. 트랜잭션 단위가 클 경우에는 단계별로 커밋을 처리하는 Savepoint를 활용할 수도 있습니다.
        • NoSQL 데이터베이스에서도 MongoDB와 같은 시스템은 단일 문서 수준에서 트랜잭션을 지원하고, Cassandra는 클라이언트 라이브러리에서의 조정을 통해 일부 트랜잭션 효과를 제공할 수 있습니다.
      • 분산 트랜잭션 및 분산 락
        • 여러 시스템 간 트랜잭션을 위해 2PC (Two-Phase Commit) 또는 SAGA 패턴을 사용합니다. SAGA 패턴은 트랜잭션 단위로 실행되는 작업을 분리하고, 오류 발생 시 보상 작업을 수행해 데이터 일관성을 유지합니다.
        • 데이터 충돌을 방지하기 위해 분산 락을 적용할 수 있습니다. 예를 들어, Redis의 SETNX 명령을 사용해 분산 잠금을 구현하여, 하나의 리소스에 동시에 접근하는 것을 방지할 수 있습니다.
      • 재시도 및 지연 재시도 (Retry & Backoff)
        • 네트워크 문제나 일시적인 오류에 대해 재시도 정책을 설정하여 실패한 요청을 다시 시도할 수 있습니다. 무조건 재시도하기보다 지수적 백오프(Exponential Backoff) 전략을 사용해 지연 시간을 점진적으로 늘리면 서버에 부담을 줄일 수 있습니다.
          • 과도한 요청 방지: 동일한 요청을 반복적으로 보내는 것을 막아 서버가 과부하에 걸리지 않도록 하여, 서버가 일정한 시간 동안 안정적으로 요청을 처리할 수 있습니다.
          • 서버 복구 시간 확보: 지연 시간이 늘어날수록 서버가 안정화되거나 부하를 해소할 시간이 생기므로, 문제 해결 후 요청을 성공적으로 처리할 가능성이 커집니다.
          • 네트워크 효율성 향상: 클라이언트와 서버 간의 불필요한 트래픽을 줄이고, 네트워크 자원을 효율적으로 사용하게 합니다.
          • 트랜잭션 유지 시간이 너무 길어지면 잠금 자원이나 연결 자원이 오래 점유되어 다른 요청이나 트랜잭션에 영향을 줄 수
            • 최대 재시도 횟수와 백오프 한계 설정(무한히 기다리지 않도록)
            • 장기 트랜잭션을 여러개의 작은 단위로 분리하거나 비동기 처리하여 트랜젝션이 길어지지 않도록
            • 트랜젝션 타임아웃 설정
            • 회복 가능한 트랜잭션 설계
              • 장애나 재시도가 필요한 경우에도, 중간까지 완료된 트랜잭션 부분이 유지되고 나머지 작업을 이어갈 수 있도록 분산 트랜잭션 관리SAGA 패턴을 활용해 트랜잭션을 보다 유연하게 설계합니다.
        • Circuit Breaker 패턴을 함께 사용해 오류가 지속될 경우 특정 시간 동안 요청을 차단해 전체 시스템의 안정성을 높일 수 있습니다.
      • 데이터 복제 및 백업
        • 데이터 손실을 방지하기 위해 백업 및 복제 전략을 설정합니다. 예를 들어, RDBMS에서는 정기적인 백업과 함께 로그 기반 복구(Log-based Recovery)를 사용해 장애 발생 시 특정 시점으로 데이터를 복원할 수 있습니다.
        • 분산 시스템의 경우 데이터 복제를 통해 여러 노드에 데이터를 분산 저장하여 데이터 유실 가능성을 줄입니다. Cassandra나 MongoDB와 같은 분산 DB에서는 노드 간 자동 복제를 지원해 복구성을 높입니다.
      • 이벤트 소싱과 로그 기반 복구
        • 이벤트 소싱을 통해 상태 변화가 발생할 때마다 이벤트를 저장해, 장애 발생 시 해당 이벤트를 재생하여 이전 상태로 복구할 수 있습니다.
        • 로그 기반 복구 시스템은 Kafka와 같은 메시지 큐에 이벤트를 기록해 실시간으로 복구할 수 있으며, 장애가 발생해도 로그를 재생하여 데이터 상태를 원래대로 복원할 수 있습니다.
  • 마지막으로 비용 절감 측면에서 클라우드 서비스를 활용하거나 데이터 파이프라인의 최적화가 필요할 수 있습니다.

2. 대용량 데이터를 실시간으로 처리해야 한다면 어떤 아키텍처를 선택할 것인가요?

  • 실시간 데이터 처리가 필요한 경우 이벤트 기반 아키텍처(EDA)와 스트리밍 처리 시스템을 선호합니다. Apache Kafka와 같은 메시지 브로커를 통해 데이터를 스트리밍으로 전송하고, Apache Flink 또는 Spark Streaming을 사용해 데이터를 실시간으로 처리합니다. 이렇게 하면 지연을 최소화하면서도 높은 처리량을 유지할 수 있습니다.
  • CQRS: 조회와 데이터변경을 시스템적으로 분리하여 각각 최고의 성능을 낼 수 있도록 개선, 독립적으로 확장 가능하도록 개발
    • 변경: RDBMS, 조회: MONGO, ELASTIC..

 

3. 대용량 데이터 처리 중 병목현상을 해결한 경험이 있나요?

  • 디비
    • 이전 프로젝트에서 MySQL 데이터베이스에서 많은 데이터를 읽어와야 하는 작업이 있었는데, 특정 쿼리에서 병목 현상이 발생했습니다. 이를 해결하기 위해 인덱스를 최적화하고, 비동기 처리를 도입해 읽기 작업을 분산했습니다. 또한, 캐싱 레이어를 추가해 반복되는 읽기 작업을 줄였고, 결과적으로 처리 속도를 크게 향상시켰습니다.
  • 서비스
    • 긴 트랜젝션을 여러 개의 작은 트랜젝션으로 나눔
    • 트랜젝션 안에 외부 api 호출이 있어
      • api호출 성공 시 db작업하도록 개선
      • transaction outbox pattern + polling publish pattern으로 바꾼 적 있음

 

4. MapReduce와 같은 분산 처리 기법을 설명해 주시고, 이를 언제 사용하면 좋을까요?

MapReduce는 대규모 데이터를 분산하여 처리할 수 있게 해주는 프레임워크입니다. 데이터를 맵(Map) 단계에서 분산하여 처리하고, 리듀스(Reduce) 단계에서 그 결과를 통합합니다. 주로 배치 작업에 적합하며, 대규모 로그 분석, 대량의 텍스트 데이터 처리, ETL 작업 등에 사용됩니다.

 

5. 대용량 데이터의 효율적 관리를 위해 어떤 데이터베이스를 사용할 것인가요?

데이터 특성과 처리 요구 사항에 따라 데이터베이스를 선택

  • 실시간 읽기 및 쓰기가 빈번한 경우 : Redis와 같은 인메모리 데이터베이스를 고려
  • 로그나 이벤트 데이터를 저장 :  쓰기 성능확장성이 중요한데, 이러한 요구에 맞는 데이터베이스로는 Cassandra와 MongoDB, Elasticsearch 등
    • Cassandra
      • 분산형 NoSQL 데이터베이스로, 특히 칼럼 패밀리(Column Family) 기반의 데이터 모델을 사용하는 시스템. 높은 쓰기 성능과 수평 확장성 덕분에 대량의 로그 데이터를 빠르게 저장 가능. 분산 구조로 데이터가 여러 노드에 분산되어 저장되고 복제되어 고가용성과 안정성이 높습니다.
      • 적합한 경우: 대규모의 로그 데이터를 수집하고 분석하는 경우, 트래픽이 많이 발생하는 IoT 데이터 또는 웹 트래픽 로그 등에서 효율적입니다.
      • 장점: 특히 쓰기 성능이 뛰어나며 노드 간의 데이터 복제와 장애 허용성이 우수해, 고가용성 환경에서도 데이터를 안정적으로 처리할 수 있습니다. 노드 추가 시 용량이 수평으로 확장되며 성능 저하 없이 대규모 데이터를 처리할 수 있습니다.
    • MongoDB
      • 문서 기반 NoSQL 데이터베이스로, JSON 형식의 유연한 데이터 구조를 지원합니다. 복잡한 스키마를 요구하지 않기 때문에 로그 데이터의 다양한 필드와 동적 스키마를 저장하는 데 적합합니다.
      • 적합한 경우: 애플리케이션 로그, 이벤트 데이터를 JSON 형태로 저장해 실시간으로 조회하고 분석해야 하는 경우. 대화형 애플리케이션에서 발생하는 로그 데이터와 이벤트 처리에 적합합니다.
      • 장점: 샤딩(데이터 분산 저장)을 통해 수평 확장이 가능하며, 다양한 인덱싱을 통해 실시간 쿼리에 적합한 구조를 제공합니다.
    • Elasticsearch
      • 분산형 검색 엔진으로 로그와 이벤트 데이터를 저장하고, 실시간 검색과 분석을 수행하는 데 특화된 시스템입니다. Kibana와 함께 사용하면 데이터를 시각화할 수 있어 로그 모니터링과 분석에 특히 유용합니다.
      • 적합한 경우: 로그 모니터링, 애플리케이션 성능 관리(APM), 보안 로그 분석, 실시간 검색 등이 필요한 경우. ELK(Elasticsearch, Logstash, Kibana) 스택으로 많이 사용됩니다.
      • 장점: 실시간 검색과 시각화 기능이 강력하며, 텍스트 기반의 로그 데이터에서 유용한 인사이트를 빠르게 얻을 수 있습니다.
  • 데이터 일관성이 중요한 트랜잭션성 데이터 : MySQL, PostgreSQL과 같은 RDBMS가 적합

RDBMS

  • 데이터 일관성이 중요할 때.
  • 읽기와 쓰기 작업이 동시에 중요하며, 균형이 필요할 때.
  • 데이터가 정형화되어 있고 관계형 모델에 적합할 때.

ACID(Atomicity, Consistency, Isolation, Durability)를 엄격히 준수하기 때문에:

 

  • 쓰기 성능이 뛰어난 NoSQL 시스템(Kafka, Cassandra 등)에 비해 상대적으로 느릴 수 있음.
  • 대규모 읽기 작업은 JOIN, GROUP BY와 같은 복잡한 쿼리 때문에 느려질 수 있음.

A 계좌에서 100원을 출금하고 B 계좌에 입금.

  1. Atomicity: 출금과 입금이 모두 성공하거나 둘 다 실패.
  2. Consistency: 트랜잭션 전후에 전체 잔액(예: 1,000원)은 변하지 않음.
  3. Isolation: 다른 트랜잭션은 이 작업이 완료되기 전까지 중간 상태를 볼 수 없음.
  4. Durability: 트랜잭션이 완료된 후에는 시스템 오류가 발생해도 데이터가 보존됨.

 

6. 대용량 데이터 처리에서 성능을 최적화하기 위해 사용할 수 있는 기법에는 어떤 것들이 있나요?

아키텍쳐적인 방법:

  • 캐싱 레이어를 추가해 빈번히 조회되는 데이터를 미리 저장
  • 비동기 처리를 통해 응답시간을 줄여 타임아웃 방지 
    • 네트워크 요청, 파일 IO등의 시간이 오래걸리는 작업을 비동기로 처리하면 메인 스레드가 다른 작업을 수행할 수 있어 리소스를 효율적으로 사용할 수 
  • 배치 처리로 시스템 자원을 효율적으로 사용
    • 데이터를 한꺼번에 처리해 트랜잭션 관리나 데이터 일관성 유지에 유리
    • 주로 비업무 시간이나 서버 부하가 적은 시간대에 작업을 수행해 시스템 자원을 효율적으로 사용

디비적인 방법:

  • 인덱스를 적절히 사용하여 데이터 검색 속도를 높임
  • 데이터 파티셔닝과 샤딩을 통해 데이터베이스 부하를 분산

7. 대용량 데이터를 다룰 때 발생할 수 있는 장애 및 복구 전략에 대해 설명해 주세요.

대용량 데이터 시스템에서 장애가 발생할 경우, 데이터 유실을 방지하고 빠르게 복구하는 것이 중요

서비스적:

  • 디비 등 데이터 복제(master replica)를 통해 고가용성을 확보
  • Kafka와 같은 시스템에서는 메시지 리플레이를 통해 복구
  • Dead Letter Queue (DLQ)와 Retry 메커니즘을 통해 이벤트 처리 실패에 대비.

인프라적:

  • 장애가 발생했을 때 특정 노드로 트래픽을 우회하거나, 백업 데이터에서 복원
  • 클라우드 서비스를 사용할 경우 데이터 센터의 지역 분산(재해복구 관련 DR)을 통해 데이터 유실 위험을 줄여

MSA: 

  • (IF 분산) 소스 내부적으로는 circuit braker pattern 적용하여 장애가 전파되지 않도록

대용량 시스템의 장애와 분산 시스템의 장애는 약간 관점이 다르다.

하지만 대용량 시스템의 단점인 SPOF를 막기 위해서는 분산 시스템으로 구성해야하고 그렇게되면 결국 대용량 + 분산 시스템의 특징을 모두 지닐 수 밖에 없게 된다.

728x90
반응형

'architecture > micro service' 카테고리의 다른 글

[Dead Letter] PDL, CDL  (0) 2024.11.14
E2E(end to end) 테스트  (0) 2024.11.13
transaction outbox pattern + polling publisher pattern  (0) 2024.11.07
2PC vs 2PL  (1) 2024.11.06
[arch] EDA, event sourcing, saga, 2pc  (0) 2024.02.29
반응형

트랜잭션 아웃박스 패턴폴링 퍼블리셔 패턴분산 시스템에서 데이터베이스와 메시징 시스템 간의 메시지 전달을 보장하기 위해 자주 함께 사용되는 패턴입니다. 특히 이벤트 주도 아키텍처에서 데이터베이스 상태 변화와 해당 이벤트 발행의 일관성을 유지해 줍니다.

아웃박스 패턴(유실방지)

배경 및 필요성

분산 시스템에서는 데이터베이스에 데이터를 저장하는 트랜잭션과 외부 메시징 시스템에 이벤트를 보내는 작업이 분리되어 있기 때문에, 두 작업이 동시에 성공하거나 실패하도록 일관성 있는 처리가 어려울 수 있습니다. 특히, 트랜잭션 내에서 데이터베이스는 성공했는데 메시지 큐로의 전송은 실패하는 경우, 데이터 일관성이 깨질 수 있습니다. 이 문제를 해결하는 방법 중 하나가 아웃박스 패턴입니다.

트랜잭션 아웃박스 패턴의 주요 흐름:

  1. 데이터베이스 트랜잭션 및 아웃박스 기록 생성:
    • 애플리케이션이 데이터베이스 변경(예: 주문 생성)을 해야 할 때, 메인 테이블(예: Orders)에 데이터를 저장하는 동시에, 하나의 트랜잭션 안에서 "아웃박스" 테이블에 관련 이벤트 정보를 기록합니다.
    • 이 아웃박스 테이블에는 메시징 시스템에 발행할 이벤트와 관련된 정보가 포함되며, 아직 발행되지 않았음을 나타내는 상태 정보도 포함됩니다.
  2. 트랜잭션 커밋:
    • 모든 데이터베이스 작업(메인 테이블과 아웃박스 테이블에 저장 작업)이 하나의 트랜잭션으로 묶이기 때문에, 트랜잭션이 성공적으로 커밋되면 데이터베이스와 이벤트 기록이 일관성을 유지하게 됩니다.

아웃박스 패턴의 구조

  1. 아웃박스 테이블:
    • 서비스의 데이터베이스에 아웃박스 테이블을 둡니다. 이 테이블은 데이터베이스의 업데이트 내용과 함께 발행할 이벤트를 저장합니다. 즉, 비즈니스 로직에서 데이터베이스에 기록할 때, 메시지 큐에 보내야 할 이벤트 정보도 이 테이블에 같이 저장됩니다.
  2. 로컬 트랜잭션:
    • 데이터베이스에 데이터를 저장할 때, 같은 트랜잭션 내에서 아웃박스 테이블에도 이벤트 데이터를 함께 기록합니다. 단일 트랜잭션을 통해 데이터베이스의 데이터와 이벤트 정보를 동시에 커밋하므로, 이 과정에서 실패가 발생하면 전체 트랜잭션이 롤백됩니다.
  3. 이벤트 리스너/폴링 프로세스:
    • 별도의 폴링 서비스 또는 트리거가 아웃박스 테이블을 주기적으로 스캔하여 새로운 이벤트가 있으면 이를 메시징 시스템(예: Kafka, RabbitMQ)으로 전송합니다. 이 작업은 별도의 비동기 프로세스로 수행되며, 메시지 전송이 성공하면 해당 이벤트는 아웃박스 테이블에서 삭제되거나 상태가 업데이트됩니다. (폴링 주기에 따른 지연 있음)
  • 데이터베이스 상태와 이벤트 전송 상태의 일관성을 보장
  • 메시지 전송 실패 시에도 데이터는 손실되지 않으며, 시스템은 나중에 재시도할 수 있음
    • 유실 발생 시 배치로 재발행

폴링 퍼블리셔 패턴

폴링 퍼블리셔 패턴은 아웃박스 테이블에 저장된 이벤트를 주기적으로 조회(polling)하여 메시징 시스템에 발행하는 역할을 합니다.

폴링 퍼블리셔 패턴의 주요 흐름:

  1. 주기적인 조회:
    • 폴링 작업은 일정한 간격으로 아웃박스 테이블을 조회하여 발행되지 않은 이벤트를 찾습니다.
  2. 메시지 발행:
    • 발행되지 않은 이벤트를 찾아 메시징 시스템(예: Kafka 또는 RabbitMQ)으로 발행합니다.
    • 발행이 성공하면 해당 이벤트의 상태를 “발행 완료”로 업데이트하여, 동일한 이벤트가 다시 발행되지 않도록 합니다.

두 패턴을 결합한 이점

  • 일관성 보장: 트랜잭션 아웃박스 패턴을 통해 데이터베이스 변경과 이벤트 기록을 하나의 트랜잭션에서 처리할 수 있어 데이터와 이벤트의 일관성을 유지할 수 있습니다.
  • 내결함성: 폴링 퍼블리셔 패턴을 통해 이벤트 발행이 실패하더라도 재시도가 가능해 내결함성을 확보할 수 있습니다.
  • 확장성: 메시징 시스템을 통해 이벤트가 발행되므로, 여러 마이크로서비스가 이 이벤트를 구독하여 비동기로 처리할 수 있어 시스템 확장성에 유리합니다.

세팅 예시

  • 예시 1. mysql + kafka

1. 트랜젝션 처리 시 outbox 테이블에 이벤트 정보도 추가

@Transactional
public void createOrder(Order order) {
    // 1. Orders 테이블에 주문 정보 저장
    ordersRepository.save(order);
    
    // 2. Outbox 테이블에 이벤트 기록
    OutboxEvent event = new OutboxEvent(
        order.getId(),
        "ORDER",
        "ORDER_CREATED",
        new JSONObject().put("orderId", order.getId()).put("status", order.getStatus()).toString(),
        "PENDING"
    );
    outboxRepository.save(event);
}

2. 폴링용 스케줄 생성 -> 카프카로 이벤트 발행

@Scheduled(fixedDelay = 5000) // 5초마다 실행
public void publishEvents() {
    List<OutboxEvent> pendingEvents = outboxRepository.findByStatus("PENDING");
    
    for (OutboxEvent event : pendingEvents) {
        try {
            // 1. Kafka에 이벤트 발행
            kafkaTemplate.send("order-events", event.getPayload());
            
            // 2. 발행 성공 시, Outbox 테이블에서 상태를 'COMPLETED'로 업데이트
            event.setStatus("COMPLETED");
            outboxRepository.save(event);
            
        } catch (Exception e) {
            // 발행 실패 시 별도의 로깅 또는 재시도 처리
            logger.error("Failed to publish event: " + event.getEventId(), e);
        }
    }
}

  • 예시2. 스케줄 대신 디비 변경 감지 이용하여 이벤트 전송

MySQL Kafka 커넥터Debezium을 사용하면 트랜잭션 아웃박스 패턴을 더욱 쉽게 구현할 수 있습니다. 이 조합은 변경 데이터 캡처(Changed Data Capture, 데이터베이스에서 발생하는 삽입(INSERT), 업데이트(UPDATE), 삭제(DELETE)와 같은 변경 사항을 실시간으로 감지하고 추적하는 기술) 방식을 통해 MySQL 데이터베이스의 변화(즉, 새로운 아웃박스 레코드)를 자동으로 Kafka에 발행하는 구조를 제공합니다. 이를 통해 폴링을 위한 추가 프로세스 없이, 이벤트가 발생할 때마다 Kafka에 실시간으로 이벤트를 전송할 수 있습니다.

  • Debezium은 MySQL Kafka 커넥터 중에서도 가장 널리 사용되고 인기 있는 CDC 커넥터입니다. Debezium은 MySQL을 포함해 다양한 데이터베이스의 변경 사항을 실시간으로 감지하여 Kafka에 전송하는 강력한 기능을 제공하는 오픈소스 CDC 플랫폼입니다. Kafka와 MySQL을 연동하는 데 있어서 CDC를 필요로 할 때, Debezium이 대표적인 솔루션으로 많이 활용됩니다.
  • MySQL Kafka 커넥터는 Kafka Connect 프레임워크를 사용하여 MySQL 데이터베이스와 Apache Kafka 간에 데이터를 스트리밍하는 도구입니다. 주로 MySQL에서 발생한 데이터 변경 사항을 Kafka로 전송하는 데 사용됩니다. 이를 통해 MySQL 데이터베이스의 변경 로그를 실시간으로 Kafka로 스트리밍하고, Kafka의 여러 소비자가 이 데이터를 처리할 수 있습니다.

Debezium을 활용한 MySQL Kafka 커넥터 구현

Debezium은 CDC 플랫폼으로, MySQL의 바이너리 로그(binlog)를 읽어 Kafka로 변경 사항을 스트리밍할 수 있도록 합니다. MySQL의 binlog는 데이터베이스에 일어나는 모든 변경 사항을 기록하며, Debezium은 이를 Kafka 이벤트로 변환해 전송합니다. 

  1. 트랜잭션 아웃박스 패턴 적용: 애플리케이션에서 데이터베이스에 트랜잭션을 수행할 때, Outbox 테이블에 이벤트 정보를 함께 기록합니다.
  2. Debezium이 변경 감지 및 Kafka로 발행:
    • Debezium은 MySQL 바이너리 로그를 모니터링하고, Outbox 테이블에 새로운 행이 추가되면 해당 이벤트를 자동으로 Kafka로 발행합니다.
      • 바이너리 로그를 통한 순서 보장 및 오프셋을 활용한 발행 보장 -> 실패시 재시도로 발행 보장
    • 예를 들어, Outbox 테이블에 새로운 주문 이벤트가 추가되면 Debezium이 이를 감지해 Kafka의 dbserver1.yourDatabase.Outbox 토픽으로 해당 데이터를 스트리밍합니다.
  3. Kafka 소비자 서비스: Kafka에 연결된 소비자 서비스는 dbserver1.yourDatabase.Outbox 토픽에서 이벤트를 수신하여, 그 이벤트를 바탕으로 필요한 로직을 실행하거나 다른 서비스로 전달합니다.

장점

  • 실시간 처리: Debezium이 CDC 방식으로 아웃박스 테이블의 변경을 감지하여 바로 Kafka에 발행하므로 실시간으로 이벤트를 처리할 수 있습니다.
  • 추가 폴링 프로세스 불필요: 별도의 폴링 프로세스를 구현할 필요가 없으므로 시스템 자원을 절약할 수 있습니다.
  • 내결함성: Debezium은 Kafka로 이벤트 발행 중 문제가 발생하더라도 Kafka의 내장된 내결함성 기능 덕분에 안정적으로 이벤트를 재시도하고, 누락 없이 처리할 수 있습니다.
  • 처리량 증설 가능; 아웃박스 테이블 (이벤트 키를 기반으로) 파티셔닝을 통한 처리량 증대 가능 
    • 로그를 순서대로 읽느라 단일 커넥터 사용 -> 테이블에 쌓이는 속도가 더 많아서 slow
    • 분산처리로 속도 향상: 토픽(이벤트 키) 별로 outbox table 분리하여 분산 처리 가능토록(같은 키는 같은 테이블에 쌓이도록 -> 순서보장)

 

참고: https://youtu.be/DY3sUeGu74M?si=L4jk0qBOdTcRYHPb

728x90
반응형
반응형

2024.11.06 - [개발/sql] - 2 Phase Lock & mysql

2024.02.29 - [architecture/micro service] - [arch] EDA, event sourcing, saga, 2pc

 

2PC, 2PL 모두 알아봤는데 뭔가 미래에도 헷갈릴 것 같아서 정리...

 

1. 2PC (Two-Phase Commit)

2PC는 분산 트랜잭션 관리 방식으로, 여러 시스템에서 분산된 트랜잭션을 일관되게 관리하고 커밋/롤백을 보장하는 프로토콜

2PC는 MSA의 서로 다른 서비스 간(회원 컴포넌트 & 배송 컴포넌트), 즉 분리된 데이터의 저장에 대해서(하나의 동작이지만) 하나의 트랜젝션으로 묶지 못하기 때문에 데이터가 틀어지지 않게 하는 방법이다

2PC는 현재 잘 안 쓰이고 MSA의 경우 결국 SAGA chreography가 더 자주 쓰이는 듯. 비동기에 이벤트 기반인 게 제일 안전하다. 결국 메시지 브로커를 누가 누가 잘 쓰냐의 경쟁이랄까..

동작 방식:

  • Phase 1: Prepare Phase
    • 트랜잭션 코디네이터가 참가자들에게 트랜잭션을 커밋할 준비가 되었는지 물어봅니다 (Vote).
    • 각 참가자는 트랜잭션이 커밋 가능한지, 아니면 롤백해야 하는지를 답변합니다.
    • 모든 참가자가 "Yes"를 응답하면 커밋을 진행하고, 하나라도 "No"를 응답하면 롤백합니다.
    • ex. 주문 서비스(코디네이터)는 각 서비스(결제, 재고, 배송 등)에게 트랜잭션을 준비할 수 있는지 확인합니다. 각 서비스는 작업을 완료할 준비가 되었는지 확인하고, 준비되면 "Yes"를 응답합니다. 각 서비스는 해당 작업을 실제로 실행하지 않고, 작업을 예약해 놓습니다.
    • 트랜젝션 걸고 락 필요
  • Phase 2: Commit/Rollback Phase
    • 트랜잭션 코디네이터는 모든 참가자가 "Yes"라고 응답하면 커밋을 확정하고, 하나라도 "No"라고 응답하면 롤백합니다.
    • 트랜잭션 코디네이터는 모든 참가자들에게 최종 결과를 전달합니다.
    • ex. 모든 서비스가 "Yes" 응답을 하면, 주문 서비스는 모든 서비스에 대해 트랜잭션을 커밋하도록 지시합니다. 하나라도 "No" 응답이 있으면, 주문 서비스는 모든 서비스에 롤백을 지시합니다. 
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;
import javax.naming.InitialContext;

public class TwoPhaseCommitExample {

    public static void main(String[] args) {
        try {
            // JNDI를 통해 트랜잭션 관리자를 조회
            InitialContext ctx = new InitialContext();
            UserTransaction utx = (UserTransaction) ctx.lookup("java:comp/UserTransaction");

            // 1단계: 트랜잭션 시작
            utx.begin();
            System.out.println("Transaction started");

            // 참여자 1: 데이터베이스 A
            DatabaseParticipant dbA = new DatabaseParticipant("DB_A");
            dbA.prepare();

            // 참여자 2: 데이터베이스 B
            DatabaseParticipant dbB = new DatabaseParticipant("DB_B");
            dbB.prepare();

            // 2단계: 커밋 요청
            dbA.commit();
            dbB.commit();
            System.out.println("Transaction committed successfully");

            // 트랜잭션 종료
            utx.commit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

// 참여자 역할을 하는 클래스
class DatabaseParticipant {
    private String name;

    public DatabaseParticipant(String name) {
        this.name = name;
    }

    // 준비 단계
    public void prepare() {
        try {
            connection.setAutoCommit(false); // 트랜잭션 시작
            // 락 잡기
            PreparedStatement stmt = connection.prepareStatement("SELECT * FROM accounts WHERE id = ? FOR UPDATE");
            stmt.setInt(1, 1); 
            stmt.executeQuery();

            // 데이터 수정
            connection.prepareStatement("UPDATE accounts SET balance = balance - 100 WHERE id = 1").executeUpdate();

            System.out.println(name + " is prepared for the transaction");
        } catch (SQLException e) {
            e.printStackTrace();
            rollback();
        }
    }

    // 커밋 단계
    public void commit() {
        System.out.println(name + " has committed the transaction");
    }

    // 롤백 단계
    public void rollback() {
        System.out.println(name + " has rolled back the transaction");
    }
}

 

 

장점:

  • 원자성 보장: 모든 참가자가 트랜잭션을 커밋하거나 모두 롤백하여 트랜잭션의 원자성을 보장합니다.
  • 분산 트랜잭션 처리: 여러 시스템에서 하나의 트랜잭션을 관리할 수 있습니다.

단점:

  • Blocking 문제: 트랜잭션이 실패하거나 코디네이터가 실패하면, 참가자는 "결정할 수 없다"는 상태로 대기 상태에 빠질 수 있습니다. 트랜잭션 코디네이터나 참가자 서비스가 다운되면 트랜잭션이 중단될 수 있습니다.
  • 성능 저하: 2PC는 동기식으로 트랜잭션을 처리하기 때문에, 각 서비스 간의 통신과 협의 과정에서 지연이 발생할 수 있습니다.
  • 복잡성: 2PC는 구현이 복잡하며, 특히 장애 복구와 같은 시나리오에서 상태를 관리하는 데 어려움이 있을 수 있습니다.
  • 데드락: 준비 단계에서 여러 트랜잭션이 교착 상태에 빠질 수 있으므로, 트랜잭션 순서 및 타임아웃을 적절히 설정해야 합니다. prepare 했는데 commit 못하고 죽으면,, 영원한 데드락?(타임아웃 설정 필요)
  • 락 경합: 준비 단계에서 락이 너무 오래 유지되면, 다른 트랜잭션이 대기 상태가 되어 성능 저하가 발생할 수 있습니다. 이를 해결하려면 트랜잭션 범위를 최소화하고, 락 해제를 신속히 수행해야 합니다.

2. 2PL (Two-Phase Locking)

2LC는 데이터베이스의 동시성 제어 방법으로, 트랜잭션이 데이터에 대해 잠금을 설정하고 해제하는 순서를 규정하는 방식

디비(mysql) 내부에서 같은 요청이 여러번 들어왔을 때, 어떻게 잠그냐~ 하는 방법

@Transactional  Isolation과 연관하여

= 데이터 일관성과 성능 간의 균형을 맞추는 방법, 기본적으로데이터베이스의 격리 수준을 기반으로 동작

1. READ_COMMITTED (기본)

  • 매핑된 2PL 방식: Basic 2PL
    • 트랜잭션이 진행되면서 필요한 시점에만 락을 걸고, 트랜잭션이 종료될 때 락을 해제
    • Dirty Read, Non-Repeatable Read 방지
    • Phantom Read가 발생할 수 있으며, 이 경우 Repeatable ReadSerializable 격리 수준을 사용해야 함

2. READ_UNCOMMITTED

  • 매핑된 2PL 방식: None (락을 관리하지 않음)
    • 트랜잭션 간에 락을 걸지 않으며, Dirty Read가 가능하고 동시성 제어가 거의 없음
    • 2PL 방식이 적용되지 않음

3. REPEATABLE_READ(mySql default)

  • 매핑된 2PL 방식: S2PL (Strict 2PL)
    • 트랜잭션이 데이터를 읽고 쓰는 동안 해당 데이터를 락을 걸고 트랜잭션 종료 시점에 락을 해제
    • Phantom Read, Non-Repeatable ReadDirty Read 방지

4. SERIALIZABLE

  • 매핑된 2PL 방식: SS2PL (Strong Strict 2PL)
    • 트랜잭션이 시작될 때 모든 락을 미리 걸고, 트랜잭션 종료 시점에 락을 해제
    • Phantom Read, Non-Repeatable Read, Dirty Read를 방지하고, 모든 트랜잭션 간의 충돌을 방지
    • 트랜잭션이 끝날 때까지 모든 락을 보유하므로 성능이 안 좋음

 

동작 방식:

  • Phase 1: Growing Phase
    • 트랜잭션은 필요한 잠금을 획득할 수 있으며, 이 시점에서만 데이터에 대한 잠금을 증가시킬 수 있습니다. 새로운 잠금을 얻거나 기존 잠금을 확장할 수 있습니다.
  • Phase 2: Shrinking Phase
    • 트랜잭션은 잠금을 더 이상 추가할 수 없으며, 잠금을 해제하는 시점입니다. 잠금 해제는 commit 또는 rollback 후에 이루어집니다.

장점:

  • Serializable 격리 수준 보장: 이 방식은 데이터의 일관성을 유지하면서, 트랜잭션 간의 충돌을 방지할 수 있습니다.
  • 동시성 제어: 두 개의 단계에서 데이터 잠금과 해제를 관리하여 동시성 문제를 해결합니다.

단점:

  • 교착 상태 (Deadlock): 만약 여러 트랜잭션이 서로의 잠금을 기다리게 되면 교착 상태가 발생할 수 있습니다. 이를 해결하려면 교착 상태 감지 및 회피 기법이 필요합니다.
  • 성능 저하: 잠금 관리가 복잡하여 성능에 부정적인 영향을 미칠 수 있습니다.

 

728x90
반응형
반응형

MACAddress

EthernetSwitching

IPAddress

StaticRouting

DynamicRouting

 

transport layer L4 port정보; TCP, UDP

internet layer L3

network access layer L2

 

 

 

 

 

 

 

 

전체 ip?   2^32 개

공인 ip와 사설 ip를 나누는 이유: 공인 ip갯수 초과

cdn(contents delivery network)

 

broadcast: 목적지를 가지지 않고 통신; ARP request

bum traffic: unicast + multicast + unknown unicast(ARP reply 시 목적지 맥이 없을 때 발생)

 

스위치

  • 여러개의 물리적 랜에 걸쳐서 존재할 수 있는 논리적 브로드 캐스트 도매인을 나누는 것

VLAN

  • L2를 논리적으로 분리하는 기술
  • L2안에서 다른 VLAN간 통신할 수 없음
  • vlan별로 mac address를 분리 

 

ARP(address resolution protocol)

  • ip는 알지만 mac address를 모를 때 사용하는 프로토콜
  • mac address를 알아야 packet을 완성할 수 있다(헤더를 완성할 수 있다)
  • 브로드캐스트로 묻고(request) 유니캐스트로 답변(reply)
  • targetIp
  • 해당하는 서버 아니면 패킷 드랍
  • reply 못 받으면 engress buffer에 저장
    • 몇 초 기다리다 clear시킬 수 있음..(하드웨어 별 다름)

 

같은 네트워크? 앞에 3덩어리까지 같으면 같은 네트워크

 

ip가 왜 나왔을까? 맥이 고유값이면 ip가 필요없지 않나

ip는 2^32개 만큼 사용 가능 

국가별 대역이 있음; ip routing하기 위해서; 빨리 처리하기 위해서

mac address는 고유하지 않다!

윈도우는 윈도우 전용(MS가 만든) 더 간편한 방법이 있음 하지만 표준은 아님

 

 

untag가 비효율적이라 도입한 방식이 tag

hypervisor switch는 태그 값

 

같은 대역은 맥으로 통신한다. 맥테이블은 L2; 같은 VLAN

라우팅 태이블을 보지 않아도 된다(게이트웨이까지 안가도 된다); 최단거리

대역이 다른면 게이트웨이로 보내고 해당 대역대의 스위치가 dst의 맥 정보 모르면 스위치가 ARP request 보내고 학습한 후 원래 패킷 보냄

다른 대역간은 라우터 통신

 

라우팅?

  • 데이터를 출발지에서 목적지까지 가는 최적의 경로를 설정해주는 과정
  • 정적 라우팅: 엔지니어가 수동으로 경로 지정
  • 동적 라우팅: 라우터가 다른 라우터들과 경로를 주고 받아 best selection으로 자동으로 하게끔
    • best selection에 해당하는 중간에 라우터가 죽으면 
    • 다른 경로로 감(hidden routing)
  • next hop: 다음 가야할 지점
  • 맥 어드래스는 one hop by hop으로 맥이 세팅됨(경유지의 맥이 박힘)
    • 맥만 까지고 encap decap 반복 나머지는 그대로 
    • L2헤더를 깨서 버리고 다시 조립(맥 변경)
    • L3헤더의 정보는 변하지 않는다(IP 불변)

 

NAT

세션

  • TCP 연결되면 "세션이 맺어졌다"..
  • handshaking 시도 시 1800초(30분) 동안 아무 소식이 없으면 "세션 끊어짐(flushed)"
  • 브라우저 끌 때도 세션을 꺼야함! (4way hand shake) FIN은 호스트 a, b건 어디서든 가능
  • 한 컴터에서 세션의 갯수는 정해져 있음
    • 브라우저 여러개 띄우면 세션 테이블안에서 찾는 시간도 소요됨
  • 통신 시 first packet은 반드시 syn 이어야 함; 아니면 드랍
  • 라우터와 각 서버와의 타임아웃 값이 다르면 한쪽만 세션이 끊겨서 세션이 드랍될 수 있어서 맞추는게 중요
  • tcp 세션은 기본적으로 30분(timeout)_ nhn cloud의 경우
  • 세션을 늘리는 것에 방어적인 이유는 세션이 쌓여서 장애를 발생할 수 있음

 

세션 테이블

  • 테이블에 매치되는 세션이 있는가
  • 없으면 세션 만든다

 

NAT 

  • 인터넷 나갈 때 공인 ip로 바꿔줌
  • 24bit/22bit단위로 대역별로 nat ip를 쪼개서 운영(맨 마지막 ip split 덩어리)
    • 이런 세션이 65538개 생기면 하단의 소스를 인지할 수 있는 기준이 사라지게 됨
  • 같은 L3에 묶인 여러 ip가 하나의 NAT ip로(공인) 변환되어 나가는데
  • reply 받을 때 처음 받은 source port를 reuse하기 때문에 해당 값으로 각각이 누구로 부터온 응답(어떤 세션인지)인지 분류 가능
  • 회사별로 소스포트 range가 정해져 있음(reserve)

 

Loop network

  • ttl(time to live): max로 넘어갈 수 있는 hop의 카운트; 라우팅(L3)에만 해당됨
  • 여기는 L2(스위치)라서 ttl 카운트와 상관없이 계속 살아 있고 loop이 발생
    • L4(로드발랜싱)

 

이중화

모든 네트워크는 이중화

VRRP: 게이트웨이를 이중화

 

 

 

728x90
반응형

+ Recent posts