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

포인트

  • 안정성: 데이터는 소실되면 안됨
  • 가용성: 이메일과 사용자 데이터를 여러 노드에 복제하여 계속 동작하게
  • 확장성: 사용자 수가 늘어나도 성능 저하 안되게
  • 막대한 저장 용량 필요

 

구 프로토콜과 방식

저장소는 파일 시스템의 디렉터리 but 수십억 개의 이메일을 검색하고 백업하기엔 디스크 IO가 병목이 되고 서버 장애 등 안정적이지 못함

단일 장비 위에서만 동작하도록 설계

 

분산 메일 서버 아키텍쳐

보낼 때

외부 전송 큐에 보관되는 모든 메시지에는 이메일을 생성하는데 필요한 모든 메타데이터가 포함되어 있음

위 5번 과정으로 인해 외부 SMTP규모를 독립적으로 조정할 수 있게 된다.

외부 전송 큐에 오래 남아 있으면 확인 필요

1. 이메일을 보낼 큐의 소비자를 추가

2. 받는 서버에 장애 발생: 재전송 필요; 지수적 백오프

 

받을 때

 

디비는? 완벽한 디비는 없음..

RDB? 데이터 크기가 작을 때 적합. BLOB 써도 접근할 때마다 많은 디스크 IO발생

분산 객체 저장소나 Nosql은 키워드 검색이나 읽음 표시 등의 기능을 제공하기 어려움

  • 강력한 데이터 일관성 보장
  • 디스크 IO 최소화
  • 가용성 아주 높아야 하고 일부 장애를 감냉해야
  • incremental backup이 쉬워야

 

유저 키를 파티션키로 사용하여 같은 샤드에 저장

파티션키: 데이터를 여러 노드에 분산하는 역할; 데이터가 모든 노드에 균등하게 분배되도록(유저 키)

클러스터키: 같은 파티션에 속한 데이터를 정렬하는 역할(폴더)

noSQL 기반으로 할 경우 파티션키/클러스터키 외의 필터링을 어플리케이션에서 하거나 쿼리로 하기보단

테이블을 비정규화해서 테이블 자체를 나눠버리는 게 질의 성능을 향상시켜야 하는 대규모 서비스에 더 맞다.

 

일관성을 위해 replica 필요

  • 장애가 발생하면 클라이언트는 main이 복원될 때까지 동기화/갱신 작업을 완료할 수 없음
  • 일관성을 위해 가용성을 희생해야
  • 가용성을 위해서는 여러 지역의 데이터 센터에 다중화하고 일반적으로는 가까운 데이터센터에서 통신하다 장애가 났을 때 타 지역 데이터센터에 보관된 메시지를 이용한다.

 

이메일 스레딩

JWZ 알고리즘의 주요 목적

  • 이메일 간 Reply-To 관계를 파악해 계층적 트리 구조를 만듦
  • 이메일 클라이언트에서 스레드 형태로 메시지를 표시하기 위해 사용.
  • 이메일 그룹화를 통해 사용자에게 논리적이고 직관적인 대화 흐름을 제공.

알고리즘 작동 원리

  1. 헤더 정보 수집
    • Message-ID: 각 이메일의 고유 식별자.
    • In-Reply-To: 답장 대상 이메일의 Message-ID.
    • References: 해당 이메일이 참조한 이전 이메일의 Message-ID 목록.
  2. 이메일 헤더의 아래 정보를 기반으로 스레드 관계를 결정합니다:
  3. 스레드 생성 과정
    1. 모든 이메일의 Message-ID와 **Reply 관계(In-Reply-To, References)**를 분석.
    2. 이메일을 **노드(Node)**로 보고, Message-ID를 키로 하여 초기 노드 목록을 생성.
    3. 각 이메일의 In-Reply-To 및 References를 순회하며 부모-자식 관계를 설정.
    4. 스레드 트리를 형성하며, 루트 노드(시작 이메일)부터 트리가 확장됨.
  4. 루트 노드와 고아 처리
    • 루트 노드: Reply 관계가 없는 독립된 이메일로, 새로운 스레드의 시작점이 됨.
    • 고아 메시지: 부모가 없는 이메일. (헤더 정보가 손상되거나 누락된 경우 발생)
      • 별도의 루트 노드로 처리하거나, 관련성이 가장 높은 메시지에 병합.
  5. 정렬
    • 시간 순서(예: 날짜/시간) 또는 논리적 Reply 순서에 따라 트리를 정렬.

 

 

이메일 검색을 위해

  • elastic search 활용하거나 자체 개발할 솔루션
  • 디스크 I/O 병목 주의
  • 색인을 구축하는 프로세스는 다량의 쓰기 연산을 발생시키므로 LSM(Log Structured Merge) 트리를 사용하여 디스크에 저장되는 색인을 구조화하는 것이 바람직

LSM 트리가 색인 구축에 적합한 이유

  1. 쓰기 연산의 효율성
    • LSM 트리는 데이터를 먼저 메모리(RAM) 내의 MemTable에 기록하고, 특정 조건(예: 크기 초과)이 충족되면 이를 **정렬된 SSTable(파일 단위)**로 디스크에 순차적으로 저장합니다.
    • 디스크에 순차 쓰기가 이루어지므로 디스크 I/O 비용이 감소하고, 색인을 구축할 때 발생하는 다량의 쓰기 작업을 효율적으로 처리할 수 있습니다.
  2. 쓰기 병목 현상 완화
    • 기존의 B-트리와 같은 데이터 구조는 **랜덤 쓰기(Random Write)**가 많아지는 단점이 있습니다.
    • 반면, LSM 트리는 랜덤 쓰기를 최소화하고, 데이터를 버퍼링 후 배치 처리하여 쓰기 병목을 완화합니다.
  3. 색인 업데이트의 성능 향상
    • 색인을 업데이트할 때, 기존 데이터를 즉시 덮어쓰지 않고 새로운 데이터를 추가한 뒤 Compaction(압축) 과정을 통해 병합합니다.
    • 이를 통해 쓰기와 병합 작업이 분리되어 성능이 향상됩니다.
  4. 다량의 색인 생성
    • 색인을 구축하는 도중에 읽기 연산이 발생하더라도, LSM 트리는 메모리 내 데이터와 디스크 데이터를 병합하여 읽기 요청을 처리할 수 있습니다.
    • 색인 작업 도중에도 안정적으로 데이터를 처리할 수 있는 구조를 제공합니다.

LSM 트리 작동 과정

  1. MemTable: 데이터를 메모리에 기록 (쓰기 연산 집중).
  2. Flush: MemTable이 가득 차면, 이를 디스크에 **SSTable(Sorted String Table)**로 기록.
  3. Compaction: 여러 개의 SSTable을 병합하여 디스크 공간을 최적화하고 읽기 성능을 향상.
728x90
반응형
반응형

포인트

  • 동시성은 높으나 초당 예약 건수는 높지 않음
  • 약간의 지연시간 가능

 

데이터베이스로 RDB 선택

  • 인덱스 등을 사용하여 읽기가 압도적인 흐름을 잘 지원(no sql은 쓰기에 빠름)
  • ACID 보장(이중 청구, 이중 예약, 잔액 마이너스 등 방지)
  • 데이터 모델링(호텔, 객실 등) 가능
  • 7,300만 개 정도의 데이터는 하나의 데이터베이스로 저장하기에 충분하지만 SPOF가능성이 있어 replica를 두는 게 좋다.
  • 데이터가 많아진다면 현재 및 향후 데이터만 저장하고 나머지는 cold storage에 옮기거나
  • 데이터 베이스를 호텔 키를 기반으로 샤딩한다.

 

동시성; 이중 예약 문제

  • 멱등성 api; 주문 키를 디비 pk로 사용하여 두 번 저장되지 않게
  • 서로 다른 사용자의 동시 디비 접근을 막기 위해 디비 락
    • 비관락 select for update 충돌이 있을 것이라 가정하고 미리 락; 모든 연산을 직렬화; 데이터에 대한 경합이 심할 때; 교착 상태 빠질 수, 성능 낮아짐
    • 낙관락: 테이블에 버저닝, 락을 걸지 않아 비관락보다 빠름 하지만 동시성 수준이 높으면 성능이 급격히 나빠짐
    • constraint(디비 제약조건); 낙관락과 유사, 제약 조건 맞으면 롤백; 디비별로 지원하지 않을 수도

 

규모 확장

  • 호텔 아이디를 기반으로 한 데이터베이스 샤딩
  • 예약 서비스는 잔여 객실 질의는 캐시에다 하고 갱신은 디비에다 한다. 디비에 갱신이 일어나면 비동기로 캐시 데이터를 갱신한다.
  • 캐시: 낡은 데이터 소멸되도록 TTL 설정; 레디스를 두고 잔여 객실 질의할 때 캐시를 쓸 수 있게
    • 키: hotelId_roomtypeId_yyyymmdd
    • 값: 잔여 객실 수
  • 캐시 갱신: 비동기로
    • 갱신은 어플리케이션 단에서 할 수도 있고 카프카 소스 커넥터 + 디비지움(CDC)를 사용하여 디비 변경 사항을 감지하여 캐시에 반영하게 한다.

 

MSA 일관성

여러 개의 단일 트랜젝션을 어떻게 일치시킬까

2PC: 여러 노드에 걸친 하나의 트랜젝션

한 노드에 장애 나면 중단; 트랜젝션 참여 노드가 많아질수록 통신 오버헤드와 응답 지연 증가

1단계: 준비 단계 (Prepare Phase)

  • 코디네이터(또는 트랜잭션 관리자)가 트랜잭션에 참여하는 모든 노드에 Prepare 명령
  • 각 노드는 트랜잭션을 수행할 준비가 되었는지 확인하고, 로컬에서 트랜잭션의 상태를 저장(로그 기록)한 뒤, 코디네이터에게 승인(YES) 또는 거부(NO)를 응답
    • 응답이 YES이면 해당 작업을 커밋할 준비가 되었다는 의미.
    • 응답이 NO이면 트랜잭션을 중단해야 한다는 의미.

2단계: 커밋 또는 롤백 단계 (Commit or Rollback Phase)

  • 모든 노드가 YES를 응답하면 코디네이터는 트랜잭션을 커밋(Commit)
  • 한 노드라도 NO를 응답하면 코디네이터는 트랜잭션을 롤백(Rollback)
  • 각 노드는 코디네이터의 명령에 따라 커밋 또는 롤백을 수행한 뒤 결과를 다시 코디네이터에게 알림

 

사가: SAGA 트랜잭션분산 시스템에서 장기 실행 트랜잭션을 관리하기 위한 설계 패턴으로, 분산 트랜잭션의 일관성을 보장하면서도 2PC의 성능 및 확장성 문제를 해결하기 위해 고안

  • 데이터의 최종적 일관성(Eventual Consistency)을 보장.
  • 보상 작업(Compensating Transaction)을 통해 롤백 대신 이전 상태로 복구.
  • 분산 락(Distributed Lock)을 사용하지 않아 확장성과 성능이 뛰어남.
  • 네트워크 및 시스템 장애에 대한 복원력을 높임.
  1. Choreography (코레오그래피)
    • 각 서비스가 다른 서비스로 직접 이벤트를 발행하고 구독.
    • 중앙 제어 지점이 없음.
    • 유연성이 높지만, 서비스 간 결합도가 증가할 수 있음.
    • 예시:
      • A 서비스가 작업 완료 후 이벤트를 발행 → B 서비스가 이벤트를 받아 다음 작업 수행 → C 서비스도 같은 방식으로 진행.
  2. Orchestration (오케스트레이션)
    • 중앙 컨트롤러(오케스트레이터)가 각 서비스를 호출하고 트랜잭션을 제어.
    • 서비스 간 결합도는 낮지만, 중앙 집중식 제어가 필요.
    • 예시:
      • 중앙 오케스트레이터가 A, B, C 서비스를 순차적으로 호출하고 결과에 따라 보상 작업 수행.

