2016. 1. 31.

Edge server와 Ribbon, 그리고 API 구성

서론


여기서 말하는 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를 기준)의 경우는 아래의 과정을 거쳐서 설치할 수 있다.

  • Settings > Plugins > Install JetBrains plugin... 을 클릭한다.
  • lombok을 검색한다.
  • Lombok Plugin을 선택하고 설치를 한다.
  • Settings > Build, Execution, Deployment > Compiler > Annotation Processors 를 선택한다.
  • Enable annotation processing 에 체크한다. (매번 프로젝트를 새로 진행할 때 이 부분을 잊어버려서 Lombok 이 정상 동작하지 않는 경우가 많았다. 나처럼 멍청한 실수를 하시는 분들이 없기를 바란다)



기본 구성


샘플의 시스템 구성은 앞서 서론에서 이야기한 Calista 블로그의 글에 정리된 것과 같다.

[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 에 조회를 요청하면 아래와 같은 파일에서 정보를 찾게 된다.

  • 기본 application.properties 또는 application.yml
  • spring.appliation.name 에 설정된 이름의 설정파일.
  • spring.profiles.active 에 해당되는 설정파일
예를 들어서 production 이라는 application에서 test라는 profile로 요청한 경우 아래와 같은 순서로 조회하며, 후순위 조회의 정보가 가장 큰 우선순위를 가진다. 즉, 같은 설정이 있을 경우 후순위 조회된 정보로 업데이트된다고 보면 된다.

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를 보면 매우 간단하다. 별다른 설명이 필요 없을 것으로 보인다.

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 Page getProducts(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에 있는 소스를 본다면 조금은 이해가 쉽지 않을까 하는 생각을 해본다.



2016. 1. 17.

Service Discovery Server와 Git을 이용한 Configuration Server

서론


이제 실제로 Spring-cloud 를 이용해서 마이크로서비스 아키텍처 스타일의 애플리케이션을 만드는 방법에 대해서 정리를 할까 한다.

우선은 전체 구조에서 가장 근간이 될 Service Discovery Server와 Cofiguration Server 로 시작해 보자.
Configuration Server 는 이전 포스트에서 한 번 정리를 한 적이 있으니 이번에는 Git을 이용해서 설정하는 법에 대해서만 정리할 예정이다.

이 포스트에서 사용된 소스를 실행하기 위한 부분이나 필요사항은 아래와 같다.
  • gradle에 대한 기본 사용법 (버전 2.9와 2.10에서 확인했다)
  • IDEA 15(이클립스에서도 될 것으로 보이나 테스트는 못해봤다)
  • spring-io dependency-management-plugin : 0.5.4.RELEASE
  • spring-io : 2.0.1.RELEASE
  • spring-boot : 1.3.1.RELEASE
  • git(local git, github) 에 대한 기본 지식
  • git repository : https://github.com/roadkh/blog-cloud-sample.git
Gradle 과 Git은 나도 능숙하게 쓰는 편은 아니므로 혹시 문제가 있으면 알려주시기를...

전체 소스는 아래와 같이 확인이 가능하다.
  git clone https://github.com/roadkh/blog-cloud-sample.git
  git checkout blog_01

프로젝트 Gradle 구성


설정하는 부분에 대한 설명은 이전 포스트를 참고하기 바란다. 해당 포스트와 전체적으로 같다.

다만 settings.gradle 파일만 아래와 같이 변경되었다.

rootProject.name = 'blog-cloud'

['client', 'server', 'api'].each {
    def projectDir = new File(rootDir, it)

    // 만약 그룹디렉터리가 없으면 생성한다.
    if( !projectDir.exists() ) {
        projectDir.mkdirs()
    }

    // 모듈 그룹 디렉터리 하위에 디렉터리가 있으면 서브프로젝트로 등록한다.
    projectDir.eachDir { dir ->
        include ":${it}-${dir.name}"
        project(":${it}-${dir.name}").projectDir = new File(projectDir.absolutePath, dir.name);
    }
}

server 디렉터리에 configuration과 discovery 디렉터리를 생성하자.
이제 Refresh All Gradle projects를 한다. (Eclipse에서는 import project 에서 gradle 프로젝트를 통해 프로젝트를 추가할 수 있다)
이렇게 하면 :server-configuration 프로젝트와 :server-discovery 프로젝트가 생성된다.

Service Discovery Server로 Eureka 서버 구성


Spring-cloud 프로젝트에는 Service discovery server로 Consul, Eureka 등 여러 가지가 있다. Netflix OSS 프로젝트를 이용해서 구성하기로 했으니 Eureka를 이용해서 구성을 해보고자 한다.

앞에서 생성한 discovery 디렉터리로 이동해서 build.gradle을 생성하고 아래와 같이 입력한다.

apply plugin: 'spring-boot'

dependencies {
    compile 'org.springframework.cloud:spring-cloud-starter-eureka-server'
}

이제 적당한 패키지를 생성하고 그 안에 스프링부트 메인 클래스를 생성하자.
(com.road.pilot.blog.server.discovery 패키지에 DiscoveryServerApplication으로 생성했다)
해당 클래스에는 두 개의 어노테이션이 필요하다.
스프링부트 애플리케이션으로 만들기 위한 @SpringBootApplication 과 Eureka Server로 만들기 위한 @EnableEurekaServer 를 추가해준다. 여기까지 하면 기본 작업은 완성이다.
전체 소스의 내용은 아래와 같다.

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

이제 프로그램을 실행해보자. 실행이 잘 된다. 뭔가 오류가 올라가지만, 우선은 무시하자.
아마도 8080 포트(또는 8761)로 실행이 되었을 것이다.
접속을 해보면 아래와 같은 모습을 볼 수 있다.



일단 위와 같은 상태에서도 사용은 가능한 것으로 보인다. 실제로 클라이언트를 만들고 접속해보면 정상적으로 서비스가 등록된다. 다만, 콘솔을 보고 있으니 무언가 계속해서 오류가 올라간다.

java.lang.UnsupportedOperationException: Backup registry not implemented.
 at com.netflix.discovery.NotImplementedRegistryImpl.fetchRegistry(NotImplementedRegistryImpl.java:15) ~[eureka-client-1.1.147.jar:1.1.147]
 at com.netflix.discovery.DiscoveryClient.fetchRegistryFromBackup(DiscoveryClient.java:1811) [eureka-client-1.1.147.jar:1.1.147]

2016-01-16 21:22:36.236 ERROR 5647 --- [pool-9-thread-1] com.netflix.discovery.DiscoveryClient    : Can't get a response from http://localhost:8761/eureka/apps/
Can't contact any eureka nodes - possibly a security group issue?

com.sun.jersey.api.client.ClientHandlerException: java.net.ConnectException: Connection refused
 at com.sun.jersey.client.apache4.ApacheHttpClient4Handler.handle(ApacheHttpClient4Handler.java:184) ~[jersey-apache-client4-1.11.jar:1.11]
 at com.sun.jersey.api.client.filter.GZIPContentEncodingFilter.handle(GZIPContentEncodingFilter.java:120) ~[jersey-client-1.13.jar:1.13]
 at com.netflix.discovery.EurekaIdentityHeaderFilter.handle(EurekaIdentityHeaderFilter.java:28) ~[eureka-client-1.1.147.jar:1.1.147]

물론 com.netflix.discovery 패키지에 대해서 모든 로그를 OFF 시켜버리면 해결이 된다.

위와 같은 현상은 왜 발생하는 것일까? 기본적으로 Eureka 서버는 Replication 을 기본으로 설계되어있는 것으로 보인다. 따라서 최소 두 개의 Eureka 서버를 만들고, Eureka 서버들끼리 서로를 등록하고 관리하게 되어있다는 것이다. 그렇다면 하나만 사용할 수는 없는 것일까?

/src/main/resources 폴더에 application.yml 을 생성하고, 약간의 설정을 추가해보겠다.

application.yml 의 Eureka 관련 설정은 아래와 같다.

eureka:
  client:
      registerWithEureka: false 
      fetchRegistry: false

registerWithEureka(properties에서는 register-with-eureka) 와 fetchRegistry(properties에서는 fetch-registry) 를 모두 false 로 바꾸고 실행하면 아까 보던 오류는 더는 나오지 않는다. 그러나 페이지를 띄워보면 또 오류가 발생할 것이다.

그렇다면 원래 설계된 대로 두 대의 Discovery 서버를 설정해 보려고 한다.
여기서 spring profile을 이용할 생각이다. spring.profile.active가 peer1 인 경우와 peer2 인 경우로 해서 두 대의 Discovery 서버를 띄울 수 있도록 할 생각이다.

/src/main/resources 폴더에 application-peer1.yml 과 application-peer2.yml 을 생성하자.

server:
  port: 8761

spring:
  application:
    name: 'discovery'
  profiles:
    active: peer1

eureka:
  instance:
    hostname: peer1
    preferIpAddress: true
    leaseExpirationDurationInSeconds: 90 #default
    leaseRenewalIntervalInSeconds: 30    #default
    metadataMap:
          instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}}
  client:
      registerWithEureka: true
      fetchRegistry: true
      service-url:
        defaultZone: http://localhost:8762/eureka/


