[MSA스터디] 5. 마이크로서비스 도구 다루기 -2

vector13·2022년 9월 18일
0

이번 chapter는 마이크로서비스 아키텍처에 적용할 몇 가지 중요한 패턴 배움
1. 서비스 디스커버리 2. 로드 밸런싱 3. API Gateway 4. 서킷 브레이커
(이를 구현할 도구와 스프링 부트에서 사실상 표준인 spring cloud netflix)

2. 현재 아키텍처

현재 따로 서비스 분리한 ui 서버와 브라우저를 포함한 시스템을 논리적 관점에서 보면

마이크로서비스 아키텍처로 한 단계씩 성장하고있음.
시스템 독립적 수정, 유연 확장 장점이 있다. 하지만 문제가 있음

  • ui페이지가 백엔드 구조 알고있음. 마이크로서비스 일부 분리하거나 결합할 경우 ui에 영향 미침
  • ui 페이지에서 곱셈, 게임화 마이크로서비스 위치 하드코딩함. (링크 직접)

해결을 위해서는 몇 가지 패턴 필요

  • 서비스 디스커버리, 로드 밸런싱 api 게이트웨이 (or 라우팅)

마이크로서비스 도구나 프레임워크

  • 유레카, 컨설(Consul), 리본, 주울 등

이런 도구들은 언제 필요하고 전부 필요할까 ?

3. 서비스 디스커버리와 로드 밸런싱

게임화 마이크로서비스는 데이터 조회 위해 곱셈 마이크로서비스의 rest api 호출

게임화 서비스는 localhost:8080 (물리적주소, IP주소와 포트) 가리키는 프로퍼티로 곱셈 서비스 위치 알고, 곱셈 인수 조회위해 요청
-> 확장할 수 없다.

  • 서비스 디스커버리 구성요소
    서비스 레지스트리 : 모든 서비스 인스턴스와 인스턴스의 이름을 추적
    레지스터 에이전트 : 모든 서비스가 서로 찾을 수 있도록 설정 제공 에이전트
    서비스 디스커버리 클라이언트 : 서비스 레지스트리에서 별칭으로 서비스 조회

컨설, 유레카 등 스프링 지원 서비스 디스커버리 도구는 다양. 이 책에서는 유레카 사용
스프링은 스프링 클라우드 프로젝트에서 넷플릭스 도구 이용할 수 있도록 rapper 제공
-> 스프링 클라우드 넷플릭스

하드코딩 링크가 없는 상황에서 서비스 인스턴스가 서로 찾을 수 있는 메커니즘이 필요!

각 서비스 참조 뒤에는 하나이상의 인스턴스 존재가능 -> 로드 밸런싱과 서비스 디스커버리는 밀접 연관

이 그림에는 세 가지 구성요소 동작 (service registry, registry agent, registry client)

  • 서비스 레지스트리 : 새로운 마이크로서비스로 배포할 것.

게임화와 곱셈 마이크로서비스는 시작 시-> 각자 registry agent로 service registry에 연락해 스스로 등록 -> 각 인스턴스는 registry 안에서 별칭 붙음. ===> http://multiplication/ 같은 주소로 찾을 수 있음

조건 : 마이크로서비스가 각자 registry client 사용해야 해당 주소가 동작!

로드 밸런싱

저 그림엔 아직 빈틈있다 (그렇군요...!)
같은 서비스에 여러 인스턴스 있을 때 유레카 동작 문제
넷플릭스는 '리본'이라는 '유레카'와 통합된 클라이언트 사이드 로드 밸런싱을 구현

곱셈 서비스 인스턴스 2개 되면 -> 같은 별칭으로 유레카 등록됨(b/c애플리케이션 이름 동일)
가정) 새 인스턴스 localhost:8082에 위치
게임화 마이크로서비스가 클라이언트 입장으로 /multiplication/ 에 접속 시도 -> 유레카는 두 url 모두 반환, 소비자가 어떤 인스턴스 호출할지 결정 (로드 밸런서인 리본과 유레카의 registry client 함께 사용)

리본은 기본적으로 round-robin 전략 활용 --> 바꾸는 방법 나중에 설명

