最近在进行功能开发的时候遇到了一个问题,提供服务的服务器为了避免单点故障所以部署了两台服务器,由于当前的服务架构比较简单,并没有引入分布式服务架构,所以如何对这两台服务器进行负载均衡调用以及重试和熔断就需要自己来实现。本来可以引入ribbon和loadbalance,但是发现引入之后需要引入springcloud框架,所以就放弃了。本着学习ribbon的原理的态度,所以准备手写一个负载均衡,满足当前的需求。
一、Ribbon是什么?
之前写过一篇关于Ribbon的工作原理的文章,本篇文章也是基于对Ribbon的源码的理解来进行的,不了解Ribbon的工作原理的可以看这篇文章Ribbon源码分析
二、实现步骤
1.定义注解
使用过Ribbon的都知道,只要在Resttemplate上面加上@LoadBalance注解就可以实现调用的负载均衡。
@LoadBalance是一个组合注解,它可以实现@Qualifier的作用,而@Qualifier的作用是当spring容器中有多个相同类型的类时,注入的时候@Qualifier可以通过类名进行注入,例如
@LoadBalanced
@Autowired(
required = false
)
private List<RestTemplate> restTemplates = Collections.emptyList();
可以将所有添加了@LoadBalance注解的RestTemplate全部注入到restTemplates集合中。如果想对@Qualifier做一个更深入的了解可以看这篇文章:@Qualifier的高级应用
通过这个注解就可以获取到需要负载均衡调用的Resttemplate了。
三、ClientHttpRequestInterceptor
下一个关键的类就是ClientHttpRequestInterceptor,通过这个拦截器,可以拦截Resttemplate的请求,通过请求的服务名称来调用自己定义的负载均衡算法,最终设置调用的服务。
有了拦截器之后就是给Resttemplate添加拦截器了,通过方法Resttemplate..getInterceptors().add()方法可以给RestTemplate添加拦截器。但是Ribbon的源码中使用了RestTemplateCustomizer进行定制化配置,所以这里准备模仿着写。
四、实现代码
定义一个带有@Qualifier的注解,添加该注解的Resttemplate会负载均衡调用
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface AiChatLoadBalance {
}
定义ClientHttpRequestInterceptor的实现类,实现负载均衡调用的主要逻辑
@Slf4j
public class AiChatRetryLoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private AiChatLoadBalancer aiChatLoadBalancer;
public AiChatRetryLoadBalancerInterceptor(AiChatLoadBalancer aiChatLoadBalancer) {
this.aiChatLoadBalancer=aiChatLoadBalancer;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
List<AiChatOpenAiServer> serverList = aiChatLoadBalancer.getServer(serviceName);
AiChatOpenAiServer aiChatOpenAiServer = aiChatLoadBalancer.chooseServer(serverList);
log.info("当前调用的地址为:{}", aiChatOpenAiServer.getAddress());
if (Objects.nonNull(aiChatOpenAiServer)) {
AiChatRequestwraper aiChatRequestwraper = new AiChatRequestwraper(request, aiChatOpenAiServer.getAddress() + originalUri.getPath());
ClientHttpResponse response = null;
try {
response = execution.execute(aiChatRequestwraper, body);
}catch (Exception e) {
log.info("调用ip={}的服务失败,失败的原因为:{}", aiChatOpenAiServer.getAddress(), e);
// 更新当前ip的调用失败时间
aiChatOpenAiServer.setUnvalibaleTime(System.currentTimeMillis());
aiChatLoadBalancer.setServerList(serviceName, serverList);
throw e;
}
if (Objects.nonNull(response)&&response.getStatusCode().isError()) {
// 更新当前ip的调用失败时间
aiChatOpenAiServer.setUnvalibaleTime(System.currentTimeMillis());
aiChatLoadBalancer.setServerList(serviceName, serverList);
}else if (response.getStatusCode().is2xxSuccessful()&&aiChatOpenAiServer.getUnvalibaleTime()!=0l) {
// 更新当前ip的调用失败时间
aiChatOpenAiServer.setUnvalibaleTime(0l);
aiChatLoadBalancer.setServerList(serviceName, serverList);
}
return response;
}
return null;
}
static class AiChatRequestwraper extends HttpRequestWrapper {
private String url;
public AiChatRequestwraper(HttpRequest request,String url) {
super(request);
this.url = url;
}
@Override
public URI getURI() {
try {
return new URI(url);
} catch (URISyntaxException e) {
}
return null;
}
}
}
AiChatLoadBalancer提供了获取提供服务列表,以及按照自己的负载均衡策略选择服务的方法
@Slf4j
@Service
@EnableConfigurationProperties({AiChatLoadBalancerProperties.class})
public class AiChatLoadBalancer {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private AiChatLoadBalancerProperties aiChatLoadBalancerProperties;
/**
* @description: 获取所有可用的服务地址
* @author: wangjinfeng
* @date: 2023/4/28 15:04
* @param: []
* @return: java.util.List<com.zyj.aichat.common.loadbalance.AiChatOpenAiServer>
**/
public List<AiChatOpenAiServer> getServer(String serverName) {
if (StringUtils.isBlank(serverName)) {
return null;
}
String serverListStr = redisTemplate.opsForValue().get(serverName);
if (StringUtils.isBlank(serverListStr)) {
serverListStr = JSON.toJSONString(aiChatLoadBalancerProperties.getServer());
redisTemplate.opsForValue().set(serverName, JSON.toJSONString(aiChatLoadBalancerProperties.getServer()), 24, TimeUnit.HOURS);
}
return JSON.parseArray(serverListStr, AiChatOpenAiServer.class);
}
/**
* @description: 选择调用服务
* @author: wangjinfeng
* @date: 2023/4/28 14:38
* @param: [serverList]
* @return: com.zyj.aichat.common.loadbalance.AiChatOpenAiServer
**/
public AiChatOpenAiServer chooseServer(List<AiChatOpenAiServer> serverList) {
if (CollectionUtils.isEmpty(serverList)) {
// 没有可用的服务
return null;
}
if (serverList.size()==1) {
return serverList.get(0);
}
List<AiChatOpenAiServer> collect = serverList
.stream()
// 过滤掉一分钟之内调用失败的
.filter(server -> {
long time = (System.currentTimeMillis() - server.getUnvalibaleTime()) / 1000;
log.info("当前服务失败窗口为:{}", time);
return time > 60;
})
.sorted(Comparator.comparing(AiChatOpenAiServer::getWeight).reversed())
.collect(Collectors.toList());
if (!CollectionUtils.isEmpty(collect)&&collect.size()>1) {
return collect.get(RandomUtil.randomInt(100)%serverList.size());
} else if (collect.size()==1) {
return collect.get(0);
}
return null;
//.findFirst()
}
public void setServerList(String serverName, List<AiChatOpenAiServer> serverList) {
redisTemplate.delete(serverName);
redisTemplate.opsForValue().set(serverName, JSON.toJSONString(serverList), 24, TimeUnit.HOURS);
}
}
最后进行配置
@Configuration
@ConditionalOnClass({RestTemplate.class})
public class AiChatLoadBalancerAutoConfiguration {
@AiChatLoadBalance
@Autowired(
required = false
)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Bean
public AiChatRetryLoadBalancerInterceptor loadBalancerInterceptor(AiChatLoadBalancer aiChatLoadBalancer) {
return new AiChatRetryLoadBalancerInterceptor(aiChatLoadBalancer);
}
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializer(final List<RestTemplateCustomizer> customizers) {
return new SmartInitializingSingleton() {
public void afterSingletonsInstantiated() {
Iterator var1 = AiChatLoadBalancerAutoConfiguration.this.restTemplates.iterator();
while (var1.hasNext()) {
RestTemplate restTemplate = (RestTemplate) var1.next();
Iterator var3 = customizers.iterator();
while (var3.hasNext()) {
RestTemplateCustomizer customizer = (RestTemplateCustomizer) var3.next();
customizer.customize(restTemplate);
}
}
}
};
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(final AiChatRetryLoadBalancerInterceptor loadBalancerInterceptor) {
return new RestTemplateCustomizer() {
public void customize(RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> list = new ArrayList(restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
}
};
}
}
总结
本文参照了Ribbon源码来自己实现负载均衡和熔断,实现了简单的需求,同时可以更好的了解Ribbon的工作原理。