System Compleat.

'Netflix'에 해당되는 글 2건

  1. 고가용 서비스 - Spring Cloud - #3 Smart microproxy - Zuul (11)
  2. Cloud software engineering

고가용 서비스 - Spring Cloud - #3 Smart microproxy - Zuul

Techs


(younjin.jeong@gmail.com, 정윤진) 

로드 밸런서라는 것을 처음 접했던것은 리눅스의 LVS 였다. 1990년대 말엽 쯤이었던것 같은데 트래픽을 분산 처리할 수 있다는 개념에 매우 놀라워 했던 기억이다. 당시 리눅스 배포판에는 모두 How to 문서가 함께 배포 되었었는데 여기에 소개된 LVS 를 구현 하려면 깡통 머신이라도 몇대는 있어야 했기 때문에 침만 꿀떡 삼키곤 했다. 

LVS - HA  http://www.linuxvirtualserver.org/  (이 사이트가 아직 살아있다니)

후에 국내 대규모 호스팅 업체에서 연구원으로 이런 저런 서비스를 개발하면서 당시 이슈가 되던 DDoS 처리와 함께 네트워크 기반의 밸런서가 아닌 리눅스 박스 기반의 밸런서를 회사에서 고려하면서 직접 구현할 기회가 생겼더랬다. 지금은 매우매우 유명해져서 인천 국제공항 짐 찾는데서 광고도 하고 있는 모 인터넷 쇼핑몰이 그때 장사가 폭발하기 시작하면서 서버의 부하 분산 처리를 해야 했는데, 기존의 서버를 웹 애플리케이션 서버와 웹 서버로 나누고 Memcached 와 같은 도구의 사용과 PHP 의 프로파일링을 opcode 레벨로 수행하기도 하고 뭐 여러가지를 붙이면서 여기에 LVS도 들였던 기억이다. 왜냐면 지금 아마존도 그렇지만 호스팅은 원가 절감에 사업 생명이 달려있기 때문에... 

어쨌든 로드 밸런싱이라는 것은 외부에서 들어오는 웹 또는 트래픽을 내부의 여러대의 서버로 분산해서 처리하는 것을 말한다. 지금은 믿기 힘들겠지만 예전에는 한대의 서버에서 웹 서버, 데이터베이스 이런것들을 다 구동시키던 시절도 있었... 그리고 로드 밸런서는 이 부하를 분산해 주는 역할을 하는 서버나 네트워크 장치 또는 소프트웨어 또는 어플라이언스를 말한다. 이런 기능이 제공하는 성능 향상은 그야말로 확장이 거의 불가능한 관계형 데이터베이스가 퍼질때까지 웹 서버를 늘릴 수 있는 장점이 있다. 게다가 로드 밸런서 뒷쪽에 있는 서버에 장애가 발생하거나 업데이트를 하는 경우에도 트래픽의 핸들링이 가능하기 때문에 서비스의 가용성이 더 높아지는 이유도 있다. 따라서 서비스에 유입되는 트래픽이 증가하는 특정 시점에는 반드시 로드 밸런서의 도입을 고려해야 하는 시점이 있었다. 물론 루프백 인터페이스의 수정과 같은 대규모 작업과 함께. 

밸런서에 대해 조금 더 이야기 해 보자면, 이런 리눅스 박스 기반의 밸런서를 구성하는 것은 뭐 약간의 기술이 필요하기 때문에 이런 구성의 편의를 위해서 리눅스 박스를 벤더별로 커스터마이징 해서 아예 네트워크 장치 처럼 내어 놓는 '어플라이언스형' 밸런서들이 유행을 타기 시작했다. 물론 편의 뿐만 아니라 다양한 네트웍적인 기능, 그리고 편리한 웹 기반의 인터페이스, 그리고 일부 암호화 같은 기능들의 경우에는 순수 소프트웨어를 사용하는 대신 별도의 ASIC을 사용하기 때문에 더 높은 성능을 보이는 것들도 있었다. 처음에는 노텔이, L4 로 대표되는 스위치로 흥하다가 이후에는 F5 와 같은 벤더에서 강력한 L7 기능과 함께 컨텐츠 캐싱과 같은 복합적인 역할을 수행하는 제품을 내어 놓기도 했고, Citrix 와 같은 벤더에서는 WAN 구간의 데이터 전송에 더 적은 트래픽을 사용하는 압축 기술 등을 적용한 제품을 내어 놓기도 한 나름대로 흥망성쇠가 있는 레이어였다. 

여담이긴 하지만 이런 로드 밸런서를 본격적으로 사용하기 전에는 RRDNS 라고 해서 동일한 네임서버의 레코드에 여러개의 서버 주소를 넣어서 요청하는 클라이언트 별로 순차적으로 응답을 주는 방식을 사용하기도 했었다. 이 DNS 작업을 서버의 IP 주소 레벨로 처리 했을때를 상상해 보면 어떨까 생각해 보길 바란다. 그래서 사람들은 서버들은 로드 밸런서에 연결하고 로드 밸런서에도 부하가 생기기 시작하면 여러개의 로드 밸런서를 마련하여 이 로드 밸런서의 주소를 DNS 에 등록하는 방식을 사용하게 된다. 그리고 여기에 발전하여 DNS 가 점점 똑똑해 지면서 클라이언트의 IP가 어느 지역에 위치한지를 GeoIP 와 같은 데이터베이스를 통해 알아내서 그 지역에 가까운 로드 밸런서의 주소를 할당해 주는 형태로 발전하게 된다. 이게 RRDNS 부터 GSLB 라고 불리는 방식으로의 발전이다. 


아무튼 이런 전통적인 방식의 로드 밸런서들은 끊임없이 발전했는데, 그중 한 부분이 바로 고가용성 부분이다. 약 7년전 까지만 하더라도 로드 밸런서를 Active-active 로 (즉 두대 다 동시에 사용가능하게) 하는 구성은 돈도 비싸고 네트웍적으로도 골치 아픈 것이었다. 두대의 어플라이언스 기반 로드 밸런서를 고가용성으로 구성하려면 거의 대부분 가능한 옵션은 Active-Standby(두대중 한대만 가용) 였고, 이 구성 마저도 로드밸런서간 세션 공유를 위한 네트웍 회선 이중화 구성, 로드 밸런서가 연결되는 라우팅 구간과의 Port trunking 을 연계한 HSRP 또는 VRRP 구성, 그리고 내부의 네트워크 장비와의 고가용성 연결 등 이게 그렇게 단순한 이야기가 아니었던 거다. 더 중요한 것은, 서비스를 운영하는 관점에서 보면 네트워크는 네트워크 장치끼리 따로 움직이는 것이 아니다. 이는 각 역할을 하는 서버와 연결되며, 각 서버는 또 기본적으로 Bonding / Teaming 이라고 불리는 이중화 기술을 사용하고 만약 윈도우를 사용한다면 클러스터링 여부에 따라 AD 구성이 필요하게 된다. 그리고 만약 데이터베이스 클러스터를 SAN 기반으로 구성했다면 역시 데이터베이스의 tablespace 위치 설정과 SAN의 Zoning 작업, HBA 설정등 모든게 거대한 하나의 세트가 된다. 이런 클러스터링의 정상 동작은 상단의 밸런서 구간, 그리고 서버와 서버의 연결 구간이 일목 요연하게 동작해야 비로소 아름답게 (라고 쓰고 비싼 돈 들여서 정상적으로 동작하는거 구경이라고 읽는..) 동작한다. 

F5 Networks LTM - 한때는 매우 아름다운 고성능의 어플라이언스  전원을 넣으면 오른쪽 다마에 불이 켜져요 

암튼 사설이 너무 길었는데, 로드 밸런서가 active-active 의 고가용성으로 구동되기 힘들었던 이유중 가장 큰 것은 바로 '세션 클러스터링' 이라는 기능때문이었다. 보통 Sticky bit 또는 persistence 라고 불리는 밸런서의 기능은 현재 어느 클라이언트가 내부의 어느 서버와 연결되었는지에 대한 연결 정보를 기억한다. 이건 개발자들에게 웹 애플리케이션에서 세션을 서버 로컬에 저장해도 되는 편리함을 제공했다. 그래서 역설적으로 이 기능이 없는 밸런서를 사용하면 웹 서비스가 정상동작 하지 않기도 하는 경우가 있었다. 