server:
  port: 8762

spring:
  application:
    name: 'discovery'
  profiles:
    active: peer2

eureka:
  instance:
    hostname: peer2
    preferIpAddress: true
    leaseExpirationDurationInSeconds: 90 #default
    leaseRenewalIntervalInSeconds: 30    #default
    metadataMap:
          instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}}
  client:
      registerWithEureka: true
      fetchRegistry: true
      service-url:
        defaultZone: http://localhost:8761/eureka/

port를 8761와 8762로 해서 두 대의 Eureka Server를 실행할 수 있도록 했다.
VM 파라미터에 -Dspring.profiles.active=peer1, -Dspring.profiles.active=peer2 를 각각 주고 실행해보자. 처음 실행하는 쪽에는 약간의 오류가 발생하겠지만, 두 대가 모두 올라오고 나면 그 뒤로 오류는 없어진다.

대신 다음과 같은 내용을 로그에서 확인할 수 있다.

2016-01-16 21:45:27.125  INFO 7021 --- [pool-8-thread-1] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_DISCOVERY/127.0.1.1:discovery:433b7810ff29e44253031b19332d9f0f: registering service...
2016-01-16 21:45:27.132  INFO 7021 --- [pool-8-thread-1] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_DISCOVERY/127.0.1.1:discovery:433b7810ff29e44253031b19332d9f0f - registration status: 204

양쪽 로그를 보면 주기적으로 계속해서 저런 로그들이 올라간다.

약 1분 정도 기다렸다가 포트 8761와 8762 에 접속해서 확인해보면 아래와 같이 등록된 인스턴스가 보인다.



이제 옵션을 잠시 보자.
eureka.instance의 항목을 먼저 보자(괄호 안은 properties를 사용할 경우).

  • preferIpAddress(prefer-ip-address) : 기본은 false 값이다. false 일 경우에는 hostname 을 이용해서 서비스 등록이 이루어진다. 위의 그림의 127.0.1.1 대신에 hostname이 입력된다.
  • leaseRenewalIntervalInSeconds(lease-renewal-interval-in-seconds): Heartbeat을 보내는 주기이다. 기본값은 30초이다.
  • leaseExpirationDurationInSeconds(lease-expiration-duration-in-seconds) : 서버는 인스턴스로부터 마지막 heartbeat을 받은 이후에 여기 설정된 시간이 지나도록 heartbeat이 오지 않으면 해당 인스턴스를 제거한다. 이 부분은 기회가 된다면 다른 포스트를 통해서 한번 확인을 해 볼 생각이다. 기본값은 90초다.
  • metadataMap.instanceId : 여러 개의 인스턴스를 띄워야 할 때 instanceId가 동일하면 대시보드의 정보가 계속 추가되는게 아니라 업데이트되는 현상이 있다. 그래서 random.value를 추가하는 것으로 해결했다(스프링 문서에도 그렇게 처리를 하고 있다)

현재는 다른 인스턴스가 없이 Discovery 서버가 서로에게 인스턴스로 등록되고 있다. 그런데 서버 두 대를 띄우고 나서 대시보드 페이지에 접속해보면 매우 한참 후에 인스턴스가 모두 올라온다.(보통은 1분~1분 30초)
이 부분은 정확하지는 않지만, 스프링 클라우드 문서를 보면 Eureka Server가 정상적으로 서비스 인스턴스에 대해 처리를 할 수 있는 것은 인스턴스, 서버, 클라이언트가 모두 같은 Metadata를 캐싱한 순간부터라고 한다. 따라서 최소 3번의 Heartbeat이 필요하다고 한다.
실제로 클라이언트를 이용해서 보면 실제로 클라이언트가 Ribbon 등을 이용하여 라우팅할 때 정상적으로 되려면 꽤 시간이 필요하다. 위 설정에서 leaseRenewalIntervalInSeconds 을 줄인다면 이 지연시간을 꽤 줄일 수 있지만, 대신 자주 heartbeat을 하게 되므로 필요한 용도에 맞게 이 부분을 조절해 줄 필요가 있다.

Git 을 이용한 Configuration Server 구성


Configuration Server에 대해서는 이전에 한 번 정리한 적이 있다.
이전 포스트에서는 로컬 저장소를 이용하는 방법을 사용했었는데, 해당 방법에는 약간의 문제가 있었다. 지금까지 확인한 문제점은 아래의 두 가지였다.
  • 싱크의 문제 : 로컬 저장소의 파일을 이용하는 경우에는 파일의 내용이 바뀌어도 Configuration Server에 질의 내용으로 얻는 설정에는 변경이 일어나지 않는다. 즉, 설정이 바뀌면 Configuration Server를 재시작해야만 변경된 설정이 질의 가능했다.
  • 프로퍼티의 분리 문제 : 이 부분은 정확히 설명하기가 좀 힘들고 재연도 어떻게 해야 할지 막막하긴 한데, 프로젝트를 진행하는 중에 일부 클라이언트의 설정이 Configuration Server에 설정된 내용을 가져오거나 그 반대의 경우가 발생했다. 이 부분은 내가 기본 폴더인 resource 폴더를 이용했기 때문에 발생한 문제인 것으로 보이지만, 어쨌건 뭔가 명확하지 않다는 부분은 문제점이 될 것으로 보인다.
그래서 이번에는 git 을 이용하는 방법을 이용해보고자 한다.
로컬에 있는 git 저장소를 이용하는 방법을 통해서 간단히 알아보겠다.
로컬에 git 저장소를 하나 만들고 Configuration Server의 bootstrap.yml에 아래와 같이 설정을 해 보았다.

spring:
  application:
    name: 'config-server'
  profiles:
    active: 'cloud'
  cloud:
    config:
      server:
        git:
          uri: /home/road/media/Dev/workspace/pilot/blog-cloud/blog-cloud-sample-config/ #local git 설정

우선은 확인을 위해 http://localhost:8888/test/default 로 접속해보았다. 아직 configuration 용으로 만들어진 어떤 파일도 없으므로 내용에는 기본 설정 정보 정도만 나온다.
로컬의 git 저장소에 test.yml 이라는 파일을 하나 만들고 적당한 프로퍼티를 추가했다.
예를 들어 test.message='This is test' 와 같은 내용을 추가했다.
그리고 commit을 한 후에 다시 접속해서 확인을 해보니 아래와 같았다.


즉, Commit 만 하면 Configuration Server의 재시동이 없이도 즉각적으로 반영된다는 이야기이다. 이 부분이 git 을 이용하게 될 경우의 장점이라 하겠다.
private git 을 이용하는 경우에는 uri, username, password 를 이용해서 설정할 수 있고, public git을 이용하는 경우에는 uri를 configuration이 있는 곳으로 설정해주면 된다.

