전송전략

  • 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

카프카는 메시지 브로커의 일종으로 Producer, Topic, Consumer, Broker 로 구성된다. 

Producer를 이용하여 이벤트를 브로커의 토픽으로 발행하고 토픽을 구독하고 있는 Consumer 가 이벤트를 처리하는 구조를 갖는다. 

 

Producer

특정 토픽으로 이벤트를 발행하는 역할

여러개의 프로듀서가 동시에 한토픽으로 이벤트를 발행할 수 있다. 

Serializer를 이용하여 JSON, Avro, Protobuf 등 다양한 이벤트를 발행할 수 있다. 

필수 설정

  • bootstrap.servers : 브로커 설정
  • key.serializer 
  • value.serializer

메커니즘

1. ProducerRecord 생성

  • Key 지정 : 키를 해싱하여 파티션을 정하는데 사용됨
  • Partition 지정 : 특정 파티션을 지정하여 전송
  • Vlaue

2. RecordBatch

  • 전송할 레코드는 파티션별로 RecordBatch 에 모이고 별도의 스레드가 전송

3. ProducerRecord 전송

  • 파티션이 지정되지 않았다면 파티셔너로 전송되고 파티셔너가 파티션을 지정함

4. 브로커 응답

  • 성공시 토픽, 파티션, 오프셋을 담은 RecordMetadata 객체를 리턴

전송방법

  • Fire and forget : 전송 후 성공/실패 여부를 신경쓰지 않음, 재시도 할 수 없는 경우 유실가능, 
  • Synchronous : Future 를 이용하여 처리
  • Asynchronous : 콜백 함수 이용

acks

  • 얼마나 많은 파티션 레플리카가 해당  레코드를 받아야 하는지 결정
  • acks=0 
  • acks=1 : 리더 레플리카가 메시지를 받는 순간 응답
  • acks=all : 모든 in-sync 레플리카에 전달된 후 응답

순서보장

  • 파티션내에서 순서는 보장됨
  • 설정에 따라 순서가 변경될 수 있음 

Broker

이벤트, Topic, Partition 등을 저장하고 관리하기 위한 서버

여러개의 브로커에 파티션을 복제하여 고가용성을 확보할 수 있다

 

Topic

이벤트를 관리하기 위한 단위 

여러개의 파티션으로 구성될 수 있다

 

Partition

이벤트를 저장하는 물리적인 단위이며, 파티션당 한개의 컨슈머를 갖을 수 있다. 

여러개의 파티션을 이용하여 멀티 컨슈머 처리가 가능하다. 

각각의 이벤트는 offset으로 관리된다. 

이벤트는 저장소에 리텐션주기에 따라 저장되어 보관된다. 

Offset을 이용하여 이전 데이터의 재처리가 가능해진다.

 

Offset

프로듀서가 발행한 이벤트는 한개의 오프셋을 갖는다. 

이벤트가 증가함에 따라 오프셋도 증가한다. 

컨슈머는 오프셋 단위로 이벤트를 처리한다. 

 

Consumer

오프셋별로 토픽에서 이벤트를 가져와 처리하고 오프셋을 커밋한다. 

이벤트의 형식에 따라 Deserialize 한다. 

일반적으로 컨슈머는 컨슈머 그룹에 속함

 

컨슈머그룹

  • 파티션수 > 컨슈머수 : 여러 파티션을 하나의 컨슈머에서 처리하게 됨
  • 파티션수 = 컨슈머수 : 각각의 파티션당 하나의 컨슈머가 처리
  • 파티션수 < 컨슈머수 : 유휴 컨슈머가 있음

 

리밸런스

  • 컨슈머에 파티션을 재할당 하는 작업
  • eager rebalance : 모든 컨슈머가 작업을 멈추고 파티션을 재 할당 받음
  • cooperative rebalance : 점진적인 재할당

메커니즘

1. subscribe topic

2. 폴링 루프

3. poll

4. deserialize

5. offset commit

 

파티션 할당 전략(PartitionAssigner)

  • Range
  • RoundRobin
  • Sticky
  • Cooperative sticky

