System Compleat.

고가용 서비스 - Spring Cloud - #4 Monitor your Micro services - Zipkin

Techs


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

서비스를 모니터링하는 것은 매우 중요하다. 모니터링 동작은 기본적으로 각종 시스템의 지표를 주기적으로 취득하여 이를 로그로 남기거나 또는 이 로그 스트림을 어딘가로 보내 그래프를 만들고, 특정 값 또는 특정 문자열이 발견되면 알람을 울리는 식의 단계를 가진다. 이런 각종 시스템의 지표, 이를 테면 CPU, 메모리, 디스크, 네트워크 사용율과 같은 정보들은 OS에서 제공하는 /proc 하위 디렉토리 내용을 참조하거나 별도의 커맨드를 크론으로 주기적으로 돌려서 실행하곤 한다. 

이런 행위는 애플리케이션 서버에서도 동일하게 발생하는데, 보통 웹 애플리케이션 서버와 관련된 프로세스를 체크하거나 역시 이 서버들이 내리는 로그를 모니터링 하고 여기에 문제가 발생한 경우 알람을 울리는 역할을 하도록 하는 경우가 대부분이다. 물론, 역시 커스텀 로그의 생성도 가능하겠다. 


로그는 서비스 운영에 있어 굉장히 중요한 자산이다. 하지만 대부분의 레거시 시스템들에서는 이런 로그를 별도로 모아서 서비스에 어떤일이 발생했었는지, 또는 고객들의 서비스 사용 패턴에 대해 분석한다던지 하는 작업을 하는 경우는 별로 없었다. 거의 대부분 운영체제와 프로세스가 만들어 내는 로그들은 디스크에 차곡차곡 쌓이다가 어느 순간 디스크 사용율이 98% 정도에 이르면 시원하게 로그를 비워주는 작업을 하고는 했다. 하지만 이런 로그의 처리 방식은 최근 클라우드 기반의 (또는 클라우드 기반이 아니더라도) 서비스에서는 도저히 용납될 수 없는 처리 방식이라고 할 수 있다. 따라서 각 서버들이 생산해 내는 로그들은 깔때기 처럼 로그를 어느 한군데로 원격으로 수집하고, 이를 별도의 클라우드 스토리지에 자연스럽게 연계하여 저장하고, 저장된 정보를 하둡이나 데이터웨어 하우징에서 처리하도록 구성한다. 이런 방식의 로그 처리에 대해서는 아래의 링크를 살펴보면 보다 더 자세한 정보와 함께 다양한 옵션을 확인할 수 있겠다.  (https://logging.apache.org/log4j/2.x/manual/appenders.html) 로그 스트림 처리에 대해서는 나중에 기회가 되면 소개를. 

http://www.slideshare.net/petervandenabeele/akka-streams-kafka-kinesis-49863296


사실 로그는 오늘 언급할 주된 범주가 아니다. 로그는 사실 "어떤 사건이 발생했다" 라는 정보를 감지하는데는 매우 중요하지만, "왜 발생했는지" 에 대해서 설명을 해 주진 않는다. 아니 사실 해 주기는 하는데, 이는 로그를 스트림으로 처리해서 일련의 사건 흐름을 되짚어 보는 형태의 분석에 대한 구성이 필요하고, 이는 생각보다 꽤나 노력이 드는 일이다. 어쨋든 우리에게는 "왜 이런일이 발생했는가" 에 대한 내용을 볼 필요가 있는데, 마이크로 서비스를 구성하게 되는 경우 이는 각각의 서버 인스턴스에서 발생하는 로그를 위에 소개한 방법으로 전부 취합해서 시각화 하고 분석을 돌려야 하는 노력이 필요하다. 그러므로, 그래서 오늘 소개하는 도구는 바로 이런 일을 간단히 해 줄 수 있는 도구다. 


Zipkin 이 하는 기본적인 일은, 외부로 받은 요청과 그 요청을 처리하기 위해 발생하는 내부 요청을 기록한다. 그리고 이 기록된 요청에 대해 일목 요연하게 시각화 해 주며, 따라서 특정 요청을 처리하기 위해 어떤 서비스들이 참조 되었고 또 어떤 서비스가 제 속도를 내고 있지 못하는지의 여부를 쉽게 확인할 수 있다. 이는 트위터에서 만든 도구인데, 홈페이지는 http://zipkin.io  


백문이 불여일견, 백견이 불여일행. 


스프링 클라우드에서는 이 Zipkin 을 정말로 매우 쉽게 사용할 수 있도록 한다. 먼저 이전 포스팅에서 사용했던 헬로월드 클라이언트와 서버에 이 Zipkin 을 붙여 보도록 하자. 


먼저 zipkin-service. properties 파일을 demo-config 에 추가한다. 

server.port=${PORT:9411}
spring.datasource.initialize = false
spring.sleuth.enabled = false
zipkin.store.type = mem

Config server 가 참조하는 코드 저장소에 커밋하고 푸시한다. 


아래의 설정을 application.properties 에 추가한다. 이 설정은 모든 서비스에 글로벌로 적용된다. 

spring.sleuth.sampler.percentage=1.0
spring.sleuth.log.json.enabled=true


아래의 순서로 Zipkin 서버를 준비한다. 

- http://start.spring.io 에 간다. 

- artifact 에 zipkin-service 이름을 준다. 

- dependencies 에 Config client, Discovery, Zipkin UI, Zipkin server 를 추가하고 프로젝트를 생성하여 다운로드 받는다. 

- IDE 를 열어 아래와 같이 애플리케이션에 Zipkin 과 Eureka 사용을 위한 어노테이션을 추가한다. 

spring-cloud-zuul-proxy-demo/zipkin-service/src/main/java/com/example/ZipkinServiceApplication.java

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import zipkin.server.EnableZipkinServer;

@SpringBootApplication
@EnableZipkinServer
@EnableEurekaClient
public class ZipkinServiceApplication {

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


spring-cloud-zuul-proxy-demo/zipkin-service/src/main/resources/bootstrap.properties

- bootstrap.properties 를 추가하고 Config server 참조를 위한 설정과 애플리케이션의 이름을 할당한다. 

mvn spring-boot:run 으로 서버를 구동시켜 보자. 아래와 같은 화면이 보인다면 성공. 

자, 그러면 이제 각 서비스로 부터 실제 리퀘스트 데이터를 모아보기로 한다. 별도로 해 줄 것은 없고, helloworld-client 와 helloworld-service 의 두 모듈의 pom.xml 에 아래의 dependancy 를 추가한다. 

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>


그리고 지난 포스팅에서와 마찬가지로 Config server, 유레카 서비스, 헬로 월드 서버, 헬로 월드 클라이언트, zipkin 서비스의 순서대로 서비스를 모두 구동한다. 구동이 완료되면, zuul 을 통해 서비스에 요청을 해 보도록 하자. 그리고 zipkin ui 로 진입하여 find 버튼을 누르면 아래와 같은 내용이 나타난다. 

Zipkin 에서 보이는 데이터는 크게 두개인데, 하나는 전체 요청을 추적하는 trace 와, 다른 하나는 각각의 내부 서비스간 요청과 응답을 기록하는 span 의 두가지다. 이 데이터를 통해 위와 같이 각 리퀘스트의 처리에 걸리는 시간을 보여준다. 하나를 클릭해 보면 아래와 같은 세부 정보를 볼 수 있다. 

이는 외부로 요청을 받은 시점부터 이 요청을 처리하기 위해 내부 서비스간 발생한 요청에 대한 처리 시간을 보여준다. 헬로 월드 클라이언트는 헬로월드 서비스로 요청을 포워딩 했고 여기에 각각이의 처리 시간이 나타난다. 그리고 이 전체 요청의 처리에는 7ms 가 걸렸음을 확인할 수 있다. 이런 방식으로 내부 서비스가 다수 존재한다면, 예를 들어 캐시 마이크로 서비스가 존재하는데 여기서 지연시간이 증가했다면 서비스가 느려진 이유는 캐시 미스가 증가한 것으로 생각할 수 있다. 또는 별도의 데이터베이스나 메세지 큐와 같은 서비스를 참조하는 마이크로 서비스가 있다면 여기에서 발생한 지연 역시 확인할 수 있다. 즉, 이 도구는 "왜" 를 설명하기 위한 것이다.

우측 상단의 JSON 을 살펴보면 친절하게 JSON으로 만들어진 이 요청에 대한 데이터를 확인할 수 있다. 

그리고 서비스간 구조가 복잡하다면 아래의 그림과 같이 서로 어떻게 연관되어 있는지 그림도 그려 준다. 


마지막으로 언급할 내용은, 이 데이터들은 무슨 데이터웨어 하우징 도구에 넣고 "우리 반년전 상태가 어땠지?" 하는 질문에 사용하는 도구가 아니다. 즉, 이 데이터는 길어야 하루나 이틀 정도를 두고 "현재 우리 서비스의 상태를 빠르게 참조할 수 있는" 도구로 사용하는 것이 올바르다. 필요하다면 pgsql 과 같은 데이터베이스를 사용하여 각 요청을 저장할 수도 있겠다. 본 데모에서는 메모리에 저장하는 옵션을 사용했다. 

한가지더, 모든 요청을 기록할 필요가 없다. 위의 설정 내용을 보면 spring.sleuth.sampler.percentage=1.0 이런 설정이 있는데, 이는 데모를 위해서 전체 요청을 추적하는 것이다. 이럴 필요가 없이 적절한 수량의 요청을 샘플로 사용하여 서비스에 불필요한 부하를 주지 않도록 하자. 트위터의 경우에는 1/6,000,000 의 비율로 샘플링을 수행한다고 한다. 즉, 6백만 요청당 1개를 zipkin 에 넣고 보는 것이다. 


서킷 브레이커 보다 zipkin 이 먼저 나왔다. 어쨌든 도움 되는 포스팅이기를. 


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

고가용 서비스 - 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) 


고가용 서비스 - Spring Cloud - #2 Configure to Config server

Techs


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

언제나 설정은 프로세스 동작을 위한 필수 요소였다. 오래된 대부분의 서버 데몬들은 /etc/ 아래에 위치한 설정들을 필요로 했고, 이 설정들은 보통 데몬이 동작할 서버의 네트워크 정보와 같은 것들, 그리고 데몬 (또는 프로세스)의 기능을 조정하는 각종 플래그나 변수들로 이루어져 있다. 그리고 대부분의 경우 이 설정이 변경되어 적용이 필요하다면 프로세스의 재시작 또는 리로드가 필수적이다. 따라서 운영중인 서비스에 포함된 데몬의 설정 변경은 프로세스 재시작으로 인한 서비스에 순단, 또는 그 데몬의 캐시 사용 여부등에 따라 웜업(warm up)등의 작업, 그리고 필요에 따라 이중화를 통한 무중단을 고려해야 하는 등의 고급진 운영의 기술이 필요하다. 하지만 대부분의 사람들은 어려운 것 보다는 쉬운 방향으로 일을 처리하기 때문에, 서비스에 중단 시간을 사전에 공지하고 이 시간내에 설정의 변경, 프로세스의 재시작, 정상 동작의 확인과 같은 일을 수행한다. 따라서 서비스를 사용중인 사용자들은 서비스 중단을 경험하게 되는데, 이런 패턴은 고객을 위한 행동이라기 보다는 운영의 편의를 위한 방식이라고 할 수 있다. 

서버를 클라우드로 바뀌면 모든게 해결되는 줄 알던 시대가 있다. 다운 타임도 스르륵 사라지는 줄 알던 분들도 꽤 있었다. 그렇지 않다. 클라우드의 기본은 "무엇이든 언제든지 뽀개질 수 있다" 이며, 대신 "언제든 조달 가능한 리소스가 있다" 의 가치를 통해 뽀개짐을 새로운 대체재로 치환 하는 기술이 핵심이라고 할 수 있다. 따라서 모든 애플리케이션 및 서비스에 포함되는 컴포넌트는 "뽀개지면 요렇게" 의 사상을 가지고 만들어져야 궁극적인 제로 다운타임을 확보할 수 있을 것이다. 


그럼 그 "뽀개지면 요렇게" 는 어떻게 처리하는 것이 좋을까. 다음번에 자세히 소개하겠지만, Netflix 에서는 Simian Army 라는 도구를 운영한다. 이 도구가 하는 일은 무려 프로덕션 서비스에서 동작하는 서버들을 랜덤하게 죽인다. 물론 프로덕션 뿐만 아니라 새로운 서비스를 개발하거나 배포하기 전에도 테스트에 사용된다. 즉, 이 테스트를 통과하지 못하면 프로덕션에 배포될 수 없다. 우리 나라에 운영 하시는 분들은 아마 이런 구성을 상상이나 하겠는가. 잘 돌아가는 서버를 끈다니 이 무슨 천인 공노할 일인가 말이다. 라고 생각하시겠지. 다음번 소개 이전에 궁금하신 분들은 넷플릭스의 테크 블로그로. (http://techblog.netflix.com/2011/07/netflix-simian-army.html

Chaos Goriila - Netflix Simain Army

위의 이미지는 카오스 고릴라 (Chaos Gorillia) 의 이미지다. Simian Army 에서 카오스 고릴라가 하는 일은 아마존 웹 서비스의 특정 AZ(Availability Zone) 전체의 인스턴스를 다운시킨다. 즉, 데이터 센터 레벨에서의 고가용성을 프로덕션에서 테스트 한다는 말이 되겠다. 한국의 서비스 중에 이런 레벨의 고가용성을 유지하는 회사가 어디 있겠냐 말이다. 


어쨌든 애플리케이션의 설정 변경을 통한 빌드, 재배포, 그리고 이 재배포로 인해 발생하는 다운타임은 그다지 달가운 것이 아니다. 그리고 설정의 변경때마다 새로운 빌드와 새로운 배포를 해야 한다는것은, 만약 매뉴얼 작업으로 처리하고 있다면 꽤나 귀찮고 불편한 일일 것이다. application.properties 에 설정을 변경하면 새로운 jar 또는 war 를 만들어야 하고 만약 톰캣을 따로 사용하고 있다면 파일을 일단 서버에 전송한 후 특정 위치에 배치한 후 프로세스를 재시작한다. 뭐 서버 한두대 돌릴때야 별 문제 없는 방법일지도 모르지만 넷플릭스와 같이 수십, 수백, 수천대의 인스턴스가 동작하고 있는 상황이라면 어떨까. 이런건 업데이트 공지 한시간으로는 안된다. 

그리고 한가지, 아마존 닷컴이 업데이트 한다고 서비스 1시간 다운 시키면 아마 뉴스에 날거다. 주가는 떨어지고. 아마존 웹 서비스는 어떤가. 


우리는 이러한 문제가 왜 발생하는지 알아야 할 필요가 있다. 즉, 기능 업데이트를 제외하고 애플리케이션의 설정(또는 feature flag)을 바꿨기 때문에 서비스에 중단이 발생한다는 것은 사실 달가운 일이 아니라는 것이다. 그 귀찮은 배포와 프로세스 재시작이 기다리는 요단강을 하루에 수십번 넘는다는 것은 즐거운 일이 아니다. 따라서 이런 문제를 해결하기 위해, 2011년 쯤에 Heroku 를 만든 팀이 만들어낸 컨셉인 12 factor 라는게 있다. 내용은 한글로도 있으며, 페이지는 http://12factor.net 에 방문해 보면 현대의 애플리케이션이 어떻게 만들어져야 하는지에 대한 컨셉을 담고 있다. 그것 중, 오늘 이야기할 부분, 바로 설정과 관련된 핵심 그림은 아래와 같다. 

https://12factor.net/ko/build-release-run

2. Config 에서 따온 그림은 아니지만, 중요한 내용은 바로 여기에 있다. 설정과 코드는 분리 되어야 하는데, 이것이 합쳐진 것이 바로 릴리즈 라는 개념인 것이다. 이것이 왜 중요한지 간략하게 요약해 보면 다음과 같다. 

- application.properties 또는 application.yml 이라는 것이 본시 설정이지만 코드 영역에 포함 된 것이다. 

- 따라서 변경을 하면 새로 빌드해야 한다. 이것은 새로운 배포를 필요로 한다. 

- 대부분의 애플리케이션에서 프레임워크 기본 설정 외에 기능에 필요한 설정은 코드 내에 포함되는 경우가 많다. 

- 이것은 배포의 대상이 많으면 많을 수록 (예. 개발 / 테스트 / 프로덕션 환경) 서로 다른 버전의 설정이 코드에 반영되어야 하므로, 관리를 위해 다수의 repository 를 운영하게 된다. 

- 결국 코드는 seamless 하게 각 환경으로 배포될 수 없고, 이는 사람의 손과 복잡한 코드 저장소의 관리를 수행해야 하는 사태가 발생한다. 

- 이 모든것이 설정이 코드에 포함되어 있기 때문에 발생한다. 


물론 이런 문제를 방지해 주는 여러가지 기법들이 있지만, 전술했듯 사람은 쉬운 쪽으로 일을 한다. 특히 일이 급하면 더 그렇다. 설정을 따로 분리해 내는 것 보다 그냥 코드 안에 도메인이나 IP를 때려넣는 것이 더 편하다. "어? 내 PC에서는 잘 되는데...?" 


만약 설정 정보를 API 요청을 통해 한꺼번에 받아올 수 있는 웹 서비스가 있다면 어떨까. 그리고 이 웹 서비스는 코드 저장소를 설정 파일의 소스로 사용한다면 어떨까. 물론 파일 시스템을 사용할 수도 있다. 일단 이것이 오늘 소개할 스프링 클라우드 Config Server 이다. 넷플릭스에서는 Archaius (http://techblog.netflix.com/2012/06/annoucing-archaius-dynamic-properties.html) 라는 도구를 사용한다고 한다. 


스프링 클라우드에서 제공하는 Config server 에 대한 자세한 정보는 다음의 링크에서 얻을 수 있다. https://cloud.spring.io/spring-cloud-config/spring-cloud-config.html 


Config server 의 사용은 이전에 살펴본 넷플릭스 유레카와 마찬가지로 서버와 클라이언트로 구성되어 있으며, 스프링 이니셜라이저 (http://start.spring.io)를 사용한다면 매우 쉽게 사용이 가능하다. 순서는 아래와 같다. 

Spring Cloud Config Server

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

- artifact 에 config-service 와 같은 그럴듯한 이름을 넣어준다. 

- Dependencies 에서 Config Server 를 찾아 추가한다. 


- Generate Project 를 눌러 Zip 파일을 다운로드하고, 이를 IDE 를 사용해서 연다. 

- config-service/src/main/java/com/example/ConfigServiceApplication.java 에 @EnableConfigServer 어노테이션을 추가한다. 

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;


@SpringBootApplication
@EnableConfigServer
public class ConfigServiceApplication {

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

- application.properties 파일에 아래의 설정을 추가한다. 설정 파일이 위치한 코드 저장소는 github 를 사용했는데, 별도의 git 또는 파일 저장소를 사용할 수 있다. 자세한 내용은 위의 Config Server 설정 부분을 참조 

spring.application.name=config-server
spring.cloud.config.server.git.uri=https://github.com/younjinjeong/spring-cloud-event-sourcing-config
server.port=8888

- 애플리케이션을 빌드하고 실행한다. 

mvn clean package
java -jar target/config-server-0.0.1-SNAPSHOT.jar

따로 설명하지는 않았지만, 스프링 부트 애플리케이션을 구동하기 위해 별도의 WAS 를 사용하지 않았음에 주목한다. 최근 이런 Fat JAR 를 어떻게 서비스에 구동해야 하는지 여쭈어 보는 분들이 종종 계신데, 많이 목격되는 것은 다커(Docker) 를 사용하거나 Cloud Foundry 와 같은 런타임 플랫폼을 사용할 수 있다. 자세한 설명은 따로. 어쨌든 서비스가 정상동작 하는지 확인하기 위해서, 위의 github 를 사용하는 경우 다음의 링크를 통해 확인이 가능하다. http://localhost:8888/user-service/cloud

 

어쨌든 위와 같은 작업을 통해 이제 웹 요청을 통해 코드 저장소에 저장된 설정을 전달할 수 있는 서버가 준비되었다. 서버가 준비되었다면, 클라이언트를 연결해 본다. 


Config Server client 

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

- artifact 에 config-client 를 넣는다. 

- Dependencies 에 Config client 를 선택한다. 이외에 원하는 부트 애플리케이션을 만들기 위해 Web, Rest repo, H2 등 원하는 도구를 선택한다. 

- Generate project 를 눌러 프로젝트 파일을 다운로드 받고 압축을 해제하여 IDE 로 개봉한다. ㅎ 

- 별도의 어노테이션 추가는 필요 없지만, 설정 파일에 Config server 의 위치를 명시해야 한다. config-client/src/main/resources/bootstrap.properties 파일을 생성하고 (application.properties 가 아님에 주의) 아래와 같이 Config server 의 위치를 명시해 준다. 

spring.cloud.config.uri=http://localhost:8888
spring.application.name=config-client

- 만약 Github 나 별도의 코드 저장소를 사용한다면, config-client.properties 를 생성하고 아래와 같이 간단한 설정을 넣고 Config 서버에 연결된 코드 저장소에 push 한다. 

server.port=${PORT:9999}

- 이전에 만들어 둔 Config 서버를 시작한다. 

- 지금 생성한 config-client 애플리케이션을 시작한다. 

.   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.4.0.RELEASE)

2016-09-19 12:28:37.699 INFO 3977 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at: http://localhost:8888
2016-09-19 12:28:42.443 INFO 3977 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Located environment: name=config-client, profiles=[default], label=master, version=afb483b02756c38f5c2ce3a3e0ad149aefe100c3
2016-09-19 12:28:42.443 INFO 3977 --- [ main] b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource [name='configService', propertySources=[MapPropertySource [name='https://github.com/younjinjeong/spring-cloud-event-sourcing-config/config-client.properties'], MapPropertySource [name='https://github.com/younjinjeong/spring-cloud-event-sourcing-config/config-client.yml'], MapPropertySource [name='https://github.com/younjinjeong/spring-cloud-event-sourcing-config/application.yml']]]
2016-09-19 12:28:42.503 INFO 3977 --- [ main] com.example.ConfigClientApplication : No active profile set, falling back to default profiles: default

애플리케이션을 시작하면, 위와 같이 아름다운 Spring 아스키 이미지와 함께 메세지를 확인할 수 있는데, 내용인 즉슨 다음과 같다. 

Fetching config from server at: http://localhost:8888 

애플리케이션이 시작되면서 Config server 로 부터 설정 내용을 가져온다. 그리고 애플리케이션이 정상적으로 시작되었다면, 아래의 내용을 확인할 수 있을것이다. 

Tomcat started on port(s): 9999 (http) 

즉, 설정 파일의 내용을 스프링 애플리케이션이 참조하여 해당 애플리케이션의 포트를 9999로 설정한 것이다. 스프링 부트의 특징중 하나는 각종 설정의 오버라이드가 매우 쉽다는 점인데, 이것들은 서로 참조되는 순서가 있다. 첫번째로 참조되는 것이 바로 bootstart.properties 로, 이 파일에 있는 내용은 애플리케이션의 이름, config server 의 위치와 같이 거의 변경되지 않는 내용을 넣는다. 두번째로 참조되는 것이 이 경우에는 Config server 에 있는 설정 파일의 내용인데, 여기에서 역시 참조 순서가 있다. config server 가 참조하는 코드 저장소에 있는 설정 파일의 이름에 application.properties 가 있다면, 여기에 있는 내용은 모든 config server 를 참조하는 config client 들이 참조하는 전역 설정과 같은 것이다. 그리고 이와 함께 [application-name].properties 가 있다면, 이 application.properties 위에 설정을 merge 해서 사용한다. 이때 동일한 설정이 있다면 이기는 쪽은 애플리케이션이름.프로퍼티 파일이 되겠다. 

이러한 우선 순위 규칙에 따라 애플리케이션 설정의 참조 순서를 적용할 수 있으며, 더욱 중요한 것은 이를 통해 설정과 코드를 분리 할 수 있다는 것이다. 스프링 부트 애플리케이션은 시작할때 서버의 설정 내용을 참조하여 스스로를 설정하며, 이는 12 factor 애플리케이션의 config 부분에 취급되는 매우 중요한 내용중 하나가 되겠다. 그리고 이 포스팅에서 구체적으로 언급하지는 않지만, 각 설정 파일을 환경 별로 생성할 필요 없이 사용될 환경을 지정해서 사용하는 것도 가능하다. 이는 프로파일 이라고 불리는데, 위의 애플리케이션의 시작 로그를 살펴 보면 아래와 같은 로그가 남는것을 확인 할 수 있다. 

No active profile set, falling back to default profiles: default

따라서 여러분이 사용하는 환경 별로 test, staging, production, cloud 와 같은 형태로 구성하여 사용할 수 있다는 것이다. 


Config 에는 다소 민감한 내용들이 있을 수 있다. 이를테면 특정 애플리케이션의 패스워드, 데이터베이스 연결정보, 클라우드 서비스 공급자의 API 키와 같은 내용들이다. 이런 내용들을 보호하기 위해 암/복호화를 사용할 수도 있겠다. Config server 의 보안에 대해서는 아래의 링크를 살펴보기를 권고한다. https://cloud.spring.io/spring-cloud-config/spring-cloud-config.html#_security 


마지막으로, Config 서버는 애플리케이션의 재시작 없이 설정을 변경할 수 있는 메커니즘을 제공한다. 즉, 이미 온라인 상태에서 동작하고 있는 애플리케이션의 feature flag라던가, a/b 테스트 용도의 기능 변경과 같은 것들을 이 변경 가능한 설정에 적용할 수 있다. 적용하는 방법은 아래의 Josh 코드에서 살펴볼 수 있다. 이는 @RefreshScope 라는 어노테이션을 사용하는 것으로 가능하다. 아래의 간단한 코드는 message 라는 설정이 config repo 에 있고, 이를 즉시 변경할 수 있는 예제이다. 

@RefreshScope
@RestController
class MessageRestController {

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

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

이렇게 구성된 코드는 설정 파일에서 (또는 config repo에서) message 로 지정된 내용을 가져와 /message 에 요청이 있을때 응답한다. 만약 예전의 방법으로 이를 구성한다면, 메세지를 변경할 때마다 (데이터베이스를 사용하던지) 아니면 설정을 수정/ 또는 코드를 수정하여 새로 빌드 후 배포해야 동작할 것이다. 하지만 Config client 에서는 이를 empty post 요청을 보냄으로서 처리가 가능하다. 이에 대한 설명은 아래의 링크를 참조한다. http://cloud.spring.io/spring-cloud-static/spring-cloud.html#_refresh_scope


유레카와 마찬가지로, config server 역시 '설정 및 적용'을 위한 하나의 마이크로 서비스 애플리케이션으로 생각할 수 있다. 이는 당연하게도 유레카 서비스와 함께 연동해서 사용할 수 있으며, 이 경우 더 좋은 가시성을 확보할 수 있다. 따라서 자바, 그중에서도 스프링을 사용하여 마이크로 서비스를 구현하려는 경우에는 바로 이 유레카와 config 서버 두개를 기본 리소스로 확보하여 마이크로 서비스 구조를 확장할 수 있는 방법을 제시한다. 


다음번에는 이렇게 구성된 서비스들 간 API 게이트웨이 및 마이크로 프락시 역할을 하는 넷플릭스의 Zuul 을 스프링 클라우드에서 사용하는 방법에 대해 소개해 보도록 하겠다. 도움이 되시길 바라며. 


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



고가용 서비스 - Spring Cloud - #1 DNS to Eureka

Techs


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

언어와 기술의 홍수에서 살다보면 정작 뭐가 중요한지 잊어버리는 경우가 많다. 클라우드의 시대에는 이것이 점점 더 가속화 되었는데 그 대표적인 예가 운영자에게 코드를 배우도록 강요하고, 개발자에게 운영의 기술을 가지도록 하는 것이다. 게다가 클라우드 서비스 자체 뿐만 아니라 그 위에서 동작하는 수많은 새로운 도구들의 출현은 가만히 보고 있자면 숨이 막힐 지경이다. 그것들 중에 어느 포인트가 가장 재미가 있을까 생각을 해 보니, 역시 스프링 클라우드에 대해 이야기 해보는게 좋을것 같다. 

아래의 그림은 DNS가 어떻게 동작하는지 보여준다. 

http://www.thewindowsclub.com/dns-lookup


그림은 thewindowsclub.com 이라는 페이지에서 가져왔다. 예전에 그려둔게 있는것 같은데, 아무튼 없네. 

DNS는 인터넷의 시작점과 함께 존재했던 도구다. DNS를 모르는 개발자나 운영자는 없겠지만, 그것이 실제로 어떻게 동작하는지에 대해 이해하는것은 조금 다른 이야기니까 몇글자 적어보면, DNS는 일단 Domain Lookup System (또는 서비스)다. 모든 사용자 컴퓨터에 보관된 네트워크 정보는 크게 내 아이피 주소와 네트워크 마스크, 그리고 다른 네트워크로 넘어갈때 내 통신을 처리해 줄 게이트웨이의 주소, 그리고 이 DNS 서버의 주소를 기입하게 된다. 집에서 쓰는 공유기에는 편리하게도 DHCP 라는 프로토콜을 통해 이 내용들이 컴퓨터가 연결되면 자동으로 설정되지만, 대부분의 서버 네트워크에서는 이런 것들을 수동으로 설정하여 관리한다. 

어쨌든 www.mydomain.com 과 같은 주소를 브라우저에 넣게 되면 이는 네트워크 정보에 담겨져 있는 DNS 서버로 물어본다. 이때 순서가 있는데, .com .net .io 와 같은 최상위 도메인에 대한 정보를 가지고 있는 서버들을 ROOT DNS 라고 한다. 이 ROOT DNS 서버 정보들은 사전에 공개 되어 있으며, IANA 와 같은 기관에 의해 관리된다. 기본 동작은 이 ROOT 서버에 mydomain 에 관련된 정보를 어디서 찾아야 하는지 물어보고, ns.mydomain.com 과 같은 DNS 서버 주소를 찾게 되면 다시 이 ns.mydomain.com 에서 www 에 대응하는 IP 주소를 넘겨 받아 결국 www.mydomain.com 으로 직접 IP 연결을 통해 접근하게 된다. 

이 과정들은 bind9 과 같은 DNS 서버에서 recursive 라는 플래그를 통해 "내가 대신 물어봐 줄께" 를 켜거나 꺼는 방법으로 서버 관리자는 설정할 수 있다. 즉, 내 컴퓨터에 설정된 1차 및 2차 DNS 서버에서 www.mydomain.com 을 찾기위해 각각 다른 DNS 서버로 물어보는 동작을 대신 처리해 주고, 마지막 결과인 A 레코드, 즉 IP 주소만을 내 컴퓨터로 되돌려 주어 내 컴퓨터는 www.mydomain.com = IP (ex. 1.1.1.1) 과 같은 정보를 가지게 되는 것이다. 그러면 컴퓨터는 1.1.1.1 서버로 http GET 요청을 보내게 되고 해당 서버의 정보가 정확하다면 서버는 GET 요청을 처리해서 돌려주며, 이렇게 받은 데이터를 브라우저 화면에 표시하는게 브라우저에 도메인 주소를 찍을때 발생하는 동작들이다. 

일단 이러한 메커니즘이 왜 필요한지 생각해 볼 필요가 있다. 첫째로는 사람은 숫자보다는 문자를 더 잘 기억한다. 또, 그렇게 기억하는 것이 편리하다. www.amazon.com 이 54.239.25.208 보다 외우기가 쉽다. 두번째로, 어떤 사유에 의해서이건 도메인과 아이피 주소는 바뀐다. 그것이 장애에 의한 고가용성 처리를 위해서건, 단순히 서버를 KT에서 Amazon 으로 옮겨서건 간에 이름과 주소는 바뀔 수 있다. 예를 들면 내 이름은 바뀔 가능성이 매우 낮지만 (거의 없지만), 내가 사는 주소는 언제든 바뀔 수 있는 것이다. 따라서 DNS 는 이런 인터넷 상의 특정 서비스로의 접근을 위한 주소 해석 체계를 제공한다. 

이것은 인터넷에서 서비스간 연결을 위해 사용되기도 하지만, 서비스 내부에서 웹 서버가 데이터베이스 서버를 찾아갈 때 사용하기도 한다. 이 두가지는 보통 external / internal 이라는 용도로 구분하여 사용하곤 하는데, external 의 경우 www.service.com 과 같은 대표 도메인과 메일 처리를 위한 MX 레코드 등 인터넷으로 부터의 참조 목적을 위해 사용하고, internal 의 경우 db-1.service.com, web-1.service.com 과 같이 내부 리소스에 대한 정보를 제공하기 위해 사용된다. 하지만 대부분 internal 의 경우에는 DNS 를 사용하는 대신 DNS 이전에 참조 될 수 있는 /etc/hosts 를 사용하는 경우가 대부분이다. 이는 DNS 를 유지하는 것 보다 /etc/hosts 파일을 보수 하는 것이 더 쉽기 때문이다. 그 말인 즉슨, DNS 서버는 유지하고 관리하는데 추가적인 노력이 "꽤" 많이 드는 서비스 라는 것이다. 그리고 이 DNS 서비스에 등록되어 인터넷에서 "유일하게" 식별 될 수 있는 주소를 FQDN (Fully Qualified Domain Name) 이라고 하며 이는 서비스 코드 내에서 다른 서비스 참조 또는 다른 서비스에 API 요청을 수행할때 이 도메인 주소, 또는 hosts 에 등록된 주소를 사용했다. 

이전에 많이 사용하던 DNS 의 특징을 이 외에도 종합해 보면 다음과 같다. 

- 서비스가 정상적으로 동작하려면 레코드를 사전에 등록해서 사용해야 한다. 

- 최근에는 가능한 DNS 서버도 많이 있지만, 어쨌든 DNS 서버는 기본적으로 서비스에 대한 healthcheck 를 수행하지 않는다. 

- 레코드에 대한 변경이 발생하는 경우 업데이트 및 반영에 시간이 필요하다. 주로 TTL(time to live) 값을 통해 위에 설명한 "되물어보기" 를 피하기 위한 캐시 용도로 사용하는데, 만약 TTL 값이 1시간이라면 TTL 1시간 만료 직전 59분 59초에 이 요청을 수행한 클라이언트는 다음 1시간 동안 이전의 레코드를 캐시에 가지고 요청하게 된다. 즉, DNS 서버가 변경되었을때 클라이언트들에 즉시 업데이트 할 수 있는 메커니즘을 가지고 있지 않다. 

- 따라서 TTL 값을 짧게 잡으려고 하는데, 이 경우에는 DNS 서버에 심각한 부하가 발생할 수 있다. 

- 대표적으로 사용되는 bind9 의 경우 변경 사항의 업데이트를 위해서는 zone 파일의 리로드 또는 프로세스의 재시작이 필요하다. DNS 서비스 프로세스 재시작 해 본적 있는가봉가 



클라우드 이전에는 이런 구성은 사실 문제가 되는 경우가 매우 드물었다. 관리자가 서버 이전을 해야 하는데 TTL 이 기본인 1주일로 잡혀있는 것을 잊어버리고 IP 부터 변경하여 1주일 동안 서비스가 되니 마니 하는 장애 상황이 생길 정도로 말이다. 즉, 서버의 이동과 신규 추가가 발생하는 경우가 극히 계획적이고 자주 발생하지 않기 때문에 DNS를 업데이트를 자주 하지 않아도 '한번 설정하면 어지간하면 그대로 동작하는' 상태가 유지 되었던 것이다. 하지만 클라우드에서는 어떤가. 

external 의 용도로는 기존의 DNS 체계가 인터넷과 밀착되어 있기 때문에 이는 반드시 유지해야 하는 구성이다. 하지만 internal 의 경우, 오토 스케일링, 컨테이너의 사용 등으로 인해 특정 서비스에 연결된 서버의 정보가 수시로, 정말 수시로 변경되게 되고, 이에 대한 정보를 그때그때 IP 로 관리한다는 것은 말이 안되기 때문에 서비스-서버 정보를 매핑해 주는 역할이 필요하게 된다. 따라서 DNS 체계를 사용하려고 봤더니, 이게 업데이트와 업데이트의 반영을 위한 노력이 장난이 아닌것이다. 게다가 클라우드 서비스에서 서버나 컨테이너의 생성과 소멸은 지속적으로 반복되고, 그 생성과 소멸의 시점에 즉시 반영 되어야 그 의미가 있는 것이므로 종전의 DNS 를 사용하는 방법은 옳지 않다고 할 수 있다. 

이에 우리의 변태 엔지니어들 가득한 넷플릭스에서는 Simian Army의 공격에서도 살아남을 수 있는 Eureka 라는 서비스를 만들어 냈다. 이는 오픈 소스로 공개가 되어 있으므로, 아래의 링크를 참조해 보도록 하자. 

https://github.com/Netflix/eureka 



역시 그림은 나랑 안맞... 

아무튼 이 도구가 하는 역할은 Service discovery, 즉 언놈이 어떤 정보를 가지고 동작하는지에 대한 내용을 실시간으로 서비스에서 반영하는 도구다. 위의 DNS 역할은 서비스와 해당 서비스 애플리케이션이 동작하는 위치를 정보를 "요청하는 클라이언트에게만" 전해주는 정보였다. 유레카는 각 클라이언트가 자신의 정보를 유레카 서버에 보내고, 이 정보를 받은 유레카 서버는 각 클라이언트에게 업데이트된 정보를 전달해 주는 체계를 가지고 있다. 이는 다수의 데이터 센터에서 동작할 수 있어 높은 수준의 고가용성으로 지속적으로 서비스가 가능하며, 문제가 발생하여 일순간 서비스에 문제가 된 경우에도 각 클라이언트는 유레카 서버로 부터 받은 정보를 일정 시간동안 로컬에 보유하고 있어 다른 서비스에 연결하는데 문제가 되지 않는다. 

유레카는 서버와 클라이언트로 구성되고, 클라이언트는 자신의 정보를 서버에게, 서버는 클라이언트로 부터 받은 정보를 다른 클라이언트에게 전파하는 역할을 한다. 따라서 서비스 1에 더 많은 요청을 처리하기 위해 서비스를 이루는 서버 또는 컨테이너가 늘어나는 경우, 이 늘어난 컨테이너들에서 동작하는 유레카 클라이언트들은 자신이 동작하는 순간 서버에 자신의 정보를 전달하고 이 정보가 모든 클라이언트에 업데이트 되기 때문에 DNS + Load balancer 의 구성에서 보다 더 빠른 속도로 서비스-인, 서비스-아웃이 가능하다. 

이와 유사한 동작을 하는 도구들은 몇몇 있다. HashCorp의 Consul (https://www.consul.io/) 아파치 주키퍼(https://zookeeper.apache.org/) 등. Consul 의 경우에는 Cloud Foundry 에서도 Service discovery 용도로 사용되고 있는데, 이는 주로 Golang 을 사랑하는 분들에게 많이 이용되는 것 같다. Golang 에서의 Consul 을 사용한 서비스 디스커버리 예제는 이 링크에서 참조 할 수 있다. (http://varunksaini.com/consul-service-discovery-golang/


스프링 클라우드는 단순히 넷플릭스의 OSS 도구 뿐만이 아니라 다른 OSS 생태계에서 클라우드 기반 애플리케이션에 필요한 도구들을 함께 제공한다. 위에 열거한 모든 도구는 스프링 클라우드에서 제공하고 있으며, 이것이 의미하는 바는 위의 모든 도구들이 JVM 기반에서 동작할 수 있는 애플리케이션으로서 서비스에 제공될 수 있다는 의미다. 스프링 클라우드에서 Eureka 서버와 클라이언트를 구성하는 방법은 매우 간단한데, 아래의 단계로 각각 수행하면 된다. 

Eureka 서버 

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

- artifact 에 discovery-service 라고 쓴다

- 오른쪽 Dependencies 에 Eureka Server 를 찾아 엔터를 눌러 추가한다. 

- Generate Project 를 눌러 zip 파일을 다운받고, 압축을 해제하여 프로젝트를 IDE, 이를테면 STS나 IntelliJ 와 같은 도구로 연다. 

- discovery-service/src/main/java/com/example/DiscoveryServiceApplication.java  에 @EnableEurekaServer 어노테이션을 추가한다 

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer

public class DiscoveryServiceApplication {

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

- discovery-service/src/main/resources/application.properties 에 아래와 같이 설정을 넣어준다. STS 를 사용하거나 IntelliJ IDEA ultimate 버전을 사용한다면 다양한 eureka 관련 설정 옵션을 확인할 수 있다. 

spring.application.name=discovery-service
server.port=${PORT:8761}

eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.server.enable-self-preservation=true


다 끝났다. Maven 이라면 mvn spring-boot:Run 을 사용하거나 IDE의 플레이 버튼을 눌러 애플리케이션을 실행하면 다음과 같은 Eureka 웹 콘솔을 확인할 수 있다. 

유레카 서버가 준비 되었으니, 클라이언트를 추가해 볼 차례다. 다른것은 그저 스프링 부트 애플리케이션을 만드는 것과 크게 다르지 않고, Dependencies 에 Discovery client 를 추가하면 된다. 

- http://start.spring.io 에 간다. 

- artifact 에 eureka-client 와 같은 애플리케이션 이름을 넣는다. 

- Devpendencies 에 eureka discovery 를 추가하고 Generate project 를 눌러 프로젝트를 다운 받아 압축을 풀고, IDE 로 연다. 

- @EnableEurekaClient 어노테이션을 추가한다. 

package com.example;

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

@SpringBootApplication
@EnableEurekaClient
public class EurekaClientApplication {

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

- application.properties 를 수정한다. 

spring.application.name=eureka-client
server.port=${PORT:8989}

eureka.instance.hostname=${vcap.application.uris[0]:localhost}
eureka.instance.nonSecurePort=80
eureka.instance.metadataMap.instanceId=${vcap.application.instance_id:${spring.application.name}:${spring.application.instance_id:${server.port}}}
eureka.instance.leaseRenewalIntervalInSeconds = 1
eureka.instance.lease-expiration-duration-in-seconds=5
eureka.instance.lease-renewal-interval-in-seconds=10
eureka.client.registryFetchIntervalSeconds = 5

스프링 부트 애플리케이션을 실행하고 eureka 서버의 웹 콘솔로 접근해 보면 eureka-client 가 추가된 것을 확인할 수 있다. 

동일한 클라이언트 애플리케이션을 다른 포트로 구동시켜 보자. mvn spring-boot:run -Dserver.port=8980 과 같은 형태로 쉽게 설정을 오버라이드 할 수 있다. 

그러면 EUREKA-CLIENT 라는 이름의 애플리케이션에 2개의 인스턴스가 생겨난 것을 확인할 수 있다. 이때 클라이언트 애플리케이션을 끄고 웹 콘솔을 리프레시 해 보면 등록된 클라이언트들의 정보가 사라진다. 

유레카를 통해 각 클라이언트에 전파되는 정보를 확인하고 싶다면 아래의 주소로 접근해 보자. 

http://localhost:8761/eureka/apps 

<applications>
<versions__delta>1</versions__delta>
<apps__hashcode>UP_1_</apps__hashcode>
<application>
<name>EUREKA-CLIENT</name>
<instance>
<instanceId>172.30.1.24:eureka-client:8980</instanceId>
<hostName>localhost</hostName>
<app>EUREKA-CLIENT</app>
<ipAddr>172.30.1.24</ipAddr>
<status>UP</status>
<overriddenstatus>UNKNOWN</overriddenstatus>
<port enabled="true">80</port>
<securePort enabled="false">443</securePort>
<countryId>1</countryId>
<dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
<name>MyOwn</name>
</dataCenterInfo>
<leaseInfo>
<renewalIntervalInSecs>10</renewalIntervalInSecs>
<durationInSecs>5</durationInSecs>
<registrationTimestamp>1474180884996</registrationTimestamp>
<lastRenewalTimestamp>1474181029539</lastRenewalTimestamp>
<evictionTimestamp>0</evictionTimestamp>
<serviceUpTimestamp>1474180884492</serviceUpTimestamp>
</leaseInfo>
<metadata>
<instanceId>eureka-client:8980</instanceId>
</metadata>
<homePageUrl>http://localhost:80/</homePageUrl>
<statusPageUrl>http://localhost:80/info</statusPageUrl>
<healthCheckUrl>http://localhost:80/health</healthCheckUrl>
<vipAddress>eureka-client</vipAddress>
<secureVipAddress>eureka-client</secureVipAddress>
<isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
<lastUpdatedTimestamp>1474180884996</lastUpdatedTimestamp>
<lastDirtyTimestamp>1474180884463</lastDirtyTimestamp>
<actionType>ADDED</actionType>
</instance>
</application>
</applications>

이 정보들은 클라이언트가 부트될때 서버로 보내지는 정보들이며, 이는 각 유레카 클라이언트에 전파된다. 이와 같은 동작은 서비스에 어떤 애플리케이션이 얼마나 많은 숫자의 인스턴스로 동작하는지 즉각적인 확인을 가능하게 할 뿐만 아니라, 각 서비스간 연동을 위해 별도의 DNS 체계를 구축할 필요가 없다는 점이다. 유레카를 사용하는 경우, 클라이언트간 로드 밸런싱을 위해 별도의 FQDN을 사용하는 대신 http://EUREKA-CLIENT/your/api/endpoint 의 형태로 요청할 수 있기 때문이다. 

이 유레카 서비스 자체는 각 애플리케이션의 인스턴스 정보만을 공유한다. 이 자체로 "서비스 디스커버리"라는 부분의 역할에만 충실한 마이크로 서비스이며, 이 마이크로 서비스가 제공하는 기능을 통해 다른 컴포넌트들과 유기적으로 연동이 가능하다. 대표적인 것이 Zuul 과 Ribbon 인데, 이에 대해서는 다음에 다시 자세히 설명하는 걸로. 


결론적으로 이 유레카와 같은 도구는 서비스 인스턴스 (서버나 컨테이너와 같은 애플리케이션 기동의 베이스가 되는 리소스들)의 정보 매핑에 예전 레거시에서 사용하던 hosts 나 internal DNS 의 역할과 유사한 동작을 수행하지만 보다 빠르게 리소스의 상태가 변경되는 클라우드에 더욱 맞는 도구라고 할 수 있다. 그리고 이런 도구의 다양한 조합을 통해 애플리케이션의 고가용성을 구현할 수 있다. 스프링 클라우드는, 스프링 개발자라면 누구나 이니셜라이저를 통해 쉽게 유레카와 같은 도구를 사용할 수 있게 한다. 이전에도 언급했지만 Consul 과 Zookeeper 와 같은 도구 역시 스프링 클라우드에 포함되어 있다. 


다음번에는  Config server 에 대해 조금 더 살펴보는 것으로. 

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




Diagnostics, in computer engineering - DiagnOPs

Techs


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

Apple Service Diagnostic, ASD

http://www.macrepaircentral.us/powerbook-g4-15-inch-double-layer-sd-10-2005/apple-service-diagnostic-asd.html

2013년 즈음의 일이다. 아마존 웹 서비스에서 솔루션 아키텍트라는 일을 하고 있었을 때다. 저녁에 갈매기살을 구워서 소맥이랑 먹으면 참 좋은 계절이었던 기억이 아마도 가을이었나 보다. 그런 가을날, 나는 어느 대기업의 미팅룸에서 오후 6시에 잡힌 장애 원인 분석과 관련된 미팅에 앉아있었다. 해당 대기업은 아마존 웹 서비스의 고객이었는데 어떤 모바일 서비스의 업데이트에 일반적인 3계층 구조의 서비스를 운영중이었는데 어느날 부터인가 데이터베이스 부하가 한도를 넘기 시작해서 인스턴스 타입을 가장 높은 것으로 옮긴지 얼마 되지 않는 이력을 가지고 있는 서비스였다. 오후 6시부터 미팅을 시작하는 것도 사실 뭐 썩 기쁜 순간은 아니었지만, 그 미팅 시작으로 부터 약 2시간 동안 벌어진 일은 나름 인생의 얇은 고민중의 하나가 되었다. 

회의에는 약 20여명의 엔지니어들과 두어명의 사업 담당이 있었다. 엔지니어들의 구성은 ELB (로드 밸런서) 전문 팀, EC2 (서버) 전문팀, WAS (Web application server) 전문팀, DBA팀, 애플리케이션을 개발했던 개발팀, 그리고 해당 시점의 상황을 해결하기 위해 투입된 "문제 분석" 팀이 있었다. 각각은 약 3~5 명으로 이루어져 있었으며, 회의의 시작은 문제 분석팀에서 지난 1주일간 수집한 각종 서비스 지표를 공개하고 이를 나머지 팀들과 공유하고 토론하는 형태였다. 

서비스에 발생한 문제는 이런식이었다. 밸런서에서는 내부로 포워딩한 요청에 에러가 지속적으로 발생하는 상태였고 (5xx), 인느 웹 애플리케이션 서버로 부터의 응답 지연으로 인한 것이었으며, 이는 데이터베이스로 부터의 응답에 지연이 발생하는 것이었다. 하지만 데이터베이스 팀에서는 우리의 디비 서버는 CPU 자원이 놀고 있는 상태므로 이건 데이터베이스의 문제가 아니다 라고 하는 내용의 회의가 1시간 반동안 진행되었다. 사실, 아마존 웹 서비스의 솔루션 아키텍트는 어떤 문제에 대해 고객이 스스로 문제를 찾게끔 서비스에 대한 이해를 높이는 방식으로 접근해야 한다. (아니 했었다) 따라서 나에게 질문이 돌아오기 전 까지 나는 침묵을 지킬수 밖에 없었고, 회의 시작 15분 후에 공개된 여러가지 서비스 지표에서 이미 답이 나온 것으로 내심 확정하고 있었던 나는 나머지 1시간 30분이 매우 답답할 수 밖에 없었다. 

결론부터 말하면, 문제는 주기적으로 발생하는 EBS 볼륨의 부하였다. 지금은 그런 문제가 별로 없지만 마그네틱 볼륨이 주를 이루던 때였고, 기억을 더듬어 보면 당시 4000이 최고이던 PIOPS 볼륨에 IO queue length 가 매우 높은 상태였지만 EC2 인스턴스에서는 부하 지표가 관찰되지 않는 그런 류의 성능 문제였다. 따라서 나의 권고는 EBS optimized 인스턴스를 사용할것, 필요하다면 EBS PIOPS 볼륨을 여러개 만들어 데이터베이스에 table space 를 적용할 것 여기에서 더 많은 IO가 필요하다면 EBS 볼륨 몇개를 스트라이핑 하여 table space 로 할당하여 사용하되, 주기적 백업을 절대 소흘히 하지 말 것 이었다. 

http://theevansconsultinggroup.com/2015/05/21/the-dangerous-case-of-the-endless-meeting/

이 회의에서 나는 수많은 팀의 수많은 인원들이, 그리고 해당 클라우드 서비스를 충분히 숙지한 사람들이 왜 그렇게 서로 답이 없는 회의를 길게 하는지 몰랐다. 아니 몰랐다기 보다는 이해할 수 없었다. 서비스의 흐름상 디스크 부하가 발생하면 데이터베이스의 응답이 느려지고 이는 모든 서비스의 이상을 설명할 수 있다. 하지만 각각을 담당하는 팀은 자신의 서비스에 문제가 발생했음을 부인하고 있었고 문제 해결을 위해 투입된 팀은 이 팀들에게 직접적으로 어디가 잘못되었다 하고 이야기 할 수 없는 기술적/정치적 상황에 놓여 있었던 것 같다. 


또 다른 일은 2008년 즈음의 일인것 같다. 당시엔 기본적으로 베오울프의 모델을 따르는 분자식 모델을 만드는 계산 클러스터를 모 업체의 요청에 따라 아파트형 공장에 앵글을 짜 넣어서 만들고 있었다. 이 전에 동일한 업체에서 요구했던 60대 클러스터의 성능을 기록적으로 향상 시켜주었던 전례가 있어 해당 업체의 카이스트 출신 사장님께서는 나를 전폭적으로 믿고 클러스터 구현의 용역을 맡겼다. 당시에 계산에 사용하던 툴은 가우시안과 PC Gamess 라는 툴이었는데, 원래 20초 정도 걸려야할 간단한 H2O 계산이 30분이 지나도 안끝나자 사업 진행에 큰 문제가 발생해 해결을 위해 이업체 저 업체 찾아 다녔지만 누구도 답을 주지 못했다고 했다. 그래서 지푸라기라도 잡는 심정으로 당시 개인적으로 상심하여 장돌뱅이처럼 떠돌던 나와 첫 미팅을 하게 되었고, 미팅 시작 20분여만에 답을 주었다. 

이 베오울프 아님

사장은 믿지 않았고, 내가 말한 방법으로 해결 될 수 있는지 확인하고 싶어 했다. 그러자며 나는 콘솔을 달라고 했고, 그 업체 방문전 일하던 회사에서 동일한 문제로 3주를 삽질하다 얻은 결과를 커널 컴파일 옵션에 적용했다. 멀티 코어가 처음 나오던 시절 즈음이었던 것 같은데, 아무튼 커피 한잔과 담배 한대 태울동안 컴파일은 종료되었고, 재시작 된 PC급 서버에서 계산을 돌려보라고 콘솔을 물려주고 난 5분 후 나는 사장의 동그래진 눈과 함께 이후 40분 동안 그 회사의 사업 설명을 들어야 했다. 음, 계산은 20초 정도가 정상으로 추정 했었지만 5초만에 종료 되었고 그 회사의 물리학 박사님 께서는 내가 무슨 옵션을 조정했는지 받아적고 싶어 했다. 기분 참 좋은 묘한 순간으로 기억난다. 

그런 과거로 인해 사장님은 이번에는 250대 규모로 클러스터를 확장하고 싶어했고, 그걸 나에게 맡겼다. 따라서 나는 간단한 BOINC 기반의 리눅스 클러스터를 만들었는데, 그때 기억나는 것이 하드웨어 였다. 당시 나는 검증이 되고 이미 사용중인 하드웨어를 구매하자고 했었으나, 사장은 이번에 AMD에서 새로 나오는 멀티 코어를 (쿼드 코어 초기 제품이었던 것 같다) 싸게 공급 받을수 있다고 해서 그걸 구매하려고 했다. 대당 단가 15~19만원 선의 무지막지한 가격으로 준비된 250대의 서버 같은 PC 를 구성 완료해서 시험 가동하는 순간, 나는 스스로 멋있다는 생각을 하지 않을 수 없었다. 당시 많지 않던 네트워크 부팅, 비지 박스를 사용한 언제든 업데이트 할 수 있는 자동화 구성, 각각 켜지고 꺼지는 상황을 모니터링 할 수 있고, 샘플로 주어진 H2O 계산을 주어진 큐를 통해 내려 받아 완벽히 끝내는 200여대의 아주 저렴한 기계들. 프로젝트 완료는 약 1.5주 정도 소요되었고, 나머지 0.5주는 검사에 사용했다.  

하지만 문제가 찾아왔다. 15분 이상의 작업을 수행하자, 250대중 몇대가 랜덤하게 전원이 꺼졌다. 정말 문제를 찾아내기 위해 수백번은 클러스터 설정을 변경하고 재시작하는 과정을 거쳤던 것 같다. 하지만 이 문제는 특정 서버에서만 발생하는 것이 아니었다. 또 하지만, 반드시 몇대에서는 발생하는 문제였다. 나중에 표를 그려보니 거의 모든 서버에서 발생했다. 

콘솔은 메인 컨트롤 머신에만 있었으므로 커널 패닉이 나는 순간 리모트 로깅 방법도 없었다. 하여 머신 하나가 꺼질때까지 반복적으로 계산을 돌린 결과, 역시 답을 알 수 없었다. 커널의 스택 트래이스는 얻었지만 원인을 알 길이 없었다. 문제를 사장에게 알리고, 하드웨어 점검이 필요하다고 했다. 대량으로 구매한 하드웨어에서 동시에 발생한 문제였기 때문에, 당시의 나는 소프트웨어 문제인지 하드웨어 문제인지 확신할 수 없었다. 그래서 기존에 문제가 없던 60대 중 20여대를 옮겨와 새로운 클러스터에 붙이고 돌렸지만 그 20대에서는 문제가 없었다. 즉, 동일한 소프트웨어에서 문제가 없고 새로운 하드웨어에서 같은 소프트웨어를 사용했을때 문제가 발생한 것이다. 

그 후로 나는 소프트웨어적으로 결함이 없음을 해당 회사의 엔지니어와 함께 확인하고, 그 동안 발생한 문제의 범인을 프로세서로 압축한 후 메인 보드 제작 업체와 프로세서 관련 업체에 압박을 넣어보자고 사장님께 말했다. 그리고 얼마후에 전화를 받았다. (거의 1주일 정도 간격). AMD에서 사람이 와서 프로세서가 불량임을 확인했고 전량 교체받기로 했다고, 수고 했다고. 역시 IT 세상에서는 초도 물량을 대량 구입하는 게 아니..


또 다른 기억나는 사건은 국내 텔레콤 업체의 퍼블릭 클라우드를 만들때다. 당시 SugarSync 라는 도구로 파일 관련 클라우드 서비스를 제공하고 있었는데, 이를 오픈스택의 스위프트로 바꾸는 작업을 하고 있었다. 홀로 지방의 데이터센터에 내려가 추운 환경에서 자동화 코드를 만들고 돌리고 하는 일을 반복적으로 하고 있었다. 지금이야 아마존 S3가 어떻게 만들어져 있는지 관심이 많지 않지만, 2011년 즈음만 해도 클라우드를 만드는 방법에 대해 사람들이 많은 관심을 가지고 있었을때다. 

구조적으로는 단순하게 특정 도메인에 연결된 다수의 프락시 서버가 있고, 이 프락시 서버에서는 주어진 키를 기반으로 업로드된 파일 (또는 오브젝트)을 내부의 단순 jbod 스토리지를 물고 있는 스토리지 서버에 분산 저장하는 형태로 구동 되었다. 다른 것이야 뭐 오픈스택의 패키지와 문서를 자세히 살펴 보면 알 수 있는 것이지만, 이 때 발생한 문제중의 하나는 바로 네트워크 이슈였다. 지금 생각해 보면 구조적으로 BGP v4 가 일부 가능한 L3 스위치가 기본으로 매달려 있었으므로 스토리지 서버와 프락시 서버간 iBGP 와 ECMP 를 사용한 구성을 사용하면 좋았겠지만, 뭐 기본적으로는 단순 chef 에 dhcp 를 묶어 하드웨어를 준비하는 그런 구성이었다. 당연하지만 상용 리눅스를 사용하는 것이 아니었으므로, OS 및 드라이버에 지원을 받기 보다는 가장 무난한 구성을 선택하는 쪽으로 방향을 잡고 진행중이었다. 

문제는 10Gbit Intel NIC 에서 발생했는데, 고가용성을 위해 bonding 과 함께 묶어서 사용하게 되면 간헐적으로 네트워크가 중단되는 현상이었던것 같다. (정확한 문제는 가물가물) 이때는 네트워크에 발생하는 문제가 명확했기 때문에 특정 버전의 커널과 드라이버를 바꾸는 형태로 진단을 수행했고, 결과 문제가 되지 않는 구성을 발견했다. 이때도 정말 다양한 방법으로 문제를 추적했었는데, ethtool 을 사용한 각종 파라메터 변경, 인텔 드라이버의 최신, 구버전 확인, 하드웨어의 직접 교체, 스위치에 딸린 GBIC 교체, 케이블 교체 등을 포함한다. 문제의 진단과 확인에는 서버와 연결된 네트워크 장치의 로그까지 취합하여 함께 확인했고, 결국엔 원인이 드라이버에 있음을 찾아 전체 반영했다. 이것도 일주일 정도 소요 되었던 것 같다. 

https://regmedia.co.uk/2011/03/28/arista_7050_10ge_switch.jpg

사실은 지난 15년 넘는 시간 동안 이쪽 일을 하면서 겪은 장애와 말도 안되는 해결 방법을 목격한 것은 한두번이 아니다. 2000년대 중반에 어떤 회사에서는 닷넷의 소프트웨어적으로 발생한 문제에 윈도우 서버의 모든 시스템 파일을 엎어치워 버리는 무식한 방법을 목격한 적도 있다. 당연히 문제는 해결되지 않았다. 그리고 이런 문제는 매우 다양한 곳에서 발생한다. 캐시의 효과, 디스크의 효과, 프로세서의 효과, 그리고 네트웍과 이들 위에서 돌아가는 각종 소프트웨어들, 데이터베이스 이런 것들은 언제나 문제를 발생시킨다. 이 블로그에서도 psgql의 장애 추적과 윈도우 시스템 메모리 관련 내용이 있다. 밤잠을 설쳐가면서 찾았던 문제들, 하지만 이제는 세월이 흘러 그 문제 해결의 방법이 그대로 적용되지 않을 수 있는 것들. 

진단이란, 여러가지 충분한 지표를 가지고 어떤 문제나 현상을 정상적으로 만들 수 있는 해결 방법을 내어 놓을 수 있어야 한다. 그리고 이것은 데브옵스의 시대라고 해서, 클라우드의 시대라고 해서 서비스 공급자가 다 해결해 주는 그런 양상의 문제가 아니다. 소프트웨어 내부적으로, 또 예기치 못한 새로운 종류의 하드웨어 문제로 인한 문제들. 거의 대부분의 경우는 일반적으로 알려진 문제 해결 방법 (재부팅이라던가) 이 적용되는 경우도 있지만, 그렇지 않은 경우가 더 많은 것 같다. 이는 매우 중요한 서비스 경험이고, 매우 많은 사전 지식과 경험을 요구한다. 하지만 대부분의 현장에서 문제의 해결은 지표와 데이터에 기반하기 보다는 각 개인의 경험있는 "감"에 의지하곤 한다. 이것은 그 해결이 효과를 보았다고 해도 옳은 방향은 아니라고 생각한다. 해결 방법에는 그에 걸맞는 이유가 있어야 한다. 그 이유를 얻기위한 각종 지표가 충분하지 않다면 실험을 통해서라도 그 지표를 얻는 노력이 필요하다. 그리고 조금 더, 아니 아주 많이 복잡해질 근 미래의 소프트웨어 서비스에서는 이런 일을 전문으로 하는 팀이 있어야 하지 않을까 하는 생각을 해 본다. 

DiagnOPs 라는 조직. 개발, 프로세스, 운영등 각각의 단계에서 발생하는 문제에 대해 실험과 테스트를 통해 적절한 해결 방법을 제안할 수 있는 그룹, 팀이 필요하다는 생각이다. 그리고 이 팀의 전문성은 전체 서비스에 문제가 발생하는 동안, 그 중 일부 또는 테스트 환경에서 문제를 재현하고 합당한 해결 방법을 내어놓고, 또 적용할 수 있는 실행력이 있어야 하는 팀이다. 나중에 누가 물어보면 내가 처음 만든 말이라고 해야지. 

그래서 결국 하고 싶은 말은, 이 바닥에도 Dr. House와 그의 팀이 필요하다는 말. 왜냐 하면, 거의 모든 서비스 담당자와 운영자와 개발자는 문제 발생시 거짓말을 하니까. - "우린 아무 문제가 없어요" 


생각나는대로 쓰다 보니 뭐 그냥 그렇지만, 어쨌든 이런 역할이 필요하다는 거. 진단, 분석, 올바른 해결 방법. 그리고 적용 권한. 이것들은 서비스에 대한 사전 및 사후 테스트를 통해 겹겹이 쌓인 해당 서비스에 대한 전문성이 뒷받침 될때 가능하지 않을까 생각해 본다. 

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