티스토리 뷰

04_서비스 디스커버리 (1)

넷플릭스 OSS 디스커버리 서브는 유레카(Eureka)로 알려져있다.
유레카와의 통합을 위한 스프링 클라우드 라이브러리는 클라이언트와 서버의 두 부분으로 구성돼 있다.
  • 서버
    1. 스프링 부트 애플리케이션으로 실행
    2. 서버 API 구성
      • 등록된 서비스의 목록을 수집하기 위한 API
      • 새로운 서비스를 네트워크 위치 주소와 함께 등록하기 위한 API
    3. 서버는 각 서버의 상태를 다른 서버로 복제해 설정하고 배포함으로써 가용성을 높일 수 있음
  • 클라이언트
    1. 마이크로서비스 애플리케이션에 의존성을 포함해 사용함
    2. 클라이언트는 애플리케이션 시작 후 등록과 종료 전 등록 해제를 담당
    3. 유레카 서버로부터 주기적으로 최신 서비스 목록 받아옴

이번 장에서 다룰 내용은 아래와 같다.

  • 유레카 서버를 내장한 애플리케이션 배포하기

  • 클라이언트 측 애플리케이션에서 유레카 서버에 연결하기

  • 고급 디스커버리 클라이언트 설정

  • 클라이언트와 서버 사이의 보안 통신하기

  • 가용성을 높이기 위한 설정 및 동료 간 복제 매커니즘

  • 다른 가용 존에 클라이언트 측 애플리케이션의 인스턴스 등록하기


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);
}
}
  • 메인 애플리케이션을 실행하니
    1. 에러 발생!!! why?
      • 서버 스타터에 클라이언트의 의존성도 포함됨
      • 이는 디스커저리 인스턴스를 고가용성 모드에서 동작할 경우 디스커버리 인스턴스 사이의 동료 간 통신(peer to peer)에만 유용함
      • 단일 인스턴스로 실행할 경우에는 시작 시에 에러 로그만 찍을 뿐, 그 외에는 유용하지 않음
    2. 해결방법?
      • 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>

    • 디스커버리 클라이언트 임무
      1. 자신을 유레카 서버에 등록하고 호스트, 포트, 상태 정보 URL, 홈페이지 URL을 보냄
        • 유레카 서버는 서비스의 각 인스턴스로부터 생존신호(heartbeat) 메시지를 받음
        • 설정된 기간 동안 하트비트 메시지를 받지 못하면 레지스트리에서 서비스가 삭제됨
      2.  서버로부터 데이터를 가져와서 캐싱하고 주기적으로 변경사함을 점검
    • 클라이언트 활성화 방법
      1. @EnableDiscoveryClient 메인 클래스에 추가
        • spring-cloud-commons에 존재
      2. @EnableEurekaClient 메인 클래스에 추가
        • 컨설, 유레카, 주키퍼 등 다수의 클라이언트 구현체가 클래스 경로상에 있을 경우
        • spring-cloud-netflix에 존재하고 유레카만을 위해 작동함
    • @EableDiscoveryClient를 추가하여 클라이언트를 활성화해본다.

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

    • application.yml에 아래와 같이 설정한다.
      1. 디스커버리 서버 네트워크 주소는 EUREKA_URL
      2. 클라이언트의 리스닝(listening) 포트는 PORT 환경 변수로 정의
      3. 디스커버리 서버에 등록될 애플리케이션 이름은 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(자기 보호 모드) 떄문이다.
      1. 유레카 서버는 자신의 서비스 등록 상태를 제 시간에 갱신하지 않는 서비스의 일정 수를 넘기면 서비스 해제를 멈춤
      2. 이는 네트워크 장애가 발생했을 때 등록된 모든 서비스가 해제되는 것을 방지함
      3. application.yml의 enableSelfPreservation 속성을 false로 설정해 비활성화 할 수 있음. 물론 운영환경에서는 활성화~

    4. 프로그램 방식으로 디스커버리 클라이언트 사용하기

    • 클라이언트 애플리케이션이 시작된 후 유레카 서버로부터 등록된 서비스 목록을 가져옴
    • 하지만. 유레카 클라이언트 API를 사용할 경우도 있을 수 있음. 두 가지 방법을 제공

      1. com.netflix.discovery.EurekaClient
        • 유레카 서버가 노출하는 모든 HTTP 구현. 유레카 API 영역에 설명돼 있음
      2. 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)
      1. 서버의 행동을 재정의
      2. eureka.server.*를 접두어로 사용하는 모든 속성을 포함
      3. 전체 속성 목록은 EurekaServerConfigBean 클래스를 참조함
    • 클라이언트(Client)
      1. 유레카 클라이언트에서 사용할 수 있는 두 가지 속성 중 하나. 이것은 클라이언트가 레지스트리에서 다른 서비스의 정보를 얻기 위해 질의하는 방법의 컨피규레이션을 담당
      2. eureka.client.*를 접두어로 사용하는 모든 속성을 포함함
      3. 전체 속성 목록은 EurekaClientConfigBean 클래스를 참조함
    • 인스턴스(Instance)
      1. 포트나 이름 등의 현재 유레카 클라이언트 행동을 재정의
      2. eureka.instance.*를 접두어로 사용하는 모든 속성을 포함함
      3. 전체 속성 목록은 EurekInstanceConfigBean 클래스를 참조함

    6. 레지스트리 갱신하기

    • Self-preservation mode는 비활성화됐지만 여전히 서버가 등록 해제를 취소하는 것은 오래 걸림
    • 이유는?
      1. 모든 클라이언트 서비스가 30초(기본값)마다 서버로 하트비트를 보내기 때문
        • eureka.instance.leaseRenewalIntervalInSeconds 속성에 구성함
        • 서버가 하트비트를 받지 못하면 레지스트리에서 인스턴스를 제거하기 전에 90초를 기다림
      2. 등록을 해제해서 인스턴스로 더 이상 트래픽이 가지 못하도록 차단할 수 있기 때문
        • eureka.instance.leaseExpirationDurationInSeconds 속성으로 구성함
    • 클라이언트 설정
    1. 테스트를 위해 작은 값을 초단위로 설정
    eureka:
    instance:
    lease-renewal-interval-in-seconds: 1
    lease-expiration-duration-in-seconds: 2
    • 서버 설정
      1. 유레카는 퇴고(evict) 태스크를 백그라운드로 실행하는데, 클라이언트로부터 하트비트가 계속 수신되는지 점검함
      2. 기본값으로 60초마다 실행됨
      3. 임대를 갱신하는 주기와 임대를 만료하는 기간이 상대적으로 작은 값으로 설정돼도 서비스 인스턴스를 제거하는 데 최악의 경우 60초가 걸림
      4. 이어지는 타이머 틱(tick)의 지연은 evictionIntervalTimerInMs 속성으로 설정함. 이는 밀리초 단위임
    server:
    enable-self-preservation: false
    eviction-interval-timer-in-ms: 3000
    • 디스커버리 서버와 클라이언트 애플리케이션 인스턴스를 실행하고 인스턴스 프로세스를 강제로 종료하면 비활성화된 인스턴스가 유레카 서버에서 거의 즉시 제거됨
    • 다음은 유레카 서버의 로그 일부이다.

    • 이렇게 모든 속성을 조작해 임대 만료 제거 절차에 대한 유지 관리를 사용자 정의할 수 있음
    • 그러나 정의된 컨피규레이션이 시스템의 성능을 부족하게 만들지 않는 것도 중요함
    • 컨피규레이션 변경에 민감한 요소
    1. 부하 분산
    2. 게이트웨이
    3. 서킷 브레이커
    • 유레카는 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초마다 실행되는 백그라운드 태스크에 의해 비동기로 갱신됨
      1. registryFetchIntervalSeconds 속성으로 변경할 수 있음
    • 이는 마지막으로 시도한 값에서 변경된 내용만 가져옴
      1. 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 보안 강화하기에서 한 가지 예를 다룰 것이다. 특히 스프링 클라우드 애플리케이션의 보안에 종속적인 사례를 논의할 것이다.

    공지사항
    최근에 올라온 글
    최근에 달린 댓글
    Total
    Today
    Yesterday
    링크
    TAG
    more
    «   2024/05   »
    1 2 3 4
    5 6 7 8 9 10 11
    12 13 14 15 16 17 18
    19 20 21 22 23 24 25
    26 27 28 29 30 31
    글 보관함