MSA
- 어플리케이션을 목적에 따라 여러 개의 독립적인 서비스로 분할하여 개발하고 배포하는 방식
MSA의 특징
- 독립된 각 서비스 마다 고유의 ip와 port를 가진다.
- 각각의 서비스가 분산되어 있기 때문에, 배포와 테스트가 어렵다
MSA를 왜 쓸까?
느슨하게 결합된 서비스들의 모임으로 프로젝트를 구성함으로써, 전체 시스템 구조를 논리적으로 구분해 이해하고,
고가용성(부분 장애가 전체 장애로의 확장을 막음)을 높이며, 분산 처리를 가능하도록 하기 위함
MSA를 편리하게 하기 위한 아키텍처 컴포넌트
스프링 클라우드(Spring Cloud)
분산 시스템의 몇가지 공통된 패턴들을 빠르게 빌드할 수 있도록 다양한 툴을 제공해 준다.
예를 들어, Configuration management(설정 관리), Service Discovery(서비스 검색), circuit breakers(회로 차단), intelligent routing(라우팅), micro-proxy, control bus, short lived microservices and contract testing.
- 공식 도큐먼트 : https://spring.io/projects/spring-cloud
스프링 클라우드 피처
- Distributed/versioned configuration
- Service registration and discovery
- Routing
- Service-to-service calls
- Load balancing
- Circuit Breakers
- Distributed messaging
- Short lived microservices (tasks)
- Consumer-driven and producer-driven contract testing
Service Discory란? 왜 쓸까?
클라언트에게 필요한 서비스가 어디에 있는지 빠르게 찾으려고.
서비스를 등록/해제/조회할 수 있는 API를 제공하며, 고가용성이 보장되어야 한다.
Service Registry를 통해 서비스를 관리하며, 지속적인 서비스 상태 모니터링이 가능하다.
- Netflix Eureka, Apache Zookepper 등이 있음
Eureka
Spring Cloud Netflix는 Netflix OSS와 Spring 어플리케이션을 통합해주는 것으로, autoconfiguration 및 Spring 환경과 다른 Spring 프로그래밍 모델과의 binding을 제공합니다. 몇 가지 간단한 Annotation을 사용하면 애플리케이션 내부의 공통 패턴을 신속하게 활성화 및 구성하고, 검증된 Netflix 구성 요소를 사용하여 대규모 분산 시스템을 구축할 수 있습니다. 제공되는 패턴에는 Service Discovery(Eureka)가 포함됩니다. (출처:spring cloud netflix 공식 document) |
-> 다시 말해, Netflix OSS를 Spring에서 사용할 수 있도록 해주는 것으로,
-> Netflix는 2007년 DB 문제로 서비스가 중단된 이래 신뢰성이 높고 scale-out한 시스템을 구축하기 위해 Netflix OSS를 만들었다.
-> Netflix OSS는 1) 고가용성(하나가 망해도 다른 하나가 산다), 2) scale-out, 3) easy deploy의 특징을 갖는다.
-> Eureka 또한 Netlfix OSS의 하나로, Service Discovery를 담당하는 모듈을 말한다.
* 자세한 내용은 다음 링크에 들어가 있다. https://p-bear.tistory.com/77
Eurkea의 피처
- Service Discovery: Eureka instances can be registered and clients can discover the instances using Spring-managed beans
- Service Discovery: an embedded Eureka server can be created with declarative Java configuration
Eureka Service Discovery의 용어
- Discovery : 검색
- Registry : 등록 목록
- Eureka Client : 각 서비스에 해당되는 모듈
ㄴ 자기 자신을 Eureka Server에 등록한다.
ㄴ Eureka Server를 통해 다른 Client의 정보를 검색할 수 있다.
- Eureka Server : Eureka Client를 관리하는 서버
ㄴ Eureka Client 정보를 Registry에 등록
ㄴ Heartbeat를 통해 Client가 수행 중임을 확인
Eurkea Service Discovery 흐름의 예시
1. Eureka Client 서비스가 시작될 때, Eureka Server에 자신의 정보를 등록
2. Eureka Client는 Eureka Server로부터 다른 Client의 연결 정보가 등록되어 있는 Registry를 받고 Local에 저장한다.
3. N초마다 Eureka Server로부터 변경 사항을 갱신받는다.
4. N초마다 ping를 통해 자신이 동작하고 있다는 신호를 보낸다. 신호를 보내지 못하면 Eureka Server가 보내지 못한 Client를 Registry에서 제외시킨다
Eureka Service Discovery 사용하기
- Eureka Server
# eureka server port
server:
port: 8761
# MSA module id (eureka)
spring:
application:
name: mindchat
# Eureka server does not need to be included
eureka:
client:
register-with-eureka: false # 레지스트리에 자신을 등록할 것인가 (default : true)
fetch-registry: false # 레지스트리에 있는 정보를 가져올 것인가
service-url: #server의 위치 지정
defaultZone: http://localhost:8761/eureka
- Eureka Client
ㄴ eureka.client.register-with-eureka : 레지스트리에 자신을 등록할 것인가 여부
ㄴ eureka.client.fetch-registry : 레지스트리에 있는 정보를 검색할 것인가 여부
server:
port: 0 #랜덤 포트 사용
spring:
application:
name: user-service
eureka:
instance:
instance-id: ${spring.cloud.client.hostname}:${spring.application.instance_id:${random.value}}
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
port를 0으로 설정하면 랜덤 포트가 지정된다. 이렇게 되면 동일 서비스를 여러 개의 랜덤 포트로 빌드 후 실행이 가능하다.
(ex. user-service 앱이 port:53000과 port:533333에서 실행되고 있다)
그러나 유레카는 각각의 서비스를 프로젝트 내에서 지정한 ip/port를 통해 구분하기 때문에, http://localhost:8761에 접속하여 실행중인 application을 보면 1개의 app만 실행 중인 것으로 나타난다.
(Eureka에서 0번 포트의 앱이 두개 있더라고 하나로 치부)
이때, eureka.instance.instance-id를 사용하면 eureka에서 app을 구분하는 값을 지정된 값으로 변경할 수 있다.
Spring Gateway
Spring MVC는 동기적이며 논블로킹 처리를 하는 Servlet 기반의 프레임워크이다.
server:
tomcat:
threads:
max: 200 # 생성할 수 있는 thread의 총 개수
min-spare: 10 # 항상 활성화 되어있는(idle) thread의 개수
accept-count: 100 # 작업 큐의 사이즈
다수의 요청이 존재할 때 Spring MVC는 Queue안에 요청 사항을 받아두고, 미리 지정해둔 Thread수에 따라, Thread Pool의 Thread를 사용하여 멀티태스킹을 하게 된다. 이 때, 각각의 Thread는 동기적인 처리를 하게 되는데, 하나의 요청을 받게 되면 응답까지 한 번에 받을 수 있도록 처리하며, 하나의 함수에 대해 return을 받을 때까지 블로킹 처리를 한다.
- 동기적이다 : 호출과 응답이 동시에 이루어진다. (ex. 공을 던지고 받는다) - 비동기적이다 : 호출과 응답이 동시에 이루어지지 않는다. (ex. 기차에 공을 올려 도착할 때까지 기다린다) - 블로킹 : 함수를 콜했을 때 응답을 받기 위해 멈춰있는 상태 - 논블로킹 : 함수를 콜했을 때 응답을 받기 위해 멈춰있기 않고 다음 줄을 실행하는 상태 |
이 방식의 경우, 정해진 time-slice에 따라 Queue로 이동하며 멀티 태스킹을 하게 되는데, 다수의 Client의 요청에 대하여 DB데이터 응답 시간이 지연될 경우, 해당 시간 동안 손가락을 빨고 기다리는 상황이 발생할 수 있게 된다.
이를 해결하기 위한 방법이 Spring Webflux이다. Spring Webflux는 Node.js와 같이 Event Loop가 돈다. JavaScript는 비동기로 동작을 하는데, 함수를 Stack에 두어 LIFO 방식으로 실행하는 것은 Java와 동일하지만, 함수를 Callback Queue에 또한 넣어둠으로써 Stack이 다 비워졌을 때 FIFO으로 Queue의 함수를 실행하도록 Event Loop를 돈다.
Webflux 또한 이러한 Event Loop를 사용한다. 10초의 지연 시간이 있는 요청이 있다면 해당 요청에 대한 핸들러(=함수)에게 처리를 위임하고, 처리가 완료되는 즉시 Callback Queue에 Callback을 추가한뒤 Stack을 통해 실행하게 된다. 10초의 지연 시간 동안 처리할 다른 함수가 있다면 해당 함수가 Stack에 두어 실행된다. 이러한 처리 방식을 비동기/논블로킹 처리 방식이라 하며, 이벤트 발생에 대해 반응을 한다하여, Reactive Programming이라고 한다.
Spring MVC | Webflux |
동기 | 비동기 |
블로킹 | 논블로킹 |
명령형 프로그래밍 | 반응형 프로그래밍 |
tomcat | netty |
JDBC,JPA 네트워킹 지원 | 반응형 라이브러리(Reactor, RxJava) 지원 |
출처 : https://pearlluck.tistory.com/726
Gateway는 왜 사용해야 하나?
기본적으로 작은 서비스들당 50~100개의 app들이 돈다고 할 때,
이 많은 app들의 endpoint(ip+host+port+path)를 일일히 관리하려면 엄청난 수작업이 필요하고,
각각의 서비스들마다 공통적으로 들어가는 인증/인가, 로깅 등의 기능을 중복으로 작업하는 것에 어려움이 생긴다.
[1] 공통 로직 처리
Gateway를 일종의 Proxy(대리자)로 두게 되면, 각각의 요청사항에 대한 공통 기능(인증/인가 및 로깅)을 편리하게 작업할 수 있다.
[2] Api Routing
라우팅은 네트워크 상에서 경로를 선택하는 프로세스를 의미한다. API Routing이란 동일한 API 요청들에 대해,
각각의 클라이언트나 서비스에 따라 다른 엔드포인트를 사용할 수 있도록 해주는 것을 말한다.
(1) 로드밸런싱
API Gateway를 사용하면 여러 개의 서비스 서버로 API를 분산시켜 주기 때문에 로드밸런서 역할을 한다.
(2) 클라이언트 및 서비스별 엔드포인트 제공
API Gateway를 사용하면 요청을 공통 API로 보내더라도 각 클라이언/서비스별로 엔드포인트를 분산할 수 있다.
(3) 메세지/헤더 기반 라우팅
메세지나 헤더 기반으로 API 라우팅을 할 수 있다. 라우팅에서 요청 API에 대한 메세지를 파싱할 때 많은 파워를 소모할 수 있기 때문에, Restful API를 제공할 경우, Header에 Routing 정보를 두어 Header정보를 파싱하도록하고, Body는 포워딩 할 경우 API Gateway의 부하를 줄여줄 수 있다.
[3] Mediation 기능
(1) 메세지 포맷 변환
클라이언트와 서버가 서로 다른 메세지 포맷을 사용할 때, 이를 적절히 변환시켜 주는 역할을 의미
(2) 프로토콜 변환
내부 API는 gRPC를 사용하고 외부 API는 Restful API를 사용함으로써 내부 API의 성능을 높이면서 범용성을 높일 수 있도록 프로토콜 변환이 가능하다.
(3) Aggregation
서로 다른 API를 묶어서 하나의 API로 제공하는 것을 의미한다. 어떤 요청에 대해 A/B/C모듈 각각의 API를 호출해야할 경우, API Gateway를 사용하면 A/B/C API를 한번에 처리가 가능하다.
Gradle에서 의존성 추가
** 주의 사항1
spring-cloud-starter-gateway-mvc : Spring Mvc 기반 / Servlet API / Blocking I/O 모델 사용 / Tomcat
spring-cloud-starter-gateway : Spring Webflux 기반 / Reative API / Non-blocking I/O 모델 사용 / Netty
** 주의 사항2
spring-cloud-starter-gateway는 Webflux 기반이기 때문에 SpringMVC기반의 라이브러리와 함께 쓸 경우,
"Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway." 메세지가 발생한다.
따라서 spring-boot-starter-web 등의 MVC 기반 라이브러리는 같이 사용하면 안된다.
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-gateway' #추가
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
server:
port: 8000
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
default-zone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service #서비스id
uri: http://localhost:8081/
predicates: # 조건
- Path=/first-service/**
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
위 상태로만 gateway를 넣게 되면 /**까지 같이 엔드포인트에 포함되기 때문에 필터를 통한 작업이 추가적으로 필요하다.
Spring Cloud Gateway에 Filter 적용하기
[1] Java 코드
@Configuration
public class FilterConfig {
@Bean
public RouteLocator gateRoutes(RouteLocatorBuilder builder){
return builder.routes()
.route(r -> r.path("/first-service/**")
.filters(f -> f.addRequestHeader("first-request", "first-request-header")
.addResponseHeader("first-response", "first-request-response"))
.uri("http://localhost:8081/")
)
.route(r -> r.path("/second-service/**")
.filters(f -> f.addRequestHeader("second-request", "second-request-header")
.addResponseHeader("second-response", "second-request-response"))
.uri("http://localhost:8082/")
)
.build();
}
}
[2] property(yml)
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service
uri: http://localhost:8081/
predicates:
- Path=/first-service/**
filters:
- AddRequestHeader=first-request, first-request-header2 # key, value
- AddResponseHeader=first-response, first-response-header2
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=second-request, second-request-header2 # key, value
- AddResponseHeader=second-response, second-response-header2
Custom Filter 사용하기
@Component
@Slf4j
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {
public CustomFilter(){
super(Config.class);
}
public static class Config{
// configuration properties
}
@Override
public GatewayFilter apply(Config config) {
// Custom Pre Filter
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Custom PRE filter : request id -> {}", request.getId());
// Custom Post Filter
return chain.filter(exchange).then(Mono.fromRunnable(() -> { // Mono : 비동기 방식에서의 단일 응답값 의미
log.info("Custom POST filter : response code -> {}", response.getStatusCode());
}));
};
}
}
GlobalFilter 사용하기
- 모든 라우터에서 전역적으로 필터를 사용할 수 있다.
- GlobalFilter를 만들고, application-yml에 default-filters로 해당 필터를 입력해주면 전역 필터로 동작한다.
@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {
public GlobalFilter(){
super(Config.class);
}
@Data
public static class Config{
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Global Filter BaseMessage : {}", config.getBaseMessage());
if (config.isPreLogger()) {
log.info("Global Filter Start : request id -> {}", request.getId());
}
// Post Filter
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPreLogger()) {
log.info("Global Filter End : response code -> {}", response.getStatusCode());
}
}));
};
}
}
spring:
application:
name: apigateway-service
cloud:
gateway:
default-filters:
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
Load Balancer
기존의 경우 uri에 직접 ip:port를 전달함으로써 gateway에서 해당 ip:port로 이동했다면
gateway에 아래와 같은 형식으로 uri에 넣어줌으로써, Eureka를 거쳐 ip:port로의 이동을 할 수 있다.
- lb://{서비스 어플리케이션 name의 upper}
routes:
- id: user-service
#uri: http://localhost:8081/
uri: lb://USER-SERVICE
predicates:
- Path=/user/**
filters:
- name: CustomFilter
- name: UserLoggingFilter
args:
baseMessage: Spring Cloud Gateway User Logger Filter
preLogger: true
postLogger: true
[ 참고/출처 ]
https://joon2974.tistory.com/26
https://kinchi22.github.io/2019/09/22/api-gateway/
https://velog.io/@tritny6516/Spring-Thread-Pool
https://velog.io/@seokkitdo/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84%EB%9E%80
https://pearlluck.tistory.com/726
https://adjh54.tistory.com/207
https://djlife.tistory.com/10#google_vignette
https://velog.io/@ksh9409255/Service-Discovery
https://velog.io/@jkijki12/Eureka%EB%9E%80
'Framework > Spring' 카테고리의 다른 글
[Spring Cloud & MSA] Spring Cloud Netflix Eureka (0) | 2024.01.10 |
---|---|
[Spring Cloud & MSA] MSA와 Spring Cloud (1) | 2024.01.09 |
[스프링] Build.Gradle & application.yml 관련 메모 (0) | 2022.10.28 |
[스프링] @AutoWired 동작 원리 및 DI injection 관련 설명 모음 (0) | 2022.10.20 |
[HTTP] @RequestParam vs @RequestBody (0) | 2022.10.19 |