위 그림 클라이언트 사이드 로드 밸런싱 은 호출 하는쪽에서 로드 밸런싱이ㅏ 다른 서비스의 인스턴스 수를 알아야한다. ->> 그럴 필요 없다. 유레카와 리본이 투명하게 기능을 제공하기 때문에 코드에서 해당 기능 제공 불필요하다.
기억할건 리본은 로드 밸런싱을 '숨기지만' 여전히 클라이언트 안에 있음!

그림에서 서비스 디스커버리와 UI를 통합할 수 없는데 가능한 방법은? (UI가 곱셈 서비스 중 하나에 직접 접속 8080)

4. API 게이트웨이와 라우팅

api 게이트 웨이 패턴

서비스 디스커버리와 로드 밸런싱으로 분산 시스템 만들었음
아직 미해결 문제들은

  • 웹 클라이언트가 브라우저에서 실행중 -> 서비스 디스커버리 클라이언트 실행하거나 로드밸런싱 처리 불가 ===> 추가 컴포넌트 필요 (로드 밸런싱 유지하면서 백앤드 마이크로서비스 연결위해 )
  • 인증, api 버저닝, 요청 필터링 같은 작업들은 이 분산 시나리오에 아직 안맞음 ===> 중앙에서 집중 제어할 지점 필요 (api를 == 곱셈, 게임화 서비스의 rest endpoint)
  • REST API는 시스템 아키텍처 따라 소비자가 시스템에 의존
    cf) 소비자가 마이크로서비스 설계 알고있을 때 통계(/status/) 기능이 새롭게 추출되고 엔드포인트도 생기면 새로운 URL을 가르키도록 수정해야하는 번거러움 있음.
    ==> 내부 구조를 드러내지 않는 rest api 만들어야한다. -> 나중에 수정사항 있더라도 다른 부분 영향 주지 않고 유연하게 수정 가능 (지금은 외부 애플리케이션에 적용하려면 모든 마이크로서비스를 리팩터링해야함 😂)
    ====> 해결책은 API 게이트웨이 패턴 . '주울'은 URL 패턴을 설정하면 소비자가 내부 구조에 대한 지식이 없어도 요청을 적절한 서비스로 라우팅한다.

위 내용을 그림으로 하면

함께 동작하는 주울, 유레카, 리본

서비스 디스커버리와 로드밸런싱을 api 게이트웨이에 포함시켜 활용하는 방법
서비스 디스커버리와 로드밸런싱으로 서비스가 서로 찾을 수 있게 설계

웹 클라이언트 --> 모든 request 게이트웨이로 전송 (8000), (마이크로서비스에 직접 연결X)

주울과 게이트웨이 마이크로서비스는 유레카 등록된 라우팅 테이블에 포함 (더이상 물리적 주소가 아님)
==> 주울, 유레카, 리본이 완벽히 통합되고, api 게이트웨이, 서비스 디스커버리, 로드 밸런싱 기반의 솔류션이 된다.

그러므로
'주울' request 받고 -> URL 분석 -> routing table에서 패턴 찾음 -> '리본'이 로드밸런싱 전략(RR 등) 으로 인스턴스 고름 -> '주울'이 원래 request 일치하는 마이크로서비스인스턴스로 redirect

5. 코드 작성

이번 실습 코드는
https://github.com/wikibook/springboot-microservices/tree/master/microservices-v7 에 있음

api 게이트웨이와 주울 구현

gateway 스프링부트 application 생성해줘야함

메인 애플리케이션에
@EnableZuulProxy 을 달아주면 스프링부트 애플리케이션이 주울 게이트웨이로 동작한다! (짱..짱이다..!😏)

시스템 실행 단계

  • rabbitmq 서버 실행

  • gateway 서비스 실행
    gateway 애플리케이션 실행이 안된다.

Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'configurationPropertiesBeans' defined in class path resource

https://darkstart.tistory.com/309 블로그 글을 보니 스프링 버전이 올라가면서 hystrix(circuit breaker)에 변화가 생겼다고 한다.
build.grale에 이 내용 추가

// hystrix 
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-hystrix:1.4.6.RELEASE'

여전히 같은 에러

https://stackoverflow.com/questions/66779166/spring-cloud-netflix-hystrix-gradle-dependency-not-allowing-spring-boot-applicat 에 따라서

hystrix 관련 설정을

