728x90
반응형

[오류 상황]

기존에 서비스를 EC2에서 사용하다가 자원에 따른 비용이 너무 비싸져서 NCloud로 옮기면서 Kafka의 브로커를 3개에서 1개로 축소하였습니다. 해당 변경사항을 반영하기 위해 각 topic에 대한 replicationFactor도 모두 1로 바꾸었는데 계속해서 아래와 같은 replicationFactor가 3으로 설정되어있다는 오류가 발생하였습니다.

[2024-07-18 06:00:55,185] INFO [Admin Manager on Broker 1]: Error processing create topic request CreatableTopic(name='__transaction_state', numPartitions=50, replicationFactor=3, assignments=[], configs=[CreateableTopicConfig(name='compression.type', value='uncompressed'), CreateableTopicConfig(name='cleanup.policy', value='compact'), CreateableTopicConfig(name='min.insync.replicas', value='2'), CreateableTopicConfig(name='segment.bytes', value='104857600'), CreateableTopicConfig(name='unclean.leader.election.enable', value='false')]) (kafka.server.ZkAdminManager) org.apache.kafka.common.errors.InvalidReplicationFactorException: Replication factor: 3 larger than available brokers: 1.

[원인 및 해결]

Kafka가 원래 JVM 기반 자바 애플리케이션이라 자바와 호환성이 좋은데, Spring @Transactional 을 사용해서 카프카 메시지 produce를 하게 되면 이 과정 자체를 트랜잭션 처리를 하게 됩니다. (저도 유저 생성 시 Kafka 메시지를 보내기 위해 DB 저장 로직과 Kafka 전송 로직을 트랜잭션 처리하였습니다)

이때 카프카 내부적으로 사용하는 토픽이 _transaction_state 이고, 이 토픽의 기본 replication_factor가 3입니다.

Kafka의 _transaction_state 토픽은 트랜잭션 기반 메시징 시스템에서 중요한 역할을 합니다. 이 토픽은 Kafka 브로커가 트랜잭션의 상태를 관리하고 유지하는 데 사용됩니다.

(default로 3으로 설정되어있기 때문에 계속해서 오류가 생겼던 것입니다)

_transaction_state 가 하는 역할

Kafka의 트랜잭션 기능을 이해하려면 _transaction_state 토픽의 역할과 작동 방식을 이해하는 것이 중요합니다.

Kafka 트랜잭션 개요

Kafka 트랜잭션은 메시지의 일관성과 원자성을 보장합니다. 이를 통해 메시지 생산자와 소비자가 특정 작업을 완전히 완료하거나 전혀 완료하지 않는 트랜잭션을 수행할 수 있습니다.

Kafka의 _transaction_state 토픽은 트랜잭션 상태를 관리하고 유지하는 핵심 구성 요소입니다. 이 토픽은 트랜잭션의 상태를 저장하고, 복구 과정을 지원하며, 트랜잭션의 일관성을 보장합니다. 이를 통해 Kafka는 높은 신뢰성과 일관성을 요구하는 애플리케이션에서도 안전하게 사용할 수 있습니다.

_transaction_state 토픽의 역할

  1. 트랜잭션 상태 저장: _transaction_state 토픽은 각 트랜잭션의 상태 정보를 저장합니다. 여기서 의미하는 상태정보는 트랜잭션의 시작, 커밋, 중단 등의 상태가 포함됩니다. 이 정보는 Kafka 브로커가 트랜잭션을 추적하고 관리할 수 있도록 합니다.
  2. 복구: 브로커가 실패하거나 재시작될 때, _transaction_state 토픽의 데이터를 사용하여 트랜잭션 상태를 복구합니다. 이를 통해 시스템은 일관된 상태를 유지할 수 있습니다.
  3. 코디네이터: Kafka는 트랜잭션 코디네이터를 사용하여 트랜잭션을 관리합니다. 코디네이터는 _transaction_state 토픽을 사용하여 트랜잭션의 현재 상태를 기록하고, 각 트랜잭션의 상태를 유지합니다. 이는 트랜잭션의 커밋 또는 중단을 처리할 때 중요합니다.