Remote Git을 이용하는 것이 여러 가지 면에서 가장 좋겠지만, 한 가지 단점은 속도가 약간 느리다는 점이다. github 이나 bitbucket 등의 서비스를 연결해보니 약간의 지연이 발생을 한다. 하지만 Configuration Server의 용도는 보통 Application이 올라오는 순간에 한번 읽는 것이므로 약간의 지연이 큰 치명타는 아닐 것으로 보인다. 운영 중 설정변경에 대해 RefreshScope을 이용해서 처리하는 경우에는 약간의 영향을 받을 수 있을 테지만 말이다.

Configuration Server를 Discovery Server에 등록하기


이제 Configuration Server를 Discovery Server에 등록하자.

build.gradle 에 compile('org.springframework.cloud:spring-cloud-starter-eureka')을 추가하고 gradle을 refresh 해서 dependency를 설정한다.

다음으로 ConfigurationServerApplication 에 @EnableDiscoveryClient 어노테이션을 추가한다.

이제 Configuration Server에 application.yml(또는 application.properties)를 만들고 아래처럼 설정하자.
설정 내용은 이전 Discovery Server의 peer1, peer2와 크게 다르지는 않다. (이 설정을 그냥 bootstrap.yml 에 넣어도 된다)

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    preferIpAddress: true
    leaseRenewalIntervalInSeconds: 30
    leaseExpirationDurationInSeconds: 90
    metadataMap:
      instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}}

이제 Discover Server를 모두 실행한 후에 Configuration Server를 실행해보자.
실행하고 30초 정도를 기다리면 registration 관련된 로그가 올라온다.

이제 Discovery Server에 접속해서 확인하면 아래와 같은 화면을 볼 수 있다.


이제 Configuration Server 가 등록되었다.
다른 클라이언트들이 이제 이를 이용할 수 있게 되었고, Discovery Server의 Dashboard에서 상태를 확인할 수 있다.

결론


사실 Discovery Server와 Configuration Server는 이를 이용할 수 있는 Client 들이 있어야만 한다.
별로 효율적인 구성이 아니어서 정리하지는 않았지만, Discovery Server도 Configuration Server를 이용하도록 bootstrap.yml 을 설정하고 Configuration Server를 먼저 띄운 후에 Discovery Server를 후에 띄우고, 그 이후에 Heartbeat을 이용해서 Configuration Server를 Discovery Server에 등록되도록 구성하는 방법도 있다.

다음번에 기회가 된다면 실제로 Client 들을 만들고 오늘 정리한 내용을 이용하는 것에 대해서 정리를 해볼까 한다.

이 포스트에서 정리된 전체 소스는 https://github.com/roadkh/blog-cloud-sample 의 blog_01 branch에서 확인해 볼 수 있다.




2016. 1. 9.

Spring-Cloud 프로젝트와 Netflix OSS를 이용한 Microservice 구성

서론

그동안 정리했던 포스트의 내용은 마이크로서비스 아키텍처에 대한 기반에 대한 내용이었다.

일련의 마이크로서비스 아키텍처에 대한 정리의 시작은 사실 몇 달 전 Netflix OSS 를 보게 된 것이 그 계기가 됐다. Netflix 는 국내에서는 그리 유명하지는 않지만, 세계적으로는 2014년 기준으로 5,000만 명 이상의 가입자를 소유하고 있는 유명한 동영상 월정액 스트리밍 서비스를 제공하는 회사다. 이렇게 간단히 정리하기엔 좀 미안한 회사지만, Netflix 를 소개하는 포스트가 아닌 만큼 자세한 설명은 생략하겠다. (최근에 한국서비스가 시작됐다고 한다)

서비스 적으로도 유명하지만, 다양한 기술 기반을 가지고 있는 회사로도 유명하다. 서비스 특성상 클라우드를 가장 활발하게 이용하고 있는 회사 중 하나이면서, 그에 따른 다양한 솔루션들을 제작하는 회사이기도 하다. 특히 Netflix OSS(Open Source Software) 를 Netflix Open Source Center 를 통해서 공개하고 있기도 하다. 여기에는 마이크로서비스를 구성하는 다양한 솔루션은 물론 빅데이터/보안/사용자 인터페이스 등 다양한 모듈들이 공개되어있다.

그동안 그 모듈들의 설명이나 내용을 봐도 사실 거의 이해를 하지 못하고 있었다. 마이크로서비스 아키텍처에 대해서는 약간의 지식만 있을 뿐이었고, 또 변형된 아키텍처 하나가 나왔나 보네 하면서 넘어갔던 때였으니 말이다. 겨우 공부 겸 진행하던 프로젝트에서 Zuul proxy 정도 사용해 보면서 편리한 솔루션이라고 생각했을 뿐이었다. 이 때 Netflix OSS 중 일부 솔루션이 Spring-Cloud 프로젝트에 포함된 것을 알게 되었고, 그 내용을 보다가 마이크로서비스 아키텍처에 매력을 느끼게 돼서 다시 마이크로서비스 아키텍처 패턴부터 보게 되었다.

최근에는 따로 진행하는 사이드 프로젝트에서 클라우드를 이용해야 할 필요성이 있어 Spring-cloud 프로젝트를 이용하게 되었고, 이 과정에서 몇 가지 알게 된 내용을 정리를 해 볼까 하고 생각하게 된 것이다.

오늘은 Spring-cloud 와 Netflix OSS 를 이용해서 어떻게 마이크로서비스를 구성해 볼 수 있는지에 대해서 정리를 하고자 한다.

이번 정리는 Magnus Larsson의 An operations model for Microservices 의 도움을 많이 받았다.

마이크로서비스를 구성함에 있어서의 기본 구성요소는 무엇일까?

마이크로서비스 아키텍처를 실제로 구성한다고 하면 어떤 것들이 필요할까?

클라우드에서 돌아가는 여러 애플리케이션을 가지고 있는 하나의 서비스가 있다고 가정하자.

하나의 애플리케이션이 수평 확장이 필요한 상황이 되었다. 이때 새로운 애플리케이션을 컨테이너 또는 다른 클라우드 서버에 배포하고 실행을 하려고 한다.
잠깐, 물리적으로 서버가 다를 수도 있다고? 네트워크 대역도 다를 수 있고? 그럼 하드웨어 라우터나 하드웨어 로드밸런서로는 어떻게 처리를 해야 하지? 게다가 수시로 서비스가 확장됐다가 축소된다고? 언제 서비스가 확장될지 축소될지조차 미리 계획에 있는 것은 아니라고? 에잇. 나 안 해.
대충 위와 같은 시나리오지 않을까?.  특히 시스템 팀부터 개발팀, 테스트팀 등이 잘 갖추어져 있는 큰 조직(그런 회사를 다녀본 적이 없어서 잘 모르겠지만) 에서는 어느 정도 조직화/전문화가 이루어져 있어서 느릴지는 몰라도 잘 해결할 수 있지 않을까 생각한다. 하지만  개발팀이 서버 운영, 네트워크 운영, 개발 등을 모두 담당하고 있는 경우에는 사실 굉장히 버겁고 힘든 일이다.

그렇다면 Spring-cloud 는 어떤 식으로 이런 문제들을 해결하려고 할까.

우선 애플리케이션들은 실행되거나 종료될 때 어떤 서버에 본인의 상태를 알려주게 된다. 이 실행 상태를 보고받은 서버를 보통 Service Discovery Server(또는 Service Registration Server)라고 부르는데, 이 서비스는 주변 애플리케이션들을 등록하고 삭제하는 역할을 하게 된다. 이때 Discovery Server는 등록하려는 애플리케이션의 아이피 또는 호스트 주소와 포트 정보 등을 받아서 등록해 놓게 되고, 일정 간격으로 해당 애플리케이션의 상태 체크도 진행할 수 있게 되어있다 (각 애플리케이션이 보고하거나 아니면 Discovery Server 본인이 체크를 하거나). 