오프셋 커밋

  • 중복처리 : 1 ~ 10까지 오프셋을 처리하는 중 10번까지 처리 되었으나 5번 오프셋까지만 커밋된 상태에서 리밸런싱이 일어나는 경우 6 ~ 10은 중복 처리됨
  • 유실 :  1 ~ 10까지 오프셋을 처리하는 중 10번 오프셋이 커밋된 상태에서 리밸런싱이 일어나는 경우 
  • 자동커밋
    • enable.auto.commit=true
    • auto.commit.interval.ms=5000(default)
  • 동기 커밋 : commitSync 사용, 브로커로부터 응답이 올때까지 블록됨, 실패시 재시도.
  • 비동기 커밋 : commitAsync 사용, 응답을 기다리지 않고 다음 커밋을 바로 처리함, 재시도 하지 않음. 커밋 순서가 꼬일 수 있음
  • 현재 오프셋 커밋 : 현재 오프셋 + 1 의 오프셋이 커밋됨

스탠드얼론 컨슈머

컨슈머 그룹에 속하지 않고 특정 토픽을 컨슘처리

1. partitionsFor(topic) : 토픽의 파티션 정보 획득

2. assign(partitions) : 파티션 할당

3. poll

4. commit

 

콘트롤러

KRaft 

복제

리더레플리카

팔로워레플리카

선호리더

in sync replica

out of sync replica

 

계층화된저장소

  • 각 계층별 리텐션 주기설정
  • 로컬계층과 원격계층 독립적인 읽기 가능. 원격계층의 메시지를 캐싱하거나 카피하지 않고 바로 네트워크 계층 전송가능
  • 로컬계층 : 브로커 로컬 디스크
  • 원격계층 : HDFS / S3 등 원격저장소

파티션할당

  • 브로커 간에 고르게 분산
  • 서로다른 브로커에 배치
  • 랙 구분
  • 라운드 로빈

파일관리

  • 세그먼트 단위로 관리. 파티션을 여러개의 세그먼트로 분리
  • 액티브 세그먼트 : 현재 쓰여지고 있는 세그먼트, 삭제 안됨

파일형식

  • 세그먼트는 하나의 파일로 저장
  • 프로듀서 - 브로커 - 컨슈머 까지 전달되는 동일한 형태로 저장(제로카피 최적화)
  • 버전별 형식으로 변환하여 컨슈머 전달 (FetchMessageConversionsPerSec, MessageConversionsTimeMs 지표 확인, 클라이언트 업데이트)

인덱스

  • 오프셋 세그먼트 인덱스 : 오프셋 - 세그먼트 파일 - 파일내 위치
  • 타임스탬프 오프셋 인덱스 : 카프카 스트림즈에서 자주 사용

보존정책

  • delete : 리텐션기간이 지난 데이터 삭제
  • compact : 가장 최근 상태만 보관
    • clean
    • dirty
  • delete,compact : 보존기간이 지난 compact 메시지도 삭제

대상

  • spring boot application
  • mysql (docker)

모니터링 구성

  • prometheus
  • grafana
  • mysql_exporter

prometheus docker 설치

  • prometheus.yml boot application 설정
global:
  scrape_interval: 15s
scrape_configs:
  - job_name: boot
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['ip:port']

ip : docker 내부에서 로컬 application 으로 접속 해야 하므로 localhost로는 접속이 안됨. ip 로 설정

port : application port

 

  • docker run
docker run -d -p 9090:9090 -v /파일경로/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus

 

  • prometheus 콘솔 접속
    • http://localhost:9090/

grafana docker 설치

docker run --name grafana -d -p 80:3000 grafana/grafana
  • 접속 : http://localhost/
  • prometheus 데이터 소스 설정
    • http://localhost/connections/datasources 접속 하여 add new data source 버튼 클릭
    • prometheus 선택 
    • Name 입력
    • Prometheus Server URL 입력 : http://host.docker.internal:9090
      • localhost 로 설정 시 아래와 같은 에러로 접속 테스트 실패 
      • Post "http://localhost:9090/api/v1/query": dial tcp 127.0.0.1:9090: connect: connection refused - There was an error returned querying the Prometheus API.
    • 화면 끝에 Save & Test 버튼 클릭
  • 대시 보드 구성

mysql_exporter docker 설치

  • mysql_exporter 용 mysql 사용자(test_exp) 생성
