작년 가을에 가려고 했으나 못갔던 제주도 투어를 다음주에 가려고 한다. 

제주내에서 2박 3일, 항구까지 2일 해서 4박 5일 정도의 일정을 생각중이다. 

 

바이크를 어떻게 가져갈지 고민했는데 목포항에서 배타고 들어가기로 했다. 

  • 탁송을 보낼지
  • 배를 탈지
    • 완도에서 탈지
      • 2시간 30분으로 배 소요시간이 가장 짧다. 
      • 02:30 / 05:10, 15:00 / 17:40
    • 목포에서 탈지
      • 5시간 소요
      • 01:00 / 06:00, 08:45 / 13:15
    • 여수
      • 00:20 / 06:00

완도 배 시간은 좀 애매하다. 목포에서 1시 배를 타고 가는동안 잠을 자고 도착해서 바로 투어를 시작하는게 좋을것 같다. 

하지만 월요일에는 1시 출발하는 배가 없다. 여수를 살펴보니 00:20 출발하는 배가 있어서 결국 여수에서 출발하기로 함

인천에서 출발하는 배가 있으면 좋겠는데 무슨일인지 없다. 

 

집에서 목포항까지 6시간 13분 432KM 거리이다. 

중간에 쉬고 밥먹고 차를 23시쯤 선적한다고 해도 시간이 충분하다. 

432km 는 한번에 가는 최장거리가 될것 같다. 

 

여수 엑스포 여객선터미널까지 집에서 407KM

중간에 쉬고 밥먹고 차를 22시쯤 선적한다고 해도 시간이 충분하다. 체력이 따라줄지가 문제다.

407km 는 한번에 가는 최장거리가 될것 같다. 

 

제주도에서는

첫째날 제주 한바퀴를 돌아볼까 생각중 

둘째날 1100고지, 사려니숲길 등 내륙에서 갈만한 곳들을 투어하고 

셋째날 육지로 돌아가기

 

제주도 맛집

  • 고기덕후
  • 은희네해장국
  • 명진전복
  • 평대스낵
  • 벵디
  • 금능낙원(새벽오픈)
  • 삼무국수
  • 우진해장국
  •  

카페

  • 친봉상장
  • 동광
  • 델문도
  • 보롬왓
  • 몽상드애월
  • 서연의집(건축학개론)
  • 휘닉스
  • 카페루시아

갈곳

  • 신창 풍차해안도로
  • 516도로
  • 비자림로
  • 월정리 해안도로
  • 1100 고지
  • 방주교회
  • 산방산
  • 녹산로유채꽃도로

제주도에서 돌아올때는 완도로 갈지 목포로갈지 좀 고민된다. 

완도행 : 07:20 / 10:00, 19:30 / 22:10 

목포행 : 13:40 / 18:10, 16:45 / 21:15 

 

아침 일찍 완도로 와서 좀 천천히 올라가면서 둘러보고 여유롭게 집으로 갈지 

제주도에서 최대한 늦게까지 있다가 19:30 배를 타고 완도로 와서 일박하고 다음날 집으로 빡시게 이동할지

목포로 와서 일박하고 다음날 집으로 다시 빡시게 올라갈지 

고민된다. 

 

완도로 일찍와서 올라가는 길 중간에 일박하고 가는것으로 정했다.

 

 

 

chat gpt에게 물어본 결과

 

"At-Most-Once" 처리는 메시지가 최대 한 번 처리됨을 보장합니다. 이는 메시지가 유실될 수 있지만, 중복 처리는 발생하지 않습니다. Kafka에서 "At-Most-Once" 처리를 구현하는 방법은 다음과 같습니다:

### 1. 자동 오프셋 커밋 활성화
- **자동 오프셋 커밋:** `enable.auto.commit` 설정을 `true`로 설정하여 자동 오프셋 커밋을 활성화합니다. 이렇게 하면 메시지를 폴링한 직후 자동으로 오프셋이 커밋되므로, 처리 중에 애플리케이션이 실패하더라도 이미 커밋된 오프셋 이후의 메시지는 재처리되지 않습니다.

### 2. 메시지 처리 전 오프셋 커밋
- **오프셋 미리 커밋:** 메시지 처리 전에 미리 오프셋을 커밋함으로써, 해당 메시지가 처리되기 전에 실패가 발생하더라도 중복 처리되지 않도록 합니다. 이 방식은 메시지 유실의 가능성을 수용하는 경우에 적합합니다.

### 3. 빠른 실패 메커니즘
- **예외 처리:** 메시지 처리 중에 예외가 발생하면, 즉시 처리를 중단하고 실패로 간주합니다. 이를 통해 메시지 처리를 재시도하지 않고 다음 메시지로 넘어갈 수 있으며, 이는 "At-Most-Once" 처리의 특징을 충족시킵니다.

### 4. 최소한의 재시도 로직
- **재시도 최소화:** "At-Most-Once" 처리 모델에서는 메시지 처리를 위한 재시도 로직을 최소화하거나 사용하지 않습니다. 메시지 처리 시도가 실패하면 해당 메시지는 버려지고, 시스템은 다음 메시지로 넘어갑니다.

### 5. 신속한 메시지 소비
- **효율적인 메시지 소비:** 메시지 처리 로직을 가능한 한 신속하게 수행하여, 시스템이 빠르게 다음 메시지로 이동할 수 있도록 합니다. 이는 시스템의 전반적인 처리량을 최적화하는 데 도움이 됩니다.

"At-Most-Once" 처리 전략은 메시지 중복 처리를 방지하는 데 초점을 맞추지만, 이로 인해 메시지 유실이 발생할 수 있다는 점을 명심해야 합니다. 따라서, 이 전략은 메시지 유실이 비즈니스 로직에 큰 영향을 미치지 않는 시나리오에 적합합니다. 중요한 데이터를 처리할 때는 다른 전송 보증 방식("At-Least-Once" 또는 "Exactly-Once")을 고려하는 것이 좋습니다.

오른쪽 엄지 발가락과 발바닥이 연결되는 부위에 난생 처음 티눈이 생겼다. 

처음에는 조금 걸리적 거리는 수준이었지만 조금 지나니 걷기가 힘들어 졌고 절뚝거리게 되었다.

 

티눈을 제거하기 위해 방법을 알아보니

  • 병원에 가서 레이저나 급속냉동시켜 제거하는 방법
  • 티눈액이나 밴드를 이용하여 집에서 자가로 치료하는 방법
  • 유튜브를 보니 전문가가 메스로 깊숙히 파내는 방법도있는것 같다

병원에가서 치료하는 것은 아프고 시간도 오래걸리는 것 같아 집에서 티눈밴드를 이용하여 자가 치료하기로 했다. 

 

발바닥형 티눈밴드를 사서 붙였다. 티눈 밴드 중심에 살리실산 액이 묻어있는 동그란 핵 같은 부분을 티눈위치에 붙여야 했다. 티눈밴드만 붙이니 움직일때마다 밴드가 떨어져서 일반 밴드 두개를 붙여 안움직이도록 고정했다. 

4일쯤 지나서 제거하려고 보니 티눈 부위가 하얐게 불어 있었고 손톱깍기를 이용하여 제거하려고 했는데 살짝 대기만 해도 너무 아파서 깍지는 못하고 살살 긁어봤다. 하지만 티눈을 제거하는것은 실패 했다. 

 

다시 밴드를 붙이고 이틀정도 지나 제거를 시도해봤다. 이번에는 집에있던 핀셋과 메스와 비슷하게 생긴 이름 모를 어떤 도구를 사용했다. 

여전히 아팠지만 조금씩 긁어내고 핀셋을 이용하여 뜯어 내었다. 이렇게 반복하다 보니 어느 순간 약간의 피와 함께 덩어리 같은게 뜯어졌다.

발도 편해졌고 걷는것도 문제 없었다. 티눈이 제거 되었다고 생각했다. 

 

하루 정도가 지나고 확인해보니 티눈이 어느정도 제거 되었지만 아직 뿌리가 완전히 제거 된거 같지는 않다. 

티눈 밴드를 좀더 붙이고 몇일지나 다시 제거를 시도하면 뿌리까지 제거가 가능할것 같다. 

 

처음에 손톱깍기는 소독도 안하고 사용했는데 두번째 사용한 핀셋과 메스 비스무리한 도구는 알콜솜으로 소독하고 사용했다. 

 

 

 