서비스들이 확장 때문에 계속 늘어가는 과정에서 중간에 일부 설정이 변경되었다. 애플리케이션별로 설정이 패키지 내부에서 관리되고 있다면 이 과정에서 어디에 어떤 설정이 적용되고 있는지를 알기가 매우 힘들어진다. 만약, 중앙 설정관리 서버(Central Configuration Server)가 있어서 설정이 변경된 시점에서 해당 서비스들로 등록된 애플리케이션들에 설정을 재설정하라고 할 수 있다면 좋지 않을까? 또는 새로 확장되어 등록되는 애플리케이션들은 지금 현재의 설정으로 동작하리라는 것이 보장되면 좋지 않을까? 중앙 설정관리 서버의 역할도 매우 중요해 보인다.

마이크로서비스는 서비스마다 자기들이 담당하는 비즈니스 기능이 다르다. 따라서 다른 비즈니스 기능과의 연계가 필요한 경우에는 네트워크(Http 방식 또는 dummy bus 를 이용)를 이용해서 다른 서비스와 통신을 하게 된다. 연계된 서비스가 수시로 수평 확장과 축소가 이루어지는데 그 정보들을 다 다른 서비스들에 알려주려면 어떻게 해야 할까? 아! 앞에서 Discovery Server에 등록된 정보가 있지 않은가? 이 정보를 이용해서 라우팅과 로드밸런싱을 처리해 주는 소프트웨어가 이런 기능을 담당하게 된다. 예를 들자면 회원(member-api), 제품(product-api), 주문(order-api)이라는 세가지 서비스로 구성된 시스템이 있다고 할 경우 주문 정보를 처리하는 order-api 가 제품과 회원 정보가 필요하다면 두 서비스에 질의해서 정보를 조회해 와야 한다. 이때 order-api 는 소프트웨어 라우터와 로드밸런서 역할을 하는 서버에게 member-api 와 product-api 에 이런 정보를 요청해줘 라고 질의한다. 라우터와 로드밸런서 역할을 하는 서버는 Discovery Server에 member-api와 product-api 로 등록된 서비스들의 정보를 이용해서 적절한 라우팅과 로드밸런싱을 수행하여 정보를 order-api 가 다른 두 서비스에 정보를 요청할 수 있게 도움을 준다.

다음으로 마이크로서비스는 네트워크를 이용한 서비스 간 인터페이스를 이용하는 중에 특정 서비스 구간에서 문제가 발생할 경우 연속적인 실패가 나지 않도록 처리를 해줘야 한다. 그러기 위해서는 중간에서 서킷 브레이커가 실패 난 서버에 대해서 서킷브레이커를 오픈해주고 해당 오류가 복구되는 시점에서 다시 서킷브레이커를 클로즈 함으로써 서비스의 오류들을 조정해 줄 필요가 있다.

각 서비스의 오류 상태나 복구 상태 그리고 현재 오류가 발생하고 있는 내용은 무엇인지를 파악하기 위해서는 서킷브레이커의 정보나 각 서버의 상태를 확인할 수 있는 모니터링을 위한 서비스 또한 필요하다. 그리고 오류가 발생했을 때 단순한 네트워크 오류에 대해서는 이런 모니터링으로 처리되겠지만, 만약 프로그램상에서 발생한 오류일 경우에는 수많은 서버 또는 컨테이너를 돌아다니면서 로그를 확인할 수는 없는 노릇이지 않겠는가? 이를 위해서 중앙집중형으로 관리가 되고 검색할 수 있도록 기능을 제공해주는 서비스도 필요하다.

마이크로서비스는 모바일 앱에 서비스를 제공해주기도 하고 웹에 서비스를 제공해 줄 수도 있어야 한다. 이런 모든 과정에 자칫 모든 서비스가 외부로 공개되는 문제가 발생할 수 있다. 이를 처리하기 위해 권한을 조정하고 보안을 처리해 주는 기능을 가지고 있는 서비스도 있어야 한다.
이 과정에서 이전 포스트에서 봤던 마이크로서비스 구현 패턴의 하나인 aggregator 패턴이나 proxy 패턴에서의 역할을 하는 서비스(Edge Server)가 필요할 수도 있다.

말은 너무 많고 설명은 너무 장황하고(내가 글솜씨가 없어서 생기는 문제점이다) 서비스는 너무 많다. 간단히 정리를 해보자.

주요 기능 구성들
  • Service Discovery Server
  • Central Configuration Server
  • Dynamic Routing and Loadbalancer
  • Circuit Breaker
그외 주요기능을 지원할 수 있는 필요 구성들
  • Edge Server
  • OAuth 2.0 protected APIs
  • Monitoring
  • Centralized Log Analysis

구체적으로 Spring-Cloud + Netflix OSS 로 어떻게 구성될까?

Spring-cloud 에는 동일 기능을 하는 다른 솔루션에 대한 구현도 있지만, Netflix OSS의 구현을 기반으로 해서 정리한다 (Spring-cloud 에는 동일 기능을 하는 Netflix OSS 이외의 모듈에 대한 지원도 많이 있다).
모든 구성이 그렇지만 각 조직의 지식정도와 수준에 맞춰서 새로운 구성이 있을 것으로 보인다.
아래 구성은 Magnus Larsson 의 글을 기반으로 해서 정리한다. 

주요 기능 구성들

기능 솔루션
Service Discovery Server Netflix Eureka
Central Configuration Server Spring Cloud Configuration Server
Dynamic Routing / Loadbalancer Netflix Ribbon
Circuit Breaker Netflix Hystrix

지원 기능들

기능 솔루션
Edge Server Netflix Zuul
OAuth 2.0 protected APIs Spring-Cloud-Security
Monitoring Netflix Hystrix dashboard + Turbine
Centralized Log Analysis ELK (Elasticsearch + Logstash + Kibana)


구성된 시스템의 전체 그림은 앞서 소개했던 Magnus Larsson의 Building microservices with Spring Cloud and Netflix OSS, part 1 의 SYSTEM LANDSCAPE 에 있는 그림을 보면 이해가 빠를 것으로 생각된다.

결론

Spring-cloud + Netflix OSS 라는 제목으로 시작했지만, 사실 Spring-cloud 의 다양한 모듈 중에서 Netflix OSS 를 연계해 놓은 기능을 사용한 구성이라고 말하는 것이 더 정확할 것이다.

Springframework 는 최근 클라우드에 기반을 둔 시스템을 지원하는 프레임워크 기능 강화에 많은 부분을 할애하고 있는 것으로 보인다. Spring-boot 프로젝트들은 그 기반을 이루고 그 위에 Spring-cloud 가 뒷받침을 하고 있는데 여기에는 다양한 Springframework의 모듈들이 합쳐져서 만들어지고 있다. Hateoas, data, data-flow, actuator 등등의 많은 모듈이 Spring-Cloud 프로젝트로 함께 합쳐지고 보완되면서 나아가고 있는 것으로 보인다.

최근 계속해서 Spring-cloud 를 이용한 실험을 진행해 보고 있다. 원래 직접해 보지 않고 설명만 봐서는 잘 이해하지 못하는 수준의 실력이라 간단한 기능들과 몇 가지 간단한 실험 위주로 기능과 구성에 대해 이해해 보려고 진행한 것인데 꽤 재미가 있는 작업이 되고 있다.

아래는 위에서 설명한 내용의 기본을 제공했던 글로 현재 진행하는 pilot 과 이 블로그를 정리하는데 가장 많은 도움이 됐던 글이다.

- Spring blog : Microservice Registration and Discovery with Spring Cloud and Netflix's Eureka :
  Service Discovery Server 와 Configuration Server 구성에 대한 예제가 있다.

- Magnus Larsson : Blog Series - Building Microservices
  2015-01-09 일 현재 Part 4까지 진행된 시리즈의 인덱스 페이지이다.
  Service Discovery Server, Configuration Server, Hystrix, OAuth 2.0, Zuul Proxy 등을 이용한 구성에 대한 설명과 함께 예제 소스도 제공이 되고 있다. 현재 내가 진행중인 pilot 을 진행하기 전에 가장 도움이 됐고, 현재도 지속적으로 다시 보는 글이다.


2016. 1. 2.

The Twelve-Factor App 란 무엇인가?

서론

공식 사이트인 http://12factor.net/ 의 내용을 기반으로 해서 정리를 해 볼 까 한다.
처음 이 사이트를 방문했을 때는 한국어가 없었는데, 어떤 고마운 분이 한국어 번역판을 올려주신 듯 하다. 한국어는 http://12factor.net/ko/ 에서 확인할 수 있다.

