负载均衡

load-balancer
DerekYRC 2 years ago
parent a7ec79e936
commit f0f796ee20

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

@ -12,7 +12,7 @@
写作本项目的目的之一是降低阅读原始spring cloud源码的难度。希望掌握本项目讲解的内容之后再阅读原始spring-cloud的源码能起到事半功倍的效果所以本项目的功能实现逻辑及原理和官方保持一致但追求代码最大精简化可以理解为一个源码导读的项目。
技术能力有限且文采佳,大家可以在此[**issue**](https://github.com/DerekYRC/mini-spring-cloud/issues/1) 留言提问题和发表建议也欢迎Pull Request完善此项目。
技术能力有限且文采佳,大家可以在此[**issue**](https://github.com/DerekYRC/mini-spring-cloud/issues/1) 留言提问题和发表建议也欢迎Pull Request完善此项目。
# [服务注册](#服务注册)
> 分支: service-registry
@ -476,6 +476,526 @@ Port of the service provider: 19922
```
# [集成ribbon实现客户端负载均衡](#集成ribbon实现客户端负载均衡)
> 分支: load-balancer
## 关于ribbon
> (翻译自官方文档)ribbon是一个提供如下功能的依赖包:
> - 负载均衡
> - 容错机制
> - 支持多种协议(HTTP, TCP, UDP),支持异步和响应式的调用方式
> - 缓存和批处理
#### ribbon核心API
一、IClientConfig接口
![](./assets/load-balancer-IClientConfig.png)
定义加载和读取ribbon客户端配置的方法实现类DefaultClientConfigImpl
二、IPing接口
![](./assets/load-balancer-IPing.png)
顾名思义,判断服务是否存活,实现类:
- NoOpPing不做检查认为服务存活
- DummyPing不做检查认为服务存活
三、ServerList接口
![](./assets/load-balancer-ServerList.png)
获取服务实例列表的接口,实现类:
- ConfigurationBasedServerList基于配置获取服务实例列表
四、IRule接口
![](./assets/load-balancer-IRule.png)
负载均衡规则,实现类:
- RoundRobinRule轮询
- RandomRule随机
- WeightedResponseTimeRule根据响应时间分配权重响应时间越短权重越大
- BestAvailableRule跳过被熔断器标记为"tripped"状态的、并且选择并发请求数最小的服务实例
- ZoneAvoidanceRule根据所属zone和可用性筛选服务实例在没有多zone的情况下退化为轮询RoundRobinRule
- AvailabilityFilteringRule过滤掉一直连接失败或活跃连接数超过配置值的服务实例
- RetryRule对其他负载均衡规则的包装在一段时间内失败重试
五、ServerListFilter接口
![](./assets/load-balancer-ServerListFilter.png)
服务实例过滤器
六、ServerListUpdater接口
![](./assets/load-balancer-ServerListUpdater.png)
PollingServerListUpdater起一个周期任务更新服务实例列表
七、ILoadBalancer接口
![](./assets/load-balancer-ILoadBalancer.png)
负载均衡接口,实现类:
- BaseLoadBalancer手动设置服务实例根据负载均衡规则IRule筛选服务实例
- DynamicServerListLoadBalancer使用ServerListUpdater动态更新服务实例列表
- ZoneAwareLoadBalancer支持zone
#### 集成ribbon实现客户端负载均衡(一)
spring-cloud-commons负载均衡相关API:
- ServiceInstanceChooser接口服务实例选择器根据服务提供者的服务名称选择服务实例
```java
/**
* Implemented by classes which use a load balancer to choose a server to send a request to.
*/
public interface ServiceInstanceChooser {
/**
* Chooses a ServiceInstance from the LoadBalancer for the specified service.
*/
ServiceInstance choose(String serviceId);
/**
* Chooses a ServiceInstance from the LoadBalancer for the specified service and LoadBalancer request.
*/
<T> ServiceInstance choose(String serviceId, Request<T> request);
}
```
- LoadBalancerClient接口
```java
/**
* Represents a client-side load balancer.
*/
public interface LoadBalancerClient extends ServiceInstanceChooser {
/**
* Executes request using a ServiceInstance from the LoadBalancer for the specified service.
*/
<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
/**
* Executes request using a ServiceInstance from the LoadBalancer for the specified service.
*/
<T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
/**
* Creates a proper URI with a real host and port for systems to utilize. Some systems
* use a URI with the logical service name as the host, such as
* http://myservice/path/to/service. This will replace the service name with the
* host:port from the ServiceInstance.
*/
URI reconstructURI(ServiceInstance instance, URI original);
}
```
本节只关注ServiceInstanceChooser接口的choose方法下一节讲解LoadBalancerClient接口的三个方法。
负载均衡功能实现:
RibbonClientConfiguration配置ribbon核心API默认实现类:
```java
/**
* 配置ribbon默认组件
*/
@Configuration
public class RibbonClientConfiguration {
@Value("${ribbon.client.name}")
private String name;
@Bean
@ConditionalOnMissingBean
public IClientConfig ribbonClientConfig() {
DefaultClientConfigImpl config = new DefaultClientConfigImpl();
config.loadProperties(name);
return config;
}
@Bean
@ConditionalOnMissingBean
public IRule ribbonRule(IClientConfig config) {
ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
rule.initWithNiwsConfig(config);
return rule;
}
@Bean
@ConditionalOnMissingBean
public IPing ribbonPing(IClientConfig config) {
return new DummyPing();
}
@Bean
@ConditionalOnMissingBean
public ServerList<Server> ribbonServerList(IClientConfig config) {
ConfigurationBasedServerList serverList = new ConfigurationBasedServerList();
serverList.initWithNiwsConfig(config);
return serverList;
}
@Bean
@ConditionalOnMissingBean
public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
return new PollingServerListUpdater(config);
}
@Bean
@ConditionalOnMissingBean
public ServerListFilter<Server> ribbonServerListFilter(IClientConfig config) {
ServerListSubsetFilter filter = new ServerListSubsetFilter();
filter.initWithNiwsConfig(config);
return filter;
}
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
serverListFilter, serverListUpdater);
}
}
```
只需实现ribbon核心API中的获取服务实例列表接口ServerList实现类TutuServerList:
```java
/**
* 查询图图服务实例列表
*/
public class TutuServerList extends AbstractServerList<TutuServer> {
private static Logger logger = LoggerFactory.getLogger(TutuServerList.class);
private TutuDiscoveryProperties discoveryProperties;
private String serviceId;
public TutuServerList(TutuDiscoveryProperties discoveryProperties) {
this.discoveryProperties = discoveryProperties;
}
/**
* 查询服务实例列表
*
* @return
*/
@Override
public List<TutuServer> getInitialListOfServers() {
return getServer();
}
/**
* 查询服务实例列表
*
* @return
*/
@Override
public List<TutuServer> getUpdatedListOfServers() {
return getServer();
}
private List<TutuServer> getServer() {
Map<String, Object> param = new HashMap<>();
param.put("serviceName", serviceId);
String response = HttpUtil.get(discoveryProperties.getServerAddr() + "/list", param);
logger.info("query service instance, serviceId: {}, response: {}", serviceId, response);
return JSON.parseArray(response).stream().map(hostInfo -> {
String ip = ((JSONObject) hostInfo).getString("ip");
Integer port = ((JSONObject) hostInfo).getInteger("port");
return new TutuServer(ip, port);
}).collect(Collectors.toList());
}
public String getServiceId() {
return serviceId;
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
this.serviceId = iClientConfig.getClientName();
}
}
```
配置TutuServerList替换RibbonClientConfiguration中配置的默认实现:
```java
@Configuration
@RibbonClients(defaultConfiguration = TutuRibbonClientConfiguration.class)
public class RibbonTutuAutoConfiguration {
}
```
```java
/**
* 自定义ribbon组件
*/
@Configuration
public class TutuRibbonClientConfiguration {
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config,
TutuDiscoveryProperties discoveryProperties) {
TutuServerList serverList = new TutuServerList(discoveryProperties);
serverList.initWithNiwsConfig(config);
return serverList;
}
}
```
每一个Provider服务对应一套ribbon核心API相互隔离SpringClientFactory为每一个Provider服务对应的ribbon核心API创建一个子spring应用上下文ApplicationContext
子spring应用上下文的配置类来自于:
- SpringClientFactory的构造函数参数RibbonClientConfiguration配置类
- 修饰RibbonTutuAutoConfiguration的注解指定的属性defaultConfiguration = TutuRibbonClientConfiguration配置类处理RibbonClients注解的RibbonClientConfigurationRegistrar会将TutuRibbonClientConfiguration配置类包装为RibbonClientSpecification供SpringClientFactory使用
为了充分理解子spring容器的创建逻辑可以在下面的测试环节debug如下几个方法:
- RibbonClientConfigurationRegistrar#registerBeanDefinitions
- RibbonAutoConfiguration#springClientFactory
- SpringClientFactory的构造函数和方法
LoadBalancerClient实现类RibbonLoadBalancerClient:
```java
/**
* ribbon负载均衡客户端
*/
public class RibbonLoadBalancerClient implements LoadBalancerClient {
private SpringClientFactory clientFactory;
public RibbonLoadBalancerClient(SpringClientFactory clientFactory) {
this.clientFactory = clientFactory;
}
/**
* 选择服务实例
*/
@Override
public ServiceInstance choose(String serviceId) {
return choose(serviceId, null);
}
/**
* 选择服务实例
*/
@Override
public <T> ServiceInstance choose(String serviceId, Request<T> request) {
ILoadBalancer loadBalancer = clientFactory.getInstance(serviceId, ILoadBalancer.class);
Server server = loadBalancer.chooseServer("default");
if (server != null) {
return new TutuServiceInstance(serviceId, server.getHost(), server.getPort());
}
return null;
}
}
```
自动装配spring.factories
```yaml
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.github.cloud.loadbalancer.ribbon.config.RibbonAutoConfiguration,\
com.github.cloud.loadbalancer.ribbon.config.RibbonTutuAutoConfiguration
```
测试:
1、在mini-spring-cloud-provider-example文件夹下执行命令```mvn spring-boot:run```启动多个服务提供者
服务消费者代码:
```java
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@RestController
static class HelloController {
private RestTemplate restTemplate = new RestTemplate();
@GetMapping("/world")
public String world() {
ServiceInstance serviceInstance = loadBalancerClient.choose("provider-application");
if (serviceInstance != null) {
URI uri = serviceInstance.getUri();
String response = restTemplate.postForObject(uri.toString() + "/echo", null, String.class);
return response;
}
throw new RuntimeException("No service instance for provider-application found");
}
}
}
```
2、多次访问```http://localhost:8080/world```, 通过响应报文中的端口可知请求以轮询的方式分配给服务提供者(默认的负载均衡规则ZoneAvoidanceRule在没有多zone的情况下退化为轮询规则)
#### 集成ribbon实现客户端负载均衡(二)
简化调用方式达到如下的效果使用服务提供者的名称替换IP和端口
```java
restTemplate.postForObject("http://provider-application/echo", null, String.class);
```
实现LoadBalancerClient的execute方法和reconstructURI方法:
```java
public class RibbonLoadBalancerClient implements LoadBalancerClient {
/**
* 重建请求URI将服务名称替换为服务实例的IP:端口
*/
@Override
public URI reconstructURI(ServiceInstance server, URI original) {
try {
//将服务名称替换为服务实例的IP:端口例如http://provider-application/echo被重建为http://192.168.100.1:8888/echo
StringBuilder sb = new StringBuilder();
sb.append(original.getScheme()).append("://");
sb.append(server.getHost());
sb.append(":").append(server.getPort());
sb.append(original.getRawPath());
if (StrUtil.isNotEmpty(original.getRawQuery())) {
sb.append("?").append(original.getRawQuery());
}
URI newURI = new URI(sb.toString());
return newURI;
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
/**
* 处理http请求
*/
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
ServiceInstance serviceInstance = choose(serviceId);
return execute(serviceId, serviceInstance, request);
}
/**
* 处理http请求
*
*/
@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
try {
return request.apply(serviceInstance);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
```
- reconstructURI方法重建请求URI将服务名称替换为服务实例的IP:端口例如http://provider-application/echo被重建为http://192.168.100.1:8888/echo
- execute方法处理http请求
有了RibbonLoadBalancerClient的reconstructURI和execute方法将所有http请求委托给RibbonLoadBalancerClient即可。其实spring-cloud-commons已经帮我们配置好拦截RestTemplate的http请求委托给RibbonLoadBalancerClient的拦截器LoadBalancerInterceptor配置类如下:
![](./assets/load-balancer-LoadBalancerAutoConfiguration.png)
LoadBalancerAutoConfiguration配置类为每一个被LoadBalanced注解修饰的RestTemplate增加LoadBalancerInterceptor拦截器。
![](./assets/load-balancer-LoadBalancerInterceptor.png)
LoadBalancerInterceptor将http请求委托给LoadBalancerClient执行其中requestFactory.createRequest使用ServiceRequestWrapper包装原始的http请求
![](./assets/load-balancer-ServiceRequestWrapper.png)
ServiceRequestWrapper调用LoadBalancerClient#reconstructURI方法重建请求URI将服务名称替换为服务实例的IP:端口
测试:
服务消费者代码如下:
```java
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@Configuration
static class RestTemplateConfiguration {
/**
* 赋予负载均衡的能力
*
* @return
*/
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@RestController
static class HelloController {
@Autowired
private RestTemplate loadBalancedRestTemplate;
@GetMapping("/foo")
public String foo() {
return loadBalancedRestTemplate.postForObject("http://provider-application/echo", null, String.class);
}
}
}
```
访问```http://localhost:8080/foo```