카프카 커넥트는 언제 사용하나?

  • 카프카를 직접 코드나 API를 작성하지 않았고, 변경도 할 수 없는 데이터 저장소에 연결시켜야 하는 경우

카프카 커넥트의 구성

  • Worker
    • 커넥터와 태스크를 실행시키는 역할
    • 커넥터 설정을 내부 토픽에 저장
    • 적절한 설정값 전달
    • 소스와 싱크 커텍터의 오프셋 커밋
    • 문제 발생시 재시도
    • REST API, 설정관리, 신뢰성, 고가용성, 규모 확장성, 부하 분산 담당
    • 워커 프로세스의 장애 또는 신규 추가
      • 커넥터 클러스트안의 다른 워커들이 감지
      • 해당 워커에서 실행중이던 커넥터와 태스크를 다른 워커에 할당
  • 커넥터 플러그인
    • 커넥터 API 구현
    • Connector
      • 커넥터에서 몇 개의 태스크가 실행되어야 하는지 결정
      • 데이터 복사 작업을 각 태스크에 어떻게 분할해 줄지 결정
      • 워커로부터 태스크 설정을 얻어와서 태스크에 전달
    • Task
      • 데이터를 실제로 카프카에 넣거나 가져오는 작업 담당
      • 워커로부터 컨텍스트를 받아서 초기화
      • 소스 태스크
        • 소스 레코드의 오프셋을 저장할 수 있게 해주는 객체 포함
        • 외부 시스템을 폴링해서 워커가 카프카 브로커로 보낼 레코드 리스트 리턴
      • 싱크태스크
        • 카프카로부터 받는 레코드를 제어할 수 있게 해주는 메소드들 포함
        • 워커를 통해 카프카 레코드를 받아서 외부 시스템에 쓰는 작업 담당
  • Data API
    • 소스 커넥터에서 데이터 객체를 어떻게 생성할지에 대한 방법을 알려줌
  •  Converter
    • 소스 커넥터에서 카프카에 데이터를 어떻게 쓸지에 대한 방법을 알려줌
    • 카프카에서 데이터를 읽어 컨버터를 통해 데이터 API 레코드로 변환하여 싱크 커넥터로 전달
    • 기본 데이터 타입, 바이트 배열, 문자열, Avro, Json, 스키마 있는 JSON, Protobuf 사용가능
  • 오프셋 관리
    • 소스커넥터
      • 커넥터가 워커에 리턴하는 레코드에 논리적인 파티션과 오프셋 포함
        • 파일 인경우, 파일이 파티션, 파일안의 줄 또는 문자 위치가 오프셋
        • JDBC의 경우, 테이블이 파티션, 테이블의 레코드 id/timestamp 가 오프셋
    • 싱크커넥터
      • 토픽, 파티션, 오프셋 식별자가 포함되어 있는 카프카 레코드를 읽은 뒤 대상 시스템에 저장
      • 성공하면 오프셋 커밋

프로듀서

  • 재시도로 인해 발생하는 중복을 방지
  • 멱등적 프로듀서 + 트랜잭션
  • 멱등적 프로듀서
    • 멱등적 프로듀서 기능 on : enable.idempotence=true, acks = all
    • 식별자 생성
      • producer Id, sequence id 생성
      • 대상 토픽, 파티션
      • max.in.flights.requests.per.connection <= 5
    • 중복시 에러가 발생하지는 않지만 메트릭에는 수집됨
      • client : record-error-rate
      • broker : RequestMetrics - ErrorsPerSec
    • 예상보다 높은 시퀀스 값을 받게 될경우 out of order sequence number 에러 발생
      • 트랜잭션 기능을 사용하지 않는 다면 무시해도 됨
      • 프로듀서와 브로커 사이에 메시지 유실
      • 브로커 설정 재점검 및 언클린 리더 선출 발생 여부 확인
    • 프로듀서 재시작
      • 초기화 과정에서 브로커로부터 프로듀서id를 생성 받음
      • 프로듀서 초기화마다 새로운 id 생성
      • 새 프로듀서가 이미 기존 프로듀서가 전송한 메시지를 다시 전송할 경우 중복될 수 있음
    • 브로커 장애
      • 컨트롤러는 장애가 난 브로커가 리더를 맡고 있었던 파티션들에 대해 새 리더를 선출
      • 새 리더는 중복메시지를 체크하기 시작함
        • 리더는 인메모리 프로듀서 상태에 저장된 최근 5개의 시퀀스 넘버를 업데이트 하고 팔로워는 새로운 메시지를 복제할 때마다 자체적인 인메모리 버퍼를 업데이트함으로써 상태를 알 수 있다. 
      • 장애난 브로커의 복구 
        • 브로커는 종료되거나 새 세그먼트가 생성될 때마다 프로듀서 상태에 대한 스냅샷을 파일 형대로 저장
        • 브로커가 시작되면 파일에서 최신 상태를 읽어옴
        • 현재 리더로부터 복제한 레코드를 사용해서 프로듀서 상태를 업데이트 함
    • 한계
      • 내부로직으로 인한 재시도가 발생할 겨우 생기는 중복만 방지
        • 동일한 메시지를 producer.send를 두번 호출하면 중복 발생

트랜잭션

  • 카프카 스트림즈를 사용해서 개발된 애플리케이션에 정확성을 보장하기 위해 도입 (읽기-처리-쓰기 패턴)
  • 다수의 파티션에 대해 원자적 쓰기(atomic multipartition write) 기능도입
  • 문제상황
    • 애플리케이션 크래시에 의한 재시도
    • 좀비 애플리케이션에 의해 발생하는 재처리
  • Transactional producer
    • transactional.id 설정 
      • 재시작시에도 값이 유지됨
    • initTransactions() 호출로 초기화
      • transactional.id와 producer.id 의 대응관계를 유지함
      • 이미 있는 transactional.id 프로듀서가 initTransactions를 호출하는 경우 이전에 쓰던 producer.id값을 할당
    • Zombie fencing
      • 좀비 인스턴스가 출력 스트림에 결과를 쓰는 것을 방지
      • epoch 사용
      • initTransactions 호출 시 transactional.id에 해당하는 epoch 값을 증가 시킴
      • epoch값이 낮은 프로듀서가 전송 후 트랜잭션 커밋/중단 요청을 보낼경우 FencedProducer 에러 발생
      • close()를 호출하여 좀비 애플리케이션 종료
  • 컨슈머 격리 수준(isolation.level)
    • read_uncommitted(default) : 진행중이거나 중단된 트랜잭션에 속한 모든 레코드 리턴
    • read_committed
      • 커밋된 트랜잭션에 속한 메시지 / 트랜잭션에 속하지 않는 메시지만 리턴
      • 트랜잭션에 속한 일부 토픽만 구독하므로 트랜잭션에 속한 모든 메시지가 리턴된다고 보장되지 않음
      • 트랜잭션이 처음으로 시작된 시점 (Last Stable Offset, LSO) 이후에 쓰여진 메시지는 리턴되지 않음
        • 트랜잭션이 커밋/중단 되는 시점 또는 transaction.timeout.ms 만큼 시간이 지날 때까지 보류
    • transaction.timeout.ms
      • default : 15분
      • 종단 지연이 길어질 수 있음
  • 한계
    • 스트림 처리내에서 외부 효과를 일으키는 작업
    • 카프카 토픽에서 읽어서 데이터 베이스에 쓰는 경우
    • DB에서 읽어서 카프카에 쓰고 다른 DB에 쓰는 경우
    • 한클러스터에서 다른 클러스터로 데이터 복제
    • 발행/구독 패턴
  • 사용법
    • 카프카 스트림즈
      • processing.guarantee= exactly_once / exactly_once_beta 로 설정
    • 트랜잭션 API 사용
      • transactional.id 설정
      • enable.auto.commit = false
      • isolation.leve = read_committed
      • producer.initTransactions()
        • transactional.id 등록 또는 epoch 값 증가
      • producer.beginTransaction()
        • 프로듀서에 현재 진행중이 트랜잭션이 있음을 알림
        • 레코드 전송 시 프로듀셔가 브로커에 AddPartitionsToTxn 요청
        • 트랜잭션 로그에 기록
      • producer.sendOffsetsToTransaction
        • 트랜잭션이 커밋 되기전 호출
        • 트랜잭션 코디네이터로 오프셋과 컨슈머 그룹 아이디가 포함된 요청 전송
        • 트랜잭션 코디네이터는 컨슈머 그룹 아이디를 이용하여 컨슈머 그룹 코디네이터를 찾고 오프셋 커밋
      • producer.commitTransaction / producer.abortTransaction
        • 트랜잭션 코디네이터로 EndTxn 요청 전송
        • 트랜잭션 로그에 기록
        • 트랜잭션에 포함된 모든 파티션에 마커 쓰기
        • 트랜잭션 로그에 기록
          • 로그에기록 되었지만 코디네이터가 종료되거나 크래쉬 되면 새로 선출된 코디네이터가 마무리 짐
      • transaction.timeout.ms 
        • 해당 시간내에 커밋/중단 되지 않으면 트랜잭션 코디네이터가 중단
  • 원리
    • 찬디-램포트 스냅샷 알고리즘 : 통신채널을 통해 카머라 불리는 컨트롤 메시지를 보내고 도착을 기준으로 일관적인 상태를 결정
    • two phase commit, 트랜잭션 로그(transaction_state 내부 토픽) 사용
      • 현재 진행중인 트랜잭션과 파티션을 함께 로그에 기록
      • 커밋/중단 시도 기록
      • 모든 파티션에 트랜잭션 마커를 씀
      • 트랜잭션 종료 로그 기록
  • 모니터링
    • LastStableOffsetLag 지표
      • 파티션의 LSO가 최신 오프셋 값에서 얼마나 떨어졌는지를 나타냄
      • 이값이 무한히 증가하는 상황 : 트랜잭션 멈춤 현상