CREATE USER 'test_exp'@'localhost' IDENTIFIED BY 'test_exp' WITH MAX_USER_CONNECTIONS 3;
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'test_exp'@'localhost';
flush privileges;
  • /etc/mysql/my.cnf 파일 생성 (원하는 위치에 생성)
[client]
host=ip
port=3306
socket=/home/mysql.sock
user=test_exp
password=test_exp

 

  • docker run
    • mysql 이 docker로 떠 있는 상태에서 mysql.sock을 이용해 mysql_exporter가 mysql에 접속 해야 함
    • 따라서 mysql docker의 mysql.sock 을 mysql_exporter가 참조 하도록 해야 함
    • mysql docker의 mysql.sock은 /var/lib/mysql/mysql.sock 에 있지만, 심볼릭 링크로 걸려 있음 
    • 링크 : /var/lib/mysql/mysql.sock -> /var/run/mysqld/mysqld.sock
    • 링크 : /var/run -> /run
    • 결국 이 위치 : /run/mysqld/mysqld.sock
    • 이 파일을 mysql_exporter와 공유해야 함 
    • mysql docker 실행 시 -v /mornitoring/mysql_exporter:/run/mysqld 옵션을 주어 로컬의 /mornitoring/mysql_exporter 디렉토리로 공유하고
    • 이 디렉토리의 mysqld.sock 을 mysql_exporter와 공유하도록 함
docker run -itd -p 9104:9104 -v /etc/mysql/my.cnf:/home/.my.cnf -v /mornitoring/mysql_exporter/mysqld.sock:/home/mysql.sock --name mysql_exporter prom/mysqld-exporter --config.my-cnf=/home/.my.cnf
  • prometheus.yml mysql_exporter 설정 추가 후 prometheus 재시작
global:
  scrape_interval: 15s
scrape_configs:
  - job_name: mysql_exporter
    static_configs:
      - targets: ['ip:9104']
  • grafana 대시보드 구성

Prometheus 확인

  • http://localhost:9090/targets?search= 접속

 

 

 

 

20231004 ~ 06

10월3일 추석연휴의 마지막밤 중요한 일정을 잘 마무리하고 나니 여행이 가고 싶어져서 다음날 영월로 떠났다. 

급하게 가다보니 이번에는 혼자서 다녀오게 되었고, 혼자가는 여행이 외롭긴 했지만, 같이 하는 즐거움과 다른 즐거움이 있었다. 

2박 3일 일정으로 계획했고, 첫날은 영월까지 가는것으로 했다. 둘째날은 영월에서 목적지를 정했다. 

 

집에서 양만장까지 가서 잠깐쉬었는데 평일이라 양만장에도 라이더들이 많지 않았다. 

양만장

양만장에서 영월까지 한번에 가기는 힘들것 같아서 횡성쯤에서 쉬기 위해 카페를 검색했고 카페 토브라는 곳을 찾아서 갔는데 없어졌는지 찾을수가 없어 근처의 다른 카페인 그대쉴꽃 이라는 카페로 갔다. 손님도 나뿐이고 처음에는 음악소리도 안나오는 아주 조용한 카페였다. 내가 들어가니 조용한 음악을 틀어주셨다. 

그대쉴꽃

영월에 3시쯤 도착할듯하여 늦은 점심을 먹을 곳을 찾아봤는데 만두집이 백반기행에 나온 제천식당이라는 곳이 좋아 보여 가봤더니 문이 닫혀 있었다. 그래서 근처의 순대만드는집이라는 식당으로 가서 순대국을 먹었는데 사장님도 라이더 이시란다. 듀카티하고 수제 바이크를 갖고 있다고 하셨는데 엄청 오래타셔서 요즘엔 좀 지겨워서 안타신단다. 얼마나 타면 바이크가 지겨워질 수 있을까..

배를 채우고 소주와 간식거리를 사고 숙소로 향했다. 영월무인텔 이라는 숙소 인데 시설이 엄청 좋았다 주차장이 개별적으로 되어 있고, 셔터가 따로 다 있어서 완전 프라이빗한게 나만의 차고를 가진 느낌이었다. 퇴실하면서 사진을 찍으려고 했는데 까먹고 못 찍어 아쉽네

 

