Search

서비스 디스커버리

목차

개요

서비스 디스커버리(Service Discovery)의 등장배경

클라우드 서비스 환경에서 수평 확장( auto-scaling)같은 이유로 서비스 인스턴스의 수가 동적으로 가감하는 상황에서 가감되는 서비스의 주소(IP…)를 감지하고 서비스의 헬스체크를 하여 이용이 불가능한 서비스를 삭제하는 등 이름 그대로 서비스 인스턴스를 발견하고 관리하는 역할이 필요해졌고, 이 때 Netflix에서 지난 7년간 Cloud System으로 서비스 이전을 하면서 쌓은 MSA기술이 오픈 소스로 풀린 것이 스프링 클라우드 넷플릭스이다. 이 Netflix OSS에는 다음과 같은 서비스가 있다.
Eureka: Discovery Service
Ribbon: Client Side Load Balancer
Zuul: API Gateway
Hystrix: Circuit Breaker
EvCache
Spectator
Archaius
이 중에서 Eureka 서비스는 MSA의 핵심 요소인 Service Discovery를 지원하는 서비스로,
서비스 디스커버리가 가져야 할 장점들을 모두 가져갈 수 있게 해준다.

참고: 서비스 디스커버리가 제공해야 할 기능

고가용성
: 서비스 검색 정보를 서비스 디스커버리 클러스터의 여러 노드가 공유하는 핫 클러스터링 환경을 지원하여 한 노드가 사용할 수 없게 되면 클러스터의 다른 노드가 인계를 받을 수 있어야 한다.
P2P(Peer-to-Peer)
: 서비스 디스커버리 클러스터의 각 노드는 서비스 인스턴스 상태를 공유한다.
부하 분산
: 요청을 동적으로 부하 분산하여 서비스 디스커버리가 관리하는 모든 서비스 인스턴스에 분배해야 한다.
회복성
: 서비스 디스커버리 클라이언트는 서비스 정보를 로컬에 캐시(cache)해야 한다. 로컬 캐싱은 서비스 디스커버리의 성능을 점진적 저하시킬 수 있는데 서비스 디스커버리 서비스가 가용하지 않을 때 애플리케이션이 로컬 캐시에 저장된 정보를 바탕으로 서비스를 찾을 수 있고 동작하게 한다.
장애 내성
: 서비스 인스턴스의 비정상을 탐지하고 가용 서비스 목록에서 인스턴스를 제거해야 한다.

참고: 서비스 디스커버리 아키텍처

서비스 디스커버리 아키텍처는 위에서 소개한 기능을 어떻게 구현하는가에서 시작한다.
서비스를 어떻게 서버에 등록하는가?
서비스 클라이언트가 어떻게 서비스 정보를 검색하는가?
서비스 정보를 노드간에 어떻게 공유하는가?
서비스가 자신의 상태 정보를 서버에 어떻게 전달하는가?

Service Discovery Patterns

1. Client-Side Discovery Pattern

클라이언트에서 서버(Service Registry)에서 서비스의 위치를 찾아 호출하는 방식으로, 비교적 사용이 간단하고 클라이언트는 사용 가능한 서비스의 주소를 알고 있기에 서비스에 맞게 로드 밸런싱 방법을 선택할 수 있다는 장점이 있지만, 클라이언트와 서버간에 종속성이 생기고, 서버의 프레임워크에 맞는 서비스 검색 로직을 구현해야 한다는 단점이 있다.
개요에서 소개한 Netflix OSS가 대표적인 Client-Side Discovery Pattern을 제공하며 Netflix Eureka가 서버(Service Registry)역할을 하는 OSS다.

Structure

1.
클라이언트에서 서비스 호출시 로컬 캐시에 캐싱된 서비스 인스턴스 IP를 확인한다.
2.
클라이언트 측 캐시는 서비스 디스커버리 계층을 통해 주기적 업데이트를 한다.
3.
클라이언트가 캐시에서 찾은 IP를 사용하고 찾지 못하면 서비스 디스커버리에서 검색한다.

참고: Netflix Ribbon

Ribbon은 클라이언트 레벨에서 Load Balancing을 수행해주는 Netflix OSS중 하나.
Zuul을 통해 API Gateway를 구현할 수 있지만 Ribbon을 사용해 API Gateway없이도 애플리케이션을 직접 로드 밸런싱 할 수도 있다고 한다. Server-Side Load Balancer에 비교해서 H/W 부담을 줄일 수 있고 서버 목록 변경의 용이성 등의 장점을 가진다.

2. Server-Side Discovery Pattern

호출되는 서비스 앞에 로드 밸런서를 넣는 방식으로 클라이언트가 아닌 로드 밸런서가 서버(Service Registry)로부터 등록된 서비스의 위치를 전달하는 방식이다. 이 방식은 Client-Side와는 다르게 클라이언트와 서버간에 종속성은 없기에 의존성은 낮고, 클라이언트는 로드밸런서에 요청만 하면 되기에 서버에 맞는 검색 로직을 구현할 필욘 없다. 하지만 로드밸런서가 배포환경에서 제공되야하며 그게 안된다면 관리해야 할 구성 요소가 추가되는 것이다.
ELB(AWS Elastic Load Balancer)나 Kubernetes등이 있다.