// hystrix
	implementation 'org.springframework.cloud:spring-cloud-netflix-hystrix:2.2.10.RELEASE'
    
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-hystrix:2.2.10.RELEASE'

로 했지만 아직 해결 못함

https://kdhyo98.tistory.com/97 블로그에 따라서
각자 스프링부트에 버전에 호환되는 스프링 클라우드 버전이 있다고 한다.
https://spring.io/projects/spring-cloud
springboot version은 id 'org.springframework.boot' version '2.7.3' 이고 이에 따라 저기 들어가보면

https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-2021.0-Release-Notes
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-zuul:3.1.4.RELEASE'

로 맞춰보겠다

최종적인 build.gralde 코드

plugins {
	id 'org.springframework.boot' version '2.7.3'
	id 'io.spring.dependency-management' version '1.0.13.RELEASE'
	id 'java'
}

group = 'microservices.book'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

ext {
	set('springCloudVersion', "2020.0.1")
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	// Eureka
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
	// Ribbon
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-ribbon'
	// Zuul
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-zuul:2.2.10.RELEASE'
	implementation 'org.springframework.cloud:spring-cloud-netflix-zuul'
	implementation 'com.netflix.zuul:zuul-core:2.3.0'

}

dependencyManagement {
	imports {
		mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
	}
}


tasks.named('test') {
	useJUnitPlatform()
}

허허 이번엔 이 에러가 나왔군

java.lang.NoClassDefFoundError: org/springframework/boot/Bootstrapper
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at org.springframework.cloud.bootstrap.encrypt.DecryptEnvironmentPostProcessor.getTextEncryptor(DecryptEnvironmentPostProcessor.java:82)
	at org.springframework.cloud.bootstrap.encrypt.DecryptEnvironmentPostProcessor.postProcessEnvironment(DecryptEnvironmentPostProcessor.java:68)
	at org.springframework.boot.env.EnvironmentPostProcessorApplicationListener.onApplicationEnvironmentPreparedEvent(EnvironmentPostProcessorApplicationListener.java:102)
	at org.springframework.boot.env.EnvironmentPostProcessorApplicationListener.onApplicationEvent(EnvironmentPostProcessorApplicationListener.java:87)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:176)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:169)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:143)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:131)
	at org.springframework.boot.context.event.EventPublishingRunListener.environmentPrepared(EventPublishingRunListener.java:85)
	at org.springframework.boot.SpringApplicationRunListeners.lambda$environmentPrepared$2(SpringApplicationRunListeners.java:66)
	at java.util.ArrayList.forEach(ArrayList.java:1259)
	at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:120)
	at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:114)
	at org.springframework.boot.SpringApplicationRunListeners.environmentPrepared(SpringApplicationRunListeners.java:65)
	at org.springframework.boot.SpringApplication.prepareEnvironment(SpringApplication.java:344)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:302)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295)
	at microservices.book.gateway.GatewayApplication.main(GatewayApplication.java:12)
Caused by: java.lang.ClassNotFoundException: org.springframework.boot.Bootstrapper
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	... 31 common frames omitted

Process finished with exit code 1

꼬르륵... 문제 해결 중

결국 선택한 얼렁뚱땅 해결방법
저자 github 처럼 (현재처럼 gradle말고) maven 으로 gateway는 진행하기로 했음 ... ^^ (흐휴... 시간이 너무 많이 쓰여서 시간 안에 못할것같다는 판단하에.. 변명변명 주저리주저리 .... )

암튼 gateway 서버 키고

  • 곱셈 서비스 실행

  • 게임화 서비스 실행

  • ui폴더에서 jetty 서비스 실행

http://localhost:9090/ui/index.html로 접속 성공 😅

Zuul 이 동작하는 지 확인하려면
http://localhost:8000/trace로 접속

서비스 레지스트리 프로젝트 생성

service-registry로 생성하고
유레카 레지스트리로 변환위해서 @EnableEurekaServer 를 추가

유레카 레지스트리는 스스로를 8761 포트로 등록한다.
-> 포트 8761로 수정 안하거나 스스로 등록기능 비활성화 시(==eureka.client.register-with-eureka=false) 유레카 스스로 찾을 수없으므로 실행 시 실패함

스프링 부트 actuator : 각종 측정 정보, 매핑, 상태 정보, 로거 등 모니터링에 유용한 몇 가지 엔드포이트를 자동으로 만든다.