트랜잭션 상태 전이

트랜잭션은 여러 상태를 거치며 전이합니다.

  1. Empty: 초기 상태로, 트랜잭션이 아직 시작되지 않은 상태.
  2. Ongoing: 트랜잭션이 시작되고, 메시지가 생산되고 있는 상태.
  3. PrepareCommit: 트랜잭션이 커밋 준비 중인 상태.
  4. PrepareAbort: 트랜잭션이 중단 준비 중인 상태.
  5. CompleteCommit: 트랜잭션이 성공적으로 커밋된 상태.
  6. CompleteAbort: 트랜잭션이 성공적으로 중단된 상태.

[시나리오]

  1. 트랜잭션 시작:
    • 프로듀서가 트랜잭션을 시작합니다.
    • 트랜잭션 코디네이터가 _transaction_state 토픽에 트랜잭션 ID와 함께 상태를 기록합니다.
  2. 메시지 생산:
    • 프로듀서는 여러 메시지를 생성합니다.
    • 각 메시지는 트랜잭션 ID와 함께 기록됩니다.
  3. 트랜잭션 커밋:
    • 프로듀서가 커밋을 요청합니다.
    • 트랜잭션 코디네이터가 _transaction_state 토픽에 상태를 PrepareCommit으로 업데이트합니다.
    • 모든 메시지가 성공적으로 기록되면 상태를 CompleteCommit으로 변경합니다.
  4. 트랜잭션 중단:
    • 프로듀서가 중단을 요청하거나 오류가 발생합니다.
    • 트랜잭션 코디네이터가 상태를 PrepareAbort로 업데이트합니다.
    • 모든 메시지가 무효화되면 상태를 CompleteAbort으로 변경합니다.
728x90
반응형
728x90
반응형

작성한 docker-compose 파일은 다음과 같다

version: '3.1'
services:
  zookeeper:
    container_name: zookeeper
    image: wurstmeister/zookeeper
    ports:
      - "2181:2181"
  kafka:
    image: wurstmeister/kafka
    container_name: kafka
    ports:
      - "9092:9092"
    environment:
      KAFKA_ADVERTISED_HOST_NAME: kafka
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
    depends_on:
      - zookeeper

zookeeper → 2181:2181

kafka → 9092:9092

docker-compose 실행

docker-compose -f docker-compose-single-broker.yml up -d

docker-compose 종료

docker-compose -f docker-compose-single-broker.yml down

kafka container에 들어가서 /opt/kafka_2.13-2.8.1/bin 으로 들어가면 사용할 수 있는 명령어들을 확인할 수 있다.

kafka-topics.sh --create --zookeeper zookeeper:2181 --replication-factor 1 --partitions 1 --topic quickstart

해당 명령어로 토픽 만들어서 실행해봄

kafka UI Tool 사용

Kafka-UI Tool 을 이용하여 Kafka 관리하기

UI for Apache Kafka 사용

version: '2'
services:
  kafka-ui:
    image: provectuslabs/kafka-ui
    container_name: kafka-ui
    ports:
      - "8989:8080"
    restart: always
    environment:
      - KAFKA_CLUSTERS_0_NAME=localhost
      - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092
      - KAFKA_CLUSTERS_0_ZOOKEEPER=zookeeper:2181
 

GitHub - provectus/kafka-ui: Open-Source Web UI for Apache Kafka Management

Open-Source Web UI for Apache Kafka Management. Contribute to provectus/kafka-ui development by creating an account on GitHub.

github.com

현재 접속 가능한 URL

http://172.10.40.152:8989/

(이 주소는 현재 회사 VPN으로 연결된 서버라서 본인 우분투 서버의 public ip에 8989 포트를 이용해서 열면된다.)

[Brokers 화면]

[Topics 화면]

UI로도 만들 수 있는데, 실제 컨테이너에 들어가서 명령어를 이용해서 topic을 생성할 수도 있다.

[topic 생성해보기]

docker exec -it kafka /bin/bash