파티션과 컨슈머

  • 컨슈머는 컨슈머그룹의 일부로서 작동
  • 하나의 파티션에는 하나의 컨슈머가 할당됨
  • 파티션수보다 컨슈머수가 더 많다면 유휴 컨슈머가 생김
  • 파티션수보다 컨슈머수가 적다면 하나의 컨슈머가 여러파티션을 컨슘하여 처리함
  • 애플리케이션에서 모든 토픽의 데이터를 컨슘하기 위해 애플리케이션 별로 컨슈머 그룹을 생성해야 함

파티션 리밸런스

  • 컨슈머가 추가되거나, 크래시되었거나 파티션이 추가되는 등의 변경이 발생하면 컨슈머를 파티션에 재할당하는 작업이 실행되고 이 과정을 리밴런스라 함
  • Eager rebalance(조급한 리밸런스)
    • 모든 컨슈머가 읽기 작업을 멈추고 모든 파티션에 대한 소유권을 포기한 후 컨슈머 모두가 그룹에 다시 참여하여야 새로운 파티션을 할당받도록 하는 방법
    • Stop the world
  • Cooperative rebalance(협력적 리밸런스) / Incremental rebalance(점진적 리밸런스)
    • 컨슈머 리더가 일부 파티션 재할당을 통지하면, 해당 컨슈머들은 작업을 중단하고 소유권을 포기한 후 이 컨슈머들을 새로운 파티션에 재할당 하는 방법
    • 안정적으로 파티션이 할당될때까지 반복될 수 있음
    • Stop the world는 발생하지 않음
  • 2.4 이후 Eager rebalance가 default 였으나 3.1 부터 Cooperative rebalance가 default가 되고 Eager rebalance는 deprecated 될 예정
  • session.timeout.ms에 설정된 값(default - 2.8 : 10초, 3.0:45초)동안 컨슈머가 heartbeat를 보내지 않으면 해당 컨슈머가 죽은것으로 판단하여 리밸런스 발생
  • max.poll.interval.ms(default 5분) 에 지정된 시간동안 컨슈머가 폴링하지 않으면 해당 컨슈머는 죽은것으로 판단
    • heartbeat는 백그라운드 스레드에서 전송하므로 메인스레드가 블록 되더라도 hearbeat는 전송가능 함
    • 이러한 경우를 판단하기 위해 max.poll.interval.ms로 컨슈머가 정상적으로 컨슘하는지 확인

폴링루프

  • while(true)를 이용한 무한 루프
  • 루프내에서 컨슈머의 poll(Duration) 메소드를 호출하여 메시지를 가져오고, 처음으로 호출되는 경우 GroupCoordinator를 찾아서 컨슈머 그룹에 참가하고 파티션을 할당 받음
  • max.poll.interval.ms에 지정된 시간 이상으로 호출되지 않는 경우 컨슈머는 죽은 것으로 판정되므로 블록되지 않도록 주의 해야함

파티션 할당 전략

  • partition.assignment.strategy 설정
  • Range(default)
    • RangeAssignor
    • 컨슈머가 구독하는 각 토픽의 파티션들을 연속된 그룹으로 나눠서 할당
    • 홀수개의 파티션을 갖는 경우 앞의 컨슈머는 뒤의 컨슈머보다 많은 파티션을 하당받게 됨
  • RoundRobin
    • RoundRobinAssignor
    • 모든 구독된 토픽의 모든 파티션을 가져다 순차적으로 하나씩 컨슈머에 할당
    • 모든 컨슈머들이 동일한 수의 파티션 할당(많아야 1개차이)
  • Sticky
    • StickyAssignor
    • 파티션들을 가능한 균등하게 할당(RoundRobin과 유사)
    • 리밸런스가 발생했을때 가능하면 많은 파티션들이 같은 컨슈머에 할당
    • 컨슈머가 변경되는 오버헤드를 최소화
  • Cooperative Sticky
    • CooperativeStickyAssignor
    • Sticky 와 동일
    • 컨슈머가 재할당되지 않은 파티션으로부터 레코드를 계속해서 읽어올 수 있도록 해주는 협력적 리밸런스기능 지원