그래도 역시 이런 고가의 어플라이언스는 별로야 라고 생각했던 사람들은 리버스 프락시라는 도구에 주목한다. 세션 클러스터링 따위 REST로! 라고 사람들이 생각하기 시작하면서, 그리고 클라우드가 대두되면서 리버스 프락시는 널리 사용되기 시작한다. 이 블로그에 NginX 를 처음 소개했던게 2009년이란다. 세월 참 빠르다. 아무튼 Nginx, HAProxy, Varnish 와 같은 리버스 프락시들은 단순히 로드 밸런싱 뿐만 아니라 HTTP 헤더의 조작, uri 기반으로 내부의 서버를 선택해서 포워딩 할 수 있는 등의 참으로 아름다운 기능들을 제공하기 시작한다. 특히 나는 Nginx 를 매우 사랑했는데, 모듈을 통한 손쉬운 기능확장, 이를 테면 memcached 를 붙여 시원한 캐시를 할당한다던가 SSL 엔드포인트로 사용한다던가 CPU affinity 설정과 같은 매니악한 기능들, 그리고 uri 라우팅을 통한 인증서버, 파일(이미지) 서버, 그리고 웹 애플리케이션 서버를 내맘대로 밸런싱하는 재미가 있었기 때문이다. 

어쨌든, 세션 클러스터링 따위가 별게 아닌게 되기 시작하면서 밸런서는 소프트웨어로 손쉽게 확장할 수 있는 도구가 되었고 이런 기능성은 클라우드 위에서 매우 매력적인 도구가 되었다. 그런데, 클라우드 서비스는 대부분 밸런서를 기본으로 제공하고 (처음부터 기본은 아니었지만) 있다. 아마존의 ELB(Elastic Load Balancer)만 해도 처음에는 그냥 밸런싱만 하는 깡통이더니, sticky 지원을 시작해서 connection drain 등 점점 예전 상용 밸런서와 같은 기능을 제공하고 있다. 


옛날 이야기는 이쯤 하기로 하고, 이런 부하를 분산하는 기능을 했던 밸런서는 ELB 로 충분한데 클라우드 기반의 서비스가 발전하면서 더 많은, 즉 더 똘똘한 "무엇"이 필요하게 된다. "무엇"을 대충 정리하면 아래와 같다. 

- 단순이 네트워크적 라우팅 또는 L7 '지원' 수준의 분배가 아닌, 실제 애플리케이션 레벨에서의 백엔드 애플리케이션 연결 

- 동적인 라우팅, 즉 설정따위 변경 없이 리소스의 생성과 삭제에 따른 라우팅 즉각 반영 

- 모니터링 

- 빠른 복구, 그리고 보안성 

- 추가 기능, 예를 들면 아마존 웹 서비스에 위치한 다수의 오토 스케일링 그룹으로의 리퀘스트 라우팅 

예를 들면 샤드로 구성된 데이터베이스들에 요청을 분배해야 하는 경우를 생각해 볼 수 있다. 이러한 요구사항을 서비스를 지속적으로 개선함에 따라 발견했던 넷플릭스는, 처음에는 외부의 상용 서비스를 사용하다가 결국 Zuul 이라는 새로운 도구를 만들어 내었다.

Zuul, the Gatekeeper from Ghostbusters movie - http://villains.wikia.com/wiki/Zuul 


이름이 의미하는 바와 같이 이는 서비스의 대문을 지키는 수장의 역할을 한다. 이 Zuul 이 제공하는 기능을 정리해 보면 다음과 같다. 

- 인증 및 보안 : 각 요청이 갖추어야 할 내용을 충족하지 못한 경우 해당 요청을 거부한다. 

- 모니터링 : 모든 트래픽이 지나기 때문에 의미있는 데이터와 지표를 수집할 수 있다. 

- 동적 라우팅 : 필요에 따라 즉시 원하는 백엔드 클러스터로 트래픽을 보내고 끊을 수 있다. 

- 부하 테스트 : 신규 추가한 서비스 또는 백엔드에 트래픽을 점진적으로 증가하는 등의 방식으로 부하를 유발할 수 있다. 

- 트래픽 드랍(정확히는 Shedding) : 각 요청에 대해 제한된 이상의 요청이 발생한 경우 이를 드랍하는 방식을 사용할 수 있다. 

- 정적 응답 처리 : 특정 요청에 대해서는 백엔드로 트래픽을 보내는 대신 즉시 응답하도록 구성할 수 있다. 

- 멀티 리전 고가용성 : Zuul 은 받은 요청을 아마존 웹 서비스의 리전 레벨에서 분배할 수 있다.  


그리고 이와 같은 사용성을 기반으로 넷플릭스가 추가적으로 할 수 있는 일은 

- Test : 넷플릭스 규모의 마이크로 서비스 구성에서는 어떤 테스트는 반드시 프로덕션을 통해서만 가능한 경우가 발생한다. 이때 신규 서비스를 배포하고, 전체 트래픽 중 아주 일부의 트래픽만 이 테스트로 흘려 테스트를 수행하고 있다. 또는 이 개념을 조금 더 확장해서 Canary 테스트로 사용할 수도 있다. 배포 전 신규 버전의 서비스를 준비하고, 이 신버전으로 구버전을 대체하기 전에 동일한 요청에 대해 아주 작은 양의 트래픽만 신버전으로 흘린다. 로그를 모니터링 하고, 테스트를 통과하여 서비스에 문제가 없다는 것이 확인되면 트래픽의 비율을 조정한다. 자연스럽게 구버전으로 흐르는 트래픽은 감소하고 신버전은 증가하며, 구버전에 더 이상의 트래픽이 처리되지 않으면 모두 terminate 한다. 

https://github.com/Netflix/zuul/wiki/How-We-Use-Zuul-At-Netflix