cd /opt/kafka_2.13-2.8.1/bin

kafka-topics.sh --create --zookeeper zookeeper:2181 --replication-factor 1 --partitions 1 --topic test

Topic 생성된 것 확인 가능

[Topic으로 신호 주고받기]

test topic 듣고 있는 producer를 하나 만듬

kafka-console-producer.sh --topic test --bootstrap-server kafka:9092

그리고 hi, hello, yo-man을 차례대로 보냈다.

UI에서 해당 메시지가 온것을 확인할 수 있다. (아니면 직접 consumer를 명령어로 실행시켜서 확인할 수도 있다.)

kafka-console-consumer.sh —topic tet —from-beginning —bootstrap-server kafka:9092

카프카를 사용하는 서비스(Store Service) 배포

docker build --platform linux/amd64 -t kimtaeheon/storeservice:1.0 .

docker save -o store-service.tar kimtaeheon/storeservice:1.0

scp store-service.tar root@{harbor주소}:/root/kth

docker load -i /kth/store-service.tar

docker tag kimtaeheon/storeservice:1.0 msa.harbor.com/library/storeservice:1.0

docker push msa.harbor.com/library/storeservice:1.0
apiVersion: apps/v1
kind: Deployment
metadata:
  name: store-deployment
  labels:
    app: store
spec:
  replicas: 1
  selector:
    matchLabels:
      app: store
  template:
    metadata:
      labels:
        app: store
    spec:
      containers:
      - name: store
        image: msa.harbor.com/library/storeservice:1.0
        imagePullPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  name: store-service
spec:
  clusterIP: 10.104.193.24
  selector:
    app: store
  ports:
    - port: 8080
      protocol: TCP
      targetPort: 8080
 

오류 1 : kafka 연결이 안되는듯..

java.net.UnknownHostException: kafka

해결 : kafka docker-compose에서 KAFKA_ADVERTISED_HOST_NAME 속성 수정

version: '3.1'
services:
  zookeeper:
    container_name: zookeeper
    image: wurstmeister/zookeeper
    ports:
      - "2181:2181"
  kafka:
    image: wurstmeister/kafka
    container_name: kafka
    ports:
      - "9092:9092"
    environment:
      KAFKA_ADVERTISED_HOST_NAME: 172.10.40.152
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
    depends_on:
      - zookeeper
728x90
반응형
728x90
반응형

이전 상태

이전까지 상태

  • Deployment
    • API Gateway
    • Discovery
    • Config
  • Service
    • NodePort : API Gateway
    • NodePort : Discovery

[이전까지 완료한 상태]