SAGA 트랜잭션의 기본 흐름

  1. 각 단계(Step)는 독립적인 로컬 트랜잭션으로 실행되고 커밋.
  2. Step 중 하나가 실패하면 이전 단계에서 실행된 트랜잭션을 보상 작업으로 되돌림.
  3. 전체 작업이 성공하거나 보상 작업이 완료될 때까지 진행.

 

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

728x90
반응형
반응형

포인트

  • 광고 클릭시 로그 쌓음
    • 광고 아이디 시간 유저 국가 등
  • 집계 필요
    • 특정 광고에 대한 지난 n분간의 클릭 이벤트 수
    • 지난 x분간 가장 많이 클릭된 광고 y개
  • 광고 클릭 QPS = 10,000
  • 최대 광고 클릭은 50,000로 가정
  • 클락 하나당 0.1kb 저장 용량 필요하다고 가정; 하루 100기가; 월간 3테라

방식

  • 로그에 정형화되게 남기면 1분마다 집계하여 저장해두고 요청이 오면 집계데이터를 filter하거나 sum하거나 등 가공하여 내림
    • 원시 데이터도 저장하고 집계 데이터도 저장하고
    • 원시 데이터를 통한 재집계 지원
    • 질의는 집계된 데이터에만 
    • 시간이 지나면 원시 데이터는 아애 옮겨버려
  • 원시 데이터는 쓰기양이 많으므로 시계열 데이터에 유리한 influxDB나 카산드라 추천

집계 서비스

로그가 들어오면 메세지 큐에 쌓이고 큐에서 원시 데이터 저장소에 저장하고 집계 서비스로 흘러간다

mapReduce 프레임워크 사용; mapReduce 프레임워크에 좋은 모델은 DAG(directed acyclic graph)모델

1. MapReduce Framework

  • 개요: MapReduce는 대규모 데이터를 분산 환경에서 처리하기 위한 프로그래밍 모델.
    • Map 단계: 데이터를 키-값 쌍으로 매핑.
    • Reduce 단계: 동일한 키를 가진 값을 집계 및 계산.
  • 작업 처리 흐름:
    • 데이터는 Map 단계에서 병렬 처리되고, Shuffle/Sort 과정을 통해 Reduce 단계로 전달.
    • Reduce 단계에서 최종 결과를 생성.
  • 제약:
    • 단순한 작업 흐름으로 인해 복잡한 작업 간 의존성을 표현하거나 최적화하기 어렵다.
    • 한 번에 단일 Map → Reduce 작업만 가능하므로 다중 스테이지 워크플로우는 비효율적.

2. DAG 모델

  • 개요: DAG는 작업 간 의존성을 표현하기 위해 사용되는 방향성 비순환 그래프.
    • 각 노드: 개별 작업(예: Map, Reduce, 기타 연산).
    • 각 간선: 작업 간의 데이터 의존성을 나타냄.
    • "비순환(Acyclic)": 작업이 완료되면 되돌아가지 않음(순환 없음).
  • 특징:
    • 복잡한 작업 워크플로우를 시각적으로 표현 가능.
    • 병렬로 실행 가능한 작업을 식별하여 최적화 가능.
    • 작업 실행 순서를 명확히 정의.

3. 카프카 스트림즈 사용

  • 카프카 토픽으로 들어오는 데이터를 소비하며 시간 기반의 집계 처리
StreamsBuilder builder = new StreamsBuilder();
KStream<String, String> input = builder.stream("input-topic");
input
    .groupByKey()
    .windowedBy(TimeWindows.of(Duration.ofMinutes(1)))
    .count()
    .toStream()
    .to("output-topic");
KafkaStreams streams = new KafkaStreams(builder.build(), props);
streams.start();

1분 단위 데이터 처리 흐름

1. 데이터 수집

  • 예를 들어, Kafka의 프로듀서가 데이터를 지속적으로 전송하고, Spark/Flink/Kafka Streams가 이를 소비합니다.

2. 윈도우에 데이터 저장

  • 데이터는 버퍼 또는 임시 저장소에 저장됩니다.
    • Spark Streaming: RDD 내부에 데이터가 임시 저장됨.
    • Flink: State Backend(예: RocksDB)에서 상태 관리.
    • Kafka Streams: 내부 상태 저장소를 통해 관리.

3. 1분 타이머 완료 시 처리

  • 타이머가 1분에 도달하면:
    • 데이터를 sum, count, average 등으로 집계.
    • 집계 결과를 특정 주체로 전달.

4. 데이터 삭제

  • 1분 타이머가 종료되면 해당 윈도우의 데이터는 메모리에서 삭제되거나 다음 처리 단계로 넘깁니다.
    • Spark: 새로운 마이크로 배치를 시작하며 이전 데이터는 삭제.
    • Flink/Kafka Streams: 윈도우 종료와 함께 상태를 정리.

 

스트리밍 vs 일괄 처리

람다 아키텍처의 주요 구성

배치와 스트리밍을 동시에 지원하는 시스템 아키텍쳐

  1. Batch Layer (배치 레이어):
    • 역할: 대규모의 정적 데이터(이력 데이터)를 처리하고 정확한 분석 결과를 제공합니다.
    • 1분 동안 수집된 데이터를 주기적으로 처리해 정확한 집계 결과 생성.
    • 특징:
      • 주기적으로 데이터를 처리.
      • 데이터의 **불변성(Immutable)**을 보장하여 과거 데이터를 다시 분석하거나 복구가 가능.
    • 주요 기술:
      • Hadoop, Spark, Hive 등.
    • 산출물:
      • 주기적으로 계산된 배치 뷰(Batch View).
  2. Speed Layer (스피드 레이어):
    • 역할: 실시간으로 들어오는 데이터를 빠르게 처리하여 최신 정보를 제공합니다.
    • 실시간 데이터 스트림을 활용해 1분 이내의 최신 데이터를 반영.
    • 특징:
      • 지연(latency)을 최소화.
      • 데이터를 임시 저장하거나 간단한 처리를 수행.
    • 주요 기술:
      • Apache Kafka, Storm, Flink, Spark Streaming 등.
    • 산출물:
      • 최신 상태를 반영한 실시간 뷰(Realtime View).
  3. Serving Layer (서빙 레이어):
    • 역할: 배치 뷰와 실시간 뷰를 결합해 최종 데이터를 사용자에게 제공.
    • 배치 결과와 실시간 데이터를 통합해 최종 집계 데이터를 제공.
    • 특징:
      • 두 레이어의 결과를 통합하여 질의(Query)에 응답.
      • 읽기 요청에 최적화된 시스템.
    • 주요 기술:
      • Elasticsearch, Cassandra, HBase 등.

람다 아키텍처의 데이터 처리 흐름

  1. 데이터 수집:
    • 데이터를 Batch Layer와 Speed Layer에 동시에 전달.
  2. Batch Layer:
    • 데이터 전체를 처리해 정확한 배치 뷰를 생성.
    • 처리 속도가 느릴 수 있지만, **정확성(Accuracy)**이 핵심.
  3. Speed Layer:
    • 실시간 데이터를 처리하여 최신 상태를 반영한 실시간 뷰를 생성.
    • 처리 속도가 빠르지만, 최종적으로 정합성이 보장되지 않을 수 있음.
  4. Serving Layer:
    • 배치 뷰와 실시간 뷰를 통합해 사용자에게 최신 데이터 제공.

장점

  1. 실시간성과 정확성:
    • 배치 처리의 정확성과 실시간 처리의 신속성을 동시에 제공.
  2. 확장성:
    • 배치 레이어와 스피드 레이어 모두 분산 시스템 기반으로 확장 가능.
  3. 유연성:
    • 과거 데이터를 보존해 언제든 재처리가 가능.

단점

  1. 복잡성 증가:
    • 두 개의 레이어를 유지관리해야 하므로 운영이 복잡.
  2. 데이터 중복 처리:
    • 배치와 실시간 레이어에서 동일 데이터를 중복 처리.
  3. 비용 문제:
    • 두 레이어를 운영하므로 인프라 비용 증가.

사용 사례

  • 소셜 미디어 분석:
    • 실시간으로 트렌드를 파악(Speed Layer).
    • 장기적인 사용자 행동 분석(Batch Layer).
  • 금융 데이터 처리:
    • 실시간 거래 감시(Speed Layer).
    • 전체 거래 기록 분석(Batch Layer).

 

카파 아키텍처의 주요 개념

실시간 데이터 스트림 처리를 중심으로 구성되며, 배치 처리 레이어를 없애고 단일 스트림 처리 파이프라인으로 모든 데이터를 처리

  1. 단일 데이터 파이프라인:
    • 모든 데이터 처리를 스트림 기반으로 수행.
    • 배치와 실시간 처리를 동일한 데이터 처리 엔진을 사용해 통합.
  2. 이벤트 소싱(Event Sourcing):
    • 데이터를 이벤트 로그로 기록하며, 재처리(replay)를 통해 필요한 분석 작업 수행.
    • Kafka Topics 같은 로그 기반 저장소를 활용.
  3. 일관성(Consistency) 및 유연성:
    • 데이터의 재처리를 통해 정합성을 유지하고, 새로운 처리 로직 적용 가능.
  4. 재처리(Replay):
    • 데이터 로그를 다시 읽어 과거 데이터를 재분석하거나, 새롭게 정의된 처리 방식으로 기존 데이터를 처리.

카파 아키텍처의 주요 구성 요소

  1. 데이터 소스:
    • 데이터는 실시간으로 수집되어 로그 저장소로 들어갑니다.
    • 예: IoT 센서, 웹 클릭 로그, 트랜잭션 기록 등.
  2. 로그 저장소:
    • 데이터의 단일 진실 소스(Single Source of Truth)로 사용됩니다.
    • 예: Apache Kafka, Pulsar.
    • 로그 저장소는 데이터를 소비자가 원하는 속도로 읽고 처리할 수 있도록 지원.
  3. 스트림 처리 엔진:
    • 실시간 데이터 처리를 수행하며, 배치와 실시간 처리를 동일한 방식으로 처리합니다.
    • 예: Apache Flink, Kafka Streams, Apache Beam.
  4. 저장 및 조회 시스템:
    • 처리된 데이터를 저장하고 사용자 쿼리에 응답합니다.
    • 예: Elasticsearch, Cassandra, HBase.

카파 아키텍처의 데이터 처리 흐름

  1. 데이터 생성 및 수집:
    • 데이터 소스에서 이벤트를 수집하여 로그 저장소에 저장.
  2. 로그 저장소:
    • 모든 이벤트는 Kafka Topic과 같은 로그에 저장됩니다.
    • 이 데이터는 소비자(Consumer)가 처리할 때까지 유지.
  3. 스트림 처리:
    • 스트림 처리 엔진이 데이터를 실시간으로 처리.
    • 재처리가 필요할 경우 로그 저장소를 다시 읽어 과거 데이터를 처리.
  4. 결과 저장:
    • 처리된 데이터를 데이터베이스나 검색 시스템에 저장.
    • 결과는 사용자 요청에 따라 즉시 제공.

