728x90
반응형

발단

  • 데이터베이스 인스턴스를 띄워두고 rabbitmq 3개를 한 서버에 밀어넣어 사용중이었음
  • 그런데 계속 커넥션이 끊기는 것 같아, 분산 처리를 위해 각 서버에 띄우려고 했음
  • 그러다 보니, 집 ip 내부의 private ip에 있는 노드와 클라우드 위의 컨테이너를 어떻게 서로 하나의 클러스터로 묶을지 고민하게 됨

방법

  • tailscale 이라는 vpn 서비스를 통해 각 서버들을 하나의 가상 네트워크로 묶어버림
  • 이렇게 되면 가상 ip 가 발급되고, 같은 vpn 내부의 노드끼리는 찾아갈 수 있음
    • 외부에서 해당 ip 로는 당연히 못 찾아감

c500c552-1b39-4be4-99a2-6e74e26d4684.svg

  • 따라서 가상 네트워크로 묶을 모든 노드에 하나씩 설치하고 아래 방식으로 하나씩 인증해 묶으면 됨
curl -fsSL https://tailscale.com/install.sh | sh
  • 설치 완료 후 tailscale up 하면 브라우저로 인증 후 하나의 계정 아래에서 묶어 사용할 수 있게 됨

    sudo tailscale up
  • 가상 ip를 찾는 방

    • 100.xxx.xxx.xx 처럼 나옴
sudo tailscale status

포트 오픈

  • vpn 내부에서만 통신 가능하게 적용
    • public ip 혹은 포트포워딩이 필요 없음
    • NAT을 우회해 암호화 터널을 통과하기 때문
# Tailscale 인터페이스(tailscale0)를 통한 모든 접속 허용

sudo ufw allow in on tailscale0
  • rabbitmq 포트 설정
포트 번호 용도 설명
4369 EPMD (Erlang Port Mapper Daemon) 노드 네임 서버 (어느 노드가 몇 번 포트를 쓰는지 탐색)
25672 CLI & Inter-node Communication 노드 간 데이터 및 상태 동기화 핵심 포트
5672 AMQP Protocol 앱(클라이언트) 메시지 송수신 포트

컨테이너 설정

# 클라우드 서버 (rabbitmq-1) docker-compose.yml
services:
  rabbitmq-1:
    image: rabbitmq:3.13.3-management
    container_name: rabbitmq-1
    hostname: rabbitmq-1
    restart: always
    environment:
      - RABBITMQ_ERLANG_COOKIE=rabbitmqCookie
      - RABBITMQ_DEFAULT_USER=dong
      - RABBITMQ_DEFAULT_PASS=dong
    ports:
      - 15672:15672
      - 5672:5672
      - 4369:4369
      - 25672:25672
    extra_hosts:
      - "rabbitmq-2:192.168.0.11"
      - "rabbitmq-3:192.168.0.12"


# 개인 서버 2 (rabbitmq-3) docker-compose.yml
services:
  rabbitmq-2:
    image: rabbitmq:3.13.3-management
    container_name: rabbitmq-2
    hostname: rabbitmq-2
    restart: always
    environment:
      - RABBITMQ_ERLANG_COOKIE=rabbitmqCookie
      - RABBITMQ_DEFAULT_USER=dong
      - RABBITMQ_DEFAULT_PASS=dong
    ports:
      - 5672:5672
      - 4369:4369
      - 25672:25672
    extra_hosts:
      - "rabbitmq-1:192.168.0.10"
      - "rabbitmq-3:192.168.0.12"

클러스터 조인

docker exec -it rabbitmq-2 rabbitmqctl stop_app
docker exec -it rabbitmq-2 rabbitmqctl join_cluster rabbit@rabbitmq-1
docker exec -it rabbitmq-2 rabbitmqctl start_app
docker exec -it rabbitmq-3 rabbitmqctl stop_app
docker exec -it rabbitmq-3 rabbitmqctl join_cluster rabbit@rabbitmq-1
docker exec -it rabbitmq-3 rabbitmqctl start_app
  • 상태확인
docker exec -it rabbitmq-1 rabbitmqctl cluster_status
728x90
반응형

'데브옵스 devOps > Server' 카테고리의 다른 글

[STANDALONE] Standalone 프로그램이란?  (1) 2024.12.26
[파일전송] Rsync  (0) 2024.12.11
[배포전략] 무중단 배포란?  (1) 2024.10.05
728x90
반응형

Graceful Shutdown

  • 취소 가능한 컨텍스트 생성
    • 컨텍스트와 취소 함수가 리턴됨
    • 종료 시그널 감지 시 취소 함수 실행
ctx, cancel := context.WithCancel(context.Background()) defer cancel()

신호 감지

  • ctrl + c -> 운영체제에서 취소 신호(interrupt 신호)를 프로세스로 넘겨줌
  • signal.Notify는 신호를 감지하면 받아서 sigChan 이라는 채널로 넘겨줌
sigChan := make(chan os.Signal, 1) 
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

신호 감지 고루틴

  • 신호가 들어올때까지 sig := <- sigChan은 기다림
    • Buffered Channel 로 생각하면 될듯
  • 취소 신호(interrupt 신호)를 감지하면 signal.Notify로 sigchan 채널에 취소 신호를 넘김
  • sigChan 채널의 값을 sig 변수에 채널을 담고, 해당 블록은 해제됨
  • 로그를 찍은 후 컨텍스트 취소 메서드를 실행