structure

기본 구현

서버

1. 프로젝트 생성 및 yml추가

server: port: 8761 #유레카 서버가 동작되는 포트 eureka: client: register-with-eureka: false #해당 서버를 클라이언트로 하겠느냐 여부 fetch-registry: false #디스커버리 등록기에 자기자신(서비스) 등록 여부 server: wait-time-in-ms-when-sync-empty: 5 #서버가 요청 받기 전 대기할 초기 시간
YAML

2. Root Class에 애노테이션 등록

@SpringBootApplication @EnableEurekaServer //유레카 서버 등록 애노테이션 public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
Java

3. 서버 동작 확인

: localhost:8761 으로 동작 확인
아직 등록된 서비스 애플리케이션이 없어서 인스턴스가 보이지 않는다.

클라이언트

1. 프로젝트 생성 및 yml작성

Spring Web을 필수로 등록안해주면 에러가 생긴다고 한다.
spring: application: name: midaseurekaservice #유레카에 등록할 서비스의 논리 이름 profiles: active: default eureka: instance: prefer-ip-address: true # 서비스 IP 주소 등록 여부 client: register-with-eureka: true #서비스 등록 여부 fetch-registry: true # 서비스 위치 serviceUrl: defaultZone: http://localhost:8761/eureka/ #레지스트리 사본을 로컬로 가져온다,.
YAML

2. Root Class에 애노테이션 추가

@SpringBootApplication @EnableDiscoveryClient public class EurekaClientApplication { public static void main(String[] args) { SpringApplication.run(EurekaClientApplication.class, args); } }
Java

3. 서버/클라이언트 모두 구동 후 대시보드 확인

4. Postman으로 API 확인해보기

1.
http://localhost:8761/eureka/apps 으로 모든 서비스 인스턴스 확인하기
Result JSON
2.
http://localhost:8761/eureka/apps/midaseurekaservice 으로 특정 서비스 확인하기
Result JSON

Eureka APIs

1. 등록

: Eureka Client를 서버에 등록할 때 사용하는데 보통 클라이언트 애노테이션과 yaml을 이용해 자동으로 등록되기에 사용 빈도는 낮을 것으로 보인다.
POST /eureka/apps/{appID}
{ "instance": { "instanceId": "392d9569-3a3a-4da0-9447-bfdcc14d8b30", "hostName": "x.x.x.x", "app": "test-service", "ipAddr": "x.x.x.x", "status": "UP", "overriddenStatus": "UNKNOWN", "port": { "$": 8081, "@enabled": "true" }, "securePort": { "$": 443, "@enabled": "false" }, "countryId": 1, "dataCenterInfo": { "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", "name": "MyOwn" }, "leaseInfo": { "renewalIntervalInSecs": 30, "durationInSecs": 90, "registrationTimestamp": 0, "lastRenewalTimestamp": 0, "evictionTimestamp": 0, "serviceUpTimestamp": 0 }, "metadata": { "@class": "java.util.Collections$EmptyMap" }, "homePageUrl": "http://192.168.4.91:8081/", "statusPageUrl": "http://192.168.4.91:8081/actuator/info", "healthCheckUrl": "http://192.168.4.91:8081/actuator/health", "vipAddress": "test-service", "secureVipAddress": "test-service", "isCoordinatingDiscoveryServer": "false", "lastUpdatedTimestamp": "1580878819825", "lastDirtyTimestamp": "1580878820311" } }
JSON
body example

2. Renew(≒ ping-pong)

유레카 클라이언트는 기본적으로 30초 주기로 서버에 HeatBeat를 보내는데, 서버측에서 90초 안에 HeartBeat를 받지 못할 경우 해당 클라이언트의 인스턴스를 제거한다.
(leaseInfo 부분에서 시간을 조회할 수 있다.)
PUT /eureka/apps/appID/{instanceID}

3. Fetch Registry

클라이언트는 서버 가동시 서버에 있는 정보를 가져와 로컬 캐시에 저장하고 이 정보를 가지고 통신을 하는데 최초 모든 정보를 요청하는 API와 그 이후 30초마다 변경된 정보만을 가져오는 API 두 가지가 있다.
GET /eureka/apps ← 모든 정보를 다 조회하는 API
GET /eureka/apps/delta ← 변경된 정보만을 가져오는 API

4. Delete

서버측에서 특정 클라이언트의 인스턴스를 제거할 때 사용한다.
HeartBeat 송수신에 문제가 생겼거나 다수의 인스턴스 실행에서 Service Discovery가 제거를 못하는 경우 수동으로 사용할 수 있다.
DELETE /eureka/apps/{appId}/{instanceID}
DELETE /eureka/apps/test-service/5a7af17d-8800-4408-9855-ac55246d070d
Plain Text
example