장점

  1. 단순화:
    • 배치와 실시간 처리를 통합해 관리 복잡성 감소.
  2. 유연성:
    • 로그 저장소에서 데이터를 재처리할 수 있어 새로운 요구사항에 적응 가능.
  3. 확장성:
    • 로그 기반 시스템(Kafka)은 대규모 데이터를 효율적으로 처리하고 확장 가능.
  4. 신뢰성:
    • 로그 저장소를 통해 데이터 유실 방지.

단점

  1. 재처리 비용:
    • 로그 저장소에서 과거 데이터를 읽어 재처리하는 데 시간이 오래 걸릴 수 있음.
  2. 기술 의존성:
    • Kafka, Flink와 같은 특정 기술에 크게 의존.
  3. 성능 병목:
    • 실시간 스트림 처리 엔진이 데이터 처리 속도를 제한할 가능성.

 

집계 기준 시각

  • 이벤트 발생 시각
    • 클라이언트/요청 시간 기준 -> 정확한 집계가 가능하지만 시간 조작 가능성도 있음
  • 처리 시각
    • 서버 도착 시간 기준 -> 안정적이지만 지연되는 이벤트는 부정확함
  • 이벤트 발생 시각 + 워터마크 기법

워터마크의 개념

워터마크(Watermark)는 데이터 스트림 처리에서 이벤트 시간(Event Time)을 기반으로 지연 데이터를 처리하는 중요한 개념입니다.
주로 실시간 집계 및 윈도우 연산에서 사용되며, 늦게 도착한 데이터를 효율적으로 다룰 수 있도록 설계되었습니다.

  • 이벤트 시간(Event Time): 데이터가 실제로 생성된 시간을 의미. (예: 로그의 타임스탬프)
  • 처리 시간(Processing Time): 스트림 처리 엔진이 데이터를 처리하는 실제 시간.
  • 데이터 스트림 환경에서는 네트워크 지연이나 시스템 병목으로 인해 이벤트 시간이 비정상적으로 늦게 도착하는 경우가 있습니다. 이를 지연 데이터(Late Data)라고 합니다.

워터마크(Watermark)는 이런 지연 데이터를 다루기 위해 특정 시점까지 기다렸다가 더 이상 이벤트가 도착하지 않는다고 가정하는 기준점입니다.

워터마크의 동작 방식

  1. 윈도우 연산 수행:
    • 데이터를 특정 기간(예: 1분)으로 그룹화하여 집계.
    • 예: 09:00:00 ~ 09:01:00까지의 데이터를 집계.
  2. 지연 허용 시간 설정:
    • 워터마크는 지정된 허용 지연 시간 뒤에 도착하는 데이터는 무시하거나 별도로 처리합니다.
    • 예: 지연 허용 시간 = 10초 → 09:01:10 이후 도착한 데이터는 늦은 데이터로 간주.
  3. 워터마크 생성:
    • 스트림 처리 엔진이 워터마크를 주기적으로 생성.
    • 워터마크는 데이터의 이벤트 시간 기준으로 현재까지의 처리 상태를 나타냄.
    • 예: "현재 이벤트 시간은 09:01:05이며, 이 시점 이후에 도착한 데이터는 늦은 데이터로 처리."
  4. 윈도우 종료:
    • 워터마크가 윈도우의 종료 시간을 초과하면 해당 윈도우의 데이터를 닫고 결과를 출력.
  5. 워터마크가 짧으면 데이터 정확도는 떨어지지만 시스템 응답 지연은 낮아진다. 워터마크를 사용하면 데이터의 정확도는 높아지지만 대기 시간이 늘어나 전반적인 지연 시간은 늘어난다.

 

집계 윈도

 

  • 텀블링 윈도우: 데이터를 고정된 간격으로 집계하며 중복 데이터는 처리하지 않음. 단순하지만 중간 데이터 손실 가능.
  • 고정 윈도우: 텀블링과 동일하지만 구현 방식에 따라 다르게 표현될 수 있음.
  • 호핑 윈도우: 고정된 크기의 윈도우를 지정된 간격으로 중첩해 이동. 주기적인 데이터 분석에 적합.
  • 슬라이딩 윈도우: 작은 간격으로 데이터를 집계해 연속적으로 처리. 실시간 분석에 적합하나 계산량이 많음.
  • 세션 윈도우: 이벤트 간의 비활성 시간을 기준으로 유동적으로 윈도우 종료를 결정. 사용자 활동 및 비정기적 이벤트 추적에 유리.

 

전달 보장

  • 카프카; 정확히 한 번
  • 중복 집계 되지 않도록 오프셋 관리 철처하게
  • 오프셋은 외부 파일 저장소에 기록
  • 아래 4~6 단계의 작업을 하나의 분산 트랜젝션에 넣어야 한다.

 

집계 서비스의 규모 확장/핫스팟 문제 시

보통 규모 확장을 위해 추가 서버 투입, 다중 프로세스, 다중 노드로 처리

 

집계 서비스의 fault tolerance를 위해

스냅 샷을 찍어서 그 후로 복구(집계 데이터 포함)

 

모니터링

  • 지연 시간: 각 단계마다 timestamp 추적이 가능하도록
  • 메세지 큐 크기: 카프카의 record lag 등으로 집계 서비스 노드 추가 투입 판단
  • 시스템 자원: cpu, jvm, 디스크
  • 조정: 배치 결과랑 실시간 결과랑 비교해서 데이터 무결성 검증

728x90
반응형
반응형

시계열 데이터

시간에 따라 변화하는 데이터를 다루는 데이터 유형으로, 주로 시간 순서가 중요한 데이터

 

시계열 데이터 저장소

  • 많은 양의 쓰기 연산 부하
  • 읽기 연산의 부하는 잠시 급증하는 정도

1. 쓰기 연산이 항상 많은 경우

문제점

  1. 관계형 데이터베이스 (RDBMS):
    • 트랜잭션 처리 오버헤드:
      • RDBMS는 ACID 특성을 보장하기 위해 쓰기 작업마다 트랜잭션을 처리합니다. 이로 인해 데이터 삽입 속도가 느림
    • 스키마 설계 제약:
      • 시계열 데이터는 빠르게 증가하기 때문에, 인덱스가 방대해지고 조인 연산이 많아지면 쓰기 성능이 저하
    • 확장성 부족:
      • 수평 확장(Sharding)이 어렵기 때문에, 데이터 양이 급격히 증가하면 확장 비용이 높음
  2. NoSQL:
    • 쓰기 성능의 제약:
      • NoSQL(예: MongoDB, Cassandra)은 쓰기 성능이 좋지만, 대량의 쓰기 작업에서 인덱싱 비용이 높음
    • 디스크 I/O 증가:
      • 대량 쓰기 작업 시 디스크 I/O가 병목이 되어 성능 저하를 초래할 수도

2. 읽기가 특정 시간에 몰리는 경우

문제점

  1. 관계형 데이터베이스 (RDBMS):
    • 읽기 병목:
      • 복잡한 질의
      • join
    • 인덱스 비효율성:
      • 대규모 시계열 데이터에서 시간 기반 쿼리(예: WHERE timestamp BETWEEN ...)는 범위 검색이 많아져 인덱스 성능이 저하될 수 있습니다.
      • 인덱스를  검색하려는 모든 값에 걸 수 없음
      • 튜닝 한계
  2. NoSQL:
    • 읽기 쿼리 제약:
      • 시계열 데이터를 읽을 때, 복잡한 필터링 및 집계 작업이 필요하면 NoSQL 시스템에서는 추가적인 애플리케이션 레벨의 처리가 필요
    • 읽기 집중 시 과부하:
      • MongoDB와 같은 NoSQL 시스템은 읽기 요청이 급증할 경우, 캐싱 및 클러스터 확장이 충분하지 않으면 병목이 발생할 수도

 

시계열 데이터베이스(TSDB)(예: InfluxDB, TimescaleDB)는 쓰기/읽기 최적화, TTL, 시간 기반 쿼리 등 시계열 특화 기능으로 적합

시계열 데이터베이스(TSDB)가 적합한 이유

  1. 쓰기 최적화:
    • TSDB는 쓰기 작업을 빠르게 처리하도록 설계
    • 압축 기술: 데이터 크기를 줄이고 저장 효율을 높임.
    • Batch Writing: 쓰기 작업을 배치 처리로 최적화.
  2. 읽기 최적화:
    • 시간 기반 쿼리 최적화: 타임스탬프 기반 필터링 및 집계가 빠르게 동작.
    • 전용 인덱스: 시계열 데이터를 효율적으로 검색하도록 설계된 고유 인덱스 구조.
      • 레이블(태그) 별로 인덱스
  3. 자동 데이터 관리:
    • Retention Policy(TTL): 오래된 데이터를 자동으로 삭제하여 저장 공간을 관리.
    • Downsampling: 오래된 데이터를 요약 데이터로 변환(예: 1초 단위 -> 1시간 평균).
  4. 확장성:
    • TSDB는 수평 및 수직 확장이 용이하며, 대량의 데이터를 처리하는 데 최적화됨.
  5. 예: InfluxDB, TimescaleDB, Prometheus, OpenTSDB

 

풀 vs 푸시

  • 풀: 지표수집기가 주기적으로 지표를 요청하여 데이터를 가져옴
    • 서비스 디스커버리를 사용하여 수집해야 할 서버정보와 메타 데이터를 관리
    • 지표 수집기가 서비스 디스커버리로부터 목록을 확보하여 http로 데이터를 가져옴
    • 지표 수집기도 여러대(서버풀)준비해야 할텐데 여러 지표 수집기가 같은 서버를 찌르지 않도록 consistent hash ring 구조로 담당 서버를 지정하면 특정 서버 지표는 항상 하나의 수집 서버가 처리함을 보장할 수  있다.
    • 프로메테우스
  • 푸시: 서버기 지표 수집기를 호출하여 데이터 전송
    • 푸시 모델의 지표 수집기가 밀려드는 지표 데이터를 제 때 처리하지 못하면 일시적으로 버퍼(메모리/디스크 등)에 저장하여 재전송할 수도 있겠지만 서버가 동적으로 추가/삭제되는 과정에서 데이터가 소실될 수 있다.
    • 따라서 지표 수집기 클러스터 자체도 자동 규모 확장이 가능하게 구성하고 그 앞에 로드밸러서를 두는게 좋다.

 

카프카를 통한 규모 확장

  • 시계열 데이터 베이스 앞에 카프카를 두어 디비가 죽어도 데이터가 소실되지 않게 함
  • 지표의 태그/레이블에 따라 토픽/파티션으로 세분화 가능

 

집계(통계화) 시점

  • 데이터 저장 전에 집계한다면 스트림 프로세싱 엔진이 필요할거고 저장양이 줄긴하지만 원본데이터를 저장하지 않으니 유연성이 좋지 않다
  • 저장하고 조회 시 집계한다면 전체 대상으로 계산해야해서 속도가 느리다

 

질의 서비스

  • 캐시 고려
  • 질의 서비스를 안두고 바로 시계열 디비와 연동하는 경우도 있음

 

