서론
여기서 말하는 Edge Server는 다양한 API 서버들을 연결하는 Gatekeeper와 비슷한 역할을 그 주요 기능으로 한다. 이 외에 구성에 따라서는 외부 클라이언트와의 연결에서 Security의 진입점 같은 역할도 하기는 하지만, 그 부분은 다른 포스트에서 정리해볼까 한다.
이번 포스트를 정리할 때 사용한 예제는 대부분을 Calista 블로그의 Building microservices with Spring Cloud and Netflix OSS, part 1 에서 참고하였다. 사실 위 블로그의 내용을 거의 똑같이 따라 하면서 정리했다고 보는 것이 맞을 것 같다. 이 포스트보다 정리가 잘 되어있는 만큼 영어에 어려움이 없는 개발자라면 Calista 블로그의 내용을 보는 것이 더 나을지도 모르겠다.
한가지 참고할 수 있는 자료가 더 있는데 다른 포스트에서도 소개했던 적이 있는 Devoxx 2015 의 동영상 자료가 있다. 제목은 Getting started with Spring Cloud by Josh Long 으로 1시간 정도 라이브 코딩으로 Josh Long이 열정적으로 설명해준다. 영어를 못하는 나도 어느 정도 따라갈 수 있었던 동영상으로 강추하고 싶은 동영상이다.
시작하기에 앞서서 좋은 블로그 자료를 공개해줘서 샘플을 만들고 정리하는 데 도움을 준 Calista 블로그에 감사의 마음을 전하며, 자료를 함부로 가져다 쓰게 된 점에 대해 매우 미안한 마음을 전한다.
이번 포스트를 정리하기 위해 사용한 샘플은 아래와 같은 조건으로 작업이 되었다.
- Configuration Server 와 Discover Server : 이전 포스트 참고
- JDK 8 : 1.8.0_65
- Intellij 15.0.3
- Gradle build : 2.10
- Spring IO : 2.0.1
- Spring Boot : 1.3.2 (이전 포스트 작성후에 버전이 1.3.1에서 1.3.2로 업그레이드됐다)
- Spring Cloud : Angel.SR4
- Lombok : 1.12.6
- IDEA Lombok plugin : 0.9.7
- Configuration server의 git 저장소 : https://github.com/roadkh/blog-cloud-sample-config.git
- 설정파일은 모두 yml 을 사용했다. (모두 properties 파일로 변경한다고 해도 크게 달라지지는 않는다)
샘플 소스는 https://github.com/roadkh/blog-cloud-sample.git 에서 clone 가능하며, branch는 blog_02 에서 확인할 수 있다. 소스 관련 자세한 정보는 https://github.com/roadkh/blog-cloud-sample/tree/blog_02 에서도 확인할 수 있다.
샘플에서는 lombok을 사용해서 반복작업을 최소화했다.
lombok을 사용하기 위해서는 eclipse와 Intellij에 따라 별도의 작업이 좀 필요하다. eclipse의 경우는 https://projectlombok.org/ 또는 CoolioSo! 블로그의 Lombok 소개 및 설치방법을 참고하기 바란다.
IntellJ(IDEA 15를 기준)의 경우는 아래의 과정을 거쳐서 설치할 수 있다.
샘플에서는 lombok을 사용해서 반복작업을 최소화했다.
lombok을 사용하기 위해서는 eclipse와 Intellij에 따라 별도의 작업이 좀 필요하다. eclipse의 경우는 https://projectlombok.org/ 또는 CoolioSo! 블로그의 Lombok 소개 및 설치방법을 참고하기 바란다.
IntellJ(IDEA 15를 기준)의 경우는 아래의 과정을 거쳐서 설치할 수 있다.
- Settings > Plugins > Install JetBrains plugin... 을 클릭한다.
- lombok을 검색한다.
- Lombok Plugin을 선택하고 설치를 한다.
- Settings > Build, Execution, Deployment > Compiler > Annotation Processors 를 선택한다.
- Enable annotation processing 에 체크한다. (매번 프로젝트를 새로 진행할 때 이 부분을 잊어버려서 Lombok 이 정상 동작하지 않는 경우가 많았다. 나처럼 멍청한 실수를 하시는 분들이 없기를 바란다)
기본 구성
[Calista 블로그에서 소개한 구성 이미지] |
Entity 는 Product, Review, Recommendation 이렇게 구성이 되며, 그 관계의 그림과 같다.
보면 각 Entity가 관계를 맺도록 구성되어있지만, 위 엔티티 하나씩에 대해 처리하는 API를 구성할 것이다. 실제 서비스에서는 저렇게 구성하는 경우는 없겠지만, Microservice에서의 API 예제를 위한 것이므로 다소 무리가 있더라도 Calista의 구성을 그대로 정리해 볼까 한다.
API 서비스와 Configuration Server
API 서비스 서버들이 시작될 때 Configuration server에서 설정을 조회하도록 하기 위해서는 아래와 같은 두 가지의 작업이 필요하다.
- spring-cloud-starter-config 에 대한 dependency 를 추가한다. 샘플에서는 build.gradle에 compile('org.springframework.cloud:spring-cloud-starter-config') 을 추가했다.
- classpath에 bootstrap.yml(application.yml 이 아니다) 을 추가한다. 샘플에서는 src/main/resources 폴더에 추가했다.
Production/Recommendation/Review 프로젝트의 형태가 거의 비슷하므로, 여기서는 Production만 가지고 정리를 하고자 한다. 자세한 내용은 github의 소스를 확인하면 되리라 본다.
Discovery Server(여기서는 Eureka Server) 에 Configuration Server를 등록해서 사용하는 경우에는 이 정보를 등록하려는 서비스의 bootstrap 파일에 아래와 같이 등록해서 사용할 수 있다.
spring: cloud: config: discovery: enabled: true service-id: config-server failFast: true application: name: production
즉, Configuration server의 service id 를 이용한 접근 방법이다.
이 방법이 참 좋아 보이지만, 현재 버전에서 한가지 문제점이 있다.
Configuration을 받아가는 서비스의 server.port 를 랜덤으로 처리할 수가 없는 문제가 있다.
예를 들어서 위의 구성도를 보면 이번 샘플은 Edge server를 제외하고는 거의 모두 랜덤 포트를 이용하도록 설정이 되어있는데, 이 경우 configuration을 discovery 방식으로 사용하면 정상적으로 동작하지 않는다. 이 문제는 Discovery server에 등록될 때 port가 0으로 등록되는 문제가 있어서 그런 것으로 보인다.
따라서 위 구성과 같이 random port를 사용하고자 한다면, 위 방식은 쓸 수가 없다. 혹시 모든 서비스의 포트가 정해져 있다면, discovery 방식을 이용해서 configuration server 운영에서 유연성이 확보될 수 있다는 장점이 있지 않을까 싶다. 이것은 각자의 상황에 맞춰서 결정하면 되지 않을까 생각한다.
이번 샘플에서는 위 방식이 아닌 Configuration server의 아이피와 포트를 지정해서 사용했다.
spring: cloud: config: uri: http://localhost:8888 failFast: true application: name: production
두 예에서 옵션을 보면 모두 failFast를 true로 지정했다. 해당 값은 기본값이 false로 만약 Configuration server에서 정상적으로 설정을 조회하지 못했다면, 기본 application.yml 등의 설정을 읽어서 시작할 수 있도록 처리할 수 있다. 다만, 이번 샘플에서는 기본 application.yml 을 생성하지 않고 모두 Configuration server 에서 조회하도록 했기 때문에 fail-fast의 값을 true로 했다. 이렇게 하면 설정을 받아오지 못하면 서버 기동에 실패한다.
같은 방식으로 모든 API 프로젝트에 bootstrap.yml을 만들고 application.name 만 git에 등록된 파일명과 동일한 이름으로 만들어주면 Configuration server에 대한 서비스의 설정은 완료된다.
(https://github.com/roadkh/blog-cloud-sample-config 에서 확인할 수 있다.)
API 서비스와 Discovery Server
Discovery server 를 이용하도록 하려면 세 가지 작업이 필요하다.
- spring-cloud-starter-eureka 에 대한 dependency를 추가한다. 샘플에서는 각 서비스의 build.gradle 에 compile('org.springframework.cloud:spring-cloud-starter-eureka') 을 추가했다.
- Spring boot 의 메인 클래스에 @EnableDiscoveryClient 를 추가한다.
- application.yml 에 discovery server 설정을 추가한다. 샘플에서는 Configuration server 의 application.yml 을 이용하도록 하여 모든 서비스가 공통으로 사용하도록 했다.
예전 포스트에서 정리한 적이 있지만, Configuration 에 조회를 요청하면 아래와 같은 파일에서 정보를 찾게 된다.
샘플에서는 profile을 이용한 부분은 없다.
앞에서 설명했듯이 모든 서비스가 application.yml 의 정보는 이용하게 되므로 Discover server 관련 정보는 application.yml 에 아래와 같이 설정했다.
- 기본 application.properties 또는 application.yml
- spring.appliation.name 에 설정된 이름의 설정파일.
- spring.profiles.active 에 해당되는 설정파일
예를 들어서 production 이라는 application에서 test라는 profile로 요청한 경우 아래와 같은 순서로 조회하며, 후순위 조회의 정보가 가장 큰 우선순위를 가진다. 즉, 같은 설정이 있을 경우 후순위 조회된 정보로 업데이트된다고 보면 된다.
application.yml => production.yml => production-test.yml
application.yml => production.yml => production-test.yml
샘플에서는 profile을 이용한 부분은 없다.
앞에서 설명했듯이 모든 서비스가 application.yml 의 정보는 이용하게 되므로 Discover server 관련 정보는 application.yml 에 아래와 같이 설정했다.
eureka: client: service-url: defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/ instance: preferIpAddress: true leaseRenewalIntervalInSeconds: 30 leaseExpirationDurationInSeconds: 90 metadataMap: instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}}
위 설정의 주요내용을 간단히 정리해보면 다음과 같다.
- eureka.client.service-url : Discovery server에 대한 접속 정보이다. 콤마로 분리해서 여러 개 넣을 수 있는 것으로 보인다. 샘플에서는 peer1과 peer2가 서로 리플리케이션 관계이므로 사실 하나만 넣어도 상관없다.
- eureka.instance.preferIpAddress : false가 기본값이다. 이 경우 Discovery server의 dashboard에 접속해보면, hostname을 이용해서 등록된 것을 볼 수 있다. 샘플에서는 아이피로 등록되도록 했다.
- eureka.instance.leaseRenewalIntervalInSeconds : HeartBeat과 비슷한 의미로 보인다. 30초가 기본값으로 이 설정값 간격으로 Discovery에 registration 정보를 교환한다.
- eureka.instance.leaseExpirationDurationInSeconds : 기본값은 90초다. 이 설정값 동안 서비스가 접속하지 않으면 해당 서비스는 정상이 아니라고 판단하게 된다. (이 부분은 정확히 리스트에서 삭제되는지 아니면 상태가 Up이 아니게 되는지까지 확인을 못해봤다)
- eureka.metadataMap.instanceId : Discovery server에 등록되는 이름이다. Dashboard에서 확인하는 이름과 같다.
위 설정 중에서 두 가지만 좀 더 정리해볼까 한다.
우선 leaseRenewalIntervalInSeconds 에 대해서 정리해보자.
Discovery server에 registration 정보를 다시 등록하는 간격으로 보면 되는데, 실제로는 heartbeat과 같은 의미로 사용이 되는 것으로 보인다.
샘플의 모든 서비스를 시작하는데, Edge server를 다른 API들보다 먼저 시작하고 API를 시작한 후에 모든 서비스가 Discovery dashboard에 올라왔는데도 forward error가 발생하는 것을 확인할 수 있었다. Loadbalancer 역할을 하는 Ribbon 모듈에 정상적으로 모든 서비스가 등록되지 않아 발생하는 문제라고 하는데, 이 부분에 대해서 Spring Cloud Documentation의 이야기를 그대로 인용해서 정리할까 한다.
Why is it so Slow to Register a Service?
Being an instance also involves a periodic heartbeat to the registry (via the client’s serviceUrl) with default duration 30 seconds. A service is not available for discovery by clients until the instance, the server and the client all have the same metadata in their local cache (so it could take 3 hearbeats). You can change the period using eureka.instance.leaseRenewalIntervalInSeconds and this will speed up the process of getting clients connected to other services. In production it’s probably better to stick with the default because there are some computations internally in the server that make assumptions about the lease renewal period.
실제 서비스에서는 크게 문제가 되지 않으리라고 보이는데, 테스트할 때에는 좀 답답할 수 있다. 그럴 때는 10초 정도로 줄이면 괜찮지 않을까 싶다.
다음 instanceId 이다.
기본 application.name + hostname 의 형태였던 것으로 기억한다(이 부분 테스트를 워낙 오래전에 했어서, 지금은 정확하게 기억이 나지 않는다). 즉, 하나의 호스트에서는 동일 서비스를 여러 개 실행할 수 없다. 두 개 이상을 실행할 경우 Discovery Server에는 뒤에 실행된 애플리케이션의 정보로 바뀌게 된다. 즉, 처음에 8080 포트로 실행하고 다음에 8081로 실행을 해서 동일 서비스를 두 개 실행하게 되면, Discovery server에는 8081로 실행된 정보만 남는다는 이야기이다.
그래서 instanceId를 유일하게 만들려는 방법으로 Spring cloud documentation에는 위와 같은 형태를 예제로 사용하고 있다. 이 샘플에서는 해당 정보를 그대로 이용했다.(Pivotal의 Cloudfoundry 를 사용한다면 이 부분이 해결되는 변수를 넣을 수 있다고 한다)
Boot Main Class를 보면 매우 간단하다. 별다른 설명이 필요 없을 것으로 보인다.
모든 API 들에 공통으로 위와 같은 방법으로 적용하고 실행하면 Discovery server 에는 아래와 같은 모습으로 등록되고 dashboard에서 확인할 수 있다.(Dashboard URL 은 샘플의 경우 http://localhost:8761/ 이거나 http://localhost:8762/ 이다)
첫 부분의 그림을 보면 Edge server와 Composite API에 Ribbon Module이 포함된 것을 볼 수 있다.
나는 현재까지 구성한 모든 서버와 모듈들은 이 Ribbon의 로드밸런서를 사용하기 위해 만들어진 게 아닐까 할 정도로 가장 강력한 기능이라고 생각한다.
몇 가지 옵션이 존재하지만, 사실 아무런 설정을 하지 않고 사용해도 샘플을 구동하는 데는 부족하지 않을 정도다. 이번 포스트에서는 다른 설명은 하지 않고 구체적으로 어떤 식으로 되는지를 샘플을 통해 정리해보자.
Composite API 의 ProductCompositeServiceBean.java 의 내용을 먼저 보자.
ProductCompositeServiceBean은 RestTemplate을 이용해서 각 API에 접속해서 데이터를 가져와서 필요한 데이터를 만들어내는 역할을 하는 서비스 클래스이다.
각 API에 접속하기 위한 URL을 보면 모두 http://{discover service id}/ 의 형태를 가지고 있다. 구체적인 호스트와 도메인 또는 아이피의 정보도 없고 포트에 대한 정보도 없다. 단지, Eureka server에 등록된 service id 만을 이용해서 접속할 수 있다. 앞에서 한번 이야기했지만 Ribbon은 Round robin 방식으로 로드밸런싱을 수행한다고 한다. 실제 요청을 통해 어떻게 동작하는지 보자.
샘플을 이용해서 모든 서비스를 실행하는데 Product API 만 2개를 실행해보자.
그럼 Discovery dashboard에는 아래와 같이 PRODUCT 라는 인스턴스가 두 개가 된다.
이 상태에서 Composite API 또는 Edge server 에 Request를 보내고 두 개의 product API 의 로그를 보면 번갈아가면서 요청이 들어오는 것을 확인 할 수 있다.
아래 로그는 Composite API 의 로그 일부로 처음 요청을 했을 때 한번 나오는 로그이다. neflix의 DynamicServerListLoadBalancer 에 의해서 생성된 로그 내용으로 Ribbon 관련 정보 외에 Circuit breaker 정보 등도 포함되어있다.
Edge server는 외부 클라이언트와 서비스 간의 중간 Gatekeeper 역할을 하는 서버이다.
Zuul proxy 는 Netflix OSS 중에서 Proxy server 역할을 하는 모듈이다.
이번에는 별도의 Edge server로 구성했지만, 예전에 다른 작업을 하면서 사용자용 웹 서버에 Zuul proxy를 설정해서 API 서버와 연결을 한 적이 있는데 그런 역할을 하는 모듈이다.
Zuul proxy 는 개별 기능으로도 효용성이 높은 모듈로 Zuul 만을 이용한 예제를 보고 싶다면, Spring Guide의 Spring Security and Angular JS 를 보면 Zuul proxy 만을 사용하는 예제가 있으니 참고가 되지 않을까 싶다.
그럼 Edge server 를 샘플을 통해서 정리해보자.
우선 EdgeServerApplication.java 를 보자. 역시나 매우 간단하다.
여기까지만 하고 EdgeServer 를 실행해보면 실행 로그에 아래와 같은 로그가 나오게 된다.
로그를 보면 Eureka server에 등록된 모든 서비스 인스턴스들이 /{service instance name}의 형태로 proxy가 설정된 것을 볼 수 있다. 기본 설정 상태에서 실행하면 그렇게 된다는 거다.
저 중에서 composite api 에만 접근할 수 있도록 proxy를 만들고 싶다. 그래서 아래와 같은 설정을 Edge server의 설정 파일에 추가했다. 아래 내용은 github 에 등록되어있는 edge-server.yml 파일에서 Zuul 설정 부분이다.
우선 zuul.ignoredServices 를 통해서 모든 요청의 proxy를 제거한다.
그다음 zuul.routes.{service}.path 를 통해서 설정하면 된다.
위의 경우는 localhost:9000/composite/ 로 들어오는 모든 요청을 composite 서비스로 보낸다는 의미라 하겠다. 이제 다시 Edge server를 실행하면 위의 로그 부분은 없어지고 아래와 같은 로그가 나오게 된다.
이제 Composite API 만 공개하게 되고, 클라이언트는 설정된 /composite/produt/?size=10&page=1 라던가 /composite/product/1 같은 형태로 접속해서 서비스를 이용할 수 있게 되었다.
Zuul proxy를 이용할 때 한가지 조심해야 하는 부분이 있는데, Zuul proxy를 이용해서 대용량 파일을 보낼 때는 약간의 설정을 추가해 줄 필요가 있다. 이는 Zuul proxy의 기본 timeout 설정 때문에 발생하는 문제인 것으로 보이는데, Spring cloud documentation을 확인해 보면 아래와 같은 설정으로 해결할 수 있다고 되어있다.
Spring cloud documentation을 보면 Netflix 는 아래와 같은 목적으로 Zuul을 사용한다고 한다.
개인적으로 생각할 때는 이 포스트에 정리한 내용이 Spring cloud 를 이용해서 마이크로서비스를 구현하는 것에 있어서 가장 핵심이 되지 않을까 싶다. 이전 포스트에서 정리했던 Eureka server의 구성도 결국 이 작업을 위해서 필요한 것이 아닐까 생각한다. 처음 Spring cloud 에 관심을 갖게 된 부분도 오늘 정리한 부분 때문이었다. 특정 아이피/포트/호스트/도메인 을 이용한 API 간의 통신이 아닌 인스턴스 아이디를 이용한 통신이 가능하다는 것이 너무 매력적이었다. 게다가 로드밸런싱까지 해 준다니... 처음 접했을 때는 거의 혁명에 가깝다고 생각할 정도였다.
워낙에 뭔가를 정리하는 데에는 능력이 없어서 정리가 매끄럽지 않지만, calista 블로그의 내용가 Josh Long 의 Devoxx 2015 발표 동영상 등을 참고한 후에 내 github에 있는 소스를 본다면 조금은 이해가 쉽지 않을까 하는 생각을 해본다.
다음 instanceId 이다.
기본 application.name + hostname 의 형태였던 것으로 기억한다(이 부분 테스트를 워낙 오래전에 했어서, 지금은 정확하게 기억이 나지 않는다). 즉, 하나의 호스트에서는 동일 서비스를 여러 개 실행할 수 없다. 두 개 이상을 실행할 경우 Discovery Server에는 뒤에 실행된 애플리케이션의 정보로 바뀌게 된다. 즉, 처음에 8080 포트로 실행하고 다음에 8081로 실행을 해서 동일 서비스를 두 개 실행하게 되면, Discovery server에는 8081로 실행된 정보만 남는다는 이야기이다.
그래서 instanceId를 유일하게 만들려는 방법으로 Spring cloud documentation에는 위와 같은 형태를 예제로 사용하고 있다. 이 샘플에서는 해당 정보를 그대로 이용했다.(Pivotal의 Cloudfoundry 를 사용한다면 이 부분이 해결되는 변수를 넣을 수 있다고 한다)
Boot Main Class를 보면 매우 간단하다. 별다른 설명이 필요 없을 것으로 보인다.
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient public class ProductApiApplication { public static void main(String[] args) { SpringApplication.run(ProductApiApplication.class, args); } }
모든 API 들에 공통으로 위와 같은 방법으로 적용하고 실행하면 Discovery server 에는 아래와 같은 모습으로 등록되고 dashboard에서 확인할 수 있다.(Dashboard URL 은 샘플의 경우 http://localhost:8761/ 이거나 http://localhost:8762/ 이다)
Ribbon Module의 역할
첫 부분의 그림을 보면 Edge server와 Composite API에 Ribbon Module이 포함된 것을 볼 수 있다.
나는 현재까지 구성한 모든 서버와 모듈들은 이 Ribbon의 로드밸런서를 사용하기 위해 만들어진 게 아닐까 할 정도로 가장 강력한 기능이라고 생각한다.
몇 가지 옵션이 존재하지만, 사실 아무런 설정을 하지 않고 사용해도 샘플을 구동하는 데는 부족하지 않을 정도다. 이번 포스트에서는 다른 설명은 하지 않고 구체적으로 어떤 식으로 되는지를 샘플을 통해 정리해보자.
Composite API 의 ProductCompositeServiceBean.java 의 내용을 먼저 보자.
@Service public class ProductCompositieServiceBean implements ProductCompositeService { private static final String PRODUCT_API_URL = "http://product/"; private static final String RECOMMENDATION_API_URL = "http://recommendation/"; private static final String REVIEW_API_URL = "http://review/"; @Autowired private RestTemplate restTemplate; @Override public PagegetProducts(int page, int size, String sort) { String uri = new StringBuffer(PRODUCT_API_URL).append("/?size={size}&page={page}&sort={sort}").toString(); Map pageMap = new HashMap<>(); pageMap.put("page", page); pageMap.put("size", size); pageMap.put("sort", sort); ResponseEntity > responseEntity = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference >() { }, pageMap); if (responseEntity == null || responseEntity.getStatusCode() != HttpStatus.OK) { return null; } return responseEntity.getBody(); } @Override public Product getProductById(Long id) { String uri = new StringBuffer(PRODUCT_API_URL).append("/{productId}").toString(); Map paramMap = new HashMap<>(); paramMap.put("productId", id); ResponseEntity responseEntity = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference () {}, paramMap); if (responseEntity == null || responseEntity.getStatusCode() != HttpStatus.OK) { return null; } Product product = responseEntity.getBody(); Long productId = product.getId(); List recommendations = getRecommendationsByProduct(productId); product.addRecommenations(recommendations); List reviews = getReviewsByProduct(productId); product.addReviews(reviews); return responseEntity.getBody(); } private List getRecommendationsByProduct(Long productId) { String uri = new StringBuffer(RECOMMENDATION_API_URL).append("/byProduct/{productId}").toString(); Map paramMap = new HashMap<>(); paramMap.put("productId", productId); ResponseEntity > responseEntinty = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference
>(){}, paramMap); if(responseEntinty == null || responseEntinty.getStatusCode() != HttpStatus.OK) { return new ArrayList<>(); } return responseEntinty.getBody(); } private List
getReviewsByProduct(Long productId) { String uri = new StringBuffer(REVIEW_API_URL).append("/byProduct/{productId}").toString(); Map paramMap = new HashMap<>(); paramMap.put("productId", productId); ResponseEntity > responseEntinty = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference
>(){}, paramMap); if(responseEntinty == null || responseEntinty.getStatusCode() != HttpStatus.OK) { return new ArrayList<>(); } return responseEntinty.getBody(); } }
ProductCompositeServiceBean은 RestTemplate을 이용해서 각 API에 접속해서 데이터를 가져와서 필요한 데이터를 만들어내는 역할을 하는 서비스 클래스이다.
각 API에 접속하기 위한 URL을 보면 모두 http://{discover service id}/ 의 형태를 가지고 있다. 구체적인 호스트와 도메인 또는 아이피의 정보도 없고 포트에 대한 정보도 없다. 단지, Eureka server에 등록된 service id 만을 이용해서 접속할 수 있다. 앞에서 한번 이야기했지만 Ribbon은 Round robin 방식으로 로드밸런싱을 수행한다고 한다. 실제 요청을 통해 어떻게 동작하는지 보자.
샘플을 이용해서 모든 서비스를 실행하는데 Product API 만 2개를 실행해보자.
그럼 Discovery dashboard에는 아래와 같이 PRODUCT 라는 인스턴스가 두 개가 된다.
이 상태에서 Composite API 또는 Edge server 에 Request를 보내고 두 개의 product API 의 로그를 보면 번갈아가면서 요청이 들어오는 것을 확인 할 수 있다.
아래 로그는 Composite API 의 로그 일부로 처음 요청을 했을 때 한번 나오는 로그이다. neflix의 DynamicServerListLoadBalancer 에 의해서 생성된 로그 내용으로 Ribbon 관련 정보 외에 Circuit breaker 정보 등도 포함되어있다.
DynamicServerListLoadBalancer for client product initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=product,current list of Servers=[product:2ecbee36ab9119e9a2d2341d6b54569b, product:9b4c1ec0b4c53295dfdb6d5e21ff6237],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone; Instance count:2; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;] },Server stats: [[Server:product:2ecbee36ab9119e9a2d2341d6b54569b; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 09:00:00 KST 1970; First connection made: Thu Jan 01 09:00:00 KST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0] , [Server:product:9b4c1ec0b4c53295dfdb6d5e21ff6237; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 09:00:00 KST 1970; First connection made: Thu Jan 01 09:00:00 KST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0] ]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@4ac40560
Zuul proxy 를 이용한 Edge server
Edge server는 외부 클라이언트와 서비스 간의 중간 Gatekeeper 역할을 하는 서버이다.
Zuul proxy 는 Netflix OSS 중에서 Proxy server 역할을 하는 모듈이다.
이번에는 별도의 Edge server로 구성했지만, 예전에 다른 작업을 하면서 사용자용 웹 서버에 Zuul proxy를 설정해서 API 서버와 연결을 한 적이 있는데 그런 역할을 하는 모듈이다.
Zuul proxy 는 개별 기능으로도 효용성이 높은 모듈로 Zuul 만을 이용한 예제를 보고 싶다면, Spring Guide의 Spring Security and Angular JS 를 보면 Zuul proxy 만을 사용하는 예제가 있으니 참고가 되지 않을까 싶다.
그럼 Edge server 를 샘플을 통해서 정리해보자.
우선 EdgeServerApplication.java 를 보자. 역시나 매우 간단하다.
@SpringBootApplication @EnableDiscoveryClient @EnableZuulProxy public class EdgeServerApplication { public static void main(String[] args) { SpringApplication.run(EdgeServerApplication.class, args); } }
여기까지만 하고 EdgeServer 를 실행해보면 실행 로그에 아래와 같은 로그가 나오게 된다.
2016-01-31 14:56:36.705 INFO 6917 --- [ main] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped URL path [/product/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController] 2016-01-31 14:56:36.705 INFO 6917 --- [ main] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped URL path [/review/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController] 2016-01-31 14:56:36.705 INFO 6917 --- [ main] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped URL path [/config-server/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController] 2016-01-31 14:56:36.705 INFO 6917 --- [ main] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped URL path [/recommendation/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController] 2016-01-31 14:56:36.705 INFO 6917 --- [ main] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped URL path [/discovery/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController] 2016-01-31 14:56:36.705 INFO 6917 --- [ main] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped URL path [/composite/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
로그를 보면 Eureka server에 등록된 모든 서비스 인스턴스들이 /{service instance name}의 형태로 proxy가 설정된 것을 볼 수 있다. 기본 설정 상태에서 실행하면 그렇게 된다는 거다.
저 중에서 composite api 에만 접근할 수 있도록 proxy를 만들고 싶다. 그래서 아래와 같은 설정을 Edge server의 설정 파일에 추가했다. 아래 내용은 github 에 등록되어있는 edge-server.yml 파일에서 Zuul 설정 부분이다.
zuul: ignoredServices: '*' routes: composite: path: /composite/**
우선 zuul.ignoredServices 를 통해서 모든 요청의 proxy를 제거한다.
그다음 zuul.routes.{service}.path 를 통해서 설정하면 된다.
위의 경우는 localhost:9000/composite/ 로 들어오는 모든 요청을 composite 서비스로 보낸다는 의미라 하겠다. 이제 다시 Edge server를 실행하면 위의 로그 부분은 없어지고 아래와 같은 로그가 나오게 된다.
2016-01-31 15:07:35.079 INFO 7356 --- [ main] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped URL path [/composite/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
이제 Composite API 만 공개하게 되고, 클라이언트는 설정된 /composite/produt/?size=10&page=1 라던가 /composite/product/1 같은 형태로 접속해서 서비스를 이용할 수 있게 되었다.
Zuul proxy를 이용할 때 한가지 조심해야 하는 부분이 있는데, Zuul proxy를 이용해서 대용량 파일을 보낼 때는 약간의 설정을 추가해 줄 필요가 있다. 이는 Zuul proxy의 기본 timeout 설정 때문에 발생하는 문제인 것으로 보이는데, Spring cloud documentation을 확인해 보면 아래와 같은 설정으로 해결할 수 있다고 되어있다.
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000 ribbon: ConnectTimeout: 3000 ReadTimeout: 60000
Spring cloud documentation을 보면 Netflix 는 아래와 같은 목적으로 Zuul을 사용한다고 한다.
- Authentication
- Insights
- Stress Testing
- Canary Testing
- Dynamic Routing
- Service Migration
- Load Shedding
- Security
- Static Response handling
- Active/Active traffic management
결론
개인적으로 생각할 때는 이 포스트에 정리한 내용이 Spring cloud 를 이용해서 마이크로서비스를 구현하는 것에 있어서 가장 핵심이 되지 않을까 싶다. 이전 포스트에서 정리했던 Eureka server의 구성도 결국 이 작업을 위해서 필요한 것이 아닐까 생각한다. 처음 Spring cloud 에 관심을 갖게 된 부분도 오늘 정리한 부분 때문이었다. 특정 아이피/포트/호스트/도메인 을 이용한 API 간의 통신이 아닌 인스턴스 아이디를 이용한 통신이 가능하다는 것이 너무 매력적이었다. 게다가 로드밸런싱까지 해 준다니... 처음 접했을 때는 거의 혁명에 가깝다고 생각할 정도였다.
워낙에 뭔가를 정리하는 데에는 능력이 없어서 정리가 매끄럽지 않지만, calista 블로그의 내용가 Josh Long 의 Devoxx 2015 발표 동영상 등을 참고한 후에 내 github에 있는 소스를 본다면 조금은 이해가 쉽지 않을까 하는 생각을 해본다.
블로그 잘봤습니다. 감사합니다.
답글삭제혹시 Edge server 에 호출시 "Load balancer does not have available server for client: {service-id}" 오류 가 납니다. service-id 는 eureka 에 등록된 샘플욜 resource api server 입니다.
edge server 가 service-id 는 인식하고 라우팅을 하려고 하는 거 같은데 위와 같은 오류가 나서 500 오류를 출력하네요.
부족한 블로그 봐주시고 답글까지 감사합니다.
삭제블로그 소스를 다시 구동해 봤습니다만, 해당 오류를 볼 수가 없네요.
Configuration 서버와 Discovery(Eureka) 서버가 정상적으로 기동되었다면 일반적으로 라우팅이 되어야 할텐데 말이죠.
Edge 서버의 Exception 로그를 trace 된 부분까지 포함해서 댓글이나 메일로 주실 수 있을런지요?
제가 많은 경우를 해 보지 못한 상태에서 정리를 한 상황이라 바로 답변을 드리지 못해 죄송합니다.
아닙니다^^ 정말 유익한 정보였습니다. 감사합니다.^
삭제컨피그 서버는 필수가 아니라서 생략 했습니다.
문제는 해결했습니다....
왜그런지는 모르겠지만 maven dependency 를 수정하니 정상 작동이 되네요. spring-cloud-netflix-starter-eureka 가 아닌 spring-cloud-netflix-eureka-client 로 설정이 되어 있었습니다.
해결하셨다니 다행입니다.
삭제dependency의 문제였군요.
아직 저도 실전 적용을 못 한 상황이라 많은 경우를 경험해보지 못해서 정확한 정보를 드리지 못하고 있네요.
저 혼자 볼거라 생각하고 너무 성의없게 정리한게 아닌가 반성해봅니다. 앞으로 포스트를 쓸 때는 좀 더 노력해야겠네요. ^^