04_서비스 디스커버리 (1)
- 서버
- 스프링 부트 애플리케이션으로 실행
- 서버 API 구성
- 등록된 서비스의 목록을 수집하기 위한 API
- 새로운 서비스를 네트워크 위치 주소와 함께 등록하기 위한 API
- 서버는 각 서버의 상태를 다른 서버로 복제해 설정하고 배포함으로써 가용성을 높일 수 있음
- 클라이언트
- 마이크로서비스 애플리케이션에 의존성을 포함해 사용함
- 클라이언트는 애플리케이션 시작 후 등록과 종료 전 등록 해제를 담당
- 유레카 서버로부터 주기적으로 최신 서비스 목록 받아옴
이번 장에서 다룰 내용은 아래와 같다.
유레카 서버를 내장한 애플리케이션 배포하기
클라이언트 측 애플리케이션에서 유레카 서버에 연결하기
고급 디스커버리 클라이언트 설정
클라이언트와 서버 사이의 보안 통신하기
가용성을 높이기 위한 설정 및 동료 간 복제 매커니즘
다른 가용 존에 클라이언트 측 애플리케이션의 인스턴스 등록하기
1. 서버 측에서 유레카 서버 실행하기
- 우선 프로젝트에 스타터를 사용해 의존성을 추가한다.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
- 메인 애플리케이션 클래스에 유레카 서버를 활성화한다.
@SpringBootApplication
@EnableEurekaServer
public class SpringMsaApplication {
public static void main(String[] args) {
SpringApplication.run(SpringMsaApplication.class, args);
}
}
- 메인 애플리케이션을 실행하니
- 에러 발생!!! why?
- 서버 스타터에 클라이언트의 의존성도 포함됨
- 이는 디스커저리 인스턴스를 고가용성 모드에서 동작할 경우 디스커버리 인스턴스 사이의 동료 간 통신(peer to peer)에만 유용함
- 단일 인스턴스로 실행할 경우에는 시작 시에 에러 로그만 찍을 뿐, 그 외에는 유용하지 않음
- 해결방법?
- spring-cloud-netflix-eureka-client 의존성 제거
- 컨피규레이션 속성의 디스커버리 클라이언트 비활성화
server:
port: ${PORT:8761}
eureka:
client:
register-with-eureka: false
fetch-registry: false
- 다시 메인 애플리케이션을 실행하면 Started Eureka Server 로그가 뜬다!!
- 서버가 완전히 실행되면 간단한 UI 대시보드를 http://localhost:{port}에서 서비스됨
- /eureka/* 경로로 HTTP API 메소드를 호출할 수 있음
- /eureka/apps를 호출하여 서버 목록을 점검할 수 있음
- 이제 유레카 서버에 스스로 등록하는 서비스를 만들어보자.
2. 클라이언트 측에서 유레카 활성화하기
- 서버 측에서 유레카 클라이언트를 활성화하는 의존성은 하나뿐이다. 아래 스타터 의존성을 추가한다.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 위의 의존성을 추가하고 서버를 키면 에러가 나면서 서버가 shutdown 된다.
아래 dependency를 추가하면 정상적으로 서버에 등록 된다
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 디스커버리 클라이언트 임무
- 자신을 유레카 서버에 등록하고 호스트, 포트, 상태 정보 URL, 홈페이지 URL을 보냄
- 유레카 서버는 서비스의 각 인스턴스로부터 생존신호(heartbeat) 메시지를 받음
- 설정된 기간 동안 하트비트 메시지를 받지 못하면 레지스트리에서 서비스가 삭제됨
- 서버로부터 데이터를 가져와서 캐싱하고 주기적으로 변경사함을 점검
- 클라이언트 활성화 방법
- @EnableDiscoveryClient 메인 클래스에 추가
- spring-cloud-commons에 존재
- @EnableEurekaClient 메인 클래스에 추가
- 컨설, 유레카, 주키퍼 등 다수의 클라이언트 구현체가 클래스 경로상에 있을 경우
- spring-cloud-netflix에 존재하고 유레카만을 위해 작동함
- @EableDiscoveryClient를 추가하여 클라이언트를 활성화해본다.
@SpringBootApplication
@EnableDiscoveryClient
public class EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class, args);
}
}
- application.yml에 아래와 같이 설정한다.
- 디스커버리 서버 네트워크 주소는 EUREKA_URL
- 클라이언트의 리스닝(listening) 포트는 PORT 환경 변수로 정의
- 디스커버리 서버에 등록될 애플리케이션 이름은 spring.application.name 속성 사용
spring:
application:
name: client-service
server:
port: ${PORT:8081}
eureka:
client:
serviceUrl:
defaultZone: ${EUREKA_URL:http://localhost:8761/eureka/}
- 로컬 호스트에서 예에 애플리케이션의 독립적인 두 개의 인스턴스를 실행해보자. 이를 위해 리스닝 포트는 인스턴스 시작 시에 재정의한다.
$ java -jar -DPORT=8081 target/eurekaClient-0.0.1-SNAPSHOT.jar
$ java -jar -DPORT=8082 target/eurekaClient-0.0.1-SNAPSHOT.jar
- 유레카 서버 대시보드 화면에서 8081, 8082 포트로 등록된 두 개의 client-service 인스턴스가 있음
3. 종료 시 등록 해제
- 유레카 클라이언트에서 어떻게 등록 해제가 동작하는지 살펴보자.
- 우아하게 애플리케이션을 중지하기 위한 가장 좋은 방법은 "스프링 액추에이터의 /shutdown API를 이용하는 것"
- 스프링 부투의 액추에이터를 사용하기 위해 spring-boot-starter-actuator 의존성 추가
- 액추에이터는 기본으로 비활성화 되어 있기 때문에 application.yml에서 설정으로 활성화 시킴
management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: shutdown
- 8081포트에 대해 /shutdown API 메소드를 POST 방식으로 호출하면 아래왜 같이 종료 메세지가 뜨면서 유레카 서버 대시보드에서 사라진다.
- 유레카 서버 대시보드는 새롭게 등록되거나 취소된 서비스 이력을 편리하게 제공
- 현실에서는 우아하게 종료할 수 없는 많은 상황이 발생!
ex) 서버 머신 재시작, 애플리케이션 장애, 서버와 클라이언트 간의 네트워크 인터페이스 문제
- 디스커버리 클라이언트의 종료 절차가 시작되지 못해 서비스가 여전히 유레카 대시보드에 UP 상태로 나오는 것을 확인 할 수 있음
- 임대(lease, 유레카에 등록된 상태)는 절대 만료되지 않음
- 이런 상황을 방지하려면 서버의 기본설정을 변경해야 함!
- 왜 기본 설정에서 이런 문제가 발생할까? Self-preservation mode(자기 보호 모드) 떄문이다.
- 유레카 서버는 자신의 서비스 등록 상태를 제 시간에 갱신하지 않는 서비스의 일정 수를 넘기면 서비스 해제를 멈춤
- 이는 네트워크 장애가 발생했을 때 등록된 모든 서비스가 해제되는 것을 방지함
- application.yml의 enableSelfPreservation 속성을 false로 설정해 비활성화 할 수 있음. 물론 운영환경에서는 활성화~
4. 프로그램 방식으로 디스커버리 클라이언트 사용하기
- 클라이언트 애플리케이션이 시작된 후 유레카 서버로부터 등록된 서비스 목록을 가져옴
하지만. 유레카 클라이언트 API를 사용할 경우도 있을 수 있음. 두 가지 방법을 제공
- com.netflix.discovery.EurekaClient
- 유레카 서버가 노출하는 모든 HTTP 구현. 유레카 API 영역에 설명돼 있음
- org.springframework.cloud.client.discovery.DiscoveryClient
- 넷플릭스 EurekaClient를 대체하는 스프링 클라우드의 구현
- 모든 디스커버리 클라이언트용으로 사용하는 간단한 범용 API
- getServces, getInsatances 두 가지 메소드 있음
org.springframework.cloud.client.discovery.DiscoveryClient 사용하여 테스트
@RestController
public class TestController {
private static Logger LOGGER = LoggerFactory.getLogger(TestController.class);
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/ping")
public List<ServiceInstance> ping() {
List<ServiceInstance> instances = discoveryClient.getInstances("CLIENT-SERVICE");
LOGGER.info("INSTANCES: count={}", instances.size());
instances.stream().forEach(
it -> LOGGER.info("INSTANCE: id={}, port={}", it.getServiceId(), it.getPort()));
return instances;
}
}
/ping 종단점을 호출하면 어떤 인스턴스도 표시하지 않음. 이것은 응답 캐싱 메커니즘과 관련이 있고 다음 절에서 자세히 설명한다.
5. 고급 컨피규레이션 설정
- 서버(Server)
- 서버의 행동을 재정의
- eureka.server.*를 접두어로 사용하는 모든 속성을 포함
- 전체 속성 목록은 EurekaServerConfigBean 클래스를 참조함
- 클라이언트(Client)
- 유레카 클라이언트에서 사용할 수 있는 두 가지 속성 중 하나. 이것은 클라이언트가 레지스트리에서 다른 서비스의 정보를 얻기 위해 질의하는 방법의 컨피규레이션을 담당
- eureka.client.*를 접두어로 사용하는 모든 속성을 포함함
- 전체 속성 목록은 EurekaClientConfigBean 클래스를 참조함
- 인스턴스(Instance)
- 포트나 이름 등의 현재 유레카 클라이언트 행동을 재정의
- eureka.instance.*를 접두어로 사용하는 모든 속성을 포함함
- 전체 속성 목록은 EurekInstanceConfigBean 클래스를 참조함
6. 레지스트리 갱신하기
- Self-preservation mode는 비활성화됐지만 여전히 서버가 등록 해제를 취소하는 것은 오래 걸림
- 이유는?
- 모든 클라이언트 서비스가 30초(기본값)마다 서버로 하트비트를 보내기 때문
- eureka.instance.leaseRenewalIntervalInSeconds 속성에 구성함
- 서버가 하트비트를 받지 못하면 레지스트리에서 인스턴스를 제거하기 전에 90초를 기다림
- 등록을 해제해서 인스턴스로 더 이상 트래픽이 가지 못하도록 차단할 수 있기 때문
- eureka.instance.leaseExpirationDurationInSeconds 속성으로 구성함
- 클라이언트 설정
- 테스트를 위해 작은 값을 초단위로 설정
eureka:
instance:
lease-renewal-interval-in-seconds: 1
lease-expiration-duration-in-seconds: 2
- 서버 설정
- 유레카는 퇴고(evict) 태스크를 백그라운드로 실행하는데, 클라이언트로부터 하트비트가 계속 수신되는지 점검함
- 기본값으로 60초마다 실행됨
- 임대를 갱신하는 주기와 임대를 만료하는 기간이 상대적으로 작은 값으로 설정돼도 서비스 인스턴스를 제거하는 데 최악의 경우 60초가 걸림
- 이어지는 타이머 틱(tick)의 지연은 evictionIntervalTimerInMs 속성으로 설정함. 이는 밀리초 단위임
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 3000
- 디스커버리 서버와 클라이언트 애플리케이션 인스턴스를 실행하고 인스턴스 프로세스를 강제로 종료하면 비활성화된 인스턴스가 유레카 서버에서 거의 즉시 제거됨
- 다음은 유레카 서버의 로그 일부이다.
- 이렇게 모든 속성을 조작해 임대 만료 제거 절차에 대한 유지 관리를 사용자 정의할 수 있음
- 그러나 정의된 컨피규레이션이 시스템의 성능을 부족하게 만들지 않는 것도 중요함
- 컨피규레이션 변경에 민감한 요소
- 부하 분산
- 게이트웨이
- 서킷 브레이커
- 유레카는 self-preservation mode를 비활성화 하면 다음 화면처럼 경고 메시지를 보여줌
7. 인스턴스 식별자 변경하기
- 유래카에 등록된 인스턴스는 이름으로 묶임
- 그러나 각 인스턴스는 서버가 인식할 수 있는 유일한 ID를 보내야 함
- 모든 서비스 그룹의 Status 컬럼에 instanceId가 표시됨
- 스프링 클라우드 유레카는 다음과 같이 필드를 조합해 식별자를 자동으로 생성함
${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:$server.port}}}.
- 이 식별자는 eureka.instance.instanceId 속석을 사용해 쉽게 재정의 가능
- 테스트의 목적으로 클라이언트 애플리케이션의 일부 인스턴스를 다음 컨피규레이션 설정과 -DSEQUENCE_NO=[n] VM 인자를 사용해 시작함
- SEQUENCE_NO 입력에 기반해 리슨(listen) 포트와 디스커버리 instanceID를 도엊ㄱ으로 설정하는 클라이언트 애플리케이션의 예제 컨피규레이션임
server:
port: 808${SEQUENCE_NO}
eureka:
instance:
instance-id: ${spring.application.name}-${SEQUENCE_NO}
8. IP 주소 우선하기
- 마이크로서비스 환경을 구성하는 여러 서버를 위한 DNS가 없는 것이 일반적임
- 모든 리눅스 머신의 /etc/hosts 파일에 호스트명과 IP 주소를 추가하는 것 외에는 방법이 없음
- 해결을 위한 대안은 유레카의 등록 절차 컨피규레이션 설정에서 호스트명 대신 서비스의 IP 주소를 사용하는 것!
- 이를 위해 유레카 클라이언트의 eureka.instance.preferIpAddress 속성을 true로 설정함
- 레지스트리의 모든 서비스 인스턴스는 유레카 대시보드에 호스트명을 담은 instanceId를 사용하지만, 링크를 클릭하면 IP 주소 기반으로 리디렉션됨
- HTTP 기반으로 다른 서비스를 호출하는 리본 클라이언트(클라이언트측 부하 분산기)도 같은 원리를 따름
- 하지만 머신에 하나 이상의 네트워크 인터페이스가 있을 경우 서비스의 네트워크 위치를 결정하는 방법으로 IP 주소를 사용하면 문제 발생...
- 각 서버 머신이 다른 IP 접두사를 갖는 두 개의 네트워크 인터페이스를 가질 수 있음
- 올바른 인터페이스를 선택하기 위해 application.yml 컨피규레이션 파일에 무시할 패턴의 목록을 정의함
- 예를 들어 다음과 같이 설정하면 eth1로 시작하는 모든 인터페이스를 무시할 수 있음
spring:
cloud:
inetutils:
ignored-interfaces:
- eth1*
- 다음과 같이 선호하는 네트워크 주소를 정의하는 방법도 있음
spring:
cloud:
inetutils:
preferred-networks:
- 192.168
9. 응답 캐시
서버
- 유레카 서버는 기본적으로 응답을 캐시함
- 캐시는 30초마다 뮤효화됨. /eureka/apps HTTP API를 호출해 쉽게 확인 가능
- 클라이언트 애플리케이션이 등록된 후 바로 호출하면 방금 등록된 인스턴스가 응답에 나타나지 않음
- 30초 후에 재시도하면 새로운 인스턴스가 나옴
- 응답 캐시의 타임아웃은 responseCacheUpdateIntervalMs 속성으로 재정의할 수 있음
eureka:
server:
response-cache-update-interval-ms: 3000
- 유레카 대시보드에 등록된 인스턴스가 표시될 떄는 캐시가 없음
- REST API와는 반대로 그것은 응답 캐시를 사용하지 않음
클라이언트
- 클라이언트 측에서도유레카 레지스트리 캐싱함
- 그래서 서버에서 캐시 타임아웃을 변경해도 여전히 클라이언트에서 갱신되는 데 시간이 걸림
- 레지스트리는 기본으로 30초마다 실행되는 백그라운드 태스크에 의해 비동기로 갱신됨
- registryFetchIntervalSeconds 속성으로 변경할 수 있음
- 이는 마지막으로 시도한 값에서 변경된 내용만 가져옴
- shouldDisableDelta 속성으로 비활성화 가능
- 일반적으로 클라이언트 측에서 캐싱하는 것이 의미가 없는 경우에는 서버 측에서 캐싱하는 것도 의미가 없을 것임
- 특히 유레카는 백엔드 저장소가 없기 때문에 더욱 의미가 없을 것임
- 유레카를 사용해 단위 테스트를 개발하는 경우 캐시 없이 즉시 응답을 받아야함
eureka:
client:
registry-fetch-interval-seconds: 3
disable-delta: true
10. 클라이언트와 서버 간의 보안 통신 사용하기
- 지금까지 유레카 서버는 모든 클라이언트의 연결을 인증하지 않음
- 허가되지 않는 접근을 방지하기 위해 디스커버리 서버에 기본 인증을 사용해 최소한의 보안을 적용할 것임
서버 측의 기본 인증 활성화
- 프로젝트 의존성에 security 스타터를 추가함
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- application.yml 파일에 컨피규레이션 설정을 변경해 보안을 활성화하고 기본 자격 증명을 설정함
spring:
security:
user:
name: admin
password: admin123
- 유레카 서버를 실행하면 아래와 같이 로그인 창이 나오고 로그인 해야 대시보드를 볼 수 있음
클라이언트 측의 기본 인증 활성화
- application.yml 파일에 다음 컨피규레이션 설정과 같이 URL 연결 주소에 자격 증명을 제공해야함
eureka:
client:
service-url:
default-zone: http://admin:admin123@localhost:8761/eureka/
- 디스커버리 클라이언트와 서버 간에 인증서를 사용한 안전한 SSL 연결을 맺는 등 더 진보된 사용을 위해서는 DiscoveryClientOptinalArgs를 맞춤형으로 구현해야한다. 12장 API 보안 강화하기에서 한 가지 예를 다룰 것이다. 특히 스프링 클라우드 애플리케이션의 보안에 종속적인 사례를 논의할 것이다.
'MSA > 마스터링 스프링 클라우드' 카테고리의 다른 글
마스터링 스프링 클라우드 chap.3 (0) | 2019.01.23 |
---|---|
마스터링 스프링 클라우드 chap.2 (0) | 2019.01.21 |
마스터링 스프링 클라우드 chap.1 (1) | 2019.01.09 |