이 외에도 SpringOne platform 에서 넷플릭스의 Zuul 담당 헤드가 다양한 내용을 소개해 주었다. 넷플릭스는 Zuul 을 사용하여 50개 이상의 ELB 로 트래픽을 분배하고, 3개의 아마존 웹 서비스 리전을 사용하고 있으며, 넷플릭스 서비스의 대부분의 트래픽을 처리하고 있다고 했다. 그리고 이 도구를 Edge-gateway 라고 부르고 있다. 혹시 infoq.com 계정이 있으신 분들은 Netflix 의 Zuul 매니저의 발표를 감상하실 수 있겠다. (https://www.infoq.com/presentations/netflix-gateway-zuul?utm_source=infoq&utm_medium=QCon_EarlyAccessVideos&utm_campaign=SpringOnePlatform2016)


넷플릭스가 공개하고 있는 Zuul 에 대한 정보는 아래의 링크에서 더 찾아 볼 수 있겠다. 

https://github.com/Netflix/zuul/wiki

https://github.com/Netflix/zuul/wiki/How-We-Use-Zuul-At-Netflix


이쯤 되면 나오는 이번 시리즈 단골 멘트. 스프링 클라우드에서는 이 마이크로 프락시를 사용하기 쉽게 제공하고 있다. 게다가 이전에 1, 2 시리즈를 통해 이미 소개한 유레카와 config 서버를 연계하여 사용하는 것이 가능하다. 백문이 불여일견, 백견이 불여일행. 우리의 사랑 START.SPRING.IO 로 접근하여 마이크로 서비스 구조를 만들어 보기로 한다. 다이어그램은 아래와 같다. 

오늘도 즐거운 발그림

Zuul proxy 의 동작만을 확인하는 간단한 코드는 spring cloud 프로젝트에서도 참조할 수 있으며, 블로그의 내용은 아래의 github 링크를 참조하면 되겠다. 간단한 데모이므로 별도의 암호화 처리등은 없다. 본 데모에서 가장 중요한 것은 온라인 설정의 업데이트, 유레카를 참조한 로드 밸런싱의 처리이다. 

https://github.com/younjinjeong/demo-config 

https://github.com/younjinjeong/spring-cloud-zuul-proxy-demo


Config server 구성 

- github.com 에 가서 신규 repository 를 만든다. (위의 demo-config 참조) 

- 애플리케이션의 properties 파일을 생성한다. : application.properties / helloworld-service.properties / helloworld-client.properties / discovery-service.properties   https://github.com/younjinjeong/demo-config 참조  

- bootstrap.properties 파일에 spring.cloud.config.uri 주소를 조정한다. 

- Config server 를 시작한다. 


Discovery-service 

- start.spring.io 에 접근한다. 

- artifact 에 discovery-service 

- dependencies 에 eureka server, config client 를 추가하고 프로젝트를 다운 받는다. 

- 설정은 demo-config 의 discovery-service 를 참조 


Helloworld-service 

- http://start.spring.io 로 접근한다. 

- artifact 에 helloworld-service 대신 더 상상력 넘치는 이름을 준다. 

- dependancies 에 web, rest repositories, actuator, actuator docs, config client, eureka discovery 를 적용한다. 

- Generate project 를 눌러 프로젝트 파일을 다운로드 받고, IDE 를 사용해서 연다. 아래와 같이 간단한 코드를 작성 한다. 

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@EnableEurekaClient
public class HelloworldServiceApplication {

public static void main(String[] args) {
SpringApplication.run(HelloworldServiceApplication.class, args);
}
}

@RefreshScope
@RestController
class MessageRestController {

@Value("${message}")
private String message;

@Value("${eureka.instance.metadataMap.instanceId}")
private String instanceId;

@RequestMapping("/")
String message() {
return this.message;
}

@RequestMapping("/id")
String instanceId() { return this.instanceId; }
}


Hellowworld-client : 실제 Zuul proxy 가 동작하는 구간이다. 보통 edge-service 라는 이름을 사용하기도 한다. 

- http://start.spring.io 로 접근한다. 

- artifact 에 helloworld-client 대신 더 상상력 넘치는 이름을 준다. 

- dependancies 에 Zuul, Config client, Discovery client, Ribbon 를 적용한다. 

- Generate project 를 눌러 프로젝트 파일을 다운로드 받고, IDE 를 사용해서 연다. 


Zuul Proxy 를 사용하기 위해서는 기본적으로는 어노테이션 추가외에 아무것도 할 일이 없다. 

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class HelloworldClientApplication {

public static void main(String[] args) {
SpringApplication.run(HelloworldClientApplication.class, args);
}
}

당연한 말이지만 라우팅 설정이 필요하다. 설정에 대한 내용은 demo-config 저장소의 helloworld-client.properties 을 살펴볼 필요가 있다. 아래의 설정을 살펴 보자. 

server.port=${PORT:9999}
info.component="Zuul Proxy"

endpoints.restart.enabled=true
endpoints.shutdown.enabled=true
endpoints.health.sensitive=false

zuul.ignored-services='*'
zuul.ignoredPatterns=/**/api/**

#route 규칙은 zuul.routes.스프링애플리케이션이름=path
zuul.routes.helloworld-service=/hello/**
zuul.routes.discovery-service=/eureka/**

ribbon.ConnectTimeout=3000
ribbon.ReadTimeout=60000


정리해 보면, 

- helloworld-service 는 백엔드 서비스로 동작한다. 두개의 RequestMap을 가지는데, 하나는 "/" 요청에 대해 설정 파일에 주어진 메세지를 응답하는 것이고, 다른 하나는 /id 로 현재 동작중인 인스턴스의 정보를 서버의 정보를 리턴한다. 즉, 동일한 애플리케이션을 로컬에서 서로 다른 포트로 동작하거나, 실제 클라우드에 배포하여 로드 밸런싱이 정상적으로 수행되는지 확인 할 수 있다. 

- helloworld-client 는 edge 서비스로서 zuul proxy 를 사용하고, 유레카를 통해 얻어진 백엔드 서버 정보를 기반으로 ribbon 을 사용하여 로드 밸런싱 한다. 

- 라우팅 규칙은  "zuul.routes.[유레카를통해 얻어진 spring.application.name]=경로" 로 구성된다. 

- 당연하지만 위에 설명한 기능을 제공하기 위한 더 많은 설정이 존재한다. 


Config Server, Discovery service, helloworld-service, helloworld-client 의 순서대로 애플리케이션을 구동한다. localhost:8000/ 으로 요청하여 서비스가 정상 동작 하는지 확인한다. 정상적이라면 Hello world! 를 볼 수 있다. 

$ curl http://localhost:8000/
Hello World!

먼저 서비스의 재시작 없이 설정을 변경하는 방법을 위의 메세지 처리를 통해 확인해 보자. demo-config/helloworld-service 에서 message 설정을 원하는 메세지로 변경한다. 변경했으면, 연결된 커밋, 푸시한다. 설정이 정상적으로 반영되었다면, Config 서버에서는 변경된 최신의 설정을 바로 참조하고 있으나 서비스에는 반영이 안된것을 확인할 수 있다. 아래의 주소로 접근하면 message 의 내용이 변경되었고 이를 config server 가 들고 있는것을 확인할 수 있다. 

http://localhost:8888/helloworld-service/default

{

},

서비스에 바로 반영되지 않는 것은 원래 그렇게 디자인 했기 때문이다. 설정이 변경될 때마다 자동으로 서비스에 반영하는 것은 위험할 수도 있으며, config server 에 부담을 주지 않기 위한 것도 있다. 서비스에 변경을 적용하려면, 지난번에 설명한 바와 같이 empty post 요청을 다음과 같이 전달하면 된다. 

$ curl -X POST http://localhost:8000/refresh
["message"]

정상적으로 동작했다면, 어떤 내용이 변경되어 반영됬는지 리턴될 것이다. 그럼 이제 백엔드 서비스로 다시 직접 요청해 보도록 하자. 

$ curl http://localhost:8000/
Spring Cloud is awesome!

이 동작이 의미하는 바는 무엇인가. 설정을 변경하고 프로세스 재시작, 재배포 이런 과정을 별도로 수행하지 않아도 변경된 설정이 동작중인 서비스에 즉각 반영할 수 있는 메커니즘이 있다는 것이다. 이러한 방법은 Config server / client 를 통해 동작하며 이는 당연하게도 Zuul proxy 의 라우팅 변경에도 사용할 수 있는 것이다. 즉, 신규 애플리케이션을 만들어 동작하고 있는 중이라면, 해당 애플리케이션으로의 트래픽을 서비스 재시작 없이 변경하거나 추가할 수 있다는 의미가 된다. 


이제 로드 밸런싱을 살펴보자. 포트 8000에서 동작하고 있는 서비스는 백엔드다. 그리고 Zuul 은 9999 포트에서 동작중이다. 그리고 오늘의 주제와 마찬가지로 Zuul 이 정상적으로 프락싱을 수행하고 있는지 확인해 보도록 하자. 위의 라우팅 규칙에 따르면, Zuul proxy 서버의 /hello 로 요청을 하게 되면 위의 메세지가 리턴 되어야 한다. 

$ curl http://localhost:9999/hello
Spring Cloud is awesome!

사실 helloworld-client 애플리케이션에 보면 뭐 한것도 없다. 그럼에도 불구하고 프락싱은 정상적으로 동작하고 있는 것이다. 이제 밸런싱을 확인해야 하는데, 위의 메세지는 설정 서버로 부터 동일하게 가져와 반영되는 것이므로 밸런싱이 정상적으로 동작하는지 확인하기가 쉽지 않다. 따라서 동일한 백엔드 서비스를 다른 포트로 동작하게 하고 실제 밸런싱이 되는지 확인해 보자. 아래의 커맨드를 사용하면 동일한 애플리케이션을 다른 포트로 구동할 수 있다. 

# helloworld-service 디렉토리로 이동하여 먼저 빌드를 수행한다 
PORT=8989 java -jar target/helloworld-service-0.0.1-SNAPSHOT.jar

유레카 서비스를 확인해 보면 새로 구동한 백엔드 서비스가 HELLOWORLD-SERVICE 애플리케이션으로 2개의 인스턴스에서 동작하고 있는 것을 확인할 수 있다. 


localhost:9999/hello/id 로 요청을 수행하면 서비스 이름:포트 정보가 나타나는데, 반복적으로 요청을 수행하면 8000 포트와 8989 포트가 번갈아 가며 나타난다. 즉, 정상적으로 로드 밸런싱이 수행되고 있는 것이다. 다시 8989로 동작중인 애플리케이션을 종료하게 되면 이는 즉시 밸런싱에서 제외되고 8000번만 나타난다. 이것은 무엇을 의미하는가. 바로 동적으로 멤버의 추가와 제거가 발생하고, 이 정보가 즉각 참조되어 서비스-인, 서비스-아웃을 수행할 수 있다는 것이다. 

이러한 사용성은 필요에 따라서 전세계에 위치한 데이터센터 중 내가 원하는 지역의 어디로든 트래픽을 동적으로 분산할 수 있는 유연성을 제공한다. 그리고 이런 동작은 그 어떤 프로세스의 재시작도 없이, 그 어떤 애플리케이션의 재배포도 없이 가능하다. 이런 구성이 바로 클라우드에서 동적으로 생성되고 삭제되는 각종 서비스와 그 서비스에 할당된 애플리케이션 인스턴스를 서비스에 사용하고 제거하는 "클라우드에 맞는" 방법인 것이다. 


금번 포스팅에서는 자세히 소개하지는 않겠지만, 이 Zuul 을 사용하여 필터를 적용할 수 있다. 필터는 클라이언트의 HTTP 요청을 받고 응답하는 과정에서 리퀘스트를 라우팅 하는 동안 어떤 액션을 수행할지에 대한 범위를 지정하는 역할을 한다. 아래는 아래는 몇가지 Zuul 의 필터에 대한 특징이다. 

- Type:  리퀘스트/리스폰스 라우팅 되는 동안 필터 적용 상태의 변경을 정의함 

- Execution order: Type 안에 적용되는, 여러개의 필터 적용 순서를 정의 

- Criteria: 순서대로 실행될 필터에 필요한 조건들 

- Action: Criteria, 즉 조건이 매칭하는 경우 수행할 액션 


필터에는 아래의 타입들이 존재한다. 

- PRE: 백엔드 서버로 라우팅 되기 전에 수행되는 필터. 예를 들어 요청에 대한 인증, 백엔드 서버의 선택, 로깅과 디버깅 정보 

- ROUTING: 요청을 백엔드로 라우팅을 제어할때 사용되는 필터. 이 필터를 통해 Apache HttpClient 또는 넷플릭스 Ribbon 을 사용하여 백엔드 서버로 요청을 라우팅 (본 블로그의 예제에서는 Ribbon 을 사용중) 

- POST: 백엔드 서버로 요청이 라우팅 되고 난 후에 수행되는 필터. 예를 들면 클라이언트로 보낼 응답에 스텐다드 HTTP 헤더를 추가한다던가, 각종 지표나 메트릭을 수집하거나 백엔드에서 클라이언트로 응답을 스트리밍 하는 것등. 

- ERROR: 위의 세 단계중 하나에서 에러가 발생하면 실행되는 필터 

Zuul 은 사용자에게 커스텀 필터 타입을 정의하고 사용할 수 있도록 한다. 따라서 특정 요청을 백엔드로 보내지 않고 바로 클라이언트에 응답을 수행하는것과 같은 구성이 가능하다. 넷플릭스에서는 이런 기능을 내부의 엔드포인트를 사용하여 Zuul 인스턴스의 디버그 데이터 수집에 사용하고 있다고 한다. 아래는 Zuul 내부에서의 요청이 어떤 흐름을 가지는지 보여주는 좋은 그림이다. 

https://github.com/Netflix/zuul/wiki/How-it-Works


스프링에서 Zuul 필터의 사용은 아래의 두 코드를 살펴 보자. 

먼저 com.netflix.zuul.ZuulFilter 를 익스텐드 해서 pre 필터를 생성한다. helloworld-client 애플리케이션에 아래의 파일을 추가한다.

spring-cloud-zuul-proxy-demo/helloworld-client/src/main/java/com/example/filters/pre/SimpleFilter.java

package com.example.filters.pre;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;

public class SimpleFilter extends ZuulFilter {

private static Logger log = LoggerFactory.getLogger(SimpleFilter.class);

@Override
public String filterType() {
return "pre";
}

@Override
public int filterOrder() {
return 1;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();

log.info(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));

return null;
}

}

- filterType() 은 필터의 타입을 String 으로 리턴한다. 이 경우 pre 이며, 만약 route 에 적용했다면 route 가 리턴된다. 

- filterOroder() 는 필터가 적용될 순서를 지정하는데 사용된다. 

- shouldFilter() 이 필터가 실행될 조건을 지정한다. 위의 설명에서 Criteria 부분 

- run() 필터가 할 일을 지정한다. 


spring-cloud-zuul-proxy-demo/helloworld-client/src/main/java/com/example/HelloworldClientApplication.java 

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import com.example.filters.pre.SimpleFilter;

@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class HelloworldClientApplication {

public static void main(String[] args) {
SpringApplication.run(HelloworldClientApplication.class, args);
}

@Bean
public SimpleFilter simpleFilter() {
return new SimpleFilter();
}
}


curl http://localhost:9999/hello/id 로 요청을 해 보면, 아래와 같은 로그를 확인할 수 있다. 

2016-09-22 17:58:33.798  INFO 7307 --- [nio-9999-exec-6] com.example.filters.pre.SimpleFilter     : GET request to http://localhost:9999/hello/id


지금까지 소개한 3개의 도구, Config Server, Eureka, Zuul 의 세개는 모두 스프링 클라우드의 도구다. 스프링 클라우드에서는 클라우드에 맞는 서비스 연동을 제공하기 위해 이러한 넷플릭스 오픈소스들을 넷플릭스와 함께 만들고 있다. 이것은 시작일 뿐이며, 이 다음 번에는 서비스에 장애가 발생했을때 GET 요청을 처리할 수 있는 기법을 제공하는 Circuit breaker (Netflix Hystrix) 와 POST 메세지에 대해 고가용성의 방법으로 처리할 수 있는 방법에 대해 적어보도록 하겠다. 


이 Zuul 의 구조에 대해 더욱더 궁금하신 분들은 아래의 넷플릭스 블로그를 참조하시면 되겠다. 

http://techblog.netflix.com/2013/06/announcing-zuul-edge-service-in-cloud.html


추가로 Zuul 에 대해 더 관심이 있으신 분들 중 비밀 댓글로 이메일 주소를 적어주시는 5분께 금번 SpringOne Platform 에서 발표된 넷플릭스의 Zuul 사용에 대한 영상을 공유 할 수 있도록 하겠다. 유료 행사라 아직 전체 공개는 하지 않는 듯. (만약 공유가 잘 안되더라도 용서를.) 

https://www.infoq.com/presentations/netflix-gateway-zuul?utm_source=infoq&utm_medium=QCon_EarlyAccessVideos&utm_campaign=SpringOnePlatform2016


(정윤진, younjin.jeong@gmail.com) 


Cloud software engineering

Techs


(younjin.jeong@gmail.com, 정윤진) 

Meetup을 하고, 중국 두개 도시, 일본 동경, 서울에서 Pivotal Cloud roadshow 를 하고, SpringOne Platform 행사를 다녀오고 각종 회사의 내부 발표와 미팅을 하다보니 어느덧 추석을 목전에 두고 있다. 블로그 작성은 고사하고 써야하는 책 진도가 엄청 밀려서 달리고 달려도 데모의 컨셉과 구현을 병행하다 보니 혼이 나가는 것 같다. 그래도 스트레스는 풀어야겠다고 종종 PS4의 "더 디비전" 으로 지옥이 되어버린 뉴욕 맨하탄에서 악당들을 쏘곤 했다. 어쨌든, 참 시간은 화살과 같다. 


시장, 그리고 엔터프라이즈. 


위의 이미지는 아주 오래전부터 내부 직원들끼리 유머용으로 사용하던 것이다. 기술의 발전과 그 발전을 수용하는 단계에 대한 것인데, 2010년 초반 부터 시작된 거대한 패러다임 쉬프트가 이제 6년째에 접어들어 엔터프라이즈 들에게는 "Oh no" 정도의 단계에 오지 않았나 생각해 본다. 주목할 만한 것은 다른 세상의 모든, 즉 엔터프라이즈를 제외한 세상과 엔터프라이즈의 기술 도입에 대한 그래프의 모양이 다르다는 점이다. 이게 현실 세계의 타이밍에서 묘하게 재미있는 부분은, "Oh fuck" 단계에 들어서면 이전에 주구 장창 이야기 되었던 ROI 와 같은 질문은 더 이상 나타나지 않는다는 점이다. 

국내 클라우드 시장에서, 아마존 웹 서비스의 도입에 대해 이야기를 2012년, 2013년에 하다보면 거의 대부분의 엔터프라이즈 기업들이 데이터센터 운용 비용 및 서버 구매 비용이 어떻게 차이가 나는지 많이 물었다. 그리고 ROI 가 어떻게 개선되는지, 그리고 그 개선에 대한 사례가 없으면 우리는 절대 도입 못할거라고 했다. 비슷한 시기에 만났던 수많은 스타트업과 그리고 게임회사들은 한발 두발 이 기술에 다가서고 있었고 그 동안 어떤 회사들은 보유하고 있는 서버의 숫자의 3년 단위 비용 계획과 아마존 웹 서비스가 제공하는 동일 스펙의 서버 비용을 멋있는 엑셀 차트로 만들어 "아 이거 뭐야 전혀 저렴하지 않잖아" 의 결론을 내고 멀리해 왔다. 

이제 2016년 중반을 지나 말엽으로 가는 마당에 클라우드 도입 자체에 대해 부정적인 의견은 전과 같지 않다. 왜 그럴까. 그것은 애초부터 계산이 불가능했던 ROI 의 효과에 원래 무시되었던 서비스 속도, 확장성, 그리고 자동화를 통한 휴먼 에러의 제거 이런 것들에 대한 가치가 수많은 사례들을 통해 검증되었기 때문이다. 이를테면 3계층 구조의 서비스를 클라우드로 이전하는 방법에 있어 아주 간단하게는 그 모양 그대로 옮기는 것, 주로 지게차를 사용한다는 의미를 가지는 forklift 의 방식을 사용할 때와 이 3계층 구조를 구현 하는데 있어 손쉬운 확장성이 보다 더 낮은 성능의 저렴한 서버를 사용할 수 있도록 하는 구성이 주는 가격적 효과와 같은 것을 깨닫게 되는 것이다. 엔터프라이즈에서는 별로 크게 관심이 없지만, 최근 강력히 대두되는 서버리스와 같은 구조까지 가게 된다면 더 효율적이겠지만 말이다. 

위의 그림처럼 이것이 스타트업과 엔터프라이즈에 대한 문제일 뿐 아니라, 엔터프라이즈 자체도 이제 혁신을 하고 있는 엔터프라이즈와 그렇지 않은 엔터프라이즈로 나뉘고 있다. "Software is eating the world" 라는 말이 나온게 2010여년 즘이고, "Silicon Valley is coming" 이라는 말이 나온건 2015년이다. 이 5년의 차이를 공포로 "인식"하고 있는 J.P. Morgan Chase 같은 회사는 이미 소프트웨어 기술이 그들의 비지니스에 핵심이라는 것을 깨닫고 이 역량 확보를 위한 길에 투자하고 있다. 그리고, 이런 투자는 포츈 500대 기업의 대부분에서 발생하고 있으며, 이들은 아마존과 월마트, 아마존과 메이시 같은 효과에서 아마존 쪽에 서기를 원하는 기업들이다. 그리고 현재 돈을 가지고 있는 기업들이 미래 수익을 놓치지 않기 위해 하고 있는 행동들이기도 하다. 사실, 이런 접근에 최근 한국을 제외한 국가에서 이의를 제기하는 경우는 경험적으로 본적이 거의 없다. 

클라우드 서비스들을 사용이 자유로운 리소스 풀로 사용하는 것은 이제 아주 공통된 개념이다. 당장 하루에 페타바이트씩 쌓이는 데이터를 수용할 수 있는 공간은 여러 방식으로 조달할 수 있겠지만, 별도의 소프트웨어적인 또는 운영적인 노력 없이 바로 사용이 가능한 것은 이미 규모의 경제를 바탕으로 서비스를 제공하고 있는 클라우드 사업자의 서비스를 사용하는 것이 유일한 방법이다. 왜 페타바이트냐, 우린 기가바이트다 라고 하면 이건 이미 엔터프라이즈 규모로 사업을 하고 있지 않거나 그만한 데이터를 모을 수 있는 기술이 없거나 아니면 이전의 운영 방식에 따라서 매일 지우고 있거나일 것이다. 한동안 빅데이터에 대한 가장 큰 거부반응이 우리가 보유한 데이터는 크지 않아요와 비슷하달까. 어쨌든 요점은, 시대는 클라우드 서비스를 리소스 풀로 사용하는 것을 넘어 이 위에 돌아가는 데이터 및 서비스 소프트웨어를 어떻게 만드는가 하는 것에 더 관심이 많다. 즉 프로세서, 메모리, 디스크를 빨리 조달 받는 문제가 해결 되면 이 위에 원래 돌려야 했던 서비스 및 데이터 소프트웨어를 어떻게 처리하는가에 대한 부분의 노력에 관심이 더 많은 시대가 되었다. 아마존 웹 서비스의 사장님인 앤디 제시가 "Cloud is normal" 이라고 말하는데는 여러가지 의미가 있지만, 내맘대로 해석하면 클라우드를 사용하는 것은 기본이라는 말이다. 기본 다음은 무엇인가. 바로, 클라우드 기반의 데이터 운용 및 서비스 소프트웨어 구동, 그것이다. 그리고 그것은 처음부터 그랬듯이, 모든 가치가 있는 곳이다. 

위의 그래프에서 처럼, 엔터프라이즈의 기술 도입 그래프는 빠르게 상승하게 된다. 다만, 이전에 경험을 가진 "세상의 다른 기업"과 동일한 높이를 얼마나 빨리, 그리고 그들이 했던 실패의 반복 없이 어떻게 수행할 수 있을까. 바로 "세상의 다른 기업" 을 넷플릭스나 아마존으로 놓고, "엔터프라이즈"에는 GE를 놓고 보면 된다. 국내의 경우 회사마다 위의 단계가 다 조금씩 다른데, 우리 회사가 "Oh No" 까지 왔다면 이 이야기는 아마 관심이 좀 생기는 이야기가 될 것이라고 예상해 본다. 


뭣이 중헌디 

Value line 이라는 것이 있다. 이것을 대표적으로 설명할 수 있는 것은 바로 "올해 운영체제 튜닝해서 매출 증가를 이루어 회사에서 상 받으신분" 과 같은 질문이다. 대부분의 엔터프라이즈, 뭐 엔터프라이즈 뿐만 아니라 모든 기업은 그 영위하는 서비스와 돈 버는 방법이 있다. 그 서비스의 제공을 위한 운영 체제가 돈을 벌어주는 경우는 없다. 하지만 이것이 중요하지 않다고 할 수 없다. 최신으로 업그레이드 된 운영체제 위에서 견고하게 동작하는 소프트웨어는 강력하다. Value line 은 이것을 누가 어떻게 처리해야 하는지, 그리고 더 깊게는 이런 반복적이지만 쉽게 처리할 수 없었던 것들에 대한 해법이 있다면 과연 사업 성장 및 유지를 위해 어디에 가치 중점을 두어야 하는지에 대한 이야기다. 그리고 이것은 아래와 같은 몇가지 기술 요소들에 대한 고려를 해 볼 필요가 있다. 



첫째로, 그럼 운영체제 업데이트가 서비스 시스템에서 왜 힘들지만 중요한지의 여부다. 답은 간단하다. 위험. 리스크. 운영체제에 긴급한 보안 패치가 필요한 경우, 이 보안 패치를 적용함으로서 서비스에 무슨 문제가 발생할 지 모른다. 정확한 표현으로는, 그 운영체제에서 동작하는 소프트웨어가 이전 버전에서 처럼 제대로 동작할지 안할지를 모른다. 동작하지 않으면 서비스 다운이다. 그리고 이런 종류의 업데이트는 운영체제를 새로 설치하지 않으면 롤백도 쉽지 않다. 그래서 하지 않게 된다. 

하지만 사람들은 방법을 생각해 내게 되는데, 바로 배포 전 테스트를 한다. 머리가 있다면 당연히 사전에 테스트를 한다. 문제는 테스트를 하는 환경과 서비스 하는 환경이 또 다른 모양인 것이다. 사실 테스트 및 운영 환경을 분리하기 본격적으로 시작한 것도 몇년 되지 않는다. '리소스'와 '비용'의 문제 때문에 개발하던 환경이 운영환경이 되었던 사례는 수도 없이 많다. 클라우드는 이런 리소스 문제를 해결 해 왔고, 그래서 각각의 환경을 준비할 수 있었다. 

그렇지만 클라우드를 사용하는 환경임에도 불구하고 버전 업그레이드에 따른 문제는 지속적으로 발생하는데 여기에 가장 중요한 이유는 바로 소프트웨어가 가진 운영체제에 대한 의존성이다. 시스템 라이브러리, 웹 애플리케이션 서버가 제공하는 클래스 패스의 종속성, 그리고 그 시스템  라이브러리가 다시 운영 체제의 커널 버전과 가지는 의존성, 이런 문제들이 발생하고 이는 소프트웨어와 운영체제 및 그에 포함된 기타 라이브러리의 독립적인 업데이트를 보장할 수 없도록 한다. 따라서 각 배포 단계에 항상 서비스 소프트웨어 개발자가 개입하지 않으면 업데이트가 진행되지 않는 사태가 발생하게 되거나, 업데이트 후 문제가 발생하여 화재 진압이 필요하게 되는데 이것은 궁극적으로 업데이트를 꺼리는 환경을 만든다. 

이러한 업데이트를 꺼리게 되는 업무 환경은 사업에 도움이 될 것이 하나도 없다. 그래서, 이런 반복적인 운영 관리 작업을 어떻게 안전하고도 효율적으로 처리할 수 있을지에 대해 고려가 필요하고 이것은 클라우드 서비스가 제공하는 운영체제 템플릿을 교체하는 것 만으로는 여전히 해결되지 않기 때문에 소프트웨어와 운영체제, 그리고 소프트웨어가 사용하는 각종 라이브러리 업데이트 등에 대해 종전과는 다른 방식으로 처리해야 할 필요가 발생한다. 이것이 기본적으로 Immutable artifact 또는 동적 링크보다 static binary 가 대두되는 이유이며, 특정 소프트웨어 버전에 필요한 라이브러리 등에 대해 필요한 버전을 명시해야 하는 이유이기도 하다. 


둘째로 이렇게 빌드된 소프트웨어의 배포에 대한 처리다. 요즘엔 클라우드 종류보다 많은 배포 도구들이 존재한다. 오픈 소스부터 상용에 이르기까지, 선택할 수 있는 도구는 매우 다양하고 각 도구들이 제공하는 범위도 다르다. 이 모든 도구들에 대한 언급 보다는 빌드된 애플리케이션이 인터넷에 연결되어 동작하기 위해서 무엇을 해야 하는가에 대해서 조금 집중해 보기로 하자. 그리고 그것이 세상에서 많이 사용하고 있는 자바를 기준으로 하자. 

자동화 스크립트 또는 도구가 해야 할 일은 먼저 클라우드 서비스에 리소스를 준비하는 것이다. 이것은 보통 가상 머신이다. 가상 머신이 올바르게 준비되어 접근 가능한 상태가 되면, 부트 스트랩이나 또는 ssh 의 방법등을 통해 필요한 패키지를 준비하는데, 예를 들면 아파치 웹 서버나 톰캣과 같은 것이다. 그 준비가 끝나면 이제 젠킨스와 같은 도구에서 빌드된 파일을 전송하여 적절한 디렉토리에 위치시키고 웹 서비스 프로세스를 시작한다. 정상적으로 시작되고, 약간의 테스트가 끝나 동작할 준비가 되면 이 가상 머신을 로드 밸런서에 연결한다. 로드 밸런서가 신규 가상 머신에 대해 정상 동작 여부를 몇번의 테스트를 통해 점검하게 되면 서비스-인 상태가 되어 밸런싱을 수행할 수 있는 상태가 된다. 그리고 필요하다면 이 밸런서에 다시 도메인을 연결하고, 최근 추세에 따라 해당 Zone apex 및 레코드에 대한 TTL 을 짧게 준다. 이게 전부 매뉴얼 스크립트로 구성되어야 하는 것이다. 

하지만 클라우드 서비스들은 서버에 대한 커스텀 템플릿을 만들수 있는 환경을 제공한다. 따라서 매번 새로운 배포를 할 필요가 없이 해당 버전의 소프트웨어가 탑재되고, 톰켓이 이미 설치된 운영 체제를 구성하고 이를 템플릿으로 만들어 추가 웹 애플리케이션 서버가 필요할때 바로 바로 사용한다. 이것은 하나의 immutable artifact 로서 취급될 수 있지만, 문제는 그 제작의 번거로움과 서비스 업데이트 및 릴리즈, 또는 서버에 필요한 설정 변경이 필요할 때마다 새로 만들어 주어야 한다. 심지어 사용해야 하는 원본 운영체제의 버전 업데이트가 반영된 새로운 템플릿이 생기면 여기에 다시 이전에 스크립트로 했던 작업을 수행해야 한다. 그래서 다시 템플릿으로 만들어야 하고, 예를 들어 클라우드 서비스 공급자가 제공하는 오토 스케일 기능이라도 사용하고 있다면 이를 새로운 오토 스케일링 그룹 정책에 반영해 주어야 한다. 하루에 한두번이야 하겠지만, 만약 하루에 서비스가 4천번 업데이트 되는 경우라면 어쩌겠는가. 아니, 그냥 10번만 업데이트 하더라도 이것은 피곤한 일이다. 그리고 이러한 문제는 docker 이미지 생성에도 발생한다. 

언급하고 싶은 문제는 일단 이런 도구들을 다 컨테이너에 넣는것 자체가 약간 비효율이라는 것이다. 하여 스프링 부트(Spring Boot)에서는 스프링을 톰켓에서 구동하는 대신 톰켓을 스프링 부트 애플리케이션에 임베드 하는 방법을 제공한다. 이렇게 Jar 로 빌드된 파일은 java -jar 로 간단하게 실행 가능하다. 당연하지만 SERVER_PORT 와 같은 다양한 옵션 환경 변수를 운영 체제 또는 애플리케이션 시작에 적용할 수 있다는 것이다. java -jar SERVICE-001.jar 커맨드는 컴퓨터를 모르는 우리 어머니도 실행하실 수 있다. 즉, 애플리케이션 레벨에서 immutable artifact 상태로 빌드가 제공되고 이는 JVM만 돌릴 수 있는 환경이라면 동작을 보장하는 방법이 제공 된다는 것이다. 일단 여기에서 회사 위키에 적혀져 있는 500줄짜리 매뉴얼 스크립트를 copy & paste 할 필요가 없어진다. 


이렇게 동작하는 컨테이너 또는 가상 머신에 밸런서를 붙여야 한다. 여기에는 동적 서비스 디스커버리와 마이크로프락시, 그리고 클라이언트간 로드밸런싱의 기법이 오래된 DNS를 대체한다. 강력한 테스트와 한계를 넘나드는 변태적 애플리케이션 아키텍처의 구성으로 유명한 넷플릭스는 이런 도구들을 오픈소스로 제공한다. 서비스 디스커버리엔 유레카(Eureka), 마이크로 프락시 및 API GW 역활에는 줄(Zuul) 및 훼인(이름이 좀 그렇지만, Feign), 그리고 클라이언트간 로드 밸런싱에는 리본(Ribbon) 등이 있다. 유레카는 등록된 애플리케이션에 대한 연결 정보를 서비스 멤버의 모든 인스턴스에 동적으로 공유해 준다. 이렇게 공유된 정보는 API 요청에 따라 동적으로 Zuul 과 같은 도구를 통해 밸런싱 되고 프락싱된다. 아울러, 이 모든 도구들은 스프링 부트의 방법으로 스프링 클라우드 라는 이름으로 제공된다. 이것들은 모두 JVM 위에서 동작하며, Jar 단일 애플리케이션으로서 java -jar 로 구동이 가능하다. 게다가 대부분의 도구는 멀티 데이터센터 레벨로 고가용성 확보가 가능하며, 필요한 경우 암호화 처리 할 수 있다.  

경험상 이러한 동작이 의미하는 바를 개발자 분들께 전달할때, 이게 뭥미 라는 반응을 심심치 않게 본다. 대부분의 경우 고가용성은 사실 애플리케이션의 영역이라기 보다는 운영의 영역인 경우가 많았다. 다른 이야기는 차처하고 나서라도, 위와 같은 도구를 조합해서 사용하게 될때 얻어지는 효과는 기존 운영 영역에서 제공되던 기능들과는 애초에 레벨이 다르다. 예를 들어 넷플릭스의 Zuul 프락시에서는 Canary 테스트가 필요한 경우 특정 API 요청의 일부 트래픽만을 신규 버전의 백엔드에 보낼 수 있다. 즉, 새로운 버전의 배포를 이미 수행해 두고 여기에 동일한 API 요청을 "일부만" 보냄으로서 이게 정상 기능을 하는지 아닌지를 확인할 수 있다. 당연히 이것이 정상이라면 신규 버전을 확장하고 구 버전을 종료한다. 이런 기능이 제공하는 장점은 이런 제로 다운 타임 업데이트 안정성 뿐만 아니라 각종 실험이 가능해지고 이는 더 작은 위험으로 더 많은 일들을 가능하게 한다. 당연하지만, 위에 언급한 스크립트가 따로 필요없는 것은 덤이다. 넷플릭스는 이 도구를 다양한 종류 및 목적으로 트래픽 분배에 사용하고 있으며, 이는 스프링 클라우드로 구현이 되어 있고 따라서 스프링 부트의 사용 방법으로 가져다가 사용이 가능하다. 


셋째로는 최근에 많이 언급되고 있는 컨테이너다. 2013년 초에 다커를 처음 접할 기회가 있었는데, 처음 볼때 아 이거 대박 이라는 생각을 지울 수 없었다. 그 후 몇년뒤, 이건 역시 거대한 흐름을 만들고 있는 도구임에 틀림 없다. 한번 빌드로 다양한 환경에서 구동, 더 효율적인 리소스 사용, 그리고 가상 머신을 신규 배포하는 것에 비해 더 빠른 속도로 확장과 축소가 가능한 점 등등. 그런데 이 부분에 대해 생각할 필요가 있다. Day 1 에 다커는 분명 놀라운 기술이다. Day 2 에 프로덕션 반영을 생각하게 될 정도로. 하지만 현실은 역시 녹록치 않다. 랩탑에서 한두개 올려서 사용하면 분명 대단한데, 일단 수십개, 또는 그 이상 수백개 및 수천개 심지어는 수만개로 돌려야 할때는 어떨까. 현실에서는 많은 것들을 요구한다. 로그의 취합, 적절한 리소스로의 분배, 권한의 관리, 그리고 "업데이트". 예를 들어 하루에 10번 정도 (매우 적은 숫자의) 배포를 수행한다고 하면, 매일 10번 정도의 신규 다커 이미지 생성이 필요하다. 그리고 서비스 별로 약 10-30개 정도의 다커 이미지를 사용하고 있는 중인데 원본 이미지의 업그레이드가 발생하거나 업그레이드를 해야 할 필요가 있을때 이를 모두 새로 이미지로 만들고 또 이 새로운 이미지에서 각 소프트웨어가 무결하게 동작하는지 확인해야 한다. 이런 것들을 지속적으로 유지하다 보면 어느새 현실은 개미지옥. 


따라서 현실적으로 오케스트레이션의 문제를 제외하고 소프트웨어 신규 배포 및 업데이트 만을 놓고 보면 CI 파이프라인 안에 이미지를 동적으로 생성하고 보관해 주는 단계를 구성해 줄 필요가 있다. 즉, 코드가 레포에 올라가고 이렇게 올라간 신규 커밋을 테스트 도구가 받아다가 유닛 테스트 등을 하고 나서 문제가 없이 빌드 되면, 이 빌드된 앱을 다커 이미지로 생성하여 다커 레포로 올리는 단계가 필요하다는 것이다. 물론 RC 관리등의 추가 파이프라인으로 연결할 수 있겠지만, 어쨌든 이런 과정을 현재 수동으로 하고 있다면 근 미래에 이미지 관리에 난항을 겪게 될 것이다. 당연하지만, 여기에는 리모트 로깅, 권한 관리를 위한 툴 등이 빌드된 애플리케이션이 다커 이미지로 생성될때 함께 포함 되어야 한다. 즉, 다소 복잡하지만 정교한 관리가 CI 파이프라인 안에 포함 되어야 한다. 

오케스트레이션 문제를 제외 하지 않으면 문제는 더 복잡해 진다. 물론 다양한 도구들이 나와 있지만 스타트업들 조차 이 다커의 오케스트레이션 구현은 녹록치 않다. 다커의 에코시스템이 발전하는 것은 매우 좋고 선택의 옵션이 다양해 지므로 환영할 일이지만, 현재의 현실 세계에서는 누가 어떻게 이것을 관리하고, 또 다커의 철학인 '한번 빌드로 여러개의 환경에서 구동한다' 를 따라 데이터 센터나 퍼블릭/프라이빗 클라우드에 운영 환경을 준비할 것인가. 물론 각 클라우드 서비스에서는 최근 다커를 운용할 수 있는 환경을 제공한다. 위의 CI 파이프라인과 함께 서비스 업자가 제공하는 환경을 사용한다면 아마도 좋은 접근이 될 것이라고 예상한다. 다만 역시 이 경우에도 언급하고 싶은 것은 과연 이 서비스들에 대한 학습 시간, 누가 배울 것인가, 누가 운영할 것인가, 그리고 서로 다른 서비스가 수백 수천개의 컨테이너를 운용하고 있을때 태그 구분만으로 운영하기에 충분한가, 그리고 이것을 다양한 클라우드 서비스 공급자 별로 제공하는 툴을 따로 배울 것인가 하는 사항들이다. 

클라우드 서비스 공급자가 제공하거나 또는 Chef / Puppet 과 같은 도구들은 물론 인상적이고 훌륭하다. 다만 현장에서 이것들을 최신에 맞게 지속적으로 업데이트 관리해서 배포에 사용하는 경우는... 


넷째로, 마이크로 서비스 이야기를 좀 붙여야 할 것 같다. 마이크로 서비스가 가져다 주는 장점은 서로 관계 없는 기능들이 덩어리 지어 있기 때문에 발생하는 팀간 또는 사람간의 커뮤니케이션과 의존성을 줄이는데 그 목적이 있다. 이를 통해 코드의 양이 적어지고 따라서 운영 관리 및 신규 개발해야 하는 노력이 작아진다. 이는 더 빠른 개발과 배포를 가능하게 한다는 장점이 생기며, 아마존이나 넷플릭스와 같은 회사들이 취하고 있는 방법이다. 하지만 질문도 동시에 생긴다. 외부 요청은 하나인데 내부 서비스 100개에 발생하는 요청 현상, 즉 필연적으로 발생하는 fan out 은 어떻게 해결할 것인가. 현재 조인 관계로 복잡하게 얽힌 데이터 모델을 가진 데이터베이스 안에서 특정 기능들이 스토어드 프로시져로 기능하고 있는 것들은 어떻게 분산해 낼 것인가. 제로 다운타임 업데이트를 데이터베이스 레벨에서 어떻게 처리할 것인가. 각 서비스에 대한 보안 또는 싱글 사인 온등은 어떻게 처리할 것인가. 각 서비스는 서로 다른 개발 스택 또는 프레임 워크를 선택하는것이 옳은 것인가. 운영 관리 방법은 현재도 요청이 많은데 이게 마이크로 서비스로 분산되면 내가 할 일은 더 많아지는게 아닌가. 사람도 안뽑아 주는데. 

먼저 확실하게 말하고 싶은건, 언급한 모든 내용들은 해법이 있고 이는 소프트웨어 아키텍처 및 서비스 아키텍처로 접근 가능한 모델들이 존재한다. 그리고 이러한 모델들에 대해 분명히 학습해 둘 필요가 있고, 이런 경험이 뒷받침 되어야 진정 마이크로 서비스에 대한 접근이 가능할 것이다. 한가지 더 언급하고 싶은 것은, 이런 기법이나 구조를 구현하기 위한 도구들이 넷플릭스에서 만들어 둔 것이 많이 있고 이를 스프링 클라우드에 반영해 둔 것이 많다는 것이다. 

먼저 fan out의 경우, 가능하다면 다운이 발생하지 않는 거대한 캐시풀을 운용하는 방법이 있다. 거대한 캐시풀 이라는 말이 의미하는 것은 예를 들면 아파치 Geode 와 같은 인-메모리-데이터 그리드를 사용하거나, 넷플릭스의 EvCache 를 사용하거나 하는 방법으로 다수의 데이터센터에 존재하는 서버 리소스의 메모리에 중복 저장하여 요청하여 사용하는 방법이다. 예를 들면 수천만명이 로그인을 하고 난 후 발급 받은 토큰으로 다시 다른 마이크로 서비스에 접근이 필요할때 이 검증을 위해 다시 유저 서비스로 접근하지 않고 이 캐시를 사용하는 방법이 있을 수 있다. 이러한 캐시는 비단 캐시의 용도로 사용되는 것 뿐만 아니라 전체 서비스 내에서 일관성을 유지하는 방법으로 사용될 수도 있다. 인 메모리에 저장하는 방법 외에도 분산된 트랜젝션의 로깅과 이를 바탕으로 한 플레이백이 필요한 경우, 이를 테면 이벤트 소싱과 같은 방법을 사용한다면 NoSQL 이나 카프카와 같은 스트림 도구를 사용할 수도 있다. 


조인의 관계는 마이크로 서비스 구조에서는 보통 서비스간 요청으로 처리한다. 데이터베이스 안에서 조인으로 처리하던 것들을 다른 서비스에 대한 요청으로 바꾸는 것이다. 또는 반드시 조인이 필요한 경우라고 하면, 예를 들어 게임이라면 캐릭터와 아이템의 상관 관계와 같은 것들은 아마존의 다이나모 디비와 같은 도구를 사용하는 방법으로 바꿀 수 있다. 세컨드리 인덱스 구성의 변경을 통해 검색 조건에 따라 데이터를 저장함으로서 조인 관계에 대한 극복이 가능하다. 즉, 아이템으로 캐릭터를 검색할 수 있고, 캐릭터로 아이템을 검색해야 하는 조건을 굳이 조인이 아니라 인덱싱으로 처리가 가능하다는 것이다. 그리고 이런 다이나모 디비와 같은 도구의 구현을 참조한다면, 다른 NoSQL 데이터베이스를 사용할 수도 있다. 이런 형태의 데이터 베이스에 대한 구조 및 컨셉은 아마존의 CTO인 버너 보겔스박사(Dr. Werner Vogels) 가 공저한 논문 Dynamo 를 참조하면 더 자세한 정보를 얻을 수 있다. 링크는 여기. (http://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf) 그리고 이 논문의 아마존 버전이 다이나모 디비이며, 넷플릭스 버전의 구현이 Dynomite 다. https://github.com/Netflix/dynomite 

스토어드 프로시저와 같은 방법으로 데이터베이스 내에서 순차적으로 처리되고, 문제가 발생한 경우 롤백이 필요한 트랜젝션의 구분에 대해서는 자주 사용하는 예인 주문, 결재, 배송을 예로 들 수 있겠다. 여기에는 아마존의 Simple Work Flow 와 같은 도구의 컨셉이 사용될 수 있다. 즉 주문 서비스, 결재 서비스, 배송 서비스는 각각의 데이터 저장소를 가진 각각의 서비스로 별도로 구현되며, 이는 상품의 주문은 주문 서비스가 처리한다. 주문 서비스는 예를 들면 주문 데이터 저장을 위해 MongoDB 와 같은 저장소를 사용하고, 이를 RabbitMQ 나 SQS 같은 큐에 주문이 들어왔음을 알린다. 그럼 이 큐를 subscribe 하고 있는 "다음 프로세스를 담당하는 마이크로 서비스"인 결재 서비스는 기입된 정보를 바탕으로 데이터 저장소 없이 관계된 신용카드 트랜젝션을 처리한다. 이것이 문제 없이 끝나면 다시 RabbitMQ 나 SQS의 별도 채널을 이용하여 주문이 완료 되었다는 메세지와 함께 내용을 큐로 전달한다. 그러면 다시 배송을 담당하는 마이크로 서비스는 이 큐의 내용을 바탕으로 주문 정보를 작성하는 구조가 된다. 각 단계의 마이크로 서비스들은 로그 어그리게이션 또는 로그 스트림의 방법을 통해 처리 결과 및 에러를 취합하고, 자신의 프로세스에 이상이 발생하면 그 이상에 대해 다시 큐에 기록한다. 그러면 알림 서비스는 이 큐에 기록된 에러를 바탕으로 고객에게 어느 단계에서 문제가 발생했으니 주문을 다시 확인하라 라는 메세지를 보낼 수 있다. 

위에 설명한 방법들은 몇가지 기본적인 처리 방법들이다. 그리고 서비스에 따라 이것이 요청을 받은 즉시 처리되어야 하는지 아니면 의미 있는 시간 내에 처리가 되면 되는지에 따라 선택적으로 사용할 수 있다. 그리고 이런 것들이 가능한 이유는 클라우드 기반에서 매우 확장성 있는 메세지 큐나 캐시, 또는 데이터베이스를 구현할 수 있기 때문이다. 또한, 위와 같은 방법들은 전체 마이크로 서비스에 천편 일률적으로 사용되는 것이 아니다. 관계형 데이터베이스로 모든것을 처리하는 것이 아닌, 필요한 상황에 맞는 스토리지를 각각의 서비스가 해결할 문제에 맞도록 선택해서 사용하는 것이다. 넷플릭스는 카산드라를 많이 사용하지만, 그것으로 모든것을 처리하는 대신 EvCache 와 같은 도구를 개발했고, Dynomite 와 같은 도구도 필요했다. 중요한 것은, 마이크로 서비스는 이런 내용을 아주 기본으로 각각의 서비스를 만들고 이 서비스들에 대한 변경을 빠르게 해야 한다는 것이다. 만약 서비스를 잘못 찢어내어서 불필요한 중복이 발생했다면 이를 다시 없애고 다른 형태로 서비스에 반영할 수 있어야 하는 것이지, 무슨 처음 부터 역할을 다 구분해 놓고 팀에 쪼개서 할당하면 구현이 될거라는 방식은 절대 동작하지 않는다. 그리고 마이크로 서비스에 대한 접근에서 클라우드 오퍼레이션에 대한 이해, 즉 운영체제의 업그레이드, 라이브러리의 업그레이드 및 트러블 슈팅등과 같은 방법이 제공 되지 않는다면 이것은 분명 오퍼레이션 비용에 대한 수직 상승을 불러올 것이다. 클라우드 파운더리가 가지는 핵심 장점 중 하나가 여기에 있다. 

마이크로 서비스에 대해서 마지막으로 하고 싶은 말은, 언제 해야 하는가 이다. 기존의 모놀리틱 방식으로 개발된 애플리케이션이 충분히 작다면 마이크로 서비스를 해야 할 이유가 없다. 다시 이를 좀더 엄밀히 말하자면, 애자일 개발 방법을 이미 사용하고 있는 상태에서 해당 서비스를 개발하는 팀의 인력이 바뀌거나, 누군가 슬럼프가 갑자기 오거나 하는 별다른 문제가 없는데 속도가 떨어지고 있다면 이때를 기존 서비스에 분리가 필요한 시점이라고 볼 수 있다. 언제 무엇을 해야하는지에 대한 결정은 반드시 데이터 지표가 있어야 한다고 생각한다. 


위에 언급한 내용들에 대해 별도의 견해나 해법이 있을 수 있다. 이 글을 쓰고 있는 목적은 순전히 경험의 공유이며 이것들이 실제로 동작하는 서비스가 있고 또 기존의 서비스에서 다운타임 없이 마이그레이션 할 수 있는 방법도 있다. 그것이 바로 클라우드의 매직이 아닌가. 그리고 우리에게 필요한 것은 이런것에 집중하는 것이지, 웹 애플리케이션 설정과 업그레이드가 아니라는 것이다. 


그래서 결론은 (Pivotal) 

모든것은 비지니스다. 멀티 데이터센터 또는 멀티 클라우드를 하려는 이유가 무엇인가. 사업적으로 무중단이다. 마이크로 서비스로 구성된 넷플릭스에서 서킷 브레이커 로직이 필요한 것은 무엇인가. 특정 장애의 고립을 통한 전체 서비스 장애 확산 방지다. 데이터베이스 클러스터링을 통해 아무리 견고하게 구성해도 그것이 하나의 덩어리로 되어 있다면 서비스가 데이터베이스 장애를 데이터베이스 만으로 고립할 수 있는가? 아닐거다. 따라서 수많은 리소스를 쉽게 조달 받을 수 있는 클라우드를 기반으로 서비스에 필요한 리소스를 효율적으로 사용하고 그 위에 클라우드에 맞게 기능을 하는 서비스 소프트웨어를 얹는 것이 바로 클라우드 네이티브의 본질이다. 

마지막으로 클라우드 파운더리에 대해 언급하고 싶다. 현재 클라우드 서비스의 도입과 사용에 대해서 열기가 뜨겁다. 대부분의 경우 클라우드를 배운다는 것은 어렵다. 퍼블릭 서비스들은 각자의 운영 방식이 있고 인터페이스가 다르고 사용하는 언어가 다르다. 프라이빗 서비스 역시 마찬가지다. 이는 모든이에게 모든것을 알게하는 동시에 풀스택 개발자라는 말을 탄생시켰다고 해도 과언이 아니다. 모든이가 모든것을 아는것이 과연 가능한 일인가. 클라우드 파운드리는 이 문제를 해결한다. 개발자가 다커를 배울 필요가 없고 운영자가 개발자의 요청에 의해 데이터베이스와 네트워크를 준비하고 업데이트 때문에 곧 다가올 추석에 밤 새는 일을 없도록 한다. 위에 언급한 대부분의 운영적 문제로 발생하는 그리고 반복적으로 발생하기 때문에 새로운 서비스의 시장 공개를 더디게 하는 문제를 해결한다. 그리고 이것은, 각 클라우드 서비스를 별도로 배울 필요가 없도록 하기도 한다. Multi-AZ 에 대한 개념은 필요하지만 그것이 꼭 Multi-AZ 라는 이름으로 특정 서비스에서 사용 된다는 것을 학습하는 것은 필요 이상의 노력이다. 뒤쳐져서 갈길 바쁜 엔터프라이즈에게 스프링과 클라우드 파운더리는 value line 아래의 일에서 조직을 해방시킨다. 

쓰다보니 간만에 굉장히 장문의 글의 되었지만, 어쨌든 다음의 내용을 검색을 통해 알아보기를 강권한다. 그래도 아직 못다쓴 내용은 나중에 책으로 더. 새벽에 졸릴때 썼으므로 문장이 좀 이상한 것들은 양해를 구합니다. ㅠㅡ ㅠ 


1. 넷플릭스 오픈소스 및 넷플릭스 테크 블로그 

2. http://concourse.ci  http://ci.concourse.ci 

3. Pivotal Cloud Foundry 

4. http://start.spring.io  http://cloud.spring.io 

5. Micro services

6. https://www.youtube.com/watch?v=GTnRl_BIkzc  

7. https://run.pivotal.io 

8. SpringOne Platform 2016 videos https://www.youtube.com/watch?v=xdw_9dADM-4&list=PLAdzTan_eSPQ1fuLSBhyB4eEZF7JQM0Mx


(younjin.jeong@gmail.com, 정윤진)