the Twelve-Factor App이 지향하는 특성을 원문의 서문에 나온 굵은 긁씨의 단어들로 정리하면 다음과 같다.

declarative, clean contract, maximum portability, deployment, cloud platforms, Minimize divergence, continuous deployment, scale up

많은 단어가 있지만, 결국 지향점은 클라우드 환경에서 쉽게 확장가능한 애플리케이션이라는 것으로 귀결되지 않을까 생각한다.
즉, the Twelve-Factor App은 클라우드 환경에 적합한 애플리케이션의 특성을 12가지 요소로 정리한 내용이다. 각 특성을 자세히 보면 이전 포스트에서 정리했던 마이크로서비스의 개념과도 통하는 부분들이 많은것을 볼 수 있을 것이다. 이것이 마이크로서비스 포스트를 작성하다가 12 Factor App을 정리한 이유이다.

The Twelve-Factor 란?

사이트 첫페이지에 나오는 12개의 요소를 요약해 놓은 것을 그대로 옮겨보았다.

I. 코드베이스
버전 관리되는 하나의 코드베이스와 다양한 배포

II. 종속성
명시적으로 선언되고 분리된 종속성

III. 설정
환경(environment)에 저장된 설정

IV. 백엔드 서비스
백엔드 서비스를 연결된 리소스로 취급

V. 빌드, 릴리즈, 실행
철저하게 분리된 빌드와 실행 단계

VI. 프로세스
애플리케이션을 하나 혹은 여러개의 무상태(stateless) 프로세스로 실행

VII. 포트 바인딩
포트 바인딩을 사용해서 서비스를 공개함

VIII. 동시성(Concurrency)
프로세스 모델을 사용한 확장

IX. 폐기 가능(Disposability)
빠른 시작과 그레이스풀 셧다운(graceful shutdown)을 통한 안정성 극대화

X. dev/prod 일치
development, staging, production 환경을 최대한 비슷하게 유지

XI. 로그
로그를 이벤트 스트림으로 취급

XII. Admin 프로세스
admin/maintenance 작업을 일회성 프로세스로 실행

위의 12개 요소에 대한 제목만 봐도 대부분의 사람들은 다 이해를 하지 않을까 싶다.
그렇다고 여기서 다 정리했으니 끝 하기에는 뭔가 좀 아쉽다.
그래서 뭐 단순한 개념일지는 몰라도 정리하다 보면 꽤 난해한 부분도 있으니 각 요소마다 한번 정리해보자. 일부 요소의 경우는 논란이 되거나 동의하지 않는 사람들도 꽤 많아 보인다.
일부 개발자들의 경우는 몇가지 요소에 대해 동의할 수 없어서 12 Factor App 이라는 것 자체를 부정하는 포스트를 작성하는 경우도 있던데, 그러지 말고 도움이 될 수 있는 부분들도 많으니 참고삼아 한번 보도록 하자.

I. 코드베이스 : One codebase, Multiple deploys

12개 요소 중 처음을 코드베이스에서 시작한것은 그만큼 코드베이스가 중요하기 때문일 것이다.
1개의 애플리케이션당 1개의 코드베이스를 강조하고 있다. 즉, 애플리케이션 대 코드베이스를 1:1로 만들라는 이야기! 
그리고 한개의 코드베이스, 여러개의 배포라는 의미는 코드베이스 한가지로 여러 환경에 대한 배포가 가능하다는 것이다. 즉 코드베이스 하나를 가지고 test/staging/production... 등의 많은 환경에 배포가 가능한 것을 말한다. 즉, 배포 환경이 다르다고 해서 코드베이스를 여러 개로 나누거나 해서는 안된다는 어쩌면 요즘은 너무나도 당연해진 이야기로 시작하고 있다.

여기서 한가지 걸리는 부분은 하나의 코드베이스에 여러개의 애플리케이션을 두지 말라는 부분이다. 내 블로그 포스트 중에도 있지만 우리 회사는 주로 전체 프로젝트를 멀티 프로젝트로 구성한다. 이렇게 사용하지 말라는 것으로 보이는데 우리 같은 작은 팀에서 각 애플리케이션별로 코드베이스를 나누어야 할 것인가 하는 부분에 대해서는 좀  생각해 봐야겠다. 

II. 종속성

원문에서는 CPAN이나 Rubygems 를 이용해서 설명하고 있지만, 자바 생태계에서도 충분히 설명이 가능하다. 최근의 프로젝트를 보면 gradle이나 maven을 이용해서 프로젝트의 의존성을 관리하는 것이 당연한 것처럼 되어있다. 약 10년전쯤에 프로젝트 할 때는 ant를 이용해서 의존성을 관리하는 것도 꽤 발전된 형태였던 것으로 기억되는데, 현재는 이런 의존성관리는 물론 빌드 태스크를 담당해주는 툴의 사용이 매우 당연한 것으로 인식되고 있다.
또한, Springframework에서는 최근 Spring-boot 를 이용해서 jetty/tomcat 등의 WAS까지도 embeded 형태로 포함하여 배포가 가능하다. 즉, 최근의 애플리케이션은 거의 모든 의존성을 관리할 수 있도록 되고 있는 추세라는 것이다. spring-boot 에 docker file 까지 설정한다면, 더 많은 것들에 대한 관리도 가능하다. 다시 말해서 단순히 라이브러리들에 대한 종속성 뿐 아니라, 서비스를 구동할 때 필요한 거의 모든 것을 관리할 수 있는 시대가 되었다고 보면 될 것 같다.

III. 설정

설정은 배포 환경(Test, Staging, Production 등)에 따라 달라질 수 있는 모든 것이라고 이야기한다.
이 설정 정보들은 코드에 포함되어서는 안되는 것은 당연하고, 코드베이스에 올라가서도 안된다고 이야기하고 있다. 코드베이스는 특정 그룹으로 한정되면 안되고 상황에 따라 그룹을 추가할 수 있어야 한다. 또한 설정 정보는 여러 곳으로 분리되어 있어서도 안된다.

가끔은 각 서버마다 특정 디렉터리에 환경 파일을 미리 올려놓고 애플리케이션 실행시에 해당 디렉터리를 참고하도록 하는 경우도 있는데, 이것은 두번째 요소인 종속성에도 어긋날 뿐더라 이 에도 어긋날 수 있다.

원문에서는 envvars 또는 env 등을 이용한 예를 이야기하고 있다. 코드베이스에 포함될 일도 없고, 특정 OS나 언어에도 독립적이기 때문이다.

다만, 이 설정에 애플리케이션 내부 설정 정보는 포함되지 않는다. 원문에서는 rails의 routes.rb 파일에 저장되는 설정과 springframework의 DI 설정등은 이 설정에 포함되지 않는다고 이야기한다. 즉, 배포 환경에 따라 변화되지 않는 애플리케이션의 내부 설정들은 코드베이스에 포함되어있는 것이 가장 바람직다.

최근에 읽었던 글 중에 Pivotal 의 Josh Long 이 쓴 "Configuring It All Out" or "12-Factor App-Style Configuration with Spring" 글이 있으니 참고해보면 좋겠다.

IV. 백엔드 서비스

여기서의 백엔드 서비스는 Third-party 에 의해 서비스되는 모든 것을 이야기하는 것이다.
원문에서는 MySQL 데이터베이스라던가 SMTP 서비스, 메시지 큐 등을 예로 들고 있다.
이런 백엔드 서비스는 애플리케이션의 외부 리소스로 취급해야 한다. 즉, 설정을 변경하는 것으로 코드 변경없이 이용이 가능하도록 해야 한다는 것이다. 
테스트 때는 테스트용 데이터베이스를 이용하다가 Production으로 가면 또 다른 데이터베이스를 이용해서 서비스가 가능해야 한다. 운영중에 데이터베이스 서버의 접속정보가 바뀌어도 코드의 수정이 있어서는 안된다. 다른 예로 개발단에서는 로컬 SMTP를 이용하다가 production에서는 Google의 SMTP로 서비스가 가능해야 한다.