오프셋 커밋

  • 파티션에서의 현재위치를 업데이트 하는 작업
  • poll이 리턴한 마지막 오프셋 바로 다음 오프셋을 커밋함(수동으로 오프셋을 다룰 경우 유의)
  • 레코드를 개별적으로 커밋하지 않고 파티션에서 성공적으로 처리해 낸 마지막 메시지를 커밋 함
  • 커밋된 오프셋이 클라이언트가 마지막으로 처리한 오프셋보다 작을 경우 중복 처리 됨
  • 커밋된 오프셋이 클라이언트가 마지막으로 처리한 오프셋보다 클 경우 메시지 누락처리됨
  • 자동 커밋
    • enable.auto.commit = true 로 설정
    • 컨슈머가 대신 커밋
    • auto.commit.intervel.ms(default 5초)에 한번 poll을 통해 받은 메시지 중 마지막 메시지 오프셋 커밋
    • 커밋 인터벌 내에서 리밸런싱이 발생한다면 해당 인터벌내의 메시지는 중복 처리됨
  • 현재 오프셋 커밋
    • enable.auto.commit = false 로 설정
  • commitSync
    • 가장 간단하고 신뢰성있는 commit API
    • poll에 의해 리턴된 마지막 오프셋 커밋
    • poll에서 리턴된 모든 메시지를 처리하기 전 commit 할 경우 메시지가 누락될 수 있음
    • 브로커가 커밋 요청에 응답할 때 까지 블록됨
    • 성공하거나 재시도 불가능한 실패가 발생할때까지 재시도
  • commitAsync
    • 재시도 하지 않음
    • 콜백 처리(OffsetCommitCallback)
    • 콜백에서 재시도 처리시 커밋의 순서를 유의 해야 함
      • Apache Kafka에서 컨슈머의 `commitAsync` 메소드는 비동기적으로 오프셋을 커밋합니다. 이 방법은 `commitSync`에 비해 성능상 이점이 있지만, 비동기적 특성으로 인해 오프셋 커밋 순서가 보장되지 않는 문제가 있습니다. 즉, 늦게 시작된 커밋이 먼저 완료될 수 있으며, 이로 인해 컨슈머 재시작 시 메시지 중복 처리나 누락의 가능성이 존재합니다.

        ### 커밋 순서 강제 방법

        비동기 커밋 시 순서를 강제하는 명확한 방법은 Kafka 자체적으로 제공하지 않지만, 다음과 같은 방법으로 순서 문제를 완화할 수 있습니다:

        1. **단일 콜백 사용**: 비동기 커밋을 할 때마다 새로운 콜백을 생성하는 대신, 단일 콜백 인스턴스를 재사용하면서 내부적으로 최신 오프셋을 추적하는 방법입니다. 콜백 내에서는 커밋된 오프셋이 현재 알고 있는 최신 오프셋보다 이전인지 확인하고, 이전이면 무시합니다. 이 방법은 순서를 완벽하게 보장하지는 않지만, 순서 문제로 인한 영향을 최소화할 수 있습니다.

        2. **커밋 직렬화**: 오프셋 커밋 요청을 내부적으로 관리하는 큐를 구현하여, 이전 커밋 요청의 완료를 확인한 후에 다음 커밋을 실행하는 방법입니다. 이 방법은 성능에 영향을 줄 수 있지만, 커밋 순서를 엄격하게 관리할 수 있습니다.

        3. **비동기 커밋 후 동기 커밋으로 마무리**: 정기적으로 또는 특정 조건(예: 애플리케이션 종료 시)에서 `commitSync`를 호출하여 오프셋 커밋을 확실하게 완료하는 방법입니다. 이는 비동기 커밋 과정에서 발생할 수 있는 순서 문제를 마지막에 동기 커밋으로 보정합니다.

        4. **외부 저장소 사용**: 컨슈머가 처리한 오프셋을 외부 시스템(예: 데이터베이스)에 저장하고, 컨슈머 재시작 시 외부 시스템의 오프셋 정보를 기반으로 메시지 처리 위치를 결정하는 방법입니다. 이 방법은 추가적인 외부 시스템 의존성이 필요하지만, 컨슈머의 상태 관리를 더 세밀하게 할 수 있습니다.

        비동기 오프셋 커밋을 사용할 때는 커밋 순서가 보장되지 않는 점을 고려하여 애플리케이션의 로직을 설계해야 하며, 중복 처리나 메시지 누락을 방지하기 위한 추가적인 조치가 필요할 수 있습니다.
  • commitSync, commitAsync 동시 사용
    • 정상적인 상황에서는 commitAsync를 사용함
    • 컨슈머를 닫는 상황에서는 commitSync를 사용하여 재시도하도록 함
  • 특정 오프셋 커밋
    • 배치 처리 중 오프셋을 커밋하고 싶은 경우
    • Map<TopicPartition, OffsetAndMetadata> 을 commitSync/commitAsync에 파라미터로 넘겨 처리
    • 에러 처리
  • 컨슈머가 닫힐때 오프셋 커밋
    • ConsumerRebalanceListener 인터페이스를 구현하여 onPartitionsRevoked 메소드에 커밋 로직 구현
      • Eager rebalance 인경우 컨슈머가 읽기를 멈추고 리밸런스가 시작되기 전 호출
      • Cooperative rebalance 인 경우 리밸런스가 완료될때 할당해제해야할 파티션들에 대해서 호출

Standalone consumer

  • 컨슈머가 스스로 특정 토픽의 모든 메시지를 처리해야 하는 경우
  • 토픽을 구독할 필요가 없음
  • 리밸런스 필요없음
  • List<PartitionInfo> partitions = consumer.partitionsFor("Topic") 을 호출하여 파티션 정보를 조회
  • consumer.assign(partitions)
  • 루프를 돌면서 consumer.poll 호출하여 레코드를 가져와서 처리
  • offset commit
  • 리밸런싱을 사용하지 않으므로 파티션이 추가되는 경우를 자동으로 감지할 수 없으므로 consumer.partitionsFor를 주기적으로 호출하여 감지 하거나 애플리케이션을 재시작 해야할 수 있음

읽기 지연

  • 리더 레플리카는 팔로워 레플리카가 복제한 오프셋을 관리 함
  • 컨슈머는 모든 in sync 레플리카에 복제된 메시지만 읽을 수 있음
  • 복제가 지연되는 경우 컨슈머가 읽는 메시지도 지연될 수 있음
  • replica.lag.time.max.ms 에 정의된 시간 만큼 복제지연이 발 생할 수 있고 이 시간을 지나면 해당 레플리카는 out of sync 레플리카가 됨

 

전송전략

  • fire and forget
    • 메시지를 서버에 전송만하고 성공/실패 여부는 신경 쓰지 않음
    • 메시지 유실 가능성
      • 메시지 직렬화 실패
      • 버퍼가 가득찰 경우 타임아웃 발생
      • 전송 스레드에 인터럽트가 걸리는 경우
      • 애플리케이션은 예외에 대한 아무런 정보를 받지 않음. 
    • 거의 사용하지 않음
  • Synchronous send
    • 카프카는 기본적으로 비동기적으로 동작함
    • 다음 메시지를 전송하기 전에 send가 반환하는 Future 객체의 get 메소드를 이용하여 성공여부 확인
    • 전송지연으로 인한 스레드 대기시 성능 문제 발생가능
    • 브로커가 에러응답 반환
    • 재전송 횟수 소진
    • 거의 사용하지 않음. 예제는 많음
  • Asynchronous send
    • 콜백함수 이용(Callback 인터페이스 구현)
    • onCompletion(RecordMetadata rm, Exception e)
      • e가 null이 아닌경우 에러발생
    • 프로듀서의 메인 스레드에서 콜백이 실행됨
    • 동일한 스레드로 보낸 메시지의 콜백은 보낸 순서대로 실행됨
    • 콜백메소드에서는 블로킹 작업 수행은 권장하지 않음

응답 처리 전략 (acks)

임의의 쓰기 작업에 대한 성공 여부를 판단하기 위해 얼마나 많은 파티션 레플리카카 해당 레코드를 받아야 하는지 설정

  • acks = 0
    • 성공적으로 전달 되었다 간주하고 브로커의 응답을 기다리지 않음
    • 메시지 유실될 수 있음
    • 매우 높은 처리량이 필요할 때 사용
  • acks = 1
    • 리더 레플리카가 메시지를 받는 순간 브로커로부터 성공 응답을 받는다
    • 리더에 크래시가 나고 아직 새로운 리더가 선출되지 않은 경우 에러응답을 받고 재시도
    • 리더에 크래시가 난 상태에서 복제가 안된상태로 새 리더가 선출 될경우 메시지 유실
  • acks = all
    • 모든 in sync 레플리카에 전달되에 브로커가 성공응답
    • 가장 안전
    • 모든 레플리카에 전달되어야 하기 때문에 지연시간은 더 길어질 수 있음

에러처리

  • 재시도 가능한 에러 처리
    • retries : Integer.MAX 
    • delivery.timeout.ms 
    • delivery.timeout.ms 시간내에서 무한히 재시도
    • Network Errors: 네트워크 문제로 인해 발생하는 에러입니다. 예를 들어, Kafka 브로커와의 연결이 일시적으로 끊어진 경우 등이 있습니다.
    • Leader Election: Kafka는 파티션의 리더(replica)에게만 데이터를 쓸 수 있습니다. 리더가 다운되어 새로운 리더가 선출되는 동안 발생하는 에러도 재시도가 가능합니다.
    • Not Enough Replicas: 아직 메시지를 받을 준비가 되지 않은 복제본(replicas) 때문에 발생하는 에러입니다. 이는 복제본이 충분한 데이터를 동기화하지 못한 경우에 발생할 수 있습니다.
    • Not Leader for Partition: 메시지를 쓰려는 파티션의 리더가 변경되었을 때 발생하는 에러입니다. 새 리더로의 재할당 후에 재시도가 가능합니다.
    • Record Too Large: 프로듀서가 보내려는 메시지의 크기가 너무 커서 브로커가 처리할 수 없는 경우 발생합니다. 이는 설정을 조정하여 해결할 수 있지만, 재시도 전에 메시지 크기를 줄여야 할 수도 있습니다.
    • Timeout Errors: 요청에 대한 응답을 Kafka 브로커로부터 지정된 시간 내에 받지 못할 때 발생합니다.
  • 재시도 불가능한 에러 처리
  • Callback 이용

At least once 보장

  • 프로듀서 Retry + Ack 전략
    • acks = all 이고 delivery.timeout.ms 가 충분히 크게 잡혀 있는 경우 메시지를 모든 레플리카에 복제된 상황에서 리더 레플리카가 크래시 나는 경우 프로듀서는 request.timeout.ms 만큼 기다리고 재시도를 하게 되며, 새로 선출된 리더 레플리카에는 이미 메시지가 복제된 상태이므로 중복 저장되지만 at least once는 보장됨
    • retries, retry.backoff.ms 설정
  • 컨슈머 offset 관리
    • 컨슈머는 메시지를 처리하면 offset을 커밋하게 되고 재시도시에는 커밋한 offset 이후 부터 처리 

