diff --git a/assets/load-balancer-IClientConfig.png b/assets/load-balancer-IClientConfig.png new file mode 100644 index 0000000..ae6e25e Binary files /dev/null and b/assets/load-balancer-IClientConfig.png differ diff --git a/assets/load-balancer-ILoadBalancer.png b/assets/load-balancer-ILoadBalancer.png new file mode 100644 index 0000000..0ca3e13 Binary files /dev/null and b/assets/load-balancer-ILoadBalancer.png differ diff --git a/assets/load-balancer-IPing.png b/assets/load-balancer-IPing.png new file mode 100644 index 0000000..93753b5 Binary files /dev/null and b/assets/load-balancer-IPing.png differ diff --git a/assets/load-balancer-IRule.png b/assets/load-balancer-IRule.png new file mode 100644 index 0000000..1fddd78 Binary files /dev/null and b/assets/load-balancer-IRule.png differ diff --git a/assets/load-balancer-LoadBalancerAutoConfiguration.png b/assets/load-balancer-LoadBalancerAutoConfiguration.png new file mode 100644 index 0000000..6daa42c Binary files /dev/null and b/assets/load-balancer-LoadBalancerAutoConfiguration.png differ diff --git a/assets/load-balancer-LoadBalancerInterceptor.png b/assets/load-balancer-LoadBalancerInterceptor.png new file mode 100644 index 0000000..e0f2f09 Binary files /dev/null and b/assets/load-balancer-LoadBalancerInterceptor.png differ diff --git a/assets/load-balancer-ServerList.png b/assets/load-balancer-ServerList.png new file mode 100644 index 0000000..b2461ca Binary files /dev/null and b/assets/load-balancer-ServerList.png differ diff --git a/assets/load-balancer-ServerListFilter.png b/assets/load-balancer-ServerListFilter.png new file mode 100644 index 0000000..8a90952 Binary files /dev/null and b/assets/load-balancer-ServerListFilter.png differ diff --git a/assets/load-balancer-ServerListUpdater.png b/assets/load-balancer-ServerListUpdater.png new file mode 100644 index 0000000..ed96352 Binary files /dev/null and b/assets/load-balancer-ServerListUpdater.png differ diff --git a/assets/load-balancer-ServiceRequestWrapper.png b/assets/load-balancer-ServiceRequestWrapper.png new file mode 100644 index 0000000..fad0e2c Binary files /dev/null and b/assets/load-balancer-ServiceRequestWrapper.png differ diff --git a/changelog.md b/changelog.md index 90ee020..abf9444 100644 --- a/changelog.md +++ b/changelog.md @@ -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. + */ + ServiceInstance choose(String serviceId, Request 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 execute(String serviceId, LoadBalancerRequest request) throws IOException; + + /** + * Executes request using a ServiceInstance from the LoadBalancer for the specified service. + */ + T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest 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 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 ribbonServerListFilter(IClientConfig config) { + ServerListSubsetFilter filter = new ServerListSubsetFilter(); + filter.initWithNiwsConfig(config); + return filter; + } + + @Bean + @ConditionalOnMissingBean + public ILoadBalancer ribbonLoadBalancer(IClientConfig config, + ServerList serverList, ServerListFilter 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 { + 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 getInitialListOfServers() { + return getServer(); + } + + /** + * 查询服务实例列表 + * + * @return + */ + @Override + public List getUpdatedListOfServers() { + return getServer(); + } + + private List getServer() { + Map 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 ServiceInstance choose(String serviceId, Request 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 execute(String serviceId, LoadBalancerRequest request) throws IOException { + ServiceInstance serviceInstance = choose(serviceId); + return execute(serviceId, serviceInstance, request); + } + + /** + * 处理http请求 + * + */ + @Override + public T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest 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``` + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mini-spring-cloud-examples/mini-spring-cloud-consumer-examples/pom.xml b/mini-spring-cloud-examples/mini-spring-cloud-consumer-examples/pom.xml index 5966ed5..3bf5f17 100644 --- a/mini-spring-cloud-examples/mini-spring-cloud-consumer-examples/pom.xml +++ b/mini-spring-cloud-examples/mini-spring-cloud-consumer-examples/pom.xml @@ -22,6 +22,11 @@ com.github mini-spring-cloud-tutu-discovery + + + com.github + mini-spring-cloud-load-balancer + diff --git a/mini-spring-cloud-examples/mini-spring-cloud-consumer-examples/src/main/java/com/github/cloud/examples/ConsumerApplication.java b/mini-spring-cloud-examples/mini-spring-cloud-consumer-examples/src/main/java/com/github/cloud/examples/ConsumerApplication.java index 9ded90b..cb07b8f 100644 --- a/mini-spring-cloud-examples/mini-spring-cloud-consumer-examples/src/main/java/com/github/cloud/examples/ConsumerApplication.java +++ b/mini-spring-cloud-examples/mini-spring-cloud-consumer-examples/src/main/java/com/github/cloud/examples/ConsumerApplication.java @@ -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); + } } } diff --git a/mini-spring-cloud-load-balancer/pom.xml b/mini-spring-cloud-load-balancer/pom.xml new file mode 100644 index 0000000..9a963e0 --- /dev/null +++ b/mini-spring-cloud-load-balancer/pom.xml @@ -0,0 +1,86 @@ + + + + mini-spring-cloud + com.github + 1.0.0-SNAPSHOT + + 4.0.0 + + mini-spring-cloud-load-balancer + + + + com.github + mini-spring-cloud-tutu-discovery + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot + true + + + + org.springframework.boot + spring-boot-autoconfigure + true + + + + org.springframework.boot + spring-boot-starter + true + + + + org.springframework.cloud + spring-cloud-commons + + + + org.springframework.cloud + spring-cloud-context + + + + org.springframework.boot + spring-boot-starter-web + test + + + + cn.hutool + hutool-all + + + + com.alibaba + fastjson + + + + com.netflix.ribbon + ribbon + + + + com.netflix.ribbon + ribbon-loadbalancer + + + + com.netflix.ribbon + ribbon-core + + + + \ No newline at end of file diff --git a/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonClientConfigurationRegistrar.java b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonClientConfigurationRegistrar.java new file mode 100644 index 0000000..9a97149 --- /dev/null +++ b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonClientConfigurationRegistrar.java @@ -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 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()); + } +} diff --git a/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonClientSpecification.java b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonClientSpecification.java new file mode 100644 index 0000000..7c9b04c --- /dev/null +++ b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonClientSpecification.java @@ -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(); + } + +} diff --git a/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonClients.java b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonClients.java new file mode 100644 index 0000000..bc26248 --- /dev/null +++ b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonClients.java @@ -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 {}; + +} + diff --git a/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonLoadBalancerClient.java b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonLoadBalancerClient.java new file mode 100644 index 0000000..715dbbf --- /dev/null +++ b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonLoadBalancerClient.java @@ -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 + * @return + */ + @Override + public ServiceInstance choose(String serviceId, Request 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; + } + + /** + * 重建请求URI,将服务名称替换为服务实例的IP:端口 + * + * @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 + * @return + * @throws IOException + */ + @Override + public T execute(String serviceId, LoadBalancerRequest request) throws IOException { + ServiceInstance serviceInstance = choose(serviceId); + return execute(serviceId, serviceInstance, request); + } + + /** + * 处理http请求 + * + * @param serviceId + * @param serviceInstance + * @param request + * @param + * @return + * @throws IOException + */ + @Override + public T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request) throws IOException { + try { + return request.apply(serviceInstance); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/SpringClientFactory.java b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/SpringClientFactory.java new file mode 100644 index 0000000..b0f27f1 --- /dev/null +++ b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/SpringClientFactory.java @@ -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 { + + private static final String NAMESPACE = "ribbon"; + + public SpringClientFactory() { + super(RibbonClientConfiguration.class, NAMESPACE, "ribbon.client.name"); + } + + @Override + public C getInstance(String name, Class type) { + return super.getInstance(name, type); + } + + @Override + protected AnnotationConfigApplicationContext getContext(String name) { + return super.getContext(name); + } +} diff --git a/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/TutuServer.java b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/TutuServer.java new file mode 100644 index 0000000..f195bab --- /dev/null +++ b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/TutuServer.java @@ -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); + } +} diff --git a/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/TutuServerList.java b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/TutuServerList.java new file mode 100644 index 0000000..c228a9d --- /dev/null +++ b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/TutuServerList.java @@ -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 { + 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 getInitialListOfServers() { + return getServer(); + } + + /** + * 查询服务实例列表 + * + * @return + */ + @Override + public List getUpdatedListOfServers() { + return getServer(); + } + + private List getServer() { + Map 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(); + } +} diff --git a/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/RibbonAutoConfiguration.java b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/RibbonAutoConfiguration.java new file mode 100644 index 0000000..ce5dc49 --- /dev/null +++ b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/RibbonAutoConfiguration.java @@ -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 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()); + } +} diff --git a/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/RibbonClientConfiguration.java b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/RibbonClientConfiguration.java new file mode 100644 index 0000000..253baf7 --- /dev/null +++ b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/RibbonClientConfiguration.java @@ -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 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 ribbonServerListFilter(IClientConfig config) { + ServerListSubsetFilter filter = new ServerListSubsetFilter(); + filter.initWithNiwsConfig(config); + return filter; + } + + @Bean + @ConditionalOnMissingBean + public ILoadBalancer ribbonLoadBalancer(IClientConfig config, + ServerList serverList, ServerListFilter serverListFilter, + IRule rule, IPing ping, ServerListUpdater serverListUpdater) { + return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList, + serverListFilter, serverListUpdater); + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/RibbonTutuAutoConfiguration.java b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/RibbonTutuAutoConfiguration.java new file mode 100644 index 0000000..8761ae6 --- /dev/null +++ b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/RibbonTutuAutoConfiguration.java @@ -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 { +} diff --git a/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/TutuRibbonClientConfiguration.java b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/TutuRibbonClientConfiguration.java new file mode 100644 index 0000000..7108bb2 --- /dev/null +++ b/mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/TutuRibbonClientConfiguration.java @@ -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; + } +} diff --git a/mini-spring-cloud-load-balancer/src/main/resources/META-INF/spring.factories b/mini-spring-cloud-load-balancer/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..4142180 --- /dev/null +++ b/mini-spring-cloud-load-balancer/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.github.cloud.loadbalancer.ribbon.config.RibbonAutoConfiguration,\ + com.github.cloud.loadbalancer.ribbon.config.RibbonTutuAutoConfiguration \ No newline at end of file diff --git a/pom.xml b/pom.xml index 883297c..fb339f5 100644 --- a/pom.xml +++ b/pom.xml @@ -21,12 +21,14 @@ mini-spring-cloud-tutu-discovery mini-spring-cloud-examples/mini-spring-cloud-provider-example mini-spring-cloud-examples/mini-spring-cloud-consumer-examples + mini-spring-cloud-load-balancer 2021.0.1 1.2.79 5.7.21 + 2.3.0 @@ -53,6 +55,12 @@ 1.0.0-SNAPSHOT + + com.github + mini-spring-cloud-load-balancer + 1.0.0-SNAPSHOT + + com.alibaba fastjson @@ -64,6 +72,24 @@ hutool-all ${hutool.version} + + + com.netflix.ribbon + ribbon + ${ribbon.version} + + + + com.netflix.ribbon + ribbon-loadbalancer + ${ribbon.version} + + + + com.netflix.ribbon + ribbon-core + ${ribbon.version} +