스프링부트 MSA 구성 중 gateway 서버에서 서비스 중 일부 서비스의 로드밸런싱을 커스텀하려고 한다.
기본 로드밸런싱은 RoundRobin 방식으로 분배하게 되어 있고,
eureka에 등록된 서비스는 user서비스 하나, item서비스 둘이 있다.
이 중 두개의 item서비스를 7:3으로 분배하려고 한다.
무수히 많은 방법이 있겠으나 여기서는 3가지 방법을 안내한다.
3가지 중 가장 마음에 드는 것으로 선택하자.
AbstractGatewayFilterFactory는 gateway를 통해서 유저의 요청이 들어 올 경우 요청 정보를 가공하기 위해 사용하는 클래스이다.
유저의 요청(exchange)에는 item 서비스들의 정보(Route)가 포함이 되는데 이 부분을 내가 원하는 특정 item 서비스 정보로 덮어씌워버리는 방식이다.
AbstractGatewayFilterFactory를 상속하고 apply 부분을 커스텀하여 제작한다.
exchange에 로드밸런싱할 인스턴스 정보가 Route 객체에 저장되어 있다.
이 정보를 내가 원하는 인스턴스로 변경하면 된다.
DiscoveryClient에 라우팅할 인스턴스 정보가 포함되어 있는데,
그 안에서 item서비스 인스턴스들을 찾고 7:3으로 하나를 선택하여
새로운 Route객체에 담고 exchange에 덮어씌운다.
@Component
public class CustomFilter extends AbstractGatewayFilterFactory<Object> {
@Autowired
private DiscoveryClient discoveryClient;
private final Random random = new Random();
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
// 기존 로드밸런싱 된 라우팅 정보
Route originalRoute = (Route) exchange.getAttributes().get(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
// item 서비스에 대한 라우팅 정보가 아니면 기존 라우팅 정보를 그대로 사용
if (originalRoute == null || !"item".equals(originalRoute.getId())) {
return chain.filter(exchange);
}
// item 서비스에 대한 라우팅 정보가 있으면 item 서비스의 인스턴스 목록을 조회
List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances("item");
// item 서비스의 인스턴스가 2개가 아니면 기존 라우팅 정보를 그대로 사용
if (serviceInstanceList.size() != 2) {
return chain.filter(exchange);
}
// item 서비스의 인스턴스 목록을 URI 문자열로 변환하여 오름차순으로 정렬
List<String> serviceUriStringList = serviceInstanceList.stream()
.map(thisServiceInstance -> thisServiceInstance.getUri().toString())
.sorted()
.toList();
// 랜덤 객체를 사용해서 라우팅을 7 : 3 비율로 나누어 선택
String selectedServiceUriString = serviceUriStringList.get(random.nextInt(10) < 7 ? 0 : 1);
// 선택된 서비스 인스턴스로 새 라우트 객체 생성
Route route = Route.async()
.id(originalRoute.getId())
.uri(selectedServiceUriString)
.order(originalRoute.getOrder())
.asyncPredicate(originalRoute.getPredicate())
.filters(originalRoute.getFilters())
.build();
// 새 라우트 객체를 exchange 객체에 저장
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR, route);
return chain.filter(exchange);
};
}
}
application.yml 파일에서 item서비스의 필터를 추가한다.
ServiceInstanceListSupplier는 유저 요청과 관련된 서비스 인스턴스들의 정보를 로드밸런서에게 제공하는 클래스이다.
ServiceInstanceListSupplier 빌더의 withWeighted 메서드를 이용해서 가중치를 커스텀한다.
ItemLoadBalancerInfo 클래스는 Configuration 없이 두고
Bean을 설정할 때 @Scope("prototype")로 두어 Bean이 싱글톤이 아닌 서비스마다 적용이 되도록 설정한다.
ItemLoadBalancerConfig 클래스에서 Configuration와 LoadBalancerClient를 이용해서
서비스가 item일 때만 작동하도록 커스텀한다.
(커스텀을 여러 서비스에 공통 적용을 하려면 LoadBalancerClients를 이용해서 적용할 서비스를 추가하면된다)
public class ItemLoadBalancerInfo {
@Bean
@Scope("prototype")
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
DiscoveryClient discoveryClient,
Environment environment,
ConfigurableApplicationContext context
) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
List<ServiceInstance> sortedServiceInstanceList = discoveryClient.getInstances(name).stream()
.sorted(Comparator.comparing(thisServiceInstance -> thisServiceInstance.getUri().toString()))
.toList();
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withCaching()
.withWeighted(instance -> {
if (!"item".equals(name)) {
return 1;
}
if (sortedServiceInstanceList.size() != 2) {
return 1;
}
if (instance.getUri().toString().equals(sortedServiceInstanceList.get(0).getUri().toString())) {
return 7;
}
if (instance.getUri().toString().equals(sortedServiceInstanceList.get(1).getUri().toString())) {
return 3;
}
return 1;
})
.build(context);
}
}
@Configuration
@LoadBalancerClients(value = {
@LoadBalancerClient(name = "item", configuration = ItemLoadBalancerInfo.class),
// @LoadBalancerClient(name = "user", configuration = ItemLoadBalancerInfo.class)
})
public class ItemLoadBalancerConfig {}
ReactorServiceInstanceLoadBalancer는 gateway 서버에서 사용하는 로드밸런서 클래스이다.
이 클래스를 상속 및 가공하고 특정 서비스를 사용할 경우에만 기존의 로드밸런서 대신 사용하도록 설정하는 방식이다.
CustomLoadBalancer의 대부분은 RoundRobinLoadBalancer를 참고해서 만들었고
getInstanceResponse부분만 수정하여 커스텀한다.
ItemLoadBalancerInfo 클래스는 Configuration 없이 두고
ItemLoadBalancerConfig 클래스에서 Configuration와 LoadBalancerClient를 이용해서
서비스가 item일 때만 작동하도록 커스텀한다.
(커스텀을 여러 서비스에 공통 적용을 하려면 LoadBalancerClients를 이용해서 적용할 서비스를 추가하면된다)
public class CustomLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final String serviceId;
private final SingletonSupplier<ServiceInstanceListSupplier> serviceInstanceListSingletonSupplier;
private final Random random = new Random();
public CustomLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
this.serviceInstanceListSingletonSupplier = SingletonSupplier
.of(() -> serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new));
this.serviceId = serviceId;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = serviceInstanceListSingletonSupplier.obtain();
return supplier.get(request)
.next()
.map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
}
private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
List<ServiceInstance> serviceInstances) {
Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances);
if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
}
return serviceInstanceResponse;
}
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
return new EmptyResponse();
}
if (!"item".equals(serviceId)) {
return new DefaultResponse(instances.get(random.nextInt(instances.size())));
}
if (instances.size() != 2) {
return new DefaultResponse(instances.get(random.nextInt(instances.size())));
}
List<ServiceInstance> sortedItemServiceInstanceList = instances.stream()
.sorted(Comparator.comparing(thisServiceInstance -> thisServiceInstance.getUri().toString()))
.toList();
return new DefaultResponse(sortedItemServiceInstanceList.get(random.nextInt(10) < 7 ? 0 : 1));
}
}
public class ItemLoadBalancerInfo {
@Bean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new CustomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name
);
}
}
@Configuration
@LoadBalancerClients(value = {
@LoadBalancerClient(name = "item", configuration = ItemLoadBalancerInfo.class),
// @LoadBalancerClient(name = "user", configuration = ItemLoadBalancerInfo.class)
})
public class ItemLoadBalancerConfig {}