순서보장

  • 파티션 내에서 메시지의 순서를 보존
  • retries > 0 , max.in.flight.requests.per.connection >= 1 인 경우 순서가 뒤집어질 수 있다.
  • 성능및 신뢰성을 보장하기 위해 retries > 0, in.flight >= 2 이어야 하므로 enable.idempotence=true할 경우 in.flight 최대 5까지 요청을 허용하며, 순서도 보장되고 재전송시에도 중복이 발생하지 않도록 해줌

메시지 사이즈 고려

  • 프로듀서가 전송하는 메시지의 최대 크기를 설정하는 max.request.size와 브로커가 받아 들일수 있는 메시지의 사이즈를 결정하는 message.max.bytes 설정을 동일하게 맞춰야 함

시리얼라이저

  • 커스텀 시리얼라이저를 구현할 수 있으나, 하위호환성 유지, 직렬화/비직렬화 로직 디버깅, 여러팀에서 같이 사용하는 경우 동시 코드 변경 등의 문제가 발생할 수 있음
  • JSON, 에이브로, 스리프트, 프로토버프와 같은 범용 라이브러리 사용 권장

파티션 할당

  • 키값에 따라 항상 동일한 파티션에 할당되어야 하는 경우 토픽 생성시 파티션을 충분히 크게하고 파티션을 추가하지 않는다. 
  • 파티션 수 변경 시 할당되는 파티션도 달라 질 수 있음

인터셉터

  • 애플리케이션 공통로직을 처리해야 하는 경우 사용
  • 모니터링, 정보추적, 표준 헤더 삽입등
  • ProducerInterceptor
    • ProducerRecord<K, V> onSend(ProducerRecord<K, V> record)
      • 레코드를 브로커로 보내기전, 직렬화되기 직전 호출
      • ProducerRecord 조회 및 수정 가능
      • 유효한 ProducerRecord를 반환하는 것에 주의
    • void onAcknowledgement(RecordMetadata metadata, Exception e)
      • 브로커가 보낸응답을 클라이언트가 받았을때 호출
      • 응답을 변경할 수는 없음. 조회는 가능

 

1. Jenkins docker 설치

  • 8080포트는 사용중이므로 9091 로 변경하고 도커 내부에서는 8080포트를 사용하도록 함
  • 접속 : localhost:9091
  • sudo docker run --name jenkins-docker -d -p 9091:8080 -p 50000:50000 -v /home/jenkins:/var/jenkins_home -u root jenkins/jenkins:lts

2. jenkins tools 설정

  • gradle 설정

3. Item 설정

  • Item 이름 설정
  • pipeline으로 생성

4. Item build 구성 설정

  • Pipeline 항목에서 Pipeline script 선택 

  • script 내용
    • jenkins tools 에 설정한 gradle, jdk Name 을 사용하여 tools 설정
    • git clone 과 build 2 단계
pipeline {
    agent any

    tools {
        gradle 'gradle'
        jdk 'jdk17'
    }
    environment {
        JAVA_HOME = "tool jdk17"
    }
    stages {
        stage('Git-clone') {
            steps {
                git branch: 'develop', url: 'git address...'
            }
        }
        stage('Build') {
            steps {
                sh 'gradle init'
                withGradle {
                   sh 'gradle wrapper clean build'
                }
            }
        }
    }
}

 

5. Build 확인 

  • 해당 프로젝트 내에서 '지금 빌드' 선택

6. 문제 상황 해결

  • local redis 접속 문제 : DENIED Redis is running in protected mode because protected mode is enabled ...
    • jenkins는 docker container 로 실행되고 redis는 로컬에서 실행되는 상황
    • protected mode를 해제 하기 위해 redis password 설정
      • redis.conf 파일에 requirepass 항목으로 설정
      • application.yml에 password 설정
      • Configuration 에서 접속 설정 시 password 설정
        • lettuce    @Value("${spring.data.redis.host}")
              private String host;
              @Value("${spring.data.redis.port}")
              private int port;
              @Value("${spring.data.redis.password}")
              private String password;
              @Bean
              public RedisConnectionFactory redisConnectionFactory() {
                  RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
                  redisStandaloneConfiguration.setHostName(host);
                  redisStandaloneConfiguration.setPort(port);
                  redisStandaloneConfiguration.setPassword(password);

                  return new LettuceConnectionFactory(redisStandaloneConfiguration);
              }
        • redisson
          •     @Bean
                public RedissonClient redissonClient(){
                    Config config = new Config();
                    config.useSingleServer()
                            .setAddress("redis://"+ this.host+":"+ this.port)
                            .setPassword(password);

                    return Redisson.create(config);
                }
  • Connection refused 에러
    • host : localhost -> host.docker.internal 로 변경

 

 

계층형 아키텍처

  • 목적
    • 선택의 폭을 넓히고 변화하는 요구사항과 외부 요인에  빠르게 적응할 수 있게 해준다.
    • 애플리케이션의 목적은 비즈니스 규칙이나 정책을 반영한 모델을 만들어서 사용자가 이러한 규칙과 정책을 편리하게 활용할 수 있게 한다. 행동은 상태를 바꾸는 주체이기때문에 상태가 아니라 행동을 중심으로 모델링 해야 한다.

 

  • 문제
    • 코드에 나쁜 습관들이 스며들기 쉽게 만들고 점점 더 변경하기 어렵게 만드는 허점들을 노출한다.
      • 데이터 베이스 주도 설계 유도
      • 영속성 계층을 토대로 만들어진다. 
      • ORM 프레임워크를 결합하면 비즈니스 규칙을 영속성 관점과 섞고 싶은 유혹을 쉽게 받는다
      • 영속성 계층과 도메인 계층 사이에 강한 결합이 생긴다. 
    • 지름길을 택하기 쉬워진다. 
      • 상위계층에 위치한 컴포넌트에 접근해야 한다면 간단하게 컴포넌트를 계층 아래로 내려 버리면 된다. 
      • 어떤 것을 할수 있는 선택지가 있다면 누군가는 반드시 그렇게 하기 마련이다.
    • 테스트하기 어려워진다.
      • 간단한 변경의 경우 도메인을 웹계층에 구현하게 된다. 이런경우 테스트를 구현하려면 웹 계층에서 영속성 계층을 모킹해야 한다. 
      • 테스트의 복잡도가 올라가게 된다.
    • 유스케이스를 숨긴다.
      • 도메인 로직이 여러계층에 흩어지기 쉽다.
      • 여러개의 유스케이스를 담당하는 아주 넓은 서비스가 만들어지기도 한다. 
    • 동시 작업이 어려워진다. 
      • 영속성 계층을 먼저 개발해야 하고 그 다음에 도메인 계층, 웹계층을 만들어야 한다.

클린 아키텍처

  • 설계가 비즈니스 규칙의 테스트를 용이하게 한다. 
  • 비즈니스 규칙은 프레임워크, 데이터베이스, UI 기술, 그 밖의 외부 애플리케이션이나 인터페이스로부터 독립적일 수 있다.
  • 도메인 코드는 바깥으로 향하는 어떤 의존성도 없어야 한다. 
  • 의존성 역전원칙을 이용하여 모든 의존성이 도메인 코드를 향해야 한다.
  • 코어에는 유스케이스에서 접근하는 도메인 엔티티들이 있다. 
  • 유스케이스는 흔히 서비스라고 부르던 것들이고 단일 책임을 갖기 위해 세분화 되어야 한다. (넓은 서비스 문제 해결)
  • 도메인 계층이 영속성이나 UI 같은 외부 계층과 철저하게 분리 돼야 하므로 애플리케이션의 엔티티에 대한 모델을 각 계층에서 유지보수 해야 한다. 
  • 도매인 계층과 영속성 계층이 데이터를 주고 받을때 두 엔티티를 서로 변환해야 한다. 도메인 계층과 다른 계층들 사이에서도 마찬가지다.