2일차에는 단양쪽으로 갈까 바다쪽으로 갈까 고민하다가 단양은 작년에 다녀와서 바다쪽으로 정하고 양양의 숙소를 예약했다. 고성까지 가려고 했는데 시간이 너무 늦어질거 같아 가보지 않았고 서핑으로 핫한 양양으로 정했다. 

아내가 영월의 메밀꽃 축제를 알려줘서 먹골마을이라는 곳으로 2일차 여행을 시작했다. 메밀꽃은 하얀색인줄 알았는데 핑크색이었다. 좀 더 확짝폈으면 더 이뻤을것 같다. 

먹골마을

아침은 영월에서 백반기행에 나온 어수리나물 전문점인 박가네로 갔고 만족스런 아침식사를 했다. 어수리나물밥은 처음 먹어봤는데 향이 좋고 반찬들이 많이 나와서 정말 맛있게 먹었다. 

박가네

배를 채우고 동강을 따라 유유자적 하기 위해 동강자연휴양림으로 향했다. 캠핑장도 있었는데 나무들이 작아서 내가 좋아하는 캠핑장은 아니었지만, 전망대에서 본 전망은 정말 멋있었고, 이 전망을 아내에게 보여주기 위해 페이스타임으로 연락을 시도 했지만, 연락이 안되어 동영상과 사진으로 전달했다. 

동강자연휴양림

여기서부터 동강을 따라 가고 싶어 네비를 요리조리 목적지를 바꿔봤는데 계속 돌아가는 길만 나오다가 정선초등학교 가수분교장을 찍으니 동강을 따라 가는길로 안내 되었다. 산과 절벽사이로 동강을 따라 가는길이 평온하고 웅장하고 멋있게 느껴졌다. 

가수분교장에서 양양으로 떠났는데 산길을 오르락 내리락 하면서 와인딩을 계속했더니 정말 힘들었던거 같다. 중간에 편의점 두군데를 들리고 백반기행에 나온 양양의 단양면옥이라는 곳에서 저녁을 먹고 숙소로 가서 쉬었다. 단양면옥도 사진이 없다. 맛은 평양냉면 처럼 국물이 맹숭맹숭해서 내 스타일은 아니었다. 

양양의 숙소는 오션뷰여서 새벽에 일출을 보기로 하고 일찍 잠들었다. 

에이트호텔 해돋이

양양에 왔으니 버거를 먹으라는 아내의 제안을 받아 들여 백반기행을 마무리 하고 MTD 버거라는 곳으로 아침을 먹으러 갔는데 오픈을 안했다. 네이버에는 10시부터 영업시작이라고 했는데 전화를 걸어보니 오늘은 12시쯤 오픈할것 같다고 해서 근처의 다른 식당에서 먹기로 하고 아쉬운 마음에 사진만 찍었다. 

아침은 근처를 돌다가 하조대평양면옥 이라는 곳에서 곰탕을 먹었다. 정말 정갈하게 나왔다. 맛도 괜찮았다.

하조대평양면옥 한우곰탕

집으로 돌아가는 길은 지난번에 미시령쪽으로 갔었기 때문에 한계령쪽으로 해서 가보기로 했다. 한계령 휴게소에서 잠깐쉬고 황만장을 들렸다 갈까 하다가 너무 늦어져서 RC79라는 라이더 카페에서 잠깐 쉬고 집으로 돌아왔다. 

한계령휴게소
RC79

거의 무계획으로 한 여행이라 시행착오도 많았지만 혼자여서 편하게 다녀왔다. 여행가서 책도 좀 많이 보려고 했는데 책을 많이 못본것이 약간 아쉽다. relive를 돌아오는 길에 재게를 안해서 돌아오는 길이 없어서 아쉽다.

 

 

9/2 ~ 9/3 

본네빌 바버, 본네빌 T120, 팻밥, 카렌스 참여

 

팻밥형님이 태안에 동생이 하는 펜션에 놀러가자고 하여 친한동생 둘과 같이 다녀왔다. 

팻밥형님 집으로 가서 먼저 조인하고, 동생들은 평택호 관광단지에서 접선하기로 했다. 동생한명은 아직 바이크가 없어 차를 타고 왔는데 바이크의 세계로 꼬시는 중이다. 

 