go func() { 
    sig := <-sigChan 
    log.Printf("Received signal %s, shutting down...", sig) 
    cancel() 
}()

컨텍스트 완료 대기

  • 컨텍스트 취소 함수 (cancel())가 실행되기 전까지 대기 (블록)
  • 신호 감지 고루틴을 통해 취소 함수가 실행되면 대기중인 context의 채널이 닫힘
    • 이렇게 되면 해당 context를 사용중인 모든 함수들에 취소 신호가 전파됨
  • 컨텍스트가 취소되면 ctx.Done() 이 실행됨
<-ctx.Done()

완성본

  • 전체 코드와 흐름은 아래와 같다.
  • 취소 신호를 받아 컨텍스가 취소되기 전까지 실행할 함수를 넣어주면 된다.
    • 실행할 함수에도 취소 가능한 context(ctx) 를 넘겨줘서, Graceful 하게 고루틴을 종료시킬 수 있음

func main() {
    // 취소 가능한 context 생성
    ctx, cancel := context.WithCancel(context.Background())

    defer cancel() // 종료 취소 보장

    // 신호 채널 생성
    sigChan := make(chan os.Signal, 1)

    // 취소 신호 감지 시 채널에 넘겨주는 용도
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        sig := <-sigChan // 신호가 들어올 때 까지 대기. 신호가 들어오면 해제됨
        log.Printf("Received signal %s, shutting down...", sig)
        cancel() // context 취소 함수 실행
    }()

    // 실행할 함수
    lib.Download(ctx, url, fileName)

    <-ctx.Done()

    // 종료 로그 등, 종료
}
728x90
반응형
728x90
반응형

Channel

  • 채널은 고루틴 간의 데이터 파이프라인이다.
  • 종류는 Unbuffered channel과 buffered channel로 구분된다.
    • Unbuffered Channel: 동기적
      • 수신 채널쪽에서 송신될 때 까지 채널이 묶는다. 즉 Block을 시키고 대기한다.
      • 수신 채널이 준비되지 않다면 deadlock 에러가 발생한다.
    • Buffered Channel: 비동기적
      • 수신 채널이 데이터를 받을 준비가 되지 않더라도 지정된 버퍼만큼 데이터를 보내고 다음 작업을 수행한다.
  • main() 함수는 가장 메인으로 생성되는 Goroutine이다.

채널 예시1

  • 수신자는 goroutine으로부터 데이터가 채널을 통해 들어올 때 까지 대기한다.
func main() {
    channel := make(chan int) // 정수 타입을 받는 채널 생성

    go func() {
        channel <- 123 // 채널에 123을 보냄
    }()

    var i int

    // goroutine에서 데이터를 전송할 때까지 계속 대기
    i = <- channel // 채널의 123을 수신한다.
}

채널 예시2

  • 수신자와 송신자가 서로 기다리기 때문에, Goroutine 이 끝날 때 까지 기다리게 할 수 있음
func main() {
    done := make(chan bool)

    go func() {
        for i := 0; i < 10; i +=1 {
            fmt.Println(i)
        }

        done <- true
    }()

    <- done // 위 익명함수 Goroutine이 종료될 때까지 대기
}

Channel Buffering

Unbuffered Channel

  • 하나의 수신자가 데이터를 받을 때까지 송신자의 채널에 묶임
  • 즉 위에서 보여준 예시가 Unbuffered Channel 임
  • 만약 수신자가 준비되지 않았다면 에러 발생
    • Deadlock
func main() {
    channel := make(chan int)
    channel <- 1 // 수신 Goroutine이 없으므로 DEADLOCK
    fmt.Println(<- channel) // 별도의 Goroutine이 없으므로 DEADLOCK
}

Buffered Channel

  • 버퍼 채널을 만들어서 사용하면 수신자가 없어도 데이터를 보낼 수 있음
func main() {
    channel := make(chan int, 2) // 두개의 버퍼 채널 생성

    channel <- 10 // 수신자가 없더라도 보낼 수 있음

    fmt.Println(<- channel)

}

송수신 역할 분리

  • 채널은 기본적으로 송신과 수신 역할 전부 다 할 수 있다.
  • 그러나 특정 역할만 수행하게 정해줄 수 있다.
func main() {
    channel := make(chan string, 1) // Buffered Channel
    sendChannel(channel) // 전송
    receiveFromChannel(channel) // 수신
}

// 채널에 데이터 입력하는 역할만 수행
func sendToChannel(ch chan <- string ){
    ch <- "data"
    // data := <- ch 에러 발생
}

// 채널의 데이터를 수신하는 역할만 수행
func receiveFromChannel(ch <- chan string) {
    data := <- ch
    fmt.Println(data) // "data" 반환
}

채널 닫기

  • 채널을 닫은 이후 더 이상 해당 채널로 데이터 전송은 불가하다
  • 그러나 남은 데이터가 있다면 수신은 가능하다.
  • channel의 리턴값은 두개이다
    • 채널 내의 데이터 값과 수신 성공 여부
func main() {
    channel := make(chan int, 2)

    channel <- 1 // 채널에 데이터 전송
    channel <- 3 // 채널에 데이터 전송

    close(channel) // 채널 종료

    fmt.Println(<-channel) // 남은 데이터가 채널에 있다면 수신
    fmt.Println(<-channel) // 남은 데이터가 채널에 있다면 수신

    if _, success := <- channel; !success { // 채널에 남아있는 데이터가 없다면 false
        fmt.Println("데이터 없음")
    }

}
728x90
반응형

+ Recent posts