이번 chapter는 마이크로서비스 아키텍처에 적용할 몇 가지 중요한 패턴 배움
1. 서비스 디스커버리 2. 로드 밸런싱 3. API Gateway 4. 서킷 브레이커
(이를 구현할 도구와 스프링 부트에서 사실상 표준인 spring cloud netflix)
현재 따로 서비스 분리한 ui 서버와 브라우저를 포함한 시스템을 논리적 관점에서 보면
마이크로서비스 아키텍처로 한 단계씩 성장하고있음.
시스템 독립적 수정, 유연 확장 장점이 있다. 하지만 문제가 있음
해결을 위해서는 몇 가지 패턴 필요
마이크로서비스 도구나 프레임워크
이런 도구들은 언제 필요하고 전부 필요할까 ?
게임화 마이크로서비스는 데이터 조회 위해 곱셈 마이크로서비스의 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)
서비스 디스커버리와 로드 밸런싱으로 분산 시스템 만들었음
아직 미해결 문제들은
위 내용을 그림으로 하면
서비스 디스커버리와 로드밸런싱을 api 게이트웨이에 포함시켜 활용하는 방법
서비스 디스커버리와 로드밸런싱으로 서비스가 서로 찾을 수 있게 설계
웹 클라이언트 --> 모든 request 게이트웨이로 전송 (8000), (마이크로서비스에 직접 연결X)
주울과 게이트웨이 마이크로서비스는 유레카 등록된 라우팅 테이블에 포함 (더이상 물리적 주소가 아님)
==> 주울, 유레카, 리본이 완벽히 통합되고, api 게이트웨이, 서비스 디스커버리, 로드 밸런싱 기반의 솔류션이 된다.
그러므로
'주울' request 받고 -> URL 분석 -> routing table에서 패턴 찾음 -> '리본'이 로드밸런싱 전략(RR 등) 으로 인스턴스 고름 -> '주울'이 원래 request 일치하는 마이크로서비스인스턴스로 redirect
이번 실습 코드는
https://github.com/wikibook/springboot-microservices/tree/master/microservices-v7 에 있음
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'
여전히 같은 에러
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 : 각종 측정 정보, 매핑, 상태 정보, 로거 등 모니터링에 유용한 몇 가지 엔드포이트를 자동으로 만든다.
혀실에서느 서비스에 연결 못하거나 response 시간을 넘기는 등의 에러가 있음. == 분산 시스템의 일부분이 response 못한 것이므로 전체 시스템이 장애가 나서는 안된다 ===> '서킷 브레이커 패턴' : 일부분이 응답하지 않아 시스템 전체가 장애가 나는 시나리오에 대한 해결책
스프링 클라우드 넷플릭스는 서킷 브레이커 패턴의 구현체인 하이스트릭스 를 포함하고 있음
ZuulFallbackProvider 인터페이스를 통해서 주울과 하이스트릭스 연결
springboot context 에서 이 인터페이스 구현한 bean 주입하면
-> api gateway에서 서비스를 이용 못할 때 hystrix fallback(사전 정의된 HTTP response) 를 제공 가능
zuul 이 request를 redirect 못하면
-> 특정 서비스의 fallback있는지 체크 (== ZuulFallbackProvider의 getRoute() )
-> fallback있으면 기본 response 구성해서 return (fallbackResponse() 이용)
게임화 서비스가 곱셈 서비스의 rest api 호출해 인수 중 하나가 행운의 숫자인지 확인하는 부분 == > 서킷 브레이커가 잘 맞는 부분
(== 이 시점에 서비스 접근 못하면 비즈니스 프로세스는 실패. 서비스 중지 시 사용자가 배지 못받으므로 fallback response 구현 )
@FeignClients 생성하고 request 와 method 매핑해서 REST 서비스가 우리 코드의 일부인 것 처럼 소비
--> 코드에서 @RestTemplates 로 요청을 직접 처리한느 것을 피하고! 외부 인터페이스를 마치 우리 코드 일부인 것처럼 처리할 수 있는게 최대 장점
페인은 유레카, 리본, 하이스트릭스와 잘 어울린다.
페인 클라이언트는 유레카와 리본을 이용해 서비스 찾고 로드밸런싱 수행 + 인터페이스 레벨에서 annotation이용해 hystrix fallback 포함된 class 지정한다.
의문 : 마이크로서비스 아키텍처 구축마다 이런 작업이 필요한가? 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장도 파이팅..!