헥사고날 아키텍처(포트와 어댑터 아키텍처)

  • 클린 아키텍처의 원칙을 조금 더 구체적으로 만들어 준다.
  • 웹어댑터, 외부 시스템 어댑터 -> 입력포트(인터페이스) <- 유스케이스 -> 엔티티 <- 유스케이스 -> 출력포트(인터페이스) <- 영속성 어댑터, 외부시스템 어댑터 
  • 도메인 중심 아키텍처에 적합

아키텍처적으로 표현력 있는 패키지 구조

business

L sub business

    L adapter

        L in

            L web - controller

        L out

            L persistent adapter

            L repository

     L domain - entity

     L application

         L port

            L in - usecase

            L out

  • 이 패키지 구조는 아키텍처-코드 갭 혹은 모델-코드 갭을 효과적으로 다룰 수 있는 강력한 요소다
  • 패키지 구조가 아키텍처를 반영할 수 없다면 시간이 지남에 따라 코드는 점점 목표하던 아키텍처로부터 멀어지게 될 것이다 
  • 패키지 내의 클래스들은 특성에 따라 접근제어자를 이용하여 계층간의 우발적인 의존성을 막을 수 있다. 
  • domain 패키지 내에서는 DDD가 제공하는 모든 도구를 이용해 우리가 원하는 어떤 도메인 모델이든 만들 수 있다. 
  • 아키텍처-코드갭을 넓히고 아키텍처를 반영하지 않는 패키지를 만들어야 하는 경우도 생길 수 있다. 

도메인 모델 구현하기

  • 도메인에 관련된 주요 기능을 포함하는 엔티티 구현
  • 비즈니스 규칙 유효성 검증
  • rich domain model
    • 엔티티에서 가능한 한 많은 도메인 로직 구현
    • 상태 변경 메소드 제공
    • 비즈니스 규칙에 맞는 유효한 변경만 허용
    • 유스케이스는 사용자의 의도만 표현
  • anemic domain model 
    • 도메인 로직을 가지지 않음
    • 엔티티 상태를 표현하는 필드와 getter, setter 메서드만 포함
    • 도메인 로직은 유스케이스에 구현

 

유스케이스 구현하기

  • 하는일
    • 입력을 받는다
    • 비즈니스 규칙을 검증한다.
    • 모델 상태를 조작한다
    • 출력을 반환한다
  • 입력 유효성 검증
    • 구문상(syntatical)의 유효성 검증
    • 이로인해 유스케이스가 오염 되지 않아야 함
    • 애플리케이션 계층의 책임
    • 입력모델이 다루기(anti corruption layer) 
    • Bean validation API 사용하기
    • 입력모델 생성자에서 validation 하기
  • 비즈니스 규칙 검증
    • 유스케이스의 맥락속에서 의미적인(semantical) 유효성 검증
    • 도메인 모델의 현재 상태에 접근
    • 도메인 엔티티 안에 구현
  • 출력 모델
    • 유스케이스에 맞게 구체적일 수록 좋음
    • 계속 질문하고 의심스럽다면 가능한 한 적게 반환하자
    • 단일 책임 원칙 적용
    • 도메인 엔티티를 출력모델로 사용하고 싶은 유혹을 견뎌내야 함

웹 어댑터 구현하기

  • 주도하는 / 인커밍 어댑터
  • 웹어댑터에서 애플리케이션 서비스로 제어흐름
  • HTTP와 관련된 것은 애플리케이션 계층으로 침투해서는 안됨
    • 다른 인커밍 어댑터의 요청에 대해 동일한 도메인 로직을 수행할 수 있는 선택지
    • 좋은 아키텍처에서는 선택의 여지를 남겨둔다
  • 의존성 역전원칙
    • 포트 인터페이스를 통해 서비스 호출
    • 포트 : 애플리케이션 코어가 외부세계와 통신할 수 있는 곳에 대한 명세
  • 책임
    • HTTP 요청을 자바 객체로 매핑
    • 권한검사
    • 입력유효성 검증
    • 입력을 유스케이스의 입력 모델로 매핑
    • 유스케이스 호출
    • 유스케이스의 출력을 HTTP로 매핑
    • HTTP 응답 반환
  • 콘트롤러 나누기
    • 가능한한 좁고 다른 컨트롤러와 적게 공유하는 웹 어댑터 조각 구현
    • 클래스마다 코드는 적을 수록 좋다
    • 모든 연산을 단일 콘트롤러에 넣는 것은 데이터 구조의 재활용을 촉진하게 됨
    • 메서드와 클래스명은 유스케이스를 최대한 반영해서 지어야 함
    • 콘트롤러자체의 모델을 갖거나 원시값을 받도록 함
    • 서로다른 연산에 대한 동시 작업이 쉬워짐
    • 병합충돌이 일어나지 않게됨
    • 더 파악하기 쉽고, 테스트하기 쉽다

영속성 어댑터 구현하기

  • 주도되는 / 아웃고잉 어댑터
  • 애플리케이션에 의해 호출되고 호출하지는 않음
  • 포트 인터페이스를 통해 통신
  • 책임
    • 입력을 받는다 (입력모델)
    • 입력을 데이터베이스 포맷으로 매핑한다 (JPA 엔티티)
    • 입력을 데이터베이스로 보낸다
    • 데이터베이스 출력을 애플리케이션 포맷으로 매핑한다
    • 출력을 반환한다
  • 포트 인터페이스 나누기
    • 인터페이스 분리 원칙
    • 클라이언트가 오로지 자신이 필요로 하는 메서드만 알면 되도록 넓은 인터페이스를 특화된 인터페이스로 분리
    • 매우 좁은 포트를 만드는 것은 프러그 앤드 플레이 경험으로 만든다
  • 연속성 어댑터 나누기
    • 애그리거트 하나당 하나의 영속성 어댑터 구현
      • 영속성 기능을 이용하는 도메인 경계를 따라 자동으로 나눠짐
      • 여러개의 바운디드 컨텍스트의 영속성 요구사항을 분리하기 위한 토대
    • 여러 종류의 어댑터 구현(JPA, native sql , ..)
  • 트랜잭션 
    • 영속성 어댑터를 호출하는 서비스에 위임

경계간 매핑하기

  • 매핑하지 않기(No mapping) 전략
    • 도메인과 애플리케이션 계층은 웹이나 영속성과 관련된 특수한 요구사항에 관심이 없지만 다루게 된다. 
    • 단일 책임 원칙 위반
    • 도메인 모델에 특정 커스텀 필드를 두도록 요구할 수 있다. 파편화된 도메인 모델
    • 모든 계층이 정확히 같은 구조의, 같은 정보를 필요하다면 완벽한 선택지 
  • 양방향(Two way) 매핑 전략
    • 각 계층이 전용모델를 가지고 있어 다른 계층에는 영향이 없다
    • 깨끗한 도메인 모델
    • 단일 책임 원칙 만족
    • 매핑 책임이 명확
    • 너무 많은 보일러 플레이트 코드 발생
    • 도메인 모델이 경계를 넘어서 통신하는데 사용. 바깥쪽 계층의 요구에 따른 변경에 취약
  • 완전(Full) 매핑 전략
    • 각 연산마다 별도의 입출력 모델 사용
    • 전역 패턴으로 추천하지 않음
    • 웹계층과 애플리케이션 계층사이에서 상태 변경 유스케이스의 경계를 명확하게 할때 가장 빛을 발한다. 
  • 단방향(One way) 매핑 전략
    • 모든 계층의 모델들이 같은 인터페이스를 구현
    • 특성에 대한 getter 메소드를 제공해서 도메인 모델의 상태를 캡슐화
    • 한 계층이 다른 계층으로부터 객체를 받으면 해당 계층에서 이용할 수 있도록 무언가로 매핑
    • 계층간의 모델이 비슷할때 가장 효과적
  • 언제 어떤 전략을 사용할 것인가?
    • 그때그때 다르다
    • 특정 작업에 최선의 패턴이 아님에도 깔끔하게 느껴진다는 이유로 선택해버리는것은 무책임한 처사다
    • 어제는 최선이었던 전략이 오늘은 아닐수도 있다
    • 빠르게 코드를 짤 수 있는 간단한 전략으로 시작해서 계층 간 결합을 떼어내는 데 도움이 되는 복잡한 전략으로 갈아타기
    • 어떤 상황에서 어떤 전략을 택해야 할지 가이드라인을 정해둬야 한다