6. 서킷 브레이커와 REST 클라이언트

혀실에서느 서비스에 연결 못하거나 response 시간을 넘기는 등의 에러가 있음. == 분산 시스템의 일부분이 response 못한 것이므로 전체 시스템이 장애가 나서는 안된다 ===> '서킷 브레이커 패턴' : 일부분이 응답하지 않아 시스템 전체가 장애가 나는 시나리오에 대한 해결책

서킷 브레이커 패턴

  • 상태 기반
  1. 회로 닫힘 : reqeust 가 목적지도달하고 response 수신한다.
  2. 회로 열림 : 오류 있거나 타임아웃일어나서 연결점 중단되면 회로 열림 == 시스템 다른 부분을 호출한다.

스프링 클라우드 넷플릭스는 서킷 브레이커 패턴의 구현체인 하이스트릭스 를 포함하고 있음

하이스트릭스와 주울

ZuulFallbackProvider 인터페이스를 통해서 주울과 하이스트릭스 연결

springboot context 에서 이 인터페이스 구현한 bean 주입하면
-> api gateway에서 서비스를 이용 못할 때 hystrix fallback(사전 정의된 HTTP response) 를 제공 가능

zuul 이 request를 redirect 못하면
-> 특정 서비스의 fallback있는지 체크 (== ZuulFallbackProvider의 getRoute() )
-> fallback있으면 기본 response 구성해서 return (fallbackResponse() 이용)

REST client 의 hystrix

게임화 서비스가 곱셈 서비스의 rest api 호출해 인수 중 하나가 행운의 숫자인지 확인하는 부분 == > 서킷 브레이커가 잘 맞는 부분
(== 이 시점에 서비스 접근 못하면 비즈니스 프로세스는 실패. 서비스 중지 시 사용자가 배지 못받으므로 fallback response 구현 )

페인(Feign)으로 REST 소비자 만들기

@FeignClients 생성하고 request 와 method 매핑해서 REST 서비스가 우리 코드의 일부인 것 처럼 소비
--> 코드에서 @RestTemplates 로 요청을 직접 처리한느 것을 피하고! 외부 인터페이스를 마치 우리 코드 일부인 것처럼 처리할 수 있는게 최대 장점

페인은 유레카, 리본, 하이스트릭스와 잘 어울린다.
페인 클라이언트는 유레카와 리본을 이용해 서비스 찾고 로드밸런싱 수행 + 인터페이스 레벨에서 annotation이용해 hystrix fallback 포함된 class 지정한다.

7. 마이크로서비스 패턴과 PaaS

의문 : 마이크로서비스 아키텍처 구축마다 이런 작업이 필요한가? spring보다 더 high level에서 모든걸 추상화 해주는 framework가 없을까? springboot application 개발에만 집중하고 어디엔가 넣어서 즉시 실행은 안되나?

해결 : Paas 솔류션으로 추상화 하는 것 (Platform as a Service)

서비스 디스커버리, 로드 밸런싱, 라우팅(api 게이트웨이), 서킷 브레이커 패턴 뿐 아니라 중앙 집중식 로깅과 통합 인증 등 여러 기능이 이러한 플랫폼에 있다.
예시 ) AWS,(Amazon AWS), Google App Engine, Pivotal CloudFoundry, Microsoft Azure 등

서비스 레지스트리나 게이트웨이는 플랫폼의 일부이므로 배포 필요없고, 몇 가지 라우팅 규칙과 로드 밸런싱 정책 구성하면 된다.
마이크로서비스 설계할 때 필요한 것을 알고 있어야 하는 것이 중요!
선택 사항을 군형있게 조정하고 (비용이 드니까..^^ ) 어떻게 패턴을 구현할지 결정해야함.


wow.. 5장이 이 책을 스터디 하면서 가장 에러가 많았고 시간이 오래걸리고 그리고 가장 이해가 어려운 장이었다.
이건 절대로 한번 한다고 끝낼 수 없다는 느낌을 확실히 받았고 앞으로 길이 멀고 멀지만 마이크로서비스의 장점은 확실히 받고 가는 것 같다.
남은 6장도 파이팅..!

profile
HelloWorld! 같은 실수를 반복하지 말기위해 적어두자..

0개의 댓글