마이크로서비스의 개발, 배포, 운영에 필요한 아키텍처를 쉽게 구성할 수 있도록 지원하는
Spring Boot기반의 프레임워크로 "MSA구성을 지원하는 Springboot기반 Framework" 다
마이크로서비스 아키텍처(MSA)로 구성되어 있는 서비스들은 각자 다른 IP와 Port를 가지고 있으며 동적으로 변경될 가능성이 많기 때문에 이러한 정보에 대해서 저장하고 관리해주는 것이 Service Discovery의 역활이다.
클라이언트 사이드 디스커버리 패턴(Client-Side Discovery pattern)
: 서비스 클라이언트가 Service register에서 서비스의 위치를 찾아서 호출하는 방식
EX ) Netflix OSS ( Eureka )
서버 사이드 디스커버리 패턴(Server-Side Discovery pattern)
: 로드밸런서를 호출하면 Service Register로 부터 등록된 서비스의 위치를 전달하는 방식
EX ) AWS Elastic Load Balancer(ELB), Kubernetes
두 가지 중 Eureka에 대해 적어본다.
AWS와 같은 Cloud 시스템에서 서비스의 로드 밸런싱과 실패처리 등을 유연하게 가져가 위해 각 서비스들의 IP / Port / InstanceId를 가지고 있는 REST 기반의 미들웨어 서버
Netflix는 2007년 심각한 DB손상으로 3일간 서비스 장애를 겪은 후, 신뢰성 높고 수평확장이 가능한 Cloud System으로 이전을 해야 함을 느꼈고 이 때 쌓은 경험치를 바탕으로 MSA 기술을 Spring에게 오픈소스로 풀게 되었다.
Eureka-Server와 Eureka-Client는 REST 통신하여 상호작용을 한다.
Spring boot에서 dependencies에 Eureka server
추가하고 프로젝트 생성
@EnableEurekaServer
application.properties
server.port=8761
spring.application.name=discovery-service
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
Spring boot에서 dependencies에 Eureka Discovery Clinet
추가하고 프로젝트 생성
@EnableEurekaClient
application.yml
server.port=0
eureka.client.fetch-registry=true
eureka.client.register-with-eureka=true
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
spring.application.name=my-service1
eureka.instance.instance-id=${spring.application.name}:${spring.application.instance_id:${random.value}}
필요한 이유
API Gateway가 필요한 이유는 안전한 API유통과 Client 요청별로 유연하게 대처하기 위해서
1. Frontend 개발자 에게 통일된 요청 정보가 필요함. Eureka는 단지 등록된 서비스들을 관리하는 역할만 함.
2. Load Balance와 유저 인증등 통일된 로직을 한곳에서 할 수 있음.
3. Nexflix Zuul이 있으나 2.4부터 nexflix 서비스들은 유지만 하는상태
4. Spring 측에서 Zuul을 대체한 Spring cloud Gateway를 만듬. (netty vs tomcat)
Zuul은 Web/WAS로 Tomcat을 사용하고, SCG는 Netty를 사용
Spring Cloud Gateway
dependencies
Eureka Discovery Clinet
, Gateway
, Lombok
, Spring Web
@EnableEurekaClient
application.yml
server:
port: 8000
eureka:
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}
spring:
application:
name: gateway-service
cloud:
gateway:
routes:
- id: my-serv1
uri: lb://MY-SERVICE1
predicates:
- Path=/serv1/**
- id: my-serv2
uri: lb://MY-SERVICE2
predicates:
- Path=/serv2/**
cloud:
gateway:
routes:
- id: my-serv1
uri: lb://MY-SERVICE1
predicates:
- Path=/serv1/**
filters:
- RewritePath=/serv1/?(?<segment>.*), /$\{segment}
@RestController
public class MyController {
@GetMapping("/test")
public String test() {
return "test";
}
}
Logging Filter 와 Global Filter 적용
Logging Filter
@Component
@Slf4j
public class LoggingFilter extends AbstractGatewayFilterFactory<LoggingFilter.Config> {
public LoggingFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
GatewayFilter filter = new OrderedGatewayFilter((exchange,chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Logging Filter baseMessage: {} ", config.getBaseMessage());
if(config.isPreLogger()) {
log.info("Logging PRE Filter Start: request id -> {}", request.getId());
}
//Custom Post Filter
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if(config.isPostLogger()) {
log.info("Logging POST Filter End: response code -> {} ", response.getStatusCode());
}
}));
}, Ordered.LOWEST_PRECEDENCE); // 우선순위 지정할 수 있다
//HIGHEST_PRECEDENCE
return filter;
}
@Data
public static class Config {
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
Global Filter
@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {
public GlobalFilter() {
super(Config.class);
}
@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());
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPostLogger()){
log.info("Global Filter End : response code -> {}", response.getStatusCode());
}
log.info("Custom Post");
}));
});
}
@Getter @Setter
public static class Config {
String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
application.yml
server:
port: 8000
eureka:
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}
spring:
application:
name: gateway-service
cloud:
gateway:
routes:
- id: my-serv1
uri: lb://MY-SERVICE1
predicates:
- Path=/serv1/**
filters:
- RewritePath=/serv1/?(?<segment>.*), /$\{segment}
- name: LoggingFilter
args:
baseMessage: Hi, serv1.
preLogger: true
postLogger: true
- id: my-serv2
uri: lb://MY-SERVICE2
predicates:
- Path=/serv2/**
filters:
- RewritePath=/serv2/?(?<segment>.*), /$\{segment}
- name: LoggingFilter
args:
baseMessage: Hi, serv2.
preLogger: true
postLogger: true
default-filters:
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true