@ -22,6 +22,11 @@
<groupId>com.github</groupId>
<artifactId>mini-spring-cloud-tutu-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.github</groupId>
<artifactId>mini-spring-cloud-load-balancer</artifactId>
</dependency>
</dependencies>
<build>

@ -5,6 +5,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@ -23,12 +27,33 @@ public class ConsumerApplication {
SpringApplication.run(ConsumerApplication.class, args);
}
@Configuration
static class RestTemplateConfiguration {
/**
*
*
* @return
*/
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@RestController
static class HelloController {
@Autowired
private TutuDiscoveryClient discoveryClient;
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private RestTemplate loadBalancedRestTemplate;
private RestTemplate restTemplate = new RestTemplate();
@GetMapping("/hello")
@ -43,6 +68,23 @@ public class ConsumerApplication {
throw new RuntimeException("No service instance for provider-application found");
}
@GetMapping("/world")
public String world() {
ServiceInstance serviceInstance = loadBalancerClient.choose("provider-application");
if (serviceInstance != null) {
URI uri = serviceInstance.getUri();
String response = restTemplate.postForObject(uri.toString() + "/echo", null, String.class);
return response;
}
throw new RuntimeException("No service instance for provider-application found");
}
@GetMapping("/foo")
public String foo() {
return loadBalancedRestTemplate.postForObject("http://provider-application/echo", null, String.class);
}
}
}

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mini-spring-cloud</artifactId>
<groupId>com.github</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mini-spring-cloud-load-balancer</artifactId>
<dependencies>
<dependency>
<groupId>com.github</groupId>
<artifactId>mini-spring-cloud-tutu-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
</dependency>
</dependencies>
</project>