저장소

  • 저장 용량 최적화 필요
  • 데이터 인코딩 및 압축
  • 다운 샘플링: 기간별 해상도를 낮춰서 보관
    • 예를 들어 2주는 원본 한달이내 1분단위로 집계한 데이터 1년 이내 1시간으로 집계한 데이터 등..(해상도를 낮춘다고 표현)
  • cold storage: 저장 위치를 아애 옮겨버림 nas 같은 곳으로..

 

경보 시스템

  • 경보 규칙을 설정파일로 저장하고 캐시화
  • 규칙에 따라 질의 서비스 호출해서 계산하고 설정 임계값을 위반하면 경보 이벤트 생성
  • 경보 저장소: 키-값 저장소; 모든 경보의 상태 저장; 알림이 적어도 한번은 전달되도록 보장
  • 경보 이벤트는 카프카로
  • 해당 메세지를 읽어 다양한 채널로 알림 전송

 

728x90
반응형
반응형

메시지 큐 사용 이유

  • 컴포넌트 사이의 강한 결합 완화
  • producer/consumer을 독립적으로 확장 가능
  • 특정 컴포넌트 장애 발생해도 다른 컴포넌트는 작업 가능
  • 비동기 통신을 통한 성능 개선

메시지큐 vs 이벤트 스트리밍 플랫폼

  • 메세지 큐: rabbit mq
  • 이벤트 스트리밍 플랫폼: 카프카
  • 메세지큐 + 데이터 장기보관, 반복 소비, 스트리밍 기능 = 이벤트 스트리밍 플랫폼

 

큐?

  • producer가 메시지를 큐에 보내고 consumer는 큐를 구독하고 구독한 메시지를 소비
  • producer/consumer 는 모두 클라이언트고 서버 역할을 하는 것은 메세지 큐
  • 일대일 모델
    • 메세지는 오직 한 소비자만 가져갈 수 있음; 소비되면 삭제됨
    • 한 큐에 여러 소비자가 붙으면 큐의 병렬처리
  • 발행 구독 모델
    • 토픽: 고유한 이름을 가진; 메시지를 보내고 받을 때 토픽에 보내고 받음
    • 해당 토픽을 구독하는 모든 소비자에게 전달

메시지 큐가 서버인 이유

메시지 큐는 시스템의 중심에서 메시지를 저장하고 관리하며, 전달을 책임지는 역할을 하기 때문에 서버로 간주됩니다. 다음과 같은 이유가 있습니다:

(1) 중앙 관리 역할

메시지 큐는 모든 메시지를 중앙에서 관리합니다.

  • Producer는 메시지를 큐에 넣기만 하고,
  • Consumer는 큐에서 꺼내오기만 합니다.
    즉, 메시지의 저장, 전달, 보관 등 모든 복잡한 작업은 메시지 큐가 처리합니다. 이 중앙 관리 기능은 전형적인 서버의 역할입니다.

(2) 요청/응답 모델

  • Producer는 메시지를 큐에 요청(Request)으로 전송합니다.
  • Consumer는 큐에 요청(Request)을 보내 메시지를 가져옵니다.
  • 메시지 큐는 이 요청들을 처리하고 응답(Response)하거나, 메시지를 전달하는 서버 역할을 수행합니다.

(3) 리소스 제공자

메시지 큐는 Producer와 Consumer 사이에서 리소스를 제공하는 역할을 합니다. 예를 들어:

  • 메시지의 내구성(Durability)과 순서 보장(Order Guarantee)을 관리합니다.
  • 특정 Consumer가 일시적으로 작동하지 않아도 메시지를 보관하여 이후에 전달합니다.
    Producer와 Consumer는 메시지 큐에 의존하여 이 서비스를 이용하므로 클라이언트로 간주됩니다.

Producer와 Consumer가 클라이언트인 이유

Producer와 Consumer는 메시지 큐를 사용하는 사용자로 동작하기 때문에 클라이언트로 간주됩니다.

(1) 요청을 보냄

  • Producer는 메시지를 큐로 보내는 요청을 보냅니다.
  • Consumer는 메시지를 큐에서 가져오라는 요청을 보냅니다.
    요청(Request)을 보내는 주체는 일반적으로 클라이언트로 간주됩니다.

(2) 큐에 의존

Producer와 Consumer는 메시지 큐가 없다면 서로 직접 통신해야 합니다. 그러나 메시지 큐를 사용함으로써:

  • Producer는 큐에 메시지를 보내는 데만 집중합니다.
  • Consumer는 큐에서 메시지를 가져오는 데만 집중합니다. 결국 이 둘은 큐가 제공하는 서비스(메시지 저장 및 전달)에 의존하는 클라이언트로 동작합니다.

 

토픽, 파티션, 브로커

  • 메시지는 토픽에 보관
  • 토픽에 보관되는 데이터 양이 많으면? 파티션 사용(샤딩 기법)
  • 토픽을 여러 파티션으로 분할하여 메시지를 나눠 보냄
  • 파티션은 메세지 큐 클러스터 내의 서버에 고르게 분산 배치
  • 파티션을 유지하는 서버: 브로커
  • 파티션을 브로커에 분산하여 높은 확장성 달성; 토픽 용량 확장 시 파티션 개수 증가
  • 각 파티션은 FIFO 큐; 한 파티션 안에서는 순서 유지; 위치=offset
  • 파티션 키; 같은 키를 가진 모든 메시지는 같은 파티션으로 없으면 무작위로 보내짐
  • 소비자 그룹: 토픽을 구독하는 소비자가 여럿인 경우 각 구독자는 해당 토픽을 구성하는 파티션의 일부를 담당
    • 같은 그룹 내 소비자는 메시지를 병렬로 소비, 그러면 같은 파티션 안의 메세지를 순서대로 소비할 수는 없다
    • 어떤 파티션은 한 그룹 안에서 한 소비자만 읽게 하여 순서 보장
    • 그룹 내 소비자의 수가 구독하는 토픽의 파티션 수보다 크면 어떤 소비자는 해당 토픽에서 데이터를 읽을 수 없음
    • 처리량을 늘리려면 소비자를 추가

 

데이터를 저장하는 큐를..

  • 디비?
    • 읽기 쓰기가 대규모로 빈번하게는 부적합
  • 쓰기 우선 로그(write ahead log WAL)?
    • 지속성
    • 읽기/쓰기 모두 순차적 -> 디스크
    • 파일 하나에 계속 쓰면 관리가 힘드므로 여러 세그먼트로 분리

 

생산자 작업 흐름

producer는 메시지를 producer 내부의 라우팅 계층에 보내 어떤 브로커(의 파티션)로 보내야하는지 파악하고 전송할 메세지를 버퍼 메모리에 잠시 보관했다가 목적지로 일괄 전송(배치 양을 늘리면 응답 속도가 느려짐)하여 대역폭을 넓힌다. 리더 브로커가 받아서 저장하고 replica가 일정 수 만들어지면 데이터가 소비 가능해지며 producer에게 완료회신을 보낸다.

소비자 작업 흐름

consumer는 특정 파티션의 오프셋의 위치에서부터 이벤트를 묶어 가져온다.

푸시 vs 풀

  • 푸시: 브로커가 데이터를 consumer에게 보냄
    • 낮은 지연: 바로 보냄 하지만 consumer 속도가 느리면 부하가 걸림
  • 풀: consumer가 브로커에게 가져감
    • 소비 속도는 consumer가 알아서 조절. 만약 큐가 쌓이면 소비자를 추가하거나 기다리거나
    • 다음 오프셋부터 한 번에 가져가 배치 처리 가능
    • 단, 데이터가 없어도 시도 -> 롱 폴링으로 consumer 통신 횟수 감소 가능(가져갈 게 없어도 일정시간 기다림)

 

소비자 그룹(Consumer Group)이란?

  • 소비자 그룹(Consumer Group)은 Kafka에서 메시지를 읽어가는 Consumer의 논리적인 묶음입니다.
  • 같은 그룹에 속한 Consumer들은 특정 토픽의 파티션을 분배받아 메시지를 처리합니다.
  • 소비자 그룹 ID를 기준으로 그룹이 구분됩니다.

특징:

  1. 같은 그룹 내의 Consumer는 같은 메시지를 읽지 않습니다.
    • 파티션 하나는 소비자 그룹 내에서 단일 Consumer만 처리합니다.
    • 이를 통해 메시지 처리가 병렬로 이루어집니다.
  2. 다른 소비자 그룹은 독립적으로 동작합니다.
    • 서로 다른 소비자 그룹은 동일한 메시지를 중복으로 처리할 수 있습니다.
    • 예: groupA와 groupB는 동일한 토픽을 구독할 수 있으며, 각각 모든 메시지를 독립적으로 처리합니다.

브로커와 소비자 그룹의 관계

Kafka 브로커와 소비자 그룹은 메시지의 생산(Producer)부터 소비(Consumer)까지의 흐름에서 중요한 관계를 형성합니다.

(1) 브로커는 메시지를 저장하고 제공

  • 브로커는 토픽과 파티션을 기반으로 메시지를 저장하고 관리합니다.
  • Producer는 메시지를 브로커에 전송하고, 브로커는 이를 적절한 파티션에 저장합니다.

(2) 소비자 그룹은 메시지를 병렬로 처리

  • 각 소비자 그룹은 특정 토픽의 메시지를 구독(subscribe)합니다.
  • 그룹 내의 Consumer들은 토픽의 파티션을 나누어 처리합니다.
    • 예: 토픽에 6개의 파티션이 있고, 소비자 그룹 내에 Consumer가 3명 있으면 각 Consumer는 2개의 파티션을 담당합니다.

(3) 파티션 할당 전략

  • Kafka는 소비자 그룹 내에서 파티션을 동적으로 할당합니다.
    • Round-Robin 또는 Range와 같은 기본 전략이 사용되며, 커스텀 전략도 구현 가능합니다.
  • 새로운 Consumer가 그룹에 추가되거나 제거되면 리밸런싱(Rebalancing)이 발생하여 파티션이 다시 분배됩니다.
    • consumer rebalancing: 코디네이터(소비자들과 통신하는 브로커 노드; heatbeat, offset 정보 관리) 필요

(4) 소비자 오프셋 관리

  • 소비자 그룹은 오프셋(Offset)을 관리하여 메시지를 어디까지 처리했는지 추적합니다.
    • Kafka는 이 오프셋 정보를 브로커 내부의 __consumer_offsets라는 내부 토픽에 저장합니다.
    • 이를 통해 Consumer는 중단 후에도 이어서 처리할 수 있습니다.

 

주키퍼

  • 브로커의 상태 저장소
    • 각 소비자 그룹/소비자의 오프셋
    • 읽기와 쓰기가 잦고 데이터 일관성이 중요
  • 토픽 설정, 파티션 수, 메시지 보관 기간 등의 메타 데이터 저장소
    • 자주 변경되지 않고 일관성 중요
  • 분산 시스템의 key value 저장소

 

선정된 리더가(소비자 그룹이건, 파티션의 리더이건) replica 계획, 파티션 배치 계획 등을 정해서 코디네이터로 보내주면 코디네이터가 그걸 전파하는 방식

 

사본 동기화

ISR(in sync replicas) 리더와 동기화된 사본. 리더는 항상 ISR