아키텍처 경계 강제하기

  • 계층 경계를 넘는 의존성은 항상 안쪽 방향으로 향해야 한다.
  • 접근 제한자
    • package-private(default)
    • 자바 패키지를 통해 응집적인 모듈로 만들어 줌
    • 모듈의 진입점으로 활용될 클래스들만 public으로 만듬
    • 몇 개 정도의 클래스로만 이뤄진 작은 모듈에서 가장 효과적
  • 컴파일 후 체크
    • ArchUnit
    • 의존성 규칙 위반을 발견하면 예외
    • 패키지 사이의 의존성 방향이 올바른지 자동으로 체크
    • 실패에 안전하지는 않다
      • 오타의 경우 
      • 패키지명 리팩토링
      • 클래스를 하나도 찾지 못했을때 실패하는 테스트 추가 필요
    • 언제나 코드와 함께 유지 보수
  • 빌드 아티팩트
    • 모듈과 아키텍처의 계층 간의 의존성을 강제
    • 순환의존성 체크
    • 다른 모듈을 고려하지 않고 특정 모듈의 코드를 격리한채로 변경할 수 있음
    • 새로 의존성을 추가하는 일이 우연이 아닌 의식적인 행동이 됨
    • 빌드 스크립트 유지보수 비용

의식적으로 지름길 사용하기

  • 유스케이스간 모델 공유하기
  • 도메인 엔티티를 입출력 모델로 사용하기
  • 인커밍 포트 건너뛰기
  • 애플리케이션 서비스 건너뛰기

 

 

                                  

 

레디스(Remote Distionary Server)는 키-배류 형태의 고성능, 고가용성, 확장성을 보장하는 In memory NoSQL 저장소 이다.

레디스의 커맨드는 싱글 스레드 이벤트루프 방식으로 동작한다. 따라서 레이턴시가 높은 커맨드를 사용할때는 주의를 해야한다. 

 

자료구조

밸류타입 커맨드 성능
String SET, GET, INCR, INCRBY
MSET, MGET
O(1)
O(N)
List LPUSH, RPUSH
LPOP, RPOP, LRANGE,LTRIM, LINSERT, LSET, LINDEX
O(1)
O(N)

Set SADD,SPOP
SMEMBERS,SREM,SINTER,SUNION,SDIFF

O(1)
O(N)

SortedSet ZADD,ZRANGE,ZRANK
ZREM
O(log(N)) 
O(M*log(N))
Hash HSET, HGET
HGETALL,HMGET,HMSET
O(1)
O(N)
Hyperloglog PFADD,PFCOUNT O(1)
Bitmap SETBIT,GETBIT O(1)
Geospatial GEOADD
GEOPOS
GEOSEARCH
O(log(N))
O(1)
O(N+log(M)) 
Stream    
JSON    
Bitfield    
BloomFilter    
CuckFilter    
T-digest    
Top-K    
Count-min sketch    
     
     

 

  • 다른 자료구조가 이미 생성되어 있는 키에 다른 자료구조로 추가할때 에러를 반환함
  • 모든 아이템을 삭제하면 키도 삭제됨
  • 키가 없는 상태에서 삭제 커맨드를 실행할 경우 에러를 반환하지는 않음

캐싱전략

  • 읽기 전략
    • look a side : 애플리케이션에서 캐시를 먼저 조회하고 있으면 반환 없으면 애플리케이션이 DB를 조회하여 캐시를 채우고 반환
    • read through : 애플리케이션에서 캐시를 먼저 조회하고 있으면 반환 없으면 DB를 조회하고 캐시를 채우고 반환
  • 쓰기전략
    • write through : 데이터 업데이트시 매번 캐시도 업데이트
    • write back : 캐시에 먼저 업데이트 후  비동기로 디비에 업데이트
    • cache invalidation : 데이터가 업데이트 될때 마다 캐시를 삭제

만료시간

TTL : 데이터의 저장 기간

EXPIRE : 데이터의 만료 시간

 

복제

마스터 노드의 데이터를 슬레이브 노드로 실시간 동기화 하는 작업

 

목적

  • filover
  • 트래픽 분산(read : 슬레이브, write : 마스터) 
  • 백업 시 사용

구성

  • 마스터 노드와 다수의 슬레이브
  • 레플리카 노드에서 복제
  • 복제 그룹에는 하나의 마스터 노드 ( 마스터 노드 복제 안됨)

동작방식

  • 복제 연결
    • disk (repl-diskless-sync : no, 7 버전 미만 디폴트)
      • repalica of 명령어로 복제를 시작
      • fork로 자식 프로세스를 생성하여 마스터 노드의 RDB 스냅숏 파일을 생성(BGSAVE command)
      • RDB 생성 이후의 마스터 노드에서 실행되는 모든 커맨드는 RESP 형태로 Replication Buffer 에 저장함
      • RDB 파일을 슬레이브 노드로 전송
      • 슬레이브 노드에서는 이 파일을 로드하여 데이터를 초기화 함
      • Replication Buffer 에 쌓여있는 커맨드를 슬레이브노드로 전송하여 데이터 동기화
      • RDB 파일이 생성되는 도중 다른 복제요청이 들어올 경우 큐에 저장되고 파일 생성 완료후 동시에 복제 연결 시작
    • diskless (repl-diskless-sync : yes, 7 버전 이상 디폴트)
      • repalicaof 명령어로 복제를 시작
      • 슬레이브 노드로 소켓통신을 통해 RDB 데이터를 전송
      • RDB를 전송하기 시작한 이후의 마스터 노드에서 실행되는 모든 커맨드는 RESP 형태로 Replication Buffer 에 저장함
      • 슬레이브 노드에서는 이 RDB데이터를 RDB파일로 저장
      • 복제본의 데이터를 모두 삭제한뒤 RDB 파일 로드 
      • Replication Buffer 에 쌓여있는 커맨드를 슬레이브노드로 전송하여 데이터 동기화
      • 복제 연결이 시작된 상태에서 다른 복제 요청은 큐에 대기하고 이전 요청이 끝나기 전까지 복제를 시작할 수 없음
      • repl-diskless-sync-delay 이 옵션 값만큼 복제를 대기하고 대기하는 동안 다른 복제 요청이 들어올 경우에는 대기시간이후 동시에 복제를 시작함
  • 복제연결이후
    • 마스터에서 클라이언트가 요청한 커맨드를 실행하고 클라이언트로 응답
    • 이후 비동기로 슬레이브 노드로 커맨드 전달 
    • 마스터에서 요청처리 완료후 슬레이브로 전달하기전 마스터가 비정상 종료될 경우 슬레이브에 데이터가 유실될 수 있음

복제 데이터 일관성

  • 모든 레디스 인스턴스는 replication id를 갖고있음
  • 복제그룹의 replication id는 모두 동일함
  • INFO Replication command : replication id와 오프셋을 기준으로 마스터와 복제본간의 일치 상태를 확인

 

Partial resynchronization

  • 복제 연결이 일시적으로끊길 경우 재 연결시 RDB 파일을 새로 만들어 동기화 하는것은 비효율적임
  • 백로그 버퍼를 이용하여 복제가 끊긴 시점부터 부분적으로 복제하도록 하는 기능 (PSYNC)
  • 백로그 버퍼에 재 연동하려는 오프셋이 존재하지 않거나 replication id가 마스터와 다를 경우 에는 full resync 시도
    • repl-backlog-size : 백로그 버퍼 사이즈 설정
    • repl-backlog-ttl : 이 시간이 경과하면 삭제
  • 복제본은 마스터로 승격될 가능성이 항상 존재하기 때문에 백로그 버퍼를 해제 하지 않음

 

복제 그룹에서 새로운 마스터가 선출 될 경우

  • 기존 마스터가 비정상 종료되고 복제본 중 새로운 마스터가 선출될 경우 replication id는 새로 생성됨
  • 이 경우 승격되지 않은 다른 복제본은 새로운 마스터와 복제 연결을 하게 됨
  • 두 노드는 동일한 마스터로부터 복제 중이었으므로 Partial resynchronization이 가능함
  • 이를 위해 secondary replication id가 필요 함
  • secondary replication id : master_replid2 속성에 기존의 replid가 저장되고 복제 요청 시 마스터로 전달하여 partial resync가 가능