@ -0,0 +1,37 @@
package com.github.cloud.loadbalancer.ribbon;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import java.util.Map;
/**
* RibbonClients
*
* @author derek()
* @date 2022/3/22
*/
public class RibbonClientConfigurationRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
Map<String, Object> attrs = metadata.getAnnotationAttributes(RibbonClients.class.getName(), true);
if (attrs != null && attrs.containsKey("defaultConfiguration")) {
String name = "default." + metadata.getClassName();
registerClientConfiguration(registry, name, attrs.get("defaultConfiguration"));
}
}
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(RibbonClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition(name + ".RibbonClientSpecification",
builder.getBeanDefinition());
}
}

@ -0,0 +1,69 @@
package com.github.cloud.loadbalancer.ribbon;
import org.springframework.cloud.context.named.NamedContextFactory;
import java.util.Arrays;
import java.util.Objects;
/**
* ribbon
*
* @author derek()
* @date 2022/3/22
*/
public class RibbonClientSpecification implements NamedContextFactory.Specification {
private String name;
private Class<?>[] configuration;
public RibbonClientSpecification() {
}
public RibbonClientSpecification(String name, Class<?>[] configuration) {
this.name = name;
this.configuration = configuration;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Class<?>[] getConfiguration() {
return configuration;
}
public void setConfiguration(Class<?>[] configuration) {
this.configuration = configuration;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RibbonClientSpecification that = (RibbonClientSpecification) o;
return Arrays.equals(configuration, that.configuration)
&& Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(configuration, name);
}
@Override
public String toString() {
return new StringBuilder("RibbonClientSpecification{").append("name='")
.append(name).append("', ").append("configuration=")
.append(Arrays.toString(configuration)).append("}").toString();
}
}

@ -0,0 +1,24 @@
package com.github.cloud.loadbalancer.ribbon;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author derek()
* @date 2022/3/22
*/
@Configuration(proxyBeanMethods = false)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@Import(RibbonClientConfigurationRegistrar.class)
public @interface RibbonClients {
Class<?>[] defaultConfiguration() default {};
}

@ -0,0 +1,119 @@
package com.github.cloud.loadbalancer.ribbon;
import cn.hutool.core.util.StrUtil;
import com.github.cloud.tutu.TutuServiceInstance;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest;
import org.springframework.cloud.client.loadbalancer.Request;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
/**
* ribbon
*
* @author derek()
* @date 2022/3/22
*/
public class RibbonLoadBalancerClient implements LoadBalancerClient {
private SpringClientFactory clientFactory;
public RibbonLoadBalancerClient(SpringClientFactory clientFactory) {
this.clientFactory = clientFactory;
}
/**
*
*
* @param serviceId
* @return
*/
@Override
public ServiceInstance choose(String serviceId) {
return choose(serviceId, null);
}
/**
*
*
* @param serviceId
* @param request
* @param <T>
* @return
*/
@Override
public <T> ServiceInstance choose(String serviceId, Request<T> request) {
ILoadBalancer loadBalancer = clientFactory.getInstance(serviceId, ILoadBalancer.class);
Server server = loadBalancer.chooseServer("default");
if (server != null) {
return new TutuServiceInstance(serviceId, server.getHost(), server.getPort());
}
return null;
}
/**
* URIIP:
*
* @param server
* @param original
* @return
*/
@Override
public URI reconstructURI(ServiceInstance server, URI original) {
try {
//将服务名称替换为服务实例的IP:端口例如http://provider-application/echo被重建为http://192.168.100.1:8888/echo
StringBuilder sb = new StringBuilder();
sb.append(original.getScheme()).append("://");
sb.append(server.getHost());
sb.append(":").append(server.getPort());
sb.append(original.getRawPath());
if (StrUtil.isNotEmpty(original.getRawQuery())) {
sb.append("?").append(original.getRawQuery());
}
URI newURI = new URI(sb.toString());
return newURI;
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
/**
* http
*
* @param serviceId
* @param request
* @param <T>
* @return
* @throws IOException
*/
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
ServiceInstance serviceInstance = choose(serviceId);
return execute(serviceId, serviceInstance, request);
}
/**
* http
*
* @param serviceId
* @param serviceInstance
* @param request
* @param <T>
* @return
* @throws IOException
*/
@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
try {
return request.apply(serviceInstance);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}

@ -0,0 +1,30 @@
package com.github.cloud.loadbalancer.ribbon;
import com.github.cloud.loadbalancer.ribbon.config.RibbonClientConfiguration;
import org.springframework.cloud.context.named.NamedContextFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/**
* ApplicationContext
*
* @author derek()
* @date 2022/3/22
*/
public class SpringClientFactory extends NamedContextFactory<RibbonClientSpecification> {
private static final String NAMESPACE = "ribbon";
public SpringClientFactory() {
super(RibbonClientConfiguration.class, NAMESPACE, "ribbon.client.name");
}
@Override
public <C> C getInstance(String name, Class<C> type) {
return super.getInstance(name, type);
}
@Override
protected AnnotationConfigApplicationContext getContext(String name) {
return super.getContext(name);
}
}

@ -0,0 +1,16 @@
package com.github.cloud.loadbalancer.ribbon;
import com.netflix.loadbalancer.Server;
/**
*
*
* @author derek()
* @date 2022/3/13
*/
public class TutuServer extends Server {
public TutuServer(String host, int port) {
super(host, port);
}
}

@ -0,0 +1,75 @@
package com.github.cloud.loadbalancer.ribbon;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.github.cloud.tutu.TutuDiscoveryProperties;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractServerList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
*
*
* @author derek()
* @date 2022/3/13
*/
public class TutuServerList extends AbstractServerList<TutuServer> {
private static Logger logger = LoggerFactory.getLogger(TutuServerList.class);
private TutuDiscoveryProperties discoveryProperties;
private String serviceId;
public TutuServerList(TutuDiscoveryProperties discoveryProperties) {
this.discoveryProperties = discoveryProperties;
}
/**
*
*
* @return
*/
@Override
public List<TutuServer> getInitialListOfServers() {
return getServer();
}
/**
*
*
* @return
*/
@Override
public List<TutuServer> getUpdatedListOfServers() {
return getServer();
}
private List<TutuServer> getServer() {
Map<String, Object> param = new HashMap<>();
param.put("serviceName", serviceId);
String response = HttpUtil.get(discoveryProperties.getServerAddr() + "/list", param);
logger.info("query service instance, serviceId: {}, response: {}", serviceId, response);
return JSON.parseArray(response).stream().map(hostInfo -> {
String ip = ((JSONObject) hostInfo).getString("ip");
Integer port = ((JSONObject) hostInfo).getInteger("port");
return new TutuServer(ip, port);
}).collect(Collectors.toList());
}
public String getServiceId() {
return serviceId;
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
this.serviceId = iClientConfig.getClientName();
}
}

@ -0,0 +1,37 @@
package com.github.cloud.loadbalancer.ribbon.config;
import com.github.cloud.loadbalancer.ribbon.RibbonClientSpecification;
import com.github.cloud.loadbalancer.ribbon.RibbonLoadBalancerClient;
import com.github.cloud.loadbalancer.ribbon.SpringClientFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/**
* @author derek()
* @date 2022/3/22
*/
@Configuration
public class RibbonAutoConfiguration {
@Autowired(required = false)
private List<RibbonClientSpecification> configurations = new ArrayList<>();
@Bean
public SpringClientFactory springClientFactory() {
SpringClientFactory factory = new SpringClientFactory();
factory.setConfigurations(this.configurations);
return factory;
}
@Bean
@ConditionalOnMissingBean(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(springClientFactory());
}
}

@ -0,0 +1,105 @@
package com.github.cloud.loadbalancer.ribbon.config;
import com.netflix.client.config.DefaultClientConfigImpl;
import com.netflix.loadbalancer.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.netflix.client.config.IClientConfig;
/**
* ribbon
*
* @author derek()
* @date 2022/3/22
*/
@Configuration
public class RibbonClientConfiguration {
@Value("${ribbon.client.name}")
private String name;
@Bean
@ConditionalOnMissingBean
public IClientConfig ribbonClientConfig() {
DefaultClientConfigImpl config = new DefaultClientConfigImpl();
config.loadProperties(name);
return config;
}
@Bean
@ConditionalOnMissingBean
public IRule ribbonRule(IClientConfig config) {
ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
rule.initWithNiwsConfig(config);
return rule;
}
@Bean
@ConditionalOnMissingBean
public IPing ribbonPing(IClientConfig config) {
return new DummyPing();
}
@Bean
@ConditionalOnMissingBean
public ServerList<Server> ribbonServerList(IClientConfig config) {
ConfigurationBasedServerList serverList = new ConfigurationBasedServerList();
serverList.initWithNiwsConfig(config);
return serverList;
}
@Bean
@ConditionalOnMissingBean
public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
return new PollingServerListUpdater(config);
}
@Bean
@ConditionalOnMissingBean
public ServerListFilter<Server> ribbonServerListFilter(IClientConfig config) {
ServerListSubsetFilter filter = new ServerListSubsetFilter();
filter.initWithNiwsConfig(config);
return filter;
}
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
serverListFilter, serverListUpdater);
}
}