뭐 이 요소도 워낙 요즘은 당연하게 받아들여지는 개념이라 크게 이견은 없을 것으로 보인다.

V. 빌드, 릴리즈, 실행

빌드/릴리즈/실행은 배포와 실행의 기본적인 3단계이다. 이 각 단계는 각각 엄격하게 분리되어야 한다는 이야기다. 빌드에서는 실행가능한 애플리케이션으로 만들기 위해 코드베이스에서 코드를 가져오고 종속성들이 합쳐지고 컴파일 된다. 릴리즈 단계에서 적절한 환경설정과 결합되어지고 실행단계에서 애플리케이션이 실행되어지게 된다.

절대로 릴리즈나 실행단계에서 코드가 변경되어서는 안된다. 코드의 변경에 따른 빌드는 빌드 단계에서만 이루어져야 하는 행위인 것이다.

또 하나의 중요한 부분이 바로 롤백과 관련된 부분이다. 어떠한 툴을 쓰건 현재 버전은 이전버전으로 롤백이 가능해야 하다.

모든 릴리즈는 유니크해야 하며, 릴리즈는 변경되어서는 안된다. 릴리즈는 계속 새롭게 릴리즈되어야 한다.

우리 회사는 여전히 쉘스크립트를 이용해서 배포를 하고 있지만, 요즘 많은 회사들이 CI/CD 툴을 이용해서 배포를 진행하고 있고, 해당 CI/CD 툴 들은 대부분 빌드/릴리즈/실행 과정을 모두 담당할 수 있을 뿐 아니라, 롤백까지도 처리해 주는 것으로 알고 있다.  그런 시스템에서 개발을 하는 사람들은 어쩌면 너무 당연하다고 생각하면서 무시할 수도 있다.

하지만 한가지 생각을 해 볼 필요가 있다. 웹의 경우 static 리소스가 많이 있다. css, js, html 같은 경우가 대표적이고 jsp나 themeleaf 파일의 경우도 코드가 포함되어있긴 하지만 충분히 static resource 라고 볼수도 있다. 우선은 css, js, html 의 경우는 외부 리소스로 판단하여 별도 서버 또는 애플리케이션과 분리되어 배포가 된 경우도 많으니 어쩌면 별로 논의하지 않아도 될지 모르겠다. 하지만 jsp 나 themeleaf 파일의 경우는 어떨까? 이 파일들은 과연 코드베이스에 포함된 코드로 봐야 할 것인가 아니면 별도 리소스로 볼 것인가. 외부 리소스라면 동일한 코드베이스에 포함되면 안되는 것일까? 외부 리소스이니 변경될 때마다 릴리즈 절차와 상관없이 배포가 가능한 것이 맞지 않을까? 외부 리소스가 아니라고 생각한다면, 템플릿 파일에 태그하나 또는 오타가 있을 경우 전체 릴리즈를 다시 해야 하는 것일까? 한번쯤 생각해봐야 할 요소이지 싶다.

VI. 프로세스

모든 애플리케이션은 무상태로 동작해야 하며, 메모리나 디스크의 자원에 의한 공유가 이루어져서는 안된다. 현재 동작하는 애플리케이션에 의해서 만들어진 어떤 상태가 해당 애플리케이션이 다시 동작할 때 유지 될 것이라는 보장이 없기 때문이라는 것이다.

뭐 표현이 어려우니 간단히 웹 프로그램의 Session을 예로 들면(원문에서도 Sticky session을 예로 들고 있다), 메모리나 디스크에 저장된 Session을 사용하지 말라는 것이다. 애플리케이션에 종속된 Session은 해당 애플리케이션이 재실행되거나 이후 변경/이동 등이 발생했을 경우 정상적인 동작을 보장할 수 없기 때문이다. Session은 Redis 라던가 Memcached 등을 이용해서 사용해야 한다.(역시 이렇게 되면 외부 리소스가 된다)

VII. 포트 바인딩

이 요소는 약간 이해가 안되는 요소이다. 그래서 개념만 간단히 정리하면 다음과 같다.
애플리케이션은 포트 바인딩에 의해서 외부에 공개해야 한다. 이를 통해서 해당 애플리케이션은 다른 서비스의 백앤드 서비스가 될 수 있다.

VIII. 동시성(Concurrency)

보통 Concurrency 라고 하면 Thread를 통해 이루어지는 동시성에 집중하게 된다. 프로그램을 오래 한 사람일수록 더 그럴것이라고 생각한다. 예전에는 거의 그 의미였으니까 말이다.
하지만 여기서 말하는 동시성은 이 부분도 포함을 하긴 하지만, 기본적으로 수평확장에 의한 동시성을 이야기하고 있다. 6번째 요소인 무상태 동작과도 통한다고 할 수 있다.
12 Factor App에서 애플리케이션은 아무것도 공유하지 않으므로 수평으로 확장하는 것을 통해서 동시성을 높이는 것이 간단하고도 안정적인 작업일 수 있다.
최근의 포스트를 작성하게 된 이유이기도 한 마이크로서비스 아키텍처가 이 부분을 대표한다고 볼 수 있다. 자유롭게 확장하고 자유롭게 축소할 수 있으니 말이다.

IX. 폐기 가능(Disposability)

원문을 그대로 옮겨보면 빠른 시작과 그레이스풀 셧다운(graceful shutdown)을 통한 안정성 극대화다.
우선은 애플리케이션을 시작하는 시간의 최소화가 중요하다. 애플리케이션의 시작시간을 최소화함으로써 쉽게 배포하고 쉽게 실행하여 릴리즈 작업과 확장을 용이하게 해야 한다.
또 한편으로는 종료시간의 최소화이다.
이 부분에 대해서 원문은 두가지로 나눠서 이야기한다.
첫번째 HTTP 요청과 같은 경우는 짧은 처리시간을 통해서 Shutdown Signal을 받은 후에 애플리케이션은 신규 요청의 수신을 중지하고 기존 처리되고 있는 요청은 빠르게 처리한 후 종료되어야 한다. 파일 업로드와 같은 long polling 처리의 경우는 종료가 되어 연결이 끊어진 시점에서 바로 다시 연결을 시도하여 요청을 이어갈 수 있도록 만들어야 한다.
두번째 Worker 프로그램과 같은 경우는 현재 처리중인 작업을 다시 Worker Queue로 되돌리고 종료되야 한다.

시작과 종료외에 오류에 의한 애플리케이션 종료에도 대응해야 한다. 마이크로서비스의 failover 관련 처리(fallback method, circuit breaker 등)가 이 경우에 해당한다고 볼 수 있다.

X. dev/prod 일치

배포환경에 따른 불일치는 크게 시간, 담당자, 툴 세가지의 차이에서 비롯된다.
12 Factor App 은 이 세가지를 아래와 같은 방법으로 극복한다.
  • 시간의 차이 : 손쉬운 배포를 통해 개발된 내용을 바로 배포하여 극복
  • 담당자의 차이 : 개발자가 직접 배포와 모니터링에 참여하여 극복
  • 툴의 차이 : 개발과 배포 환경을 최대한 비슷하게 유지해서 극복
특히 12 Factor App 에서는 개발환경과 배포환경을 최대한 동일하게 유지하는 것을 중요하게 생각한다. 앞서 백엔드 서비스를 설명하면서 사실 개발에서는 H2 데이터베이스를  Production에서는 MySQL을 사용하는 것도 가능하다는 것을 이야기하려 했으나 이 요소에 어긋나는 것이라 삭제했다.
삭제한 예와 같이 보통의 개발자들은 로컬 개발환경의 백앤드 서비스를 경량화 하려고 하는 경향이 있으나, 12 Factor App에서는 이런 경향을 강력하게 거부하고 있다.
최근에는 Docker 등의 경량 Container 들이 나와있어서 사실 툴의 불일치를 해결하기가 매우 편리해 진 만큼 이 원칙을 지키면서도 본인의 로컬 환경이 어지러워지는 것을 방지할 수 있으니 정말 다행이다.