몇 개가 동기화될 때 까지 기다릴지 설정 가능; 성능과 영속성을 고려해야 함

  • ACK = all : 전체가 다 동기화 될 때까지 기다림, 영속성 중요할 경우
  • ACK = 1 : 리더가 저장하면 다음으로. 응답 지연은 개선되나 리더가 장애 생기면 복제가 안될 수 있어 소실됨
  • ACK = 0 : 생산자는 메시지만 던지고 다음으로. 재시도도 하지 않음. 메세지 손실을 감수할 때

메세지 전달 방식

  • 최대 한 번
    • 메시지가 소실되어도 재전달하지 않음(ack = 0)
    • consumer가 메시지를 읽고 처리하기 전에 오프셋부터 갱신(갱신하고 죽으면 메시지는 다시 소비될 수 없음)
  • 최소 한 번
    • acl = 0; ack = all 메시지가 브로커에게 전달되었음을 반드시 확인
    • 소비자가 데이터를 성공적으로 처리한 뒤에 오프셋 갱신(갱신하지 못하고 죽으면 중복처리)
    • 멱등성 중요(고유 키 중복처리 ㄴㄴ)
  • 정확히 한 번
    • 복잡

 

메세지 필터링

주문 토픽에 발행하는데 지불 시스템이 가져간다면? 일부만 필요

지불 토픽을 별도로 만들어서 주문 데이터르 중복 발행?

생산자와 소비자 결합이 높아져서 ㄴㄴ

-> 필터링으로 해결

다 읽고 필요 없는 걸 버리자니 성능이 저하

브로커카(메시지 큐가) 많은 일을 하면 안 된다(데이터 추출 등)

메시지의 메타 데이터에 태그를 두어 태그를 필터링하도록 하고 태그를 구독..

 

2024.09.25 - [서버 세팅 & tool/rabbitmq] - [rabbitMq] 하나의 메세지를 여러 소비자에게 동등하게 보낼 때.. 그럼 카프카는?

728x90
반응형
반응형

환경: springframework 4.2, java8

 

자바와 레디스를 연결하기 위한 레디스 클라이언트는 크게 3개가 있다.

1. Jedis

특징

  • 초기 Redis Java 클라이언트 중 하나로, 간단하고 직관적인 API 제공.
  • Redis 명령어 대부분을 지원하며, 설정이 간단함.
  • 싱글스레드 기반으로 설계됨.
  • Jedis는 Redis 클라이언트를 위한 핵심적인 기능만 포함하며, 추가적인 추상화나 고급 기능(분산 락, 세마포어 등)은 제공하지 않음
  • 다른 클라이언트(예: Redisson)에 비해 의존성이 적고, 실행 크기가 작음, 가벼움

장점

  • 사용법이 단순하고 직관적이어서 빠르게 배울 수 있음.
  • Redis와의 네이티브 한 연동 및 모든 명령어를 제공.
  • 간단한 애플리케이션에서 적합.

단점

  • Thread-Safe 하지 않음: 멀티스레드 환경에서 사용하려면 JedisPool을 사용해야 함.
  • 싱글스레드 기반이라 멀티스레드 환경에서는 성능이 저하될 수 있음.

추천 사용 시나리오

  • 단일 스레드 기반 애플리케이션.
  • Redis를 단순 캐싱 또는 데이터 저장소로 사용하는 경우.

2. Lettuce

특징

  • 비동기 및 동기 API를 모두 제공하며, Reactive Streams(Flux/Mono)도 지원.
  • 기본적으로 비동기적으로 실행되고, 결과는 CompletionStage 또는 Future를 통해 반환됨
  • Netty를 기반으로 한 non blocking I/O 모델 사용.
  • Thread-Safe: 단일 커넥션을 여러 스레드에서 공유 가능.

장점

  • 멀티스레드 환경에서 효율적이며 Thread-Safe.
  • 비동기 작업에 유리하며 고성능 제공.
  • Reactive 프로그래밍 환경과의 호환성이 뛰어남.
  • 클러스터와 Sentinel 환경 지원.

단점

  • API가 Jedis보다 다소 복잡할 수 있음.
  • 초심자에게는 학습 곡선이 약간 높음.

추천 사용 시나리오

  • 비동기 작업이 많은 고성능 애플리케이션.
  • 멀티스레드 환경.
  • 클러스터 또는 Sentinel 기반 Redis 설정.
  • Reactive 프로그래밍(Spring WebFlux 등)을 사용하는 경우.

3. Redisson

특징

  • Redis를 기반으로 한 고급 분산 기능 제공(분산 락, 분산 캐시 등).
  • Redis를 Java의 분산 데이터 구조와 유사하게 다룰 수 있는 API 제공.
  • Thread-Safe 하며, 다양한 고급 기능이 포함됨.

장점

  • 분산 락, 분산 세마포어, RMap, RList 등 고급 데이터 구조 지원.
  • 클러스터, Sentinel, 레플리카 환경에서 유연하게 동작.
  • Redis 클라이언트를 단순한 캐싱 도구 이상으로 활용 가능.

단점

  • 다른 클라이언트보다 무겁고 약간의 추가 오버헤드 발생.
  • Jedis나 Lettuce에 비해 더 많은 메모리 사용 가능.

추천 사용 시나리오

  • 분산 락, 세마포어, 큐와 같은 고급 분산 기능이 필요한 환경.
  • Redis를 데이터베이스 이상의 목적으로 사용하는 경우.
  • 복잡한 클러스터 환경.

 

jedis 사용

<!-- Spring Data Redis -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.8.4.RELEASE</version>
</dependency>

<!-- Jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
 public <T> T lockProcess(String redisKey, long lockSecond, Supplier<T> logic) {
    log.debug("redisKey: {}, ttl: {}", redisKey, lockSecond);
    try {
      acquireLock(redisKey, lockSecond);
      return logic.get();
    } finally {
      releaseLock(redisKey);
    }
  }

  private void acquireLock(String redisKey, long lockSecond) {
    final RedisCallback<Boolean> redisCallback = connection -> {
      byte[] key = redisTemplate.getStringSerializer().serialize(redisKey);
      byte[] value = redisTemplate.getStringSerializer().serialize("locked");
      return connection.setNX(key, value) && connection.expire(key, lockSecond);
    };

    boolean success = redisTemplate.execute(redisCallback);
    if (!success) {
      throw new ApiLockException("이미 처리 중 입니다. key: " + redisKey);
    }
  }

  private void releaseLock(String redisKey) {
    redisTemplate.delete(redisKey);
  }

위처럼 분산락을 jedis를 사용하여 개발하면 락을 얻고 ttl을 주고 해제하는 과정을 수동으로 개발해야 한다.

redisson으로 수정

redisson을 사용하면 아래와 같이 라이브러리에서 제공하는 기능을 사용하여 간소화하게 짤 수 있다.

public <T> T lockProcess(String redisKey, long lockSecond, Supplier<T> logic) {
    RLock lock = redissonClient.getLock(redisKey); // Redis 분산 락 생성
    log.debug("Acquiring lock for redisKey: {}, ttl: {} seconds", redisKey, lockSecond);

    try {
        // 락을 시도하고 성공할 경우 논리 실행
        if (lock.tryLock(0, lockSecond, TimeUnit.SECONDS)) { 
            return logic.get();
        } else {
            throw new ApiLockException("이미 처리 중 입니다. key: " + redisKey);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new ApiLockException("락을 획득하는 동안 인터럽트가 발생했습니다. key: " + redisKey, e);
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock(); // 락 해제
            log.debug("Lock released for redisKey: {}", redisKey);
        }
    }
}

lettuce...?

호옥시 lettuce로 바꾸면 어떻게 될까 궁금해서 chat gpt에게 물어봤다.

public <T> T lockProcess(String redisKey, long lockSecond, Supplier<T> logic) {
    try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
        RedisCommands<String, String> commands = connection.sync();
        log.debug("Acquiring lock for redisKey: {}, ttl: {} seconds", redisKey, lockSecond);

        if (acquireLock(commands, redisKey, lockSecond)) {
            try {
                return logic.get();
            } finally {
                releaseLock(commands, redisKey);
            }
        } else {
            throw new ApiLockException("이미 처리 중 입니다. key: " + redisKey);
        }
    }
}

private boolean acquireLock(RedisCommands<String, String> commands, String redisKey, long lockSecond) {
    String result = commands.set(redisKey, "locked", io.lettuce.core.SetArgs.Builder.nx().ex(lockSecond));
    return "OK".equals(result); // Redis SET NX PX 결과 확인
}

private void releaseLock(RedisCommands<String, String> commands, String redisKey) {
    commands.del(redisKey); // 락 해제
    log.debug("Lock released for redisKey: {}", redisKey);
}

lettuce는 동기 동작을 위해 .sync()를 꼭 넣어야 하고(비동기는 .async()) 나머지는 jedis와 비슷하게 직접(setnx) 락을 설정하고 ttl도 설정해야 한다.

그리고 lettuce의 StatefulRedisConnection를 사용하는 경우 close 해줘야 한다. AutoCloseable를 implement 했기 때문에 try-with-resources 사용 가능하다.

 

  • 연결 풀을 사용할 경우에는, 연결이 자동으로 풀로 반환되며 그 자체로 명시적인 종료가 필요하지 않지만, StatefulRedisConnection은 연결 풀을 사용하지 않고, 각 연결을 명시적으로 관리하는 방식.
  • 연결 풀을 사용하면 연결을 가져오고 반환하는 방식으로 관리되기 때문에, 풀에서 자동으로 연결을 회수하고 재사용할 수. 하지만 StatefulRedisConnection은 풀을 사용하지 않기 때문에 사용자가 직접 연결을 닫고 관리해야 합니다.

 

shutdown을 수동으로 해야하나..?

참고로 레디스 연결 객체인 RedisClient와 RedisConnectionFactory는 프로그램 종료 시 shutdown이 되어야 하는데, 빈으로 등록되어 있는 경우 Spring Boot에서는 자동으로 연결 종료가 처리된다. 이렇게 되면 명시적인 shutdown() 호출은 필요하지 않는다. Spring Boot는 빈 관리 및 라이프사이클을 자동으로 처리하기 때문에, 애플리케이션 종료 시 리소스를 자동으로 정리한다.

하지만 Spring Framework에서는 명시적인 shutdown() 호출이 필요할 수 있다. 왜냐하면 Spring Framework에서는 자동으로 리소스를 관리하는 기능이 내장되어 있지 않기 때문...?

  • hmmmmmm?? 진짠지 모르겠음.. 레거시에서도 수동으로 shutdown 한건 본적이 없음

 


분산락을 위해 보통 @AOP로 만들 수도 있지만 위와 같은 고차함수 방식을 더 선호한다.