@ -0,0 +1,13 @@
package com.github.cloud.loadbalancer.ribbon.config;
import com.github.cloud.loadbalancer.ribbon.RibbonClients;
import org.springframework.context.annotation.Configuration;
/**
* @author derek()
* @date 2022/3/22
*/
@Configuration
@RibbonClients(defaultConfiguration = TutuRibbonClientConfiguration.class)
public class RibbonTutuAutoConfiguration {
}

@ -0,0 +1,28 @@
package com.github.cloud.loadbalancer.ribbon.config;
import com.github.cloud.loadbalancer.ribbon.TutuServerList;
import com.github.cloud.tutu.TutuDiscoveryProperties;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ServerList;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* ribbon
*
* @author derek()
* @date 2022/3/22
*/
@Configuration
public class TutuRibbonClientConfiguration {
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config,
TutuDiscoveryProperties discoveryProperties) {
TutuServerList serverList = new TutuServerList(discoveryProperties);
serverList.initWithNiwsConfig(config);
return serverList;
}
}

@ -0,0 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.github.cloud.loadbalancer.ribbon.config.RibbonAutoConfiguration,\
com.github.cloud.loadbalancer.ribbon.config.RibbonTutuAutoConfiguration

@ -21,12 +21,14 @@
<module>mini-spring-cloud-tutu-discovery</module>
<module>mini-spring-cloud-examples/mini-spring-cloud-provider-example</module>
<module>mini-spring-cloud-examples/mini-spring-cloud-consumer-examples</module>
<module>mini-spring-cloud-load-balancer</module>
</modules>
<properties>
<spring.cloud.version>2021.0.1</spring.cloud.version>
<fastjson.version>1.2.79</fastjson.version>
<hutool.version>5.7.21</hutool.version>
<ribbon.version>2.3.0</ribbon.version>
</properties>
<dependencyManagement>
@ -53,6 +55,12 @@
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.github</groupId>
<artifactId>mini-spring-cloud-load-balancer</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
@ -64,6 +72,24 @@
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon</artifactId>
<version>${ribbon.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-loadbalancer</artifactId>
<version>${ribbon.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
<version>${ribbon.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

Loading…
Cancel
Save