🐍 Api Gateway란?
- 사용자가 설정한 라우팅 설정에 따라 각각의 엔드포인트로 클라이언트를 대신해서 요청하고, 해당 응답을 받으면 다시 클라이언트에게 전달하는 proxy 역할을 한다.
- 시스템의 내부 구조를 숨기고, 외부의 요청에 대해 적절한 형태로 가공해서 응답을 진행 할 수 있다.
Gateway 의 장점
- 인증 및 권한 부여에 대한 단일 작업
- 서비스 검색 통합
- 응답 캐싱
- 정책, 회로 차단기 및 QoS 다시 시도
- 속도 제한
- 부하 분산
- 로깅, 추적, 상관 관계
- 헤더, 쿼리 문자열 및 청구 변환
- IP 허용 목록에 추가
🐍 Spring Cloud 에서의 MSA 통신
Spring Cloud 에서 MSA간 통신을 하기 위해 많이 사용하는 방식이 2가지가 존재한다
- RestTemplate
- 전통적인 하나의 어플리케이션에서 다른 어플리케이션을 사용하기 위해 사용되는 API 였다.
- RestTemplate 를 선언하고 서비스의 URL과 전달하고자 하는 내용 그리고 메소드의 방식을 함께 전달했다.
- Feign Client
- 특정 인터페이스를 제작하여 외부로 호출하고 싶은 서비스의 이름을 등록한다. 그러면 해당 서비스는 호출한 서비스의 내용을 자유롭게 사용할 수 있는 것이다.
🐍 Spring Cloud 에서의 Load Balance (Spring Cloud Gateway)
초창기에 Load Balance는 어디에 구축되어야 하는지에 대한 고민이 있었다. 그래서 Spring Cloud에선 Ribbon 이라는 서비스를 제공하기 시작했다.
- Ribbon : Client Side Load Balancer 로써 서버 쪽에서 관리하는 로드 밸런스가 아닌 클라이언트 쪽에서 진행하고 있다
- 하지만 최근 React 등의 화면을 구현하게 되면서 비동기 방식으로 구현이 대중화 되면서 호환 문제로 인해 잘 사용하지 않는다
- 서비스의 이름만으로 호출한다
- zuul : 서버에서 사용되는 Load Balance 역할을 해줄 수 있는 서비스이지만 Ribbon 과 동일하게 비동기 방식을 사용하지 않기 때문에 많이 줄어드는 추세이다.
위에 두개는 현재는 더이상 사용되고 있지 않다. 가장 큰 이유로는 역시 대부분의 서비스가 비동기화로 이루어지고 있는 요즘 추세에 맞지 않기 때문이라고 생각이 든다. 그래서 비동기화를 지원하는 Load Balance인 Spring Cloud Gateway 를 배워보고자 한다
● Spring Cloud Gateway
- 여기서 중요하게 알고 가야 할 내용이 있다. 바로 우리가 흔히 알던 Tomcat이 아닌 Netty를 사용한다는 것이다. 왜냐하면 Netty 서버가 비동기 방식을 사용하고 있기 때문에 사용된다
● Eureka 서버 등록과 route 설정
server:
port: 8000
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service
uri: http://localhost:8081/
predicates:
- Path=/first-service/**
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
- cloud - gateway - routes : 리스트 형태로 라우트의 객체를 등록해준다.
- id : 해당 라우터의 고유값
- url : 포워딩될 주소
- predicates : 조건
- Path: 사용자가 입력한 path정보가 first-service 로 시작될 경우이다. 즉, zuul 처럼 뒤에 **로 붙는 내용만 따로 뗴어서 가는 것이 아닌 localhost:8081/first-service/welcome 이런 방식으로 가게 되는 것이다. 때문에 서비스에 매핑 정보를 바꿔주어야 한다.
● Filter 설정
필터를 사용하는 이유
Gateway의 사용 목적을 보면 클라이언트의 요청을 일괄 처리 해서 라우팅 하는 목적이라고 보는 것이 좋다.
필터는 클라이언트 요청에 대한 로깅 작업이나 부하를 분산하기 위해 라우팅, 인증에 대한 용도로 사용한다.
그래서 서비스에 대한 처리를 gateway에서 일괄적으로 처리하는 부분이 발생하게 된다
이때 Global 과 Custom 과 나아가 로깅과 관련된 필터로 Logging필터를 사용해 볼수 있다.
1. Route 정보를 등록하고 필터로 reqeust의 헤더와 response 헤더에 정보 넣기
@Configuration
public class FilterConfig {
@Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.route()
.route(r -> r.path("/first-service/**")
.filter(f -> f.addRequestHeader("first-request", "first-request-header")
.addResponseHeader("first-response", "first-response-header"))
.uri("http://localhost:8081"))
....
.builder();
)
)
- route에 chaing을 하게 되면서 람다식을 사용해서 path를 확인하고 해당 라우터에 필터를 적용해서 uri에 전달하는 내용이다. 그렇게 서비스 내부에서 자바 코드(log)를 이용해서 테스트를 진행해볼수 있다.
- 또한 자바 코드로 작성된 필터 설정은 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
- AddResponseHeader=first-response, first-response-header2
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=second-request, second-request-header2
- AddResponseHeader=second-response, second-response-header2
2. Custom Filter 적용
@Component
@Slf4j
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {
public CustomFilter() {
super(Config.class);
}
@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(() -> {
log.info("Custom POST filter: response code -> {}", response.getStatusCode());
}));
};
}
public static class Config {
// Put the configuration properties
}
}
- apply() 함수를 오버라이딩 하고 CustomFilter에서 어떤 작동을 할 것인지 적어주어야 한다.
- chain 형태로 작동
- 가장 큰 예로 preFilter에서 토큰을 검증하는 로직을 작성 할 수 있다.
- 첫번째 매개변수인 exchange를 통해, ServerHttpRequest, ServerHttpResponse를 가져온다
- Netty라는 비동기방식은 Servlet 이 아닌 Server 로 받는다
- Mono는 비동기 방식의 서버를 지원할때 단일값을 전달할 때 사용하게 된다
- 커스텀 필터는 yml 파일에서 등록해줄때 CustomFilter의 이름을 등록해주는 것으로도 작동되는 것을 확인 할 수있다.
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service
uri: http://localhost:8081/
predicates:
- Path=/first-service/**
filters:
- CustomFilter
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- CustomFilter
- 역시 서비스 로직에 들어오는 것을 확인하기 위해 check의 경로를 가진 컨트롤러를 만들어주면 확인할 수 있다
@RestController
@RequestMapping("first-service")
@Slf4j
public class FirstServiceController {
@GetMapping("/check")
public String check(HttpServletRequest request) {
log.info("Server port={}", request.getServerPort());
log.info("spring.cloud.client.hostname={}", env.getProperty("spring.cloud.client.hostname"));
log.info("spring.cloud.client.ip-address={}", env.getProperty("spring.cloud.client.ip-address"));
return String.format("Hi, there. This is a message from First Service on PORT %s"
, env.getProperty("local.server.port"));
}
}
- 그에 대한 결과로 요청을 진행하면 위에 있는 사진 처럼 gateway에서의 로그와 first-service 에서의 로그까지 확인하면서 정상적으로 들어오는 것을 확인 할 수 있다.
3. Global Filter
Custom과 만드는 방법은 매우 흡사하지만 각가의 서비스에 등록하는 것과 달리 어떤 라우트 정보가 실행되더라도 무조건 실행된다는 차이점이 있다
@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {
public GlobalFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
// Custom Pre Filter
return ((exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Global Filter baseMassage : {}", config.getBaseMessage());
if (config.isPreLogger()) {
log.info("Global Filter Start : request id -> {}", request.getId());
}
// Custom Post filter
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPreLogger()) {
log.info("Global Filter end : response code -> {}", response.getStatusCode());
}
}));
});
}
@Data
public static class Config {
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
- 여기서 사용되는 Config 의 요소는 yml 파일에서 처리되는 것을 볼 수 있다.
spring:
application:
name: apigateway-service
cloud:
gateway:
default-filters:
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
- 각각의 변수 초기화를 설정에서 하는 것을 확인 할 수 있다. pre 와 post 는 boolean 타입으로 키고 끄는 것을 확인 할 수 있다
- Global 필터는 모든 필터 중 가장 먼저 시작되고 가장 마지막에 종료된다
※현재 프로젝트 내부에 설정 파일이 설정되어 있다. 이것은 나중에 값이 바뀌면 계속 해서 빌드, 배포, 패키징해야 하는 과정을 진행해야 하기 때문에 외부로 분리를 해주는 것이 좋다.
4. Logging Filter(Custom)
@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 Filter Start: request uri -> {}", request.getURI());
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPostLogger()) {
log.info("Logging Filter End: response code -> {}", response.getStatusCode());
}
}));
}, Ordered.LOWEST_PRECEDENCE);
return filter;
}
- Global Filter를 제작하는 것과 굉장히 흡사하지만 new OrderedGatewayFilter하는 구현체를 사용한다는 것을 볼 수 있다.
- new OrderedGatewayFilter() : 해당 구현체는 생성자로 GatewayFilter와 순서에 해당하는 order를 받아 작동한다
- 해당 구현체는 GatewayFilter를 implements 하고 있기때문에 filter를 정의하는 과정도 존재한다
- Spring의 WebFlux를 사용하게 되면서 ServerHttpRequest와 ServerHttpResponse를 사용하게 된다. 이것을 사용하게 해주는것이 exchange객체이다.
- 코드의 마지막 부분을 보면 Ordered.LOWEST_PRECEDENCE를 볼수 있는데 이는 로깅 필터의 작동 타이밍을 잡아주는 것이다.
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- name: CustomFilter
- name: LoggingFilter
args:
baseMessage: Hi, there
preLogger: true
postLogger: true
- second-service 에 등록을 해준다면 밑의 사진 처럼 작동하는 것을 볼 수 있다.
5. 필터의 흐름
Gateway Handler에서 어떤 요청 정보가 들어오는지 판단하고 Global, Custom, Logging을 순서대로 지나가게 된다.
종료가 될떄는 역순으로 종료되는 것을 확인 할 수 있다
Proxied Service 는 우리가 구현한 서비스를 의미한다.
● Gateway를 eureka와 연동해보기
현재까지 제작한 gateway는 eureka를 사용하지 않고 그냥 API Gateway에서 요청을 받아 진행하는 것을 볼 수 가 있다. 하지만 우리는 Eureka를 사용하여 해당 요청을 분석해서 마이크로서비스의 위치정보를 전달 받아 정보를 통해 포워딩을 이뤄보자
- id: first-service
uri: lb://MY-FIRST-SERVICE
predicates:
- Path=/first-service/**
filters:
- CustomFilter
- id: second-service
uri: lb://MY-SECOND-SERVICE
predicates:
- Path=/second-service/**
filters:
- name: CustomFilter
- uri 를 보면 직접적인 uri가 아닌 lb뒤에 Eureka에 등록된 서비스의 이름을 통해 전달하게끔 만드는 것을 확인 할 수 있다
- 그리고 first-service 와 second-service 의 eureka 서비스를 true로 변경해준다
● 정리
- 마이크로 서비스는 확장성을 위해 랜덤포트를 부여하는 것이 좋다
- 그로인해 api gateway는 포트번호를 늘 변경해주어야 하는 번거로움으로 인해 eureka를 사용해 위치를 파악하는 것이 편하다
- gateway에서는 lb를 사용해서 로드밸런싱을 사용하기 떄문에 다양한 포트번호를 분산하여 적절하게 사용 할 수 있다
- spring cloud gateway는 zuul(요청 전달), ribbon(로드 밸런싱)의 기능을 모두 다하는 프로젝트이기 때문에 이 모든게 가능하다
Eureka 서버 딜레이
특정 서비스가 종료된다 하더라도 DiscoveryServer와 Client는 해당 서비스의 존재를 짧은 시간 동안 기억한다
서비스가 종료된 것은 30초 정도 지난 후에 인지를 하고 다른 서비스에 알리게 되면서 해당 포트의 요청에 오류가 발생한다.
이는 Eureka에서 해당 시간을 짧게 줄이면서 해결이 가능하지만 존재 유무를 판단하는 과정에 좀 더 많은 트래픽이 발생한다. 하지만 서비스 운영에 영향을 주는 정도로 심하진 않다.
'MSA > MSA 강좌 - 이도원 강사님' 카테고리의 다른 글
👨👧👦6. Spring Cloud Config (0) | 2024.04.01 |
---|---|
👨👧👦5. User Microservice 와 API Gateway - Security와 Filter 적용 (0) | 2024.03.30 |
👨👧👦 4. User & Catalogs & Orders Microservice (0) | 2024.03.29 |
👨👧👦 3. UserMicroservice ( 사용자 서비스 제작 ) - 1 (0) | 2024.03.28 |
👨👧👦 1. ServiceDiscovery 등록 ( feat. Spring Cloud Netflix Eureka ) (1) | 2024.03.26 |