아래와 같은 단점이 있기에..

  • public에만 사용가능
  • 값 넘길 때 함수 argument 첫번째 값 주의 필요
  • @Around( "@within(com.annotation.. <-- 와 같이 문자열로 관리하는 것에 대한 부담
  • 락의 범위가 넓어질 수 있음

 

고차함수 분산락을 사용할 때는 아래와 같이 하면 된다.

final String redisKey = redisKey("invenChangeExpire", String.valueOf(sno));

return this.apiLockService.lockProcess(redisKey, () -> {
	... 로직
    });
728x90
반응형
반응형

환경: java17, springboot3.3.4, springbatch5

2024.08.16 - [개발/spring-batch] - [spring-batch] springboot3 mybatis 설정 그리고 mapper

 

[spring-batch] springboot3 mybatis 설정 그리고 mapper

환경: springboot3, spring batch5, mybatis그동안 jpa만 주구장창 사용했어서 올만에 Mybatis 설정이다! 1. 디비 정보 등록(application.yml)2. 빈 등록@Configuration@RequiredArgsConstructor@MapperScan( value = {"com.batch.ranking.ma

bangpurin.tistory.com

 

 

이슈: 매퍼 못 찾음

@Bean(TOP_SLIDE_READER)
@StepScope
public MyBatisCursorItemReader<NoticeVo> topSlideNoticeReader(@Qualifier(GameReplicaDataSourceConfig.SESSION_FACTORY) SqlSessionFactory logDb) {
return new MyBatisCursorItemReaderBuilder<NoticeVo>().sqlSessionFactory(logDb)
    .queryId(Constant.SUDDA_NOTICE_MAPPER + "selectTopSlideNotices")
    .build();
}

위와 같이 springbatch + mybatis 조합으로 리더를 선언했는데 아래와 같이 mapper를 못 찾는 에러 발생

Caused by: java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for com.xx.NoticeMapper.selectTopSlideNotices
	at org.apache.ibatis.session.Configuration$StrictMap.get(Configuration.java:1097)
	at org.apache.ibatis.session.Configuration.getMappedStatement(Configuration.java:875)
	at org.apache.ibatis.session.Configuration.getMappedStatement(Configuration.java:868)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectCursor(DefaultSqlSession.java:123)

 

datasource config 쪽에 아래와 같이 mapper scan을 달았고, 링크 어노테이션을 선언했음

@Configuration
@MapperScan(value = {"com.xx.gamereplica"},
            annotationClass = GameReplicaDataSource.class,
            ...
@SuddaGameReplicaDataSource
public interface SuddaNoticeMapper {

매퍼 인터페이스에 어노테이션 꼭 선언해야 함

 

구조

그럼에도 같은 에러가 계속 반복되었는데..

참고로 프로젝트 구조는 아래와 같다.

매퍼 인터페이스와 매퍼 xml 가 같은 main package 안에 있다. xml을 찾는 게 번거로워서 같이 두자는 의도였다.

 

해결: gradle 옵션 추가

그렇기 때문에 추가적인 작업이 필요했다.

build.gradle에 아래 추가

tasks.processResources {
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

sourceSets {
    main {
        resources {
            srcDirs "src/main/java", "src/main/resources"
            include "**/*.xml"
            include "**/*.yml"
        }
    }
}

 

  • 기본 관행: 일반적으로 MyBatis 매퍼 XML 파일은 src/main/resources 디렉터리에 위치한다. 이는 Gradle 빌드 시스템에서 리소스로 자동으로 인식하고, 빌드 결과물(build/resources/main)에 포함한다.
  • 위치 변경: src/main/java에 XML 파일을 넣는 경우, 기본적으로 src/main/java는 소스 코드(Java 파일)의 경로로 간주되며, 리소스 파일로 처리되지 않는다. 따라서 빌드 과정에서 이 파일이 리소스로 인식되지 않아, MyBatis가 매퍼 파일을 찾지 못하게 된다.
  • 따라서 sourceSets 설정을 사용하여 Gradle에 src/main/java에 있는 XML 파일도 리소스로 취급하라고 명시적으로 설정한다.
  • 작동 원리: srcDirs "src/main/java", "src/main/resources" 설정을 통해 src/main/java 내의 XML 파일들을 리소스 디렉터리처럼 처리하도록 설정. 이 설정이 없으면 src/main/java는 Java 소스 코드만 포함하는 디렉터리로 인식되며, XML 파일은 빌드 결과물에 포함되지 않는다.
  • src/main/java와 src/main/resources 모두에서 XML 파일을 포함하게 되면 중복이 발생할 수 있으므로, tasks.processResources 옵션에 duplicatesStrategy를 설정하여 중복을 방지합니다.

 

 

record + xml mapper 작성 시 주의사항

record로 받을 시 resultMap이 필수며 javaType도 다 써줘야 한다(일반적으로는 optional이지만 writable property에 대해서는 javaType이 필수고 record는 불변 객체여서 writable 한 항목이 없다). javaType을 Integer로 명시했다면 레코드에서도 Integer로 받아야 한다(int 안됨)

<resultMap id="noticeVo"
    type="com.xx.notice.NoticeVo">
    <constructor>
        <idArg column="id" javaType="Integer" name="id"/>
        <arg column="type_code" javaType="Integer" name="typeCode"/>
        <arg column="startDate" javaType="LocalDateTime" name="startDate"/>
        <arg column="endDate" javaType="LocalDateTime" name="endDate"/>
        <arg column="time_interval" javaType="Integer" name="timeInterval"/>
        <arg column="title" javaType="String" name="title"/>
        <arg column="content" javaType="String" name="content"/>
        <arg column="extra_data" javaType="String" name="extraData"/>
        <arg column="regDate" javaType="LocalDateTime" name="regDate"/>
        <arg column="registrant" javaType="String" name="registrant"/>
    </constructor>
</resultMap>
public record NoticeVo(Integer id, Integer typeCode, LocalDateTime startDate, LocalDateTime endDate, Integer timeInterval, String title,
                       String content, String extraData, LocalDateTime regDate, String registrant) {

}

항목 순서, 타입 등 모든 게 중요하니 꼼꼼하게 봐야 한다. 아니면 아래 에러 발생...

Caused by: org.apache.ibatis.builder.BuilderException: Error in result map 'com.xx.gamereplica.SuddaNoticeMapper.noticeVo'. 
Failed to find a constructor in 'com.xx.notice.NoticeVo' with arg names [id, typeCode, startDate, endDate, timeInterval, title, content, extraData, regDate, registrant]. 
Note that 'javaType' is required when there is no writable property with the same name ('name' is optional, BTW). There might be more info in debug log.

 

참고로 int로 사용하고 싶으면 아래와 같이 _int로 사용하면 된다.

https://mybatis.org/mybatis-3/sqlmap-xml.html#result-maps

<resultMap id="noticeVo"
  type="com.xx.notice.NoticeVo">
  <constructor>
   <idArg column="id" javaType="_int" name="id"/>
   <arg column="type_code" javaType="_int" name="typeCode"/>
   <arg column="startDate" javaType="LocalDateTime" name="startDate"/>
   <arg column="endDate" javaType="LocalDateTime" name="endDate"/>
   <arg column="time_interval" javaType="_int" name="timeInterval"/>
   <arg column="title" javaType="String" name="title"/>
   <arg column="content" javaType="String" name="content"/>
   <arg column="extra_data" javaType="String" name="extraData"/>
   <arg column="regDate" javaType="LocalDateTime" name="regDate"/>
   <arg column="registrant" javaType="String" name="registrant"/>
  </constructor>
</resultMap>
728x90
반응형
반응형

포인트

  • 1,000,000 TPS
  • 높은 안정성
  • 트랜젝션
  • A -> B로 이체한다고 가정

 

인메모리 샤딩

계정-잔액(map) 저장

레디스 노드 한대로 100만 TPS는 무리

클러스터를 구성하고 계정을 모든 노드에 균등하게 분산시켜야(파티셔닝, 샤딩)

  • 분배하기 위해 키의 해시 값 mod 개수를 계산하여 분산

모든 레디스 노드의 파티션 수 및 주소는 주키퍼를 사용(높은 가용성 보장)

  • 이체 요청 시 각 클라이언트의 샤딩 정보를 물어봐서 클라이언트 정보를 담은 레디스 노드를 파악

데이터 내구성이 없다는 단점 존재..

분산 트랜젝션

레디스 사용 시 원자적 트랜젝션은 어떻게? RDB로 교체?

두 번째 이체(B로 입금) 시 서비스가 죽는다면?

1. 저수준: 데이터베이스 자체에 의존; 2PC

  • 실행 주체는 디비며 어플리케이션이 중간 결과를 알 수 없음
  • 모든 데이터베이스가 X/Open XA만족해야(JtaTransaction; mysql 지원)
  • 단점:
    • 두 단계가 한 트랜젝션
    • 락이 오랫동안 유지, 성능이 안 좋음
    • 조정자가 단일 장애 지점(SPOF)

 

2. try-confirm/cancel

  • -1 -> +1 해야
  • 실행 주체는 어플리케이션이며 독립적 로컬 트랜젝션의 중간 결과를 알 수 있음
  • 보상 트랜젝션이 별도로 구현되어 있고
  • 1단계 2단계가 각각 다른 트랜젝션으로 구성됨; 여러 개의 독립적인 로컬 트랜젝션으로 구성
  • 특정 데이터 베이스에 구애받지 않고 어플리케이션 단계에서 관리하고 처리
  • 실행 도중 coordinator 다운되는 것을 대비하여 각 단계 상태정보를 테이블에 저장(분산 트랜젝션 ID의 각 단계별 상태)
  • 취소를 대비해 실행 순서가 중요한데(+1을 하고 취소 시 -1을 해야 하는데 네트워크 이슈로 -1이 먼저 요청되는 경우)
    • 취소가 먼저 도착하면 디비에 마킹하고 다음에 실행 명령이 오면 이전에 취소 명령이 있는지 확인(그림 12.12)
  • 병렬가능

3. saga

  1. 모든 연산은 순서대로 정렬된다. 각 연산은 자기 디비에 독립적인 트랜젝션으로 실행된다.
  2. 연산은 첫 번째부터 마지막까지 순서대로 실행된다. 한 연산이 완료되면 다음 연산이 실행된다.
  3. 연산이 실패하면 전체 프로세스는 실패한 연산부터 맨 처음 연산까지 역순으로 보상 트랜젝션을 통해 롤백된다. 따라서 n개 연산을 실행하는 분산 트랜젝션은 보상트랜잭션 n개까지 총 2n개의 연산을 준비해야 한다.
  • choreography: 이벤트를 구독하여 작업 수행; 비동기
  • orchestration: 하나의 조정자가 모든 서비스가 올바른 순서로 작업하도록 조율

분산 트랜젝션의 경우 문제의 원인을 역추적하고 모든 계정에서 발생하는 연산을 감사(audit)할 수 없음

 

이벤트 소싱

  • command: 의도가 명확한 요청; 순서가 중요 FIFO 큐(카프카)에 들어감
    • A - $1 - C ->
  • event: 검증된 사실로 실행이 끝난 상태; 과거에 실제로 있었던 일
    • A -$1; C+$1 두 이벤트로 분리
  • state: 이벤트가 적용될 때 변경되는 내용(여기서는 잔액)
  • state machine: 이벤트 소싱 프로세스를 구동; 명령의 유효성을 검사하고 이벤트를 생성, 이벤트를 적용하여 상태를 갱신

시간 단축? (카프카 대신에)

1. 로컬 디스크

이벤트 소싱에 사용한 카프카(원격 저장소) 대신 로컬 디스크에 파일로 저장하여 네트워크 전송시간 줄일 수

순차적 읽기 쓰기가 이미 최적화되어 있어 빠름

잔액 정보를 RDB 말고 로컬 디스크에 저장: SQLite, RocksDB

  • RocksDB: LSM(log structured merge tree) 사용하여 쓰기 작업 최적화

2. mmap

최근 명령과 이벤트를 메모리에 캐시 할 수도.. 메모리에 캐시 하면 로컬 디스크에서 다시 로드하지 않아도

mmap는 디스크 파일을 메모리 배열에 대응하여 메모리처럼 접근할 수 있게 하여 실행 속도를 높일 수 있음

 

재현성 reproducibility

이벤트를 처음부터 재생하면 과거 잔액 상태 재구성 가능

이벤트 리스트는 불변이고 상태 기계 로직은 결정론적이므로 이벤트 이력을 재생하여 만든 상태는 언제나 동일

계정 잔액 정확성 재검하거나, 코드 수정 후에도 시스템 로직이 올바른지 replay로 확인 가능

 

명령 질의 책임 분리 CQRS

계정 잔액을 공개하는 대신 모든 이벤트를 외부에 보내서 외부 주체가 직접 상태를 재구축 가능(읽기에 유리하게)

읽기 전용 상태 기계는 여러 개 있을 수 있는데, 이벤트 큐에서 다양한 상태 표현을 도출할 수 있다(단순 뷰, 정산 등).

eventual consistency

 

스냅샷: 과거 특정 시점의 상태

모든 것이 파일 기반일 때 재현 프로세스의 속도를 높이는 방법?

이벤트 소싱은 항상 처음부터 다시 읽어서 상태를 파악하는데, 그 대신 주기적으로 상태 파일을 저장하여 시간을 절약 가능

그 시점부터 이벤트 처리 시작, 보통 0시

보통 하둡에 저장

모든 것을 로컬 디스크로(데이터를 한 곳에 두기엔)..  SPOF 위험..

 

높은 신뢰성을 보장할 유일한 데이터는 이벤트

높은 안정성을 제공하려면 이벤트 목록을 여러 노드에 복제해야 하는데 데이터 손실 없고 순서를 유지해야 한다.

합의 기반 복제(consensus based replication)

래프트 알고리즘 사용

  • 래프트 알고리즘(Raft Algorithm)은 분산 시스템에서 합의를 이루기 위한 분산 합의 알고리즘으로, 특히 리더 선출과 로그 복제를 단순하고 이해하기 쉽게 설계한 것이 특징
  • 일관성: Raft는 각 노드가 로그를 일관되게 유지하도록 보장하며, 리더 노드가 로그를 추가하거나 업데이트할 때, 이를 팔로워 노드들에게 전달한다. 모든 노드가 동일한 상태를 유지하도록 보장함으로써 데이터의 일관성을 유지
  • 고가용성: Raft는 단일 노드 실패 또는 리더 실패와 같은 장애를 처리할 수 있도록 설계. 클러스터의 과반수 이상이 살아 있으면, 시스템은 안정적이고 일관성 있게 동작함

 

  • Follower: 리더의 지시를 따르고, 리더의 Heartbeat를 수신하여 상태를 유지
  • Candidate: 리더가 되기 위해 투표를 요청하고, 과반수의 투표를 얻으면 리더가 됨
  • Leader: 클러스터를 관리하고, 클라이언트 요청을 처리하며, 로그 항목을 복제

 

  1. 리더 선출:
    • Follower는 Leader로부터 Heartbeat 메시지를 정기적으로 수신
    • 일정 시간 동안 Heartbeat가 없으면, Follower는 Candidate가 되어 새 리더 선출을 시도
    • 다수의 노드로부터 투표를 받아야 리더로 선출
  2. 로그 복제:
    • Leader는 클라이언트의 요청을 로그에 추가하고 이를 Follower에 복제
    • 과반수의 Follower가 로그를 수락하고 이를 확인하면 Leader는 해당 로그 항목을 커밋
  3. 일관성 유지:
    • Leader는 모든 노드가 동일한 로그 상태를 유지하도록 보장
    • 새로운 리더가 선출되면, 로그 일관성을 확보하기 위해 추가 작업을 수행

리더 장애 처리:

  1. 리더 장애 발생:
    • 리더가 장애를 겪어 더 이상 Heartbeat를 보내지 못하면, Follower는 일정 시간 동안 Heartbeat를 받지 못한 상태가 됨
    • Follower는 Election Timeout이 지나면 Candidate 상태로 전환
  2. 리더 선출 과정:
    • Candidate가 되면, 다른 노드에 투표를 요청하고 선거를 시작
    • 과반수의 투표를 얻으면 Candidate는 새로운 Leader가 됨
    • 새로운 Leader는 기존 로그의 일관성을 확인하고 필요한 경우 Follower에게 누락된 로그를 전송하여 동기화
  3. 장애 복구:
    • 장애가 발생한 리더가 복구되면, Follower 상태로 전환
    • 리더 선출 과정에서 새로운 리더가 선출되었기 때문에 복구된 노드는 더 이상 리더가 아님

팔로워 장애 처리:

  1. 팔로워 장애 발생:
    • Follower가 장애를 겪으면, 로그 복제 및 Heartbeat 수신이 중단
    • Leader는 계속해서 다른 Follower들과 로그를 복제하고 클러스터를 관리
  2. 팔로워 복구:
    • 장애에서 복구된 Follower는 Leader로부터 현재 로그 상태를 동기화
    • Leader는 AppendEntries 메시지를 통해 복구된 Follower에 누락된 로그를 보냄
    • 복구된 Follower는 로그를 복제하고, 현재 상태를 동기화한 후 정상 운영을 재개

장애 상황에 따른 동작:

  • 다수 노드 장애: 클러스터는 과반수의 노드가 살아있으면 정상 동작을 유지합니다. 과반수 이상이 실패하면 클러스터는 동작을 멈추고 장애 복구가 실시
  • 네트워크 파티션: 네트워크가 분할되면, 두 개 이상의 그룹으로 나뉨. 각 그룹은 독립적으로 리더를 선출할 수 있지만, 과반수를 차지하는 그룹만이 유효한 리더를 가질 수 있음. 네트워크가 복구되면, 하나의 리더만 유지되도록 통합

장애 허용을 위한 메커니즘:

  • Election Timeout: 리더의 장애를 감지하기 위한 타이머. 일정 시간 동안 Heartbeat가 없으면 선거가 시작
  • Majority Agreement: 리더가 되려면 과반수의 노드로부터 투표를 받아야. 이는 장애가 발생하더라도 시스템이 계속 운영될 수 있도록 보장
  • Log Consistency: 리더는 모든 팔로워가 일관된 로그 상태를 유지하도록 보장하며, 새로운 리더가 선출될 때 로그 일관성을 유지

 

CQRS에서 읽을 때(폴 vs 푸시)

풀 방식: 클라가 서버에게 request를 보낼 때 읽기 디비에서 가져옴

역방향 프록시: 캐시같이 디비에서 직접 가져가지 않고 만들어진 데이터를 가져가게 둔 중간 저장소

  • 클라이언트와 서버 간의 중간 계층으로, 클라이언트 요청을 백엔드로 전달하고, 백엔드의 응답을 클라이언트로 반환;
  • 이벤트 수신 후 역방향 프락시에 푸시하도록

프로세스 흐름:

  1. 이벤트 수신:
    • 읽기 전용 상태 기계는 외부 시스템으로부터 이벤트를 수신. 이 이벤트는 상태 업데이트를 요구하는 데이터일 수 있음.
  2. 상태 업데이트 및 푸시:
    • 상태 기계는 이벤트를 처리하고 내부 상태를 업데이트
    • 업데이트된 상태는 즉시 역방향 프록시로 푸시. 푸시된 데이터는 클라이언트가 요청하기 전에 프록시에 전달되어 준비.
  3. 클라이언트 요청 처리:
    • 클라이언트가 상태 데이터를 요청하면, 역방향 프록시는 백엔드 서버에 요청을 전달하는 대신, 이미 준비된 최신 상태를 클라이언트에 반환

비동기 이벤트 소싱 프레임워크를 동기식 프레임워크로 제공하기 위해 역방향 프록시(Reverse Proxy)를 추가하는 것은 클라이언트와 서버 간의 통신 방식의 차이를 조율하고, 비동기 시스템의 응답성을 개선하기 위한 전략

1. 비동기 이벤트 소싱 프레임워크의 특성:

  • 이벤트 소싱(Event Sourcing): 시스템 상태를 이벤트의 시퀀스로 기록하고, 현재 상태를 이벤트를 재생하여 복구하는 방식
  • 비동기 처리: 이벤트는 비동기적으로 생성되고 처리되며, 상태는 eventual consistency(최종 일관성)를 가짐. 이는 즉각적인 응답이 보장되지 않고, 처리 완료까지 시간이 소요될 수 있음

2. 동기식 프레임워크 제공의 필요성:

  • 즉각적인 응답 필요: 동기식 시스템은 클라이언트가 요청을 보내면, 즉시 결과를 반환받기를 기대. 비동기 시스템의 특성상 바로 응답을 제공하기 어려운 상황에서, 동기적 동작을 요구하는 클라이언트와의 간극을 줄일 필요가 있음
  • 클라이언트 요구: 많은 클라이언트는 동기적으로 동작하는 전통적인 API 사용에 익숙

3. 역방향 프록시의 역할: 역방향 프록시를 추가함으로써 비동기 시스템을 동기적으로 제공 가능

3.1. 응답 캐싱 및 버퍼링:

  • 이벤트 결과 캐싱: 프록시는 비동기 이벤트가 처리된 결과를 캐싱하여 클라이언트의 요청에 대해 즉시 응답. 이벤트가 아직 처리되지 않았으면, 프록시가 응답을 보류하거나 기본 응답을 반환

3.2. 동기화된 응답 시뮬레이션:

  • 상태 확인 및 응답 대기: 클라이언트의 요청이 들어오면 프록시는 이벤트 소싱 시스템에 상태를 확인하고, 동기적 방식으로 응답을 보류하다가 결과가 준비되면 반환. 이는 동기 호출로 클라이언트에 투명하게 처리되며, 실제로는 백엔드에서 비동기적으로 처리.

3.3. 비동기 이벤트의 프리로드 및 상태 추적:

  • 사전 이벤트 처리: 프록시는 예상되는 이벤트나 데이터를 미리 가져와(이벤트를 받아서) 클라이언트 요청이 들어왔을 때 빠르게 제공. 이를 통해 동기적 행동처럼 느껴지게 함.

4. 의미와 장점:

  • 사용자 경험 개선: 클라이언트는 비동기적 시스템의 지연 시간이나 일관성 문제를 느끼지 않고, 동기적으로 즉각적인 응답을 받음
  • 시스템 간 통합: 비동기 시스템을 사용하면서도 동기적 API가 필요한 클라이언트와 통합할 수 있어, 다양한 환경에서의 호환성이 증가
  • 복잡성 분리: 비동기 처리의 복잡성을 역방향 프록시에서 관리하고, 클라이언트와의 인터페이스를 단순하게 유지가능

5. 고려 사항:

  • 응답 시간 증가: 프록시에서 동기적 응답을 시뮬레이션하는 과정에서 처리 지연이 발생할 수
  • 상태 일관성 관리: 프록시가 비동기 이벤트 결과를 반환할 때, 상태 일관성을 관리하는 로직이 필요
  • 추가 인프라 비용: 역방향 프록시를 운영하는 데 추가적인 인프라와 관리 비용이 발생할 수

6. 근데 이 역할을 할 때 꼭 프록시를 써야하는가? 그냥 다른 중간 서버를 두면 되는거 아냐?

역방향 프록시를 사용함으로써 보안을 강화하거나 로드 밸런싱을 강화할 수 있음. 중간 서버는 유연성은 높지만 속도나 유지보수 등 필요..

 

분산 이벤트 소싱

TC/C 또는 사가 조정자가 단계별 상태 테이블에 각각의 작업 상태를 다 저장하여 트랜젝션 상태를 추적하는 게 포인트

 

  • 사가 또는 tc/c 적용
  • 유저가 서로 다른 위치의 디비에 있다고 가정
  • raft 알고리즘 적용
  • 역방향 프록시 적용

 

728x90
반응형
반응형

포인트

  • QPS: 43,000
  • max QPS = 215,000

 

지연시간

평균 지연 시간은 낮아야 하고 전반적인 지연 시간 분포는 안정적이어야 함.

  • 지연시간 = 중요 경로상의 컴포넌트 실행 시간의 합

지연시간을 줄이기 위해서는

  • 네트워크 및 디스크 사용량 경감
    • 중요 경로에는 꼭 필요한 구성 요소만 둔다. 로깅도 뺀다.
    • 모든 것을 동일 서버에 배치하여 네트워크를 통하는 구간을 없앤다. 같은 서버 내 컴포넌트 간 통신은 이벤트 저장소인 mmap를 통한다.
  • 각 작업 실행 시간 경감
    • 꼭 필요한 로직만
  • 지연 시간 변동에 영향을 주는 요소 조사 필요
    • gc...

 

속도

주문 추가/취소/체결 속도: O(1)의 시간 복잡도를 만족하도록

  • 추가: 리스트의 맨 뒤에 추가
  • 체결: 리스트의 맨 앞에 삭제
  • 취소: 리스트에서 주문을 찾아 삭제 

주문 리스트는 DoubleLinkedList여야 하고 Map을 사용하여 주문을 빠르게 찾아야 한다.

 

영속성

시장 데이터는 실시간 분석을 위해 메모리 상주 칼럼형 디비(KDB)에 두고 시장이 마감된 후 데이터를 이력 유지 디비에 저장한다.

 

  • KDB+는 Kx Systems에서 개발한 고성능 시계열 데이터베이스로, 대규모 데이터 분석에 최적화되어 있다. 주로 금융 업계에서 빠른 데이터 처리와 분석을 위해 사용된다.
  • 초고속 데이터 처리 능력
  • 시계열 데이터를 효율적으로 저장 및 분석
    • 시간에 따라 변화하는 데이터를 저장하고 관리하는 데 최적화된 데이터베이스
    • 시계열 데이터는 각 데이터 포인트가 타임스탬프와 함께 기록되는 데이터
  • 금융 거래 데이터, 센서 데이터 등 대규모 데이터를 실시간으로 처리 가능

 

 

어플리케이션 루프

  • while문을 통해 실행할 작업을 계속 폴링하는 방식
  • 지연시간 단축을 위해 목적 달성에 가장 중요한 작업만 이 순환문 안에서 처리
  • 각 구성 요소의 실행 시간을 줄여 전체적인 실행 시간을 예측 가능하도록 보장
  • 효율성 극대화를 위해 어플리케이션 루프는 단일 스레드로 구현하며 CPU에 고정
    1. context switch가 없어지고
    2. 상태를 업데이트하는 스레드가 하나뿐이라 락을 사용할 필요 없고 잠금을 위한 경합도 없다.
    3. 단 코딩이 복잡해짐
      • 각 작업이 스레드를 너무 오래 점유하지 않도록 각 작업 시간을 신중하게 분석해야 함

 

mmap

mmap은 메모리 맵핑 (Memory Mapping)을 의미하며, 운영체제에서 제공하는 기능으로, 파일을 프로세스의 메모리에 맵핑하여 파일의 내용을 메모리 주소 공간에서 직접 읽고 쓸 수 있도록 한다. 이를 통해 파일 입출력(I/O) 속도를 향상시키고, 프로세스 간의 메모리 공유를 효율적으로 처리할 수 있다.

/dev/shm 메모리 기반 파일 시스템으로 여기에 위치한 파일에 mmap를 수행하면 공유 메모리에 접근해도 디스크io는 발생하지 않는다.

 

이벤트 소싱

현재 상태를 저장하는 대신 상태를 변경하는 모든 이벤트의 변경 불가능한(immutable) 로그를 저장

이벤트를 순서대로 재생하면 주문 상태를 복구 가능(이벤트 순서가 중요)

지연시간에 대한 엄격한 요구사항으로 카프카 사용 불가, mmap 이벤트 저장소를 메세지 버스로 사용(카프카 펍섭 구조와 비슷)

  • 주문이 들어오면 publish되고 주문 데이터가 필요한 각 컴포넌트가 subscribe 한다.

이벤트는 시퀀스, 이벤트 유형, SBE(simple binary encoding; 빠르고 간결한 인코딩을 위해) 인코딩이 적용된 메시지 본문으로 구성되어 있다.

게이트웨이는 이벤트를 링 버퍼에 기록하고 시퀀서가 링 버퍼에서 데이터를 가져온다(pull). 시퀀서가 이벤트 저장소(mmap)에 기록한다(pub).

  • 링 버퍼(Ring Buffer), 또는 원형 버퍼(Circular Buffer)는 고정된 크기의 버퍼로, 마지막 위치에 도달하면 다시 처음 위치로 돌아가서 덮어쓰는 방식으로 동작하는 자료구조. 링 버퍼는 주로 고정 크기의 메모리 할당을 유지하면서 데이터를 효율적으로 관리하고, FIFO(First In, First Out) 방식으로 데이터를 처리하는 데 사용됨. 데이터를 넣고 꺼내기만 하고 생성이나 삭제하는 연산은 필요 없다. 락도 사용하지 않는다.

 

고가용성

서비스 다운 시 즉시 복구

  1. 거래소 아키텍처의 단일 장애 지점을 식별해야 한다.
  2. 장애 감지 및 백업 인스턴스로 장애 조치 결정이 빨라야

서버 재시작 후 이벤트 저장소 데이터를 사용해 모든 상태를 복구한다.

주 서버의 문제를 자동 감지해야 하는데, 위에서 단일 서버로 설계했기 때문에 클러스터로 구성해야 하며 주서버의 이벤트 저장소는 모든 부 서버로 복제해야 한다.

이때 reliable UDP를 사용하면 모든 부 서버에 이벤트 메시지를 효과적으로 broadcast 할 수 있다.

  • 모든 수신자가 동시에 시장 데이터를 받도록 보장
  • 멀티캐스트: 하나의 출처에서 다양한 하위 네트워크상의 호스트로 보냄, 그룹에 가입한 수신자들만 데이터를 수신. 브로드캐스트와 유니캐스트의 중간

 

부 서버도 죽으면? fault tolerant

DRM마냥 여러 지역의 데이터 센터에 복제 필요..

  1. 주 서버가 다운되면 언제, 어떻게 부서버로 자동 전환하는 결정을 내리나
    • request가 이상하면? 소스 자체가 문제라면? => 운영 노하우를 쌓을 동안 수동으로 장애 복구
  2. 부 서버 가운데 새로운 리더는 어떻게 선출
    • 검증된 리더 선출 알고리즘(주키퍼, raft..)
  3. 복구 시간 목표(Recovery Time Objective)는 얼마
    1. 어플리케이션이 다운되어도 사업에 심각한 피해가 없는 최댓값
  4. 어떤 기능을 복구(Recovery Point Objective) 해야 하나
    1. 손실 허용 범위, 거의 0에 가깝게

 

보안

  • 공개 서비스와 데이터를 비공개 서비스에서 분리하여 디도스 공격이 가장 중요한 부분에 영향을 미치지 않도록. 동일한 데이터를 제공해야 하는 경우 읽기 전용 사본을 여러 개 만들어 문제를 격리
  • 자주 업데이트되지 않는 데이터는 캐싱
  • 디도스 공격에 대비해 아래와 같이 쿼리 파람에 제한을 둔다. 이렇게 바꾸면 캐싱도 유리하다.
    • before: /data?from=123&to=456
    • after: /data/recent
  • 블랙리스트/화이트리스트 작성
  • 처리율 제한 기능 활용
728x90
반응형
반응형

포인트

  • 하루 100만건 = 초당 10건의 트랜젝션(TPS)
  • 10TPS는 별 문제 없는 양, 트렌젝션의 정확한 처리가 중요
  • 각 거래별 멱등성을 위한 트렌젝션 아이디 필요(UUID)
  • 금액은 double이 아닌 string으로 직렬화/역직렬화에 사용되는 숫자 정밀도가 다를 수 있기 때문(반올림 이슈)

디비

  • 성능(nosql)보다는 안정성, ACID를 위한 관계형 데이터 베이스 선호

데이터 저장 시 고려

  • 결제 -> 지갑; 결제 -> 원장 테이블에 저장 시 상태값을 계속 변경하여 관리..
  • 지갑/원장 테이블의 상태가 모두 바뀌어야 결제 테이블 상태 업데이트..

시스템 구성 요소가 비동기적으로 통신하는 경우 정확성 보장?

  • 관련 상태값이 특정 시간동안 부적절한 상태로 남는지 확인하는 배치 돌려서 개발자에게 알람
  • 조정: 관련 서비스 간의 상태를 주기적으로 비교하여 일치하는지 확인(마지막 방어선)
    • 약간 파일 대사 같은 느낌.. 원장과 외부 업체와 데이터 비교

조정 시 불일치가 발생하면

  1. 어떤 유형의 문제인지 파악하여 해결 절차를 자동화
  2. 어떤 유형의 문제인지는 알지만 자동화 할 수 없으면(작업 비용이 너무 높으면) 수동으로
  3. 분류가 불가(판단이 안됨) 수동으로 처리하면서 계속 이슈 트래킹필요

동기 통신

  • 성능 저하
  • 장애 격리 곤란
  • 높은 결합도/낮은 확장성(트래픽 증가 대응 힘듦)

비동기 통신: queue

  • 단일 수신자: 큐 하나를 한 수신자가 처리. 병렬 처리를 위해 스레드 여러개가 동시에 처리하게 함. 큐에서 구독 후 바로 삭제됨
    • 보통 rabbitMQ사용
  • 다중 수신자: 큐 하나를 여러 수신자가 처리. 동일한 메세지를 각기 다른 호흡으로 읽어감. 읽어도 삭제되지 않음. 하나의 메세지를 여러 서비스에 연결할 때
    • 보통 카프카 사용
  • 실패 시 재시도 큐(일시적 오류) / dead letter queue로 이동(모든 재시도 실패)

정확히 한 번 전달? exactly once

  • 최소 한 번 실행 at least once: (지수적 백오프 사용하여) 재시도; 시스템 부하와 컴퓨팅 자원 낭비
  • 최대 한 번 실행 at most once: 멱등성 보장; 고유 키를 멱등 키로 잡아야
    • 광클 시 키 값을 확인하여 이미 처리되었는지 확인하고 되었으면 재처리 하지 않고 이전 상태와 동일한 결과 반환
    • 결제가 되었지만 타임아웃 나서 결과가 시스템에 전달되지 못함 ->  다시 결제 시도 시 이중 결제로 판단하여 종전 결과 반환

 

분산 환경에서 데이터 불일치가 발생할 수 있는데 일반적으로 멱등성조정 프로세스를 활용한다.

디비 불일치는 master-replica 동기화 

 

728x90
반응형

+ Recent posts