API Gateway는 30010으로 열려있고, Discovery Service는 30007번, Config Service는 30008번으로 열려있다.
마이크로서비스인 User-service API를 요청하려면 API Gateway를 통해서 Discovery Service에 등록된 User Service API를 요청해야 한다.
즉, API Gateway의 IP로 /user-service/** 요청이 들어오면 User-Service API로 요청을 보내라는 의미이므로 API Gateway가 라우팅을 통해 User-service를 요청하는지 확인해야 된다.

(그러기 위해서 User-service에 간단한 API 인 /health_check 를 만들어둔 상태)

해결 과정

1. [Discovery Service를 제외하고 API Gateway와 User-Service 사이에는 문제가 없는지 확인]

먼저, Discovery Service에 등록해주지 않고 API Gateway가 User-Service API로 라우팅을 해주는지부터 확인을 해주었다.
그래서 API Gateway에 라우팅되는 주소에 서비스 이름인 USER-SERVICE 대신에 user-service의 ip주소를 넣어주었다.

실패 : User-Service의 NodePort를 만들어서 해당 URI를 API-Gateway에 등록

User-Service의 IP를 등록하기 위해서 NodePort를 만들고, NodePort의 해당 IP를 API Gateway에 등록하였다. 예를 들어, 쿠버네티스의 IP가 1.1.1.1 이고 NodePort를 30011 번 port를 열었다면 아래와 같이 API Gateway가 라우팅 하게 해주었다. 하지만 {API-Gateway 주소}/user-service/health_check 로 보낸 요청은 실패했다.

  cloud:
    gateway:
      routes:
        - id: user-service
          uri: <http://1.1.1.1:30011>
          predicates:
            - Path=/user-service/**

[실패 이유]
NodePort의 개념을 정확히 몰라서 그랬던 것이다. NodePort는 ClusterIP를 뚫고, 해당 포트를 외부로 연결해주는 포트일 뿐이다. 따라서 외부에서 NodePort로 접속하면 ClusterIP를 거쳐 Pod로 통신이 가능하도록 해주는 것이다.
그러나 API Gateway는 외부 NodePort를 등록하면 안된다. 쿠버네티스의 통신은 내부에서 일어나므로 API Gateway에는 ClusterIP를 등록해주어야 한다.

해결 : User-Service에 Cluster IP를 만들어서 해당 URI를 API-Gateway에 등록

Service 종류를 ClusterIP로 만들어서 해당 ClusterIP의 URI를 등록해준다. 작성한 user의 Deployment와 Service는 다음과 같다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: users-deployment
  labels:
    app: users
spec:
  replicas: 1
  selector:
    matchLabels:
      app: users
  template:
    metadata:
      labels:
        app: users
    spec:
      containers:
      - name: users
        image: msa.harbor.com/library/usersservice:1.0
        imagePullPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: users
  ports:
    - port: 46279
      protocol: TCP
      targetPort: 46279

이렇게 한 뒤 API Gateway IP로 User Service API에 요청한다면 요청이 성공한다.
그럼 API Gateway와 User Service 사이에는 문제가 없다는 의미이다.

2. API Gateway와 User-Service를 Discovery Service에 등록해서 확인

API Gateway가 정상적으로 마이크로서비스를 라우팅한다는 사실을 알았으니, Discovery Service에 API Gateway와 User-service를 등록한 뒤, 요청을 해보았다.
Eureka 페이지는 NodePort로 30007번으로 열었기 때문에 해당 화면에서 연결된 서비스들을 확인할 수 있었다.

USER-SERVICE의 URL에 마우스 커서를 올리면 연결된 곳이 나온다.
나는 USER-SERVICE의 애플리케이션(jar)은 0번 포트로 열었기 때문에 랜덤 포트로 연결되어 있다. 그래서 해당 User-service는 Spring Cloud Gateway에 등록해두었다.
http://{API Gateway IP 주소}/user-service/health_check
문제는 Discovery Service가 User Service가 열린 포트로 연결해서 호출해줘야하는데, 계속해서 user-service에 등록한 {spring.application.instance_id} 으로 연결되는게 문제였다.
로그를 확인해 본 결과 Discovery Service가 이상한 값인 spring.application.instance_id 이 값을 보고 있는데 이런 값은 등록한 적이 없으니 요청이 되지 않고 500 에러가 계속해서 발생했다.
예를 들어, users-deployment-65db46d67b-gtn8l 로 계속해서 연결되었다. 이 값은 쿠버네티스에서 Deployment를 만들고 뒤에 랜덤 해시값을 붙여서 배포하기 때문에 저런 이상한 값이 나오게 되는 것이다. 당연히 이 주소로 들어가면 연결이 되지 않는다.
내가 원하는 것은 http://{API Gateway}/user-service/health_check 로 호출하면 http://{user-service}/health_check 로 요청되는 것을 원한다.

3. User-Service 앞에 ClusterIP를 만들어줘서 ClusterIP → Pod

Discovery Service가 User-Service의 Pod를 등록하고 있어서 문제가 생기고 있다고 생각하여, ClusterIP를 User-Service 앞에 두어서 API Gateway가 Pod가 아닌, ClusterIP를 등록하도록 해서 이어줄려고 하였다.
(그럼 API Gateway는 User의 ClusterIP를 통해서 Pod이름이 뭐든 간에 User service의 API를 요청할 수 있을것이니까)
그런데 ClusterIP를 User-Service로 등록해두어도 ClusterIP가 등록되지 않고 계속해서 Discovery Service는 User Service의 Pod를 등록하고 있었다. 즉, ClusterIP가 사용되지 않고 있었다.

방안 생각

‘Discovery Service에 ClusterIP의 주소를 직접 등록하면 되지 않을까’ 라고 알려주셔서 해당 방법으로 해결하였다.

즉, 직접 Pod를 등록하지 않고 User-SVC(쿠버네티스 객체인 서비스를 의미, ClusterIP)를 Discovery Service에 등록해준다.

이유

해당 방법이 된다고 생각한 이유는 쿠버네티스의 마스터에서 User의 ClusterIP를 통해서 API를 Discovery Server로 curl 요청을 했을 때 성공하였다.
curl [http://{쿠버네티스 주소}:30007/eureka](<http://172.10.40.174:30007/eureka>) → 성공

해결 방법 1 : eureka.instance.homePageUrl 사용 (실패)

11. Service Discovery: Eureka Clients

 

11. Service Discovery: Eureka Clients

Service Discovery is one of the key tenets of a microservice-based architecture. Trying to hand-configure each client or some form of convention can be difficult to do and can be brittle. Eureka is the Netflix Service Discovery Server and Client. The serve

cloud.spring.io

User-Service에 eurkea.instance.homePageUrl 에 ClusterIP를 등록하면 해결 가능할 것이라 생각해여기에 등록했는데 결과적으로 잘 되지 않았다.

해결 방법 2 : prefer-ip-address=true , ip-address={ClusterIP 주소}

Eureka InstanceConfigBean에서 IpAddress 지정하는 부분만 찾아야한다.
그리고 해당 내용을 설정 파일에 등록해주면 된다.

eureka:
  instance:
  # home-page-url: <http://10.100.225.34>
    instance-id: ${spring.cloud.client.hostname}:${spring.application.instance_id:${random.value}}
    prefer-ip-address: true
    ip-address: {User-service의 Cluster IP 주소}

이렇게 변경해서 배포하면 성공한다.
http://{API gateway:NodePort}/user-service/health_check 를 하면 잘 요청이 되는 것을 확인할 수 있다.

 

ClusterIP에서 IP 고정

지금은 동적 할당이기 때문에 ClusterIP의 spec에서 ClusterIP를 고정할 수 있다.

apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  clusterIP: 10.104.193.23

바꾸고 kubectl replace -f users-service.yml --force 적용

728x90
반응형
728x90
반응형

1. 로컬에서 만든 도커 이미지를 private registry인 하버 서버에 올리기

프라이빗 클라우드를 사용하기 때문에 Docker Hub와 같은 Public Image Registry를 사용할 수 없었다. Private Registry인 하버의 IP 주소와 도메인 주소만을 알고 있었다.

따라서 Dockerfile을 사용해서 빌드한 이미지를 압축파일 형식인 tar 파일로 저장하여 배포해야 했다.

  • 도커를 tar 파일로 저장하기 : save , export 명령어 사용
  • tar 파일을 도커 이미지로 저장 : load, import 명령어 사용
  • save, load 같이 사용 → 도커 이미지 저장하고 로드, 해당 이미지는 원본 이미지와 동일
  • export, import 같이 사용 → 도커 컨테이너 저장하고 로드, 원본 이미지를 아카이빙하여 하나의 레이어로 저장된 이미지로 추출

save, load 명령어를 이용하여 tar 파일로 만든 뒤 harbor에 접속해서 이미지로 만들었다.

[로컬에서 해야할 일]

  1. Docker를 사용해서 빌드 파일 이미지로 생성
    1. docker build {사용자 이름}/{이미지 이름}
    2. docker build --platform linux/amd64 -t kimtaeheon/discoveryservice:1.0 .
  2. docker save -o ${파일명.tar}
  3. scp ${파일명.tar} root@${Harbor Server}:/root

[Harbor 서버에서 해야할 일]

  1. docker load -i ${파일명.tar}
  2. docker tag ${이미지} ${Harbor Domain}/${이미지}
  3. docker push ${Harbor Domain}/${이미지}

여기까지 하면 Harbor 서버에 내가 만든 이미지가 올라간다.

[쿠버네티스에서 Deployment와 Service manifest 파일 작성]

  1. Deployment는 Pod 생성을 위해
  2. Service는 NodePort로 외부에 노출시켜서 실행 확인을 위해 30007번으로 포트를 열었다
apiVersion: apps/v1
kind: Deployment
metadata:
  name: discovery-deploy
  labels:
    app: discovery
spec:
  replicas: 1
  selector:
    matchLabels:
      app: discovery
  template:
    metadata:
      labels:
        app: discovery
    spec:
      containers:
      - name: discovery-service
        image: msa.harbor.com/discovery-service
        ports:
        - containerPort: 8761
---
apiVersion: v1
kind: Service
metadata:
  name: discovery-service
spec:
  type: NodePort
  selector:
    app: discovery
  ports:
    - port: 8761
        protocol: TCP
      targetPort: 8761
      nodePort: 30007

여기까지 하면 원래 정상적으로 30007번 포트로 Eureka Server가 보여야 한다. (그런데.. ㅜㅜ)

문제 1. ImagePullBackOff

pod 상태에서 해당 문제가 생겼다. 말 그대로 이미지를 제대로 가져올 수 없어서 생기는 문제였다.

Harbor에는 정상적으로 이미지가 있는데 이 문제가 생기는 이유는 harbor는 http이고 쿠버네티스는 https 서버이기때문에 이미지를 가져올 수가 없었다.

harbor를 https로 바꾸는 방법도 있지만, 쿠버네티스에서 컨테이너 런타임 엔진으로 사용하고 있는 containerd 설정과 harbor dns를 등록해두었다.

1. containerd 설정 변경

: containerd의 설정은 /etc/containerd/config.toml 에 있다. (Docker는 /etc/docker )

이전에 설정된 것을 변경해야 하는데, insecure_skip_verify = true 로 설정해두어야 한다.

2./etc/hosts 파일에 하버 도메인 등록

/etc/hosts 파일은 호스트 이름과 IP주소를 매핑하는 로컬 호스트 파일이다. DNS를 사용하지 않고도 호스트 이름을 IP 주소로 해석할 수 있어서 내부망이나 개발 환경에서 별도의 DNS 서버를 구성하지 않고도 호스트 이름을 사용하여 다른 서버와의 통신을 지원할 수 있다.

해당 파일에 다음 내용을 추가한다.

127.0.0.1 localhost
"하버 IP 주소" "하버 도메인 이름" 

# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts

이렇게 적용하면 ImagePullBackOff 오류는 사라진다.

문제 2. CrashLoopBackOff

이번에 Pod를 확인한 결과 다음과 같은 오류가 발생했다. 해당 오류의 원인을 찾기 위해 kubectl 을 이용해서 컨테이너에 접속해보았다.

[문제 찾는 과정..]

1. 컨테이너 접속

kubectl exec -it discovery-deployment-7f88c84866-rqm4r -- /bin/bash

→ 컨테이너를 찾을 수가 없었다. 로그를 확인해보았다.

2. 로그 확인

kubectl logs discovery-deployment-7f88c84866-rqm4r

→ exec /usr/local/openjdk-17/bin/java: exec format error

이건 Dockerfile의 첫 라인이였는데 여기서부터 오류가 난 것은 이미지 생성 오류라고 생각하여 검색해보았다.

예전에도 발생했었던,, Mac M1에서 이미지 빌드하면 linux에서 오류가 생기는 문제였다.

[해결] Docker: exec /usr/openjdk-11/bin/java: exec format error

 

[해결] Docker: exec /usr/openjdk-11/bin/java: exec format error

m1 docker build

velog.io

그래서 이미지를 빌드할 때, docker build --tag dockerfile:0.1 . 이렇게만 하면 안되고 platform을 환경 변수로 줘야 한다.

docker build --platform linux/amd64 -t kimtaeheon/discoveryservice:1.0 .


여기까지 하면 Pod가 잘 생성되고 NodePort를 통해 외부 포트 30007번에서 접근 가능하다.

 

728x90
반응형
728x90
반응형

Service Discovery

ServiceDiscovery는 말 그대로 외부에서 다른 어떤 서비스들이 마이크로서비스를 검색하기 위해서 사용되는 개념이다.

우리가 가지고 있는 일종의 전화번호부(Service Registry)에서 다른 사람들의 전화번호(Microservice)를 찾는 것이라고 생각할 수 있다.

전화번호부 안에 들어간 정보는 key, value 형태로 저장이 되어 있다.

 

즉, 어떠한 서버가 어느 위치에 있는지, 어떤 서비스가 어떤 위치에 있는지 이런 것들을 등록하고 있는 정보가 Service Discovery라고 생각할 수 있다.

[ Service Registry 개념 ]

분산 환경 위에서 서로간의 서비스를 원격 호출하기 위해서는 각 서비스의 IP 주소와 PORT 번호를 알아야 한다.

MSA 어플리케이션의 경우 네트워크 주소가 동적으로 할당된다.
따라서 클라이언트가 서비스를 호출하기 위해서 찾는 매커니즘이 필요하다.

  • Service Registry는 Service Discovery를 하기 위하여 필요하다(Service Registry에 모두 등록되어 있기 때문에)
  • Service Registry는 사용가능한 서비스 인스턴스의 목록을 관리하고 서비스 등록, 해제, 조회 등을 할 수 있는 API 를 제공한다

MSA에서 Service Discovery

클라우드 환경이 되면서 IP 주소가 동적으로 생성되고 삭제되는 일이 잦아져서 서비스 클라이언트가 서비스를 호출할 때 서비스의 위치를 알아낼 수 있는 기능이 필요해졌다.

 

Service Discovery를 구현하는 디자인 패턴은 서비스 인스턴스의 네트워크 위치를 찾고 로드밸런싱하는 역할을 클라이언트가 담당하는 Client Side Discovery가 있고, 서버 쪽에서 디스커버리 로직을 구현한 방식인 Server Side Discovery 디자인 패턴이 있다.

Client Side Discovery

Service Client가 Service Registry에서 서비스의 위치를 찾아서 호출하는 방식이다.

장점

  • Server Side Discovery와 비교했을때 간단하다
  • 클라이언트가 사용 가능한 서비스 인스턴스에 대해서 알고 있기 때문에 각 서비스별 로드 밸런싱 방법을 선택할 수 있다

단점

  • 클라이언트와 서비스 레지스트리가 연결되어 있어서 종속적이다
  • 서비스 클라이언트에서 사용하는 각 프로그래밍 언어 및 프레임 워크에 대해서 클라이언트 측 서비스 검색 로직을 구현해야 한다

Server Side Discovery

호출이 되는 서비스 앞에 proxy 서버(로드 밸런서)를 넣는 방식이다.

서비스 클라이언트가 로드 밸런서를 호출하면 로드밸런서가 Service Registry로부터 등록된 서비스의 위치를 리턴하고, 이를 기반으로 라우팅하는 방식.

클라우드의 로드밸런서, AWS의 ELB나 구글 클라우드의 로드 밸런서들이 Server Side Discovery 방식으로 구현되어 있다.

쿠버네티스 환경에서 Server Side Discovery를 구현하기 편리하다.

Kubernetes 배포 환경에서 Server Side Service Discovery 구현방법

만약 public Cloud를 사용한다면 AWS에서는 AWS ELB를 제공해주고, GCP도 구글 로드밸런서를 제공해준다. 해당 서비스를 Service Discovery로 사용할 수 있다.

(private Cloud의 경우라면 MetalLB를 사용하면 된다.)

 

[kube-proxy와 ELB를 사용하는 예시]

1. 클라이언트는 해당 로드밸런서(kube-proxy)를 통해서 DNS 이름을 사용하여 ELB 에 요청을 보낸다.

2. ELB 에서는 Kube-DNS 서비스에게 DNS 이름으로 문의(query)를 요청

3. Kube-DNS 는 쿠버네티스의 저장소인 etcd 를 조회하여 호출하려는 서비스의 ip와 port 정보를 넘겨준다.

 

즉 Service registry 역할을 Kube-DNS(API 제공) 와 etcd(목록 관리) 가 나누어서 한다. 넘겨받은 정보를 ELB 에서 호출을 할때 Kube-proxy가 loadbalancer 역할을 하여 준다.

장점

  • discovery의 세부 사항이 클라이언트로부터 분리되어있어서 의존성이 낮다.
  • 분리 되어 있어 클라이언트는 단순히 로드 밸런서에 요청만 하기 때문에 각 프로그래밍 언어 및 프레임 워크에 대한 검색 로직을 구현할 필요가 없다
  • 일부 배포환경(AWS, GCP 등)에서는 이 기능을 무료로 제공한다

단점

  • 로드밸런서가 배포환경에서 제공되어야 한다
  • 로드밸런서가 제공되어있지 않다면 설정 및 관리해야하는 또 다른 고가용성 시스템 구성 요소가 된다(private cloud라면 metalLB)

Server-Side Discovery의 예로는 AWS Elastic Load Balancer(ELB), Kubernetes가 있다

Service Registry

서비스를 등록하는 Service Registry는 Service Discovery에서 사용할 서비스들을 모두 등록해 놓은 것이다.

Service Registry를 구현하는 가장 쉬운 방법은 DNS 레코드에 하나의 호스트명에 여러 개의 IP를 등록하는 방식이지만, DNS는 레코드 삭제 시 업데이트 되는 시간 등이 소요되기 때문에 적절하지 않다.

그래서 Service Registry에서는 제공되는 솔루션을 사용하는 방법이 있는데 쿠버네티스에서는 ZooKeeper나 etcd와 같은 서비스를 이용 가능하다.

 

쿠버네티스 배포 환경에서는 주로 Service Registry를 Kube DNS와 etcd를 사용하여서 구현한다.

또한, Service Discovery에 전문화된 솔루션인 Netflix의 Eureka나 Hashcorp의 Consul을 사용할 수도 있다.

[Spring Cloud Gateway + Spring Eureka Server를 선택한 이유]

프로젝트에서 프론트에서는 최대한 작업이 필요없게 구현하기 위해서 Client Side 작업이 필요없는 Server-Side Discovery를 선택하였다.

그리고 private Cloud에서 Server Side Discoery 디자인 패턴을 구현하려면 MetalLB와 같은 또다른 구성 요소를 포함해야 하는데 아직 MetalLB에 대한 이해도가 부족하고, Spring Cloud에서 제공해주는 서버 사이드 디스커버리 패턴으로 구현가능한 Spring Cloud Gateway를 Spring Eureka Server와 함께 사용하였다.

[ Service Cloud Gateway의 사용법 ]

사용법

  1. 일단 사용하려면 각각의 마이크로 서비스가 전부 다 자신의 위치 정보를 Netflix Eureka Server에다가 등록이라는 작업을 먼저 한다
  2. 마이크로서비스를 사용하고 싶은 클라이언트는 제일 먼저 자신이 필요한 어떤 요청 정보를 로드밸런서 아니면 API Gateway에다가 자신이 필요한 요청 정보를 전달하게 되면 요청 정보가 Service Discovery에 전달이 돼서 내가 필요한 정보가 어디에 있는지 묻게 된다.
  3. 그럼 서비스 디스커버리가 반환을 시켜주게 되면 사용자 요청 정보가 호출되고 결과값을 가져가게 된다.

서비스 디스커버리가 해주는 역할은 각각의 마이크로 서비스가 어디에 누가 저장되어 있으며 요청 정보가 들어왔을 때, 그 요청 정보에 따라서 필요한 서비스의 위치를 알려주는 것이다.

[ Eureka Server에 API-Gateway (spring cloud gateway) 가 등록된 화면 ]

 

728x90
반응형

+ Recent posts