집 -> 평택호 관광단지 -> 간월암 -> 신두리 식당 -> 카페? -> 풍경펜션(1박) -> 우렁이박사 -> 카페49 -> 헤어름베이커리 

총 462km

사진을 안찍은곳이 많네.. 다음부턴 빼먹지 말고 찍어야 겠다. 

간월암 - 멀리서 보기만..

간월암갔다가 중간에 점심으로 신두리식당에서 조개찜을 먹었는데 식당 사진이 없네.. 조개찜도 맛있었는데 칼국수가 사골맛이 나면서 아주 맛있었다.

카페49 밥먹고 바로 갔는데 피자도 한판 뚝딱

커피와 피자를 먹고 숙소로 이동했다. 숙소는 태안에 있는 풍경펜션이이라는 곳인데 넓은 잔디밭에 앞은 바다가 보이고 분위기 좋은 카페와 전기 자전거까지 무료로 대여해 주는 리조트 같은 펜션이었다.

우렁이박사 쌈장집인데 세종류의 장과 제육을 추가해서 먹었는데 여기도 맛집
해어름 베이커리 고급지니 비싸다

해어름 베이커리에서 동생들과는 헤어지고 팻밥형님과 중간에 편의점에서 한번쉬고 비올까 두려워 부지런히 집으로 왔다. 

 

 

 

2023.08.05 ~ 06 1박 2일

할리타는 형과 본네빌 타는 동생과 속초로 박투어를 떠났다. 

우리는 양만장에서 만났는데 이른시간(6:30)에 나왔다고 생각했지만 이미 양만장 가는 길은 막히고 있었다. 나는 갓길을 잘 안타고 다녀서 막히는 데로 차와 같이 갔더니 20분 늦게 도착했다. 

셋이 모두 바이크에 밥을 주고 첫 번째 목적지인 미시령 옛길쪽으로 향했다. 

울산바위

속초 노메드 카페를 갔는데 자리가 없어서 근처 다른 카페로 가서 커피한잔하고 팥빙수 먹고 속초 라마다로 가서 좀쉬다가 회도 한접시 먹었다. 술마셔서 더이상 라이딩은 못하고 숙소에서 영화보고 쉬었다. 숙소가 공사장뷰라 했는데 막상가보니 반은 오션뷰였다

속초라마다

둘째날은 더덕식당에서 아침을 먹었는데 나물이 너무 맛있었다. 여기는 나중에 가족들과고 와봐야겠다.

황만장 들려서 커피 한잔하고 좀 쉬다가 남양주의 윌리앤잭에서 라이딩하지 않는 형을 만나기로 해서 갔다가 점심먹고 집으로 복귀했는데 복귀 길에 또 비를 맞았다. 

황만장

 

 

2023.07.22 ~ 07.24

매년 이맘 때 철원의 핀란드하우스 펜션으로 친한가족들과 여행을 간다. 올해는 T120타는 동생과 함께 바이크를 타고 갔다. 

동생집은 인천이라 파주 네이버후드에서 만났는데.. 오픈하지 않아서 잠깐 쉬고 바로 출발했다. 잠깐 쉬는동안에도 몇명의 라이더들이 헛걸음을 했다. 24일 돌아오는 길에도 들렸는데 역시 오픈하지 않았었다. 휴가인건지 쉬는날인건지 모르겠다. 

철원까지 가는길에 처음으로 크루즈를 이용해봤는데 세상 이렇게 편할수가 없다. 신세계를 경험했다.

2일동안 놀고 다시 돌아오는 길에는 같이 갔던 일행이 차에서 우리 동영상도 찍어주었다.

 

일행들과 라이더 카페 바잘트38.1 에도 들렸다. 

날씨가 덥긴 했지만 그래도 막히지 않는길이라 시원하게 달릴수 있었고 비가 올까봐 걱정을 많이 했는데 비도 오지 않았다. 

하지만, 시내주행은 매우 고통스러웠다. 기변이후로 자꾸 시동을 꺼먹었는데 시내주행에서 가다서다를 반복하니 시동꺼먹을까봐 신경이 쓰였고, 달리지를 못하니 엔진열의 열기도 더 크게 느껴졌다.

 

+ Recent posts