복제본 계층 구조

  • A - B - C 
  • master 노드 A를 B 노드가 복제하고 C 노드는 B 노드로부터 복제하는 경우 
  • B 노드에만 데이터 변경이 발생하더라고 C 노드로 복제 되지는 않음
  • A 와 C 노드는 동일한 데이터로 복제됨

유효하지 않은 복제본

  • replica-serve-stale-data=yes(default) 옵션으로 처리방식 결정
  • 기본적으로 유효하지 않은 경우에도 클라이언트로 데이터를 전달
  • no 로 설정할 경우 Sync with master in progress 라는 오류 반환

고려사항

  • 슬레이브가 마스터의 상태를 모를 경우 데이터 유실이 발생할 수 있음
  • 네트워크 / 디스크 용량 등의 문제로 복제가 실패하는 경우에 대한 모니터링과 처리
  • 백업 기능을 사용하지 않는 경우 

센티널

마스터와 슬레이브 노드의 상태를 모니터링하여 문제 발생 시 자동으로 failover 를 진행 시킴

모니터링, 자동failover, 클라이언트에 인스턴스 구성정보 전달

 

동작방식

  • sentinel.conf 파일에 port, sentinel monitor 마스터노드명 쿼럼값 등을 입력 
    • 복제노드는 자동으로 찾으므로 따로 설정하지 않음
  • redis-sentinel sentinel.conf / redis-server sentinel.conf --sentinel 두가지 명령어중 하나로 인스턴스를 띄움
  • 최소 3대이상의 독립적인 인스턴스를 실행(SPOF 방지)
  • redis-cli -p 26379(일반적으로 레디스 포트에 2를 붙임) 로 접속
    • SENTINEL master 마스터명 을 이용하여 마스터 정보를 조회, 데이터는 조회할 수 없음
    • SENTINEL replicas 마스터명 을 이용하여 복제본 정보 조회
    • SENTINEL sentinels 마스터명 을 이용하여 센티널 정보 조회
    • SENTINEL ckquorum 마스터명 을 이용하여 쿼럼 정보 조회
  • 인스턴스 정보전달
    • 클라이언트가 센티널에 레디스 구성 정보 요청을 보내면 구성정보를 응답으로 주고 클라이언트는 마스터 노드에 직접 접속하여 커맨드 처리
  • failover
    • down-after-milliseconds 에 정의된 시간안에 ping에 대한 응답이 오지 않으면 마스터가 다운됐다고 판단. 상태를 sdown(subjectly down)으로 변경
    • 다른 센터널 서버들에 장애 상황 전달 (SENTINEL is-master-down-by-addr ip port epoch)
    • 다른 센티널 서버로부터 장애상황 응답을 받고 쿼럼값이상으로 장애 상황이 인지되면 odown(objectly down) 으로 상태 변경
      • 복제본이 다운된경우에는 sdown 상태만 관리
    • epoch(failover 버전) 값 증가, 새로운 failover 처리시 마다 1씩 증가
    • epoch값을 증가시킨 센티널은 센티널 리더 선출 요청. 증가시킨 epoch 값을 같이 전달
    • 복제본 선정
      • redis.conf 파일에 명시된 replica-priority 가 낮은
      • 마스터로부터 더 많은 데이터를 수신한 복제본
      • 모두 동일하다면 runId 사전순으로 작은 복제본
    • 선정된 복제본에는 slaveof no one 수행 기존 마스터로부터 복제 연결 중단
    • 복제 연결 변경 replicaof newip newport

클러스터

레디스 클러스터는 여러 대의 레디스 노드를 하나의 분산 시스템으로 관리하는 기술로, 데이터의 분산 저장(샤딩)과 고가용성 제공

 

  • 데이터 샤딩, 자동 failover 등을 통한 고가용성, 확장성 확보
  • 데이터 샤딩
    • 마스터 최대 1000개까지 확장 가능
    • 레디스 자체 관리
    • 키가 할당된 마스터 노드로 연결 리디렉션
    • 클라이언트는 클러스터 내에서 특정 키가 어떤 마스터에 저장돼 있는지 캐싱
    • 모든 데이터는 해시슬롯에 저장, 총 16384개
    • 해시슬롯 = CRC16(key) mod 16384 
    • 다중키는 여러개 노드의 해시슬롯에 저장되어 있을 수 있으므로 다중키 커맨드는 사용 불가. 
      • 해시태그를 이용하여 처리가능
  • 고가용성
    • 클러스터 구성에 속한 노드는 서로를 모니터링 함
    • 자동 failover
    • 클러스터 버스를 이용한 독립적인 통신, cluster_bus_port 값을 지정하지 않으면 10000을 더한 16397 포트를 사용

failover

마스터 노드에 장애가 발생할 경우 해당 마스터의 복제본은 다른 마스터 노드들에게 페일오버 시도에 대한 투표 요청

과반수 이상 마스터 노드들에서 투표를 받는 경우 해당 복제본은 마스터로 승격

 

마스터로 승격되어 슬레이브 노드가 없을때 다시 장애가 발생할 경우 cluster-require-full-coverage 에 따라 처리

  • yes (default) : 데이터의 정합성을 위해 클러 스터 전체 상태 fail 
  • no : 장애가 발생한 노드만 에러 처리

자동 복제본 마이그레이션

  • cluster-allow-replica-migration : yes
  • cluster-migration-barrier : 1 ( 마스터가 가져야할 최소 복제본 수)
  • 가장 많은 복제본이 연결된 마스터노드의 복제본 중 하나를 사용

실행

  • redis.conf 파일에 cluster-enabled = yes 로 설정
  • redis-cli -cluster create host:port host2:port --clusteer-replicas 1
  • cluster nodes 명령어로 상태 확인

동작방식

  • 하트비트패킷을 이용하여 노드들간의 상태 확인
    • epoch : 버전
    • 비트맵 : 해시슬롯 비트맵
    • 플래그 : 마스터/복제본
    • 클러스터 상태 : down/ok
    • 가십섹션 : 클러스터내의 다른 노드 정보
  • 해시슬롯 구성 전파
    • 하트비트패킷
    • 업데이트메시지 : epoch 값이 오래된 경우 업데이트 메시지를 보내 해시슬롯 구성 업데이트
    • 해시슬롯 구성의 변경은 failover / 리샤딩 중에만 발생
  • 노드핸드셰이크
    • 방향성이 없는 cluster meet 커맨드를 보내고 이를 받은 노드는 자신이 알고 있는 다른 노드들에 전파
    • 이를 통해 이미 알고 있는 노드들끼리만 통신
    • 클러스터 내부의 모든 노드들은 풀 메쉬 연결
  • 리디렉션
    • MOVED : 클라이언트가 요청한 커맨드를 처리할 키의 해시슬롯이 현재 노드가 아닐때 적절한 노드의 정보로 리턴, 해시슬롯 맵 갱신
    • ASK : 현재요청은 리디렉션하지만 이후 요청은 현재 노드로 요청. 해시슬롯 맵 갱신 안함
  • failover
    • NODE_TIMEOUT 시간보다  오랫동안 ping에 대한 응답을 받지 못한 경우 노드 유형에 관계없이 PFAIL(Possible failure) 상태로 플래그
    • 클러스터내의 다른 노드가 보낸 하트비트패킷에서 PFAIL/FAIl 알림을 받는 경우 FAIL로 플래깅
    • 복제본은 마스터 상태에 따라 직접 failover 시도
      • 마스터가 FAIL 상태
      • 마스터가 1개 이상의 해시슬롯을 갖는 경우
      • 마스터와 복제가 끊어진지 설정된 값보다 오래된 경우
    • 클러스터 내의 마스터 노드에 투표요청(FAILOVER_AUTH_REQUEST)
    • FAILOVER_AUTH_ACK 패킷으로 투표
      • 동시에 다른 복제본 승격을 방지하기 위해 NODE_TIMEOUT*2 시간동안 같은 마스터의 복제본에게 투표 금지
      • 작은 epoch 값의 AUTH_ACK는 무시
    • NODE_TIMEOUT*2 시간 동안 과반수 이상의 마스터로부터 ACK 오는 경우 FAILOVER 진행
      • 응답이 오지 않아 중단된 경우 NODE_TIMEOUT*4 지연후에 재 투표 시도 가능

모니터링

  • redis_exporter
  • node_exporter
  • alertmanager
  • prometheus
  • grafana

+ Recent posts