XI. 로그

원문에 의하면 로그는 모든 실행중인 프로세스와 백그라운드 서비스의 아웃풋 스트림으로부터 수집된 이벤트가 시간 순서로 정렬된 스트림이다. 즉 시간의 흐름에 따라 지속되는 스트림이다.
애플리케이션은 애플리케이션 자체적으로 이런 스트림을 저장할 파일을 생성하거나 관리해서는 안된다. 모든것은 실행환경 설정에 의해서 결정되어야만 한다.
예를 들어 로컬 환경에서는 콘솔에 그대로 표시가 되고 production 에서는 특정 파일에 저장되거나 중앙집중식 로그관리 시스템에 보낼 수 있어야 한다.(코드의 변경없이 실행환경에 따라서)
마이크로서비스 아키텍처에서는 많은 작은 단위의 애플리케이션이 수평확장까지 된 상태로 실행되기 때문에 보통 로그를 관리하는 시스템(예를 들면 ELK stack 등)을 함께 이용한다. 하지만, 로컬에서 개발할 때는 각자의 콘솔에 그대로 출력이 된다.
또한 12 Factor 애플리케이션은 로그를 분석할 수 있는 다양한 시스템에 의해서 과거 이력에 대한 검색이나 그래프 등을 통한 통계 또는 현황 대시보드, 특정 이벤트에 대한 알림 등의 다양한 이점을 누릴 수 있도록 개발되어진다.

XII. Admin 프로세스

설명하기가 좀 애매한데, 일회성의 유지보수나 관리 작업은 실행환경의 일회성 스크립트나 명령을 통해 실행이 되어야 한다는 것이다. 이를 위해서 12 Factor App은 실행환경과 동일한 환경에서 사용가능한 REPL을 지원하는 언어를 적극적으로 권장한다고 이야기한다. 이 때문에 주로 설명이 rails 나 python 의 예가 많은가보다.

아쉽게도 JDK의 경우는 아직까지 공식적인 REPL을 지원하지는 않는다. 하지만, java9 에는 jshell REPL 이 지원될 것으로 보이니 한번 기대해볼만하겠다.

결론

지금까지 the Twelve Factor App 에 대해서 설명했다.
위와 같은 12가지의 특성을 가지는 App을 12 Factor App 이라고 부른다.
마이크로서비스 포스트를 올리던 중에 12 Factor App 을 정리하게 된 것은 12개의 특성들 중 많은 부분이 마이크로서비스 개발에 있어서도 고려되어야 하는 상황이기 때문이다.
그건 어쩌면 당연하다. 12 Factor App도 Cloud 환경에서 실행되는 애플리케이션을 위한 것이고, 마이크로서비스도 그 시작이 Cloud 환경에서 손쉽게 확장 가능한 애플리케이션을 개발하기 위한 아키텍처로 등장했기 때문이다. 
지금 소개한 이런 특성이 동의할 수 있는 것들이냐, 절대적인 것이냐 등등을 논하고 싶지는 않다.
그저 고려한다면 확장하기 용이한 애플리케이션을 개발하는데 큰 도움이 될 것이라고 생각한다.

2016. 1. 1.

Microservice 에 대한 나만의 정리 2

서론


원래 이번 포스트는 12 Factors App에 대해 정리를 해 보려고 했다.
저번 포스트가 좀 힘들어 이번엔 간단한 거로 해볼까 하는 생각과 최근 개발 방향을 요약해서 정리할 수 있지 않을까 싶어서였는데, 이전 포스트의 정리가 워낙 엉망이었던 것 같아서 다시 한 번 정리해볼까 하는 생각을 하게 되었다.

저번 포스트는 마틴 파울러 씨의 블로그 포스트를 기반으로 해서 정리를 해 보았는데, 한 달 전쯤 봤던 Devoxx 2015 세션 중 하나였던 Refactor your Java EE application using Microservices and Containers를 진행하셨던 Couchbase의 Vice President인 Arun Gupta 씨의 동영상이 기억이 났다.
해당 세션을 정리할 생각은 아니고 거기서 소개된 대부분 내용이 그보다 이전에 Dzone 에서 받아 놓았던 Getting Started with Microservices 라는 pdf에 정리되어있었던 것이 기억나서 그것을 정리해 보고자 한다.

참고로 Arun Gupta 씨의 위 동영상은 [여기] 에서 볼 수 있다.
(본문에서는 씨라는 존칭은 생략하고 정리하겠다.)

What are microservices?


마이크로서비스는 하나의 커다란 서비스를 느슨하게 결합 돼 있는 하나의 목적을 가지는 여러 개의 서비스로 나누고, cross-functional team에 의해 개발하고 운영을 하여, 최근 디지털 비즈니스에 있어 요구되는 빠르게 개발/유지보수 되면서도 품질이 우수한 시스템을 개발하자는 아키텍처적 접근 방식이다.


Key Characteristics of microservices


이전 포스트에서 봤던 특성들을 Arun Gupta 는 아래와 같이 정리하고 있다.

  1. DDD(Domain-Driven Design) : 마이크로서비스를 위해 기능을 분리해내는 데는 DDD가 많은 도움이 된다.
  2. Single Responsibility Principle : 객체지향 설계의 기본 원칙이지만, 마이크로서비스도 이 원칙을 기본으로 한다.
  3. Explicitly Published Interface : 정보를 생산하는 쪽의 서비스가 명확한 인터페이스를 공개하면, 그 인터페이스를 통해 수요자가 되는 서비스에서 사용하게 된다.
  4. Independent DURS (Deploy, Update, Replace, Scale) : 각 서비스는 독립적으로 배포되고, 업데이트되고, 치환되며 확장될 수 있다.
  5. Lightweight communication: 이전 포스트의 Smart endpoints and dumb pipes 의 내용이다. HTTP 기반의 REST, WebSocket 기반의 STOMP를 예로 들 수 있으며, 이 외에도 RabbitMQ라던가 ActiveMQ 등도 그 예라고 할 수 있겠다.

Benefits of microservices


그렇다면 마이크로서비스의 이점은 무엇일까? 
물론 아래의 이점은 마이크로서비스가 잘 구성되어 개발되었을 때 얻을 수 있는 이점들이다.

Independent scaling


각 서비스는 일반적으로 X축 확장으로 불리는 멀티 애플리케이션(또는 서버)의 확장과 Z축 확장(Partitioning 또는 Sharding)으로 불리는 확장을 독립적으로 할 수 있다.
(스케일링과 관련된 자세한 내용은 [여기] 서 확인해 볼 수 있다.)

Independent upgrades

개별적으로 배포할 수 있고 개별적으로 업그레이드가 가능한 특성을 갖는 것이 마이크로서비스다. 따라서 당연한 이점이라 하겠다.

Easy maintenance

약간의 논란이 될 수 있을 수 있을 것 같지만, 기본적으로는 하나의 기능에 국한하여 개발되어 있으므로 비즈니스 로직도 한가지의 기능에 국한될 수밖에 없다. 따라서 비즈니스 로직이 단순하므로 그에 대한 유지보수 역시 단순하다. 하나의 비즈니스 기능을 담당하므로 코드 또한 그 규모가 작게 유지되며 이로 인해서 유지보수가 단순해질 수 있다.

Potential heterogeneity and polyglotism 

개발자 또는 팀은 기술이나 언어를 매우 자유롭게 선택할 수 있다. 본인들이 담당하는 비즈니스 기능을 개발할 때 다른 인프라적인 또는 사내 공용 라이브러리 등에 대한 제약에서 자유로워지기 때문이다. 이 자유로움은 서비스가 된 후에도 유효하다. 더 나은 기술이나 더 나은 언어가 나타난다면 자유롭게 서비스를 refactoring 하거나 심지어 rewrite 할 수도 있다.

Fault and resource isolation

모놀리스 서비스는 하나의 서비스가 Memory leak이나 종료되지 않은 데이터베이스 커넥션이 쌓인다거나 하는 문제가 발생할 경우 전체 서비스에 영향을 받지만, 마이크로서비스는 오류 발생의 영향을 해당 서비스만 받게 된다. 나머지 서비스는 이런 에러들에서 벗어날 수 있고 이전 포스트에서 설명했듯이 우아하게 이런 에러들을 처리하기까지 해서 사용자들에게는 영향을 미치지 않게 된다.

Improved communication across teams

이전 포스트와 이번 포스트의 특성에서도 나왔지만, 마이크로서비스를 개발하는 팀은 cross-functional team이다. 즉 해당 비즈니스 기능을 서비스로 만들어내는 데 필요한 모든 인력이 있는 조직인 것이다. 따라서 모든 인원은 해당 비즈니스 기능을 만들기 위해서 노력한다. 당연히 조직 내 모든 사람이 서로 소통해서 비즈니스 기능을 구성하기에 쉬운 조직구조를 가지게 된다는 것이다.

Operational Requirements for microservices


그렇다고 마이크로서비스가 모든 문제를 해결해줄 Silver bullet 인 것은 아니라는 것을 Arun Gupta 도 이야기하고 있다. 진정한 성공에는 그만한 투자가 이루어져야 한다는 것인데 그렇다면 어떤 부분들이 있을까?

Service Replication

각각의 서비스는 결국 X축 확장과 Y축 확장이 필요하다.

Service Discovery

실제 마이크로서비스를 구현해서 사용하고 있는 애플리케이션들은 PaaS 와 같은 분산 시스템에서 돌아가는 경우가 많다. 일명 클라우드에서 서비스되는 경우가 많다는 것이다. VM이나 Container 기반으로 돌아가기도 하는 등 Immutable infrastructure 상에서 서비스가 운영되고 있다. 문제점은 언제 서비스가 어떤 시스템에 배포될지를 사전에 알 수 없는 경우가 많다는 것이다. 즉, 어떤 서비스가 어떤 아이피와 포트로 서비스될지 서비스가 실제 배포되기 전에는 알 수 없는 경우도 많다. 이런 문제를 해결하기 위해 등장한 것이 service registration과 discovery이다. 즉, 각 서비스는 구동되면서 본인에 대한 정보를 등록하고 다른 서비스들은 서비스 요청을 하기 전에 이 등록된 정보를 제공하는 서비스에 질의를 던져서 정보를 얻어서 요청하게 된다. 이 부분은 나중에 마이크로서비스를 구성하는 일반적인 구성들을 따로 다른 포스트에 정리해 볼 예정이다.

Service Monitoring

분산 서비스의 가장 중요하고 어려운 부분이 이 서비스 모니터링과 로깅 부분이라 할 수 있다.
이 부분에서 최근 가장 화제가 되는 오픈 기반 도구는 ELK(Elasticsearch + Logstash + Kibana)인 것 같다. 시스템 구성적으로나 개발 적으로나 이 부분이 가장 많은 투자가 이루어질 부분이지 않을까 싶다.

Resiliency

모든 소프트웨어는 무조건 오류가 발생한다. 이 세상에 오류가 전혀 발생하지 않는 소프트웨어가 있을까? 마이크로서비스는 모놀리스와 다르게 여러 개의 소프트웨어가 서로 협력하여 서비스하게 된다. 따라서 그 소프트웨어가 많아지면 많아질수록 더 많은 오류의 가능성이 생겨난다. 마이크로서비스는 오류가 해당 서비스 이외의 서비스에 영향을 미치지 않도록 설계되어야 한다. 따라서 오류를 어떻게 일어나지 않게 할 것인가도 중요하지만, 그보다 중요한 것은 오류를 어떻게 다룰 것인가다. 오류가 발생한 시스템에는 오류가 복구될 때까지 요청 가능 서비스에서 제외했다가 오류가 복구되면 다시 해당 요청이 이루어지도록 하는 처리가 필요하다. 보통은 이 부분을 Circuit Breaker 와 소프트웨어 기반의 로드밸런서가 담당하게 된다.

DevOps

최근 몇 년간 가장 많이 들은 이야기가 DevOps 아닐까?
마이크로서비스가 성공하기 위해서는 자동화된 빌드와 배포(CI/CD) 가 필수적이라 할 수 있다. (이 부분은 이전 포스트의 마틴 파울러도 이야기하고 있다. 너무 당연하다고 생각해서 설명은 간략했지만...) 마이크로서비스는 개별적으로 배포가 일어나고 업그레이드가 일어난다. 따라서 전체적인 조율을 할 수 있는 DevOps 적인 기반일 필요한 것이다. 내가 이해하는 바로는 이 기능은 각 팀에도 존재해야 하고 전사적으로 존재해야 하지 않을까 한다.

Good design principles for existing monoliths


Arun Gupta는 마틴 파울러와 다르게 모놀리스에서 점진적으로 발전하는 마이크로서비스를 주장하고 있다. 모놀리스에도 적용될 수 있는 디자인 원칙은 다음과 같다.


  • MVC
  • Well-defined API
  • 중복제거(DRY)
  • CoC 원칙 (Clash of Clan 이 아니다)
  • 데메테르의 법칙(Law of Demeter) : 설명은 [여기]를 참고
  • DDD
  • YAGNI(You Aren't Going to Need It) : 필요하기 전까지는 만들지 말라.

Microservices Design Pattern


마이크로서비스에도 패턴이 있을까? 마이크로서비스라는 것은 대상이 되는 프로젝트에 따라서 정말 다양한 변형이 만들어질 수 있는 아키텍처라고 생각한다. 따라서 정확한 정답도 없다. 그래서 마이크로서비스가 아직도 나에겐 어려운 것일지도 모른다.

다만 일반적인 패턴들이 존재하기는 한데, 그 부분은 아래의 그림들이 좋은 참고가 되지 않을까 생각한다.

패턴들은 이름과 그림만 소개한다. 자세한 소개는 내가 허접스럽게 설명을 하는 것보다는 [여기]를 참고하는 것이 더 현명한 선택이지 않을까 싶다.

Aggregator pattern




Proxy pattern


Chained pattern


Branch pattern


Shared resources


Asynchronous Messaging pattern



결론


Arun Gupta 는 이외에 모놀리스를 마이크로서비스로 리팩토링하는 것에 대한 간단한 설명은 있으나, 이 부분은 되도록 서론 부분의 Arun Gupta의 동영상을 보는 것이 더 좋을 것 같다. 이유는 나 자신도 마틴 파울러의 생각처럼 모놀리스를 마이크로서비스로 리팩토링을 진행하는 것에는 찬성하지 못하기 때문이다. 자칫하면 더 어지러운 서비스가 될 수 있다고 생각한다. 단, 잘 설계된 모놀리스 애플리케이션을 가지고 있는 회사라면 마이크로서비스로의 점진적인 리팩토링도 충분히 가능할 것 같다.

만약 이글을 본 분 중에 마이크로서비스를 도입하고 싶다면, 애플리케이션의 구조가 어떻게 되어있는지를 차분하게 바라볼 필요가 있을 것 같다. 서비스별로 잘 구조화되어있고, 서로 간의 관계도 역시 명확하게 잘 설계되어있다면, 서론에서 소개한 동영상을 꼭 한번 보시고 진행하시는 것을 추천해 드린다.

이전의 포스트가 너무 개념적이어서 좀 더 요약이 잘 되어있는 Arun Gupta의 개념을 정리하고 싶어서 이 포스트를 시작했다. 결국 아직까지 정확한 개념을 이해하지 못한 탓인지 아니면 내 정리의 기술이 좋지 않아서인지 약간은 어수선하고 덜 정리된 포스트로 마무리를 하는 느낌이다.

혹시 이글을 읽고 뭔가 마이크로서비스의 개념을 잡고 싶으셨거나 도움을 받기 바랬던 분들이 있다면 송구스럽다.

원래 이 포스트는 2015년의 마지막 포스트로 쓰고 있었는데, 약간의 사고로 2016년의 첫 포스트가 되고 말았다. 다음 포스트에서 12 Factors App 을 정리하고 이젠 실제적인 마이크로서비스를 구성하는 것에 관한 포스트들을 정리해 볼까 한다.