Spring-Cloud之Ribbon负载均衡-3
一、负载均衡是指将负载分摊到多个执行单元上,常见的负载均衡有两种方式。一种是独立进程单元,通过负载均衡策略,将请求转发到不同的执行单元上,例如 Ngnix 。另一种是将负载均衡逻辑以代码的形式封装到服务消费者的客户端上,服务消费者客户端维护了一份服务提供的信息列表,有了信息列表,通过负载均衡策略将请求分摊给多个服务提供者,从而达到负载均衡的目的。
Ribbon Netflix 公司开源的一个负载均衡的组件,它属于上述的第二种方式,是将负载均衡逻辑封装在客户端中,并且运行在客户端的进程里。 Ribbon是一个经过了云端测试的 IPC库,可以很好地控制 HTT TCP 客户端的负载均衡行为。
Spring Cloud 构建的微服务系统中, Ribbon 作为服务消费者的负载均衡器,有两种使用方式, 1)RestTemplate 相结合,2)Feign 相结合(默认方式)。
二、用于生产环境的Ribbon的子模块为
1)ribbon-loadbalancer :可以独立使用或与其他模块 起使用的负载均衡器 API。
2)ribbon-eureka :Ribbon 结合 Eureka 客户端的 API ,为负载均衡器提供动态服务注册列表信息。
3)ribbon-core: Ribbon 的核心 API。
三、使用RestTemple和Ribbon来消费服务
1)通过Spring-Cloud之Eureka注册与发现-2来配置一个Eureka-Server和两个Eureka-Client端口分别为8670和8673/8674,效果如下:
2)在Eureka-Clien中编写一个获取端口的rest接口
package com.cetc.web.rest; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/test") public class TestResource { @Value("${server.port}") private Integer port; @GetMapping("/getPort") public Integer getPort() { return port; } }
3)将Ribbon注册到Eureka-Server中端口8675。
a、添加依赖
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
b、配置文件
server: port: 8675 eureka: instance: hostname: rest client: service-url: defaultZone: http://127.0.0.1:8670/eureka/ # 实际开发中建议使用域名的方式 spring: application: name: rest
c、效果
4)编写配置文件
package com.cetc.config; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class RibbonConfiguration { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }
注意:@LoadBalanced加上过后,RestTemplate就可以结合Ribbon使用了
5)编写服务测试
package com.cetc.service; public interface IRibbonService { Integer getPort(); }
package com.cetc.service.impl; import com.cetc.service.IRibbonService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class RibbonServiceImpl implements IRibbonService { @Autowired private RestTemplate restTemplate; @Override public Integer getPort() { return restTemplate.getForObject("http://client/api/test/getPort", Integer.class); } }
注意:这里使用的是生产者的应用名称来进行访问的。
package com.cetc.web.rest; import com.cetc.service.IRibbonService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/ribbon") public class RibbonResource { @Autowired private IRibbonService ribbonService; @GetMapping("/getPort") public Integer getPort() { return ribbonService.getPort(); } }
效果:
四、LoadBalancerClient,负载均衡器的核心类, LoadBalancerCiient 可以获取负载均衡的服务提供者的实例信息。
1)通过Eureka-Server获取方式来轮流访问接口
package com.cetc.web.rest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/ribbon") public class RibbonResource { @Autowired private LoadBalancerClient loadBalancerClient; @GetMapping("/testRibbon") public String testRibbon() { ServiceInstance client = loadBalancerClient.choose("client"); return client.getHost() + ":" + client.getPort(); } }
测试效果:
说明:这里通过choose()方法轮流的去获取接口。
2)不从Eureka-Server上面获取注册列表,那么久需要自己维护一份列表
配置:
server: port: 8675 ribbon: eureka: enabled: false stores: ribbon: listOfServers: example.com, baidu.com
@GetMapping("/testRibbon") public String testRibbon() { ServiceInstance client = loadBalancerClient.choose("stores"); return client.getHost() + ":" + client.getPort(); }
测试:
五、源码解析(因为源码部分偏多,所以我这里只做主要部分讲解)
1)我们还是从LoadBalancerClient出发,跟踪实现可以发现RibbonLoadBalancerClient。
2)通过跟踪choose()方法我们可以接触到ILoadBalancer接口
package com.netflix.loadbalancer; import java.util.List; public interface ILoadBalancer {
//添加Server服务 void addServers(List<Server> var1); //选择服务 Server chooseServer(Object var1); //标注服务下线 void markServerDown(Server var1); /** @deprecated */ @Deprecated List<Server> getServerList(boolean var1); //获取可用服务 List<Server> getReachableServers(); //获取全部服务 List<Server> getAllServers(); }
3)追踪ILoadBalancer的实现类我们可以发现DynamicServerListLoadBalancer。
这个类的主要构造方法为:
public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping, ServerList<T> serverList, ServerListFilter<T> filter, ServerListUpdater serverListUpdater) { super(clientConfig, rule, ping); this.isSecure = false; this.useTunnel = false; this.serverListUpdateInProgress = new AtomicBoolean(false); this.updateAction = new UpdateAction() { public void doUpdate() { DynamicServerListLoadBalancer.this.updateListOfServers(); } }; this.serverListImpl = serverList; this.filter = filter; this.serverListUpdater = serverListUpdater; if (filter instanceof AbstractServerListFilter) { ((AbstractServerListFilter)filter).setLoadBalancerStats(this.getLoadBalancerStats()); } this.restOfInit(clientConfig); } public DynamicServerListLoadBalancer(IClientConfig clientConfig) { this.isSecure = false; this.useTunnel = false; this.serverListUpdateInProgress = new AtomicBoolean(false); this.updateAction = new UpdateAction() { public void doUpdate() { DynamicServerListLoadBalancer.this.updateListOfServers(); } }; this.initWithNiwsConfig(clientConfig); }
IRule:根据IRule的不同实现类的算法来达到处理负载均衡的策略
IPing:用于向Server发送ping信号,来判断是否正常连接
4)我们在构造方法里面跟踪都最终执行了restOfInit()方法,然后在restOfInit()中我们可以看到updateListOfServers()方法。
@VisibleForTesting public void updateListOfServers() { List<T> servers = new ArrayList(); if (this.serverListImpl != null) { servers = this.serverListImpl.getUpdatedListOfServers(); LOGGER.debug("List of Servers for {} obtained from Discovery client: {}", this.getIdentifier(), servers); if (this.filter != null) { servers = this.filter.getFilteredListOfServers((List)servers); LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}", this.getIdentifier(), servers); } } this.updateAllServerList((List)servers); }
这里的核心就是this.serverListImpl.getUpdatedListOfServers();
我们查看这里的serverListImpl发现他是ServerList接口。跟踪可以发现实现类DiscoveryEnabledNIWSServerList。
然后查看getUpdatedListOfServers()实现方法,跟踪里面的obtainServersViaDiscovery()方法。
private List<DiscoveryEnabledServer> obtainServersViaDiscovery() { List<DiscoveryEnabledServer> serverList = new ArrayList(); if (this.eurekaClientProvider != null && this.eurekaClientProvider.get() != null) { EurekaClient eurekaClient = (EurekaClient)this.eurekaClientProvider.get(); if (this.vipAddresses != null) { String[] var3 = this.vipAddresses.split(","); int var4 = var3.length; for(int var5 = 0; var5 < var4; ++var5) { String vipAddress = var3[var5]; List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, this.isSecure, this.targetRegion); Iterator var8 = listOfInstanceInfo.iterator(); while(var8.hasNext()) { InstanceInfo ii = (InstanceInfo)var8.next(); if (ii.getStatus().equals(InstanceStatus.UP)) { if (this.shouldUseOverridePort) { if (logger.isDebugEnabled()) { logger.debug("Overriding port on client name: " + this.clientName + " to " + this.overridePort); } InstanceInfo copy = new InstanceInfo(ii); if (this.isSecure) { ii = (new Builder(copy)).setSecurePort(this.overridePort).build(); } else { ii = (new Builder(copy)).setPort(this.overridePort).build(); } } DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, this.isSecure, this.shouldUseIpAddr); des.setZone(DiscoveryClient.getZone(ii)); serverList.add(des); } } if (serverList.size() > 0 && this.prioritizeVipAddressBasedServers) { break; } } } return serverList; } else { logger.warn("EurekaClient has not been initialized yet, returning an empty list"); return new ArrayList(); } }
这里最重要的就是EurekaClient,通过他去获取具体的服务的注册列表信息。而EurekaClient的实现类DiscoveryClient我们在上一章就具体讲过了,这里不赘述了。
5)另外一点,Ribbon什么时候向Eureka-Server获取Eureka-Client的信息呢。
通过在BaseLoadBalancer类中我们可以发现调用了setupPingTask()方法
void setupPingTask() { if (!this.canSkipPing()) { if (this.lbTimer != null) { this.lbTimer.cancel(); } this.lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + this.name, true); this.lbTimer.schedule(new BaseLoadBalancer.PingTask(), 0L, (long)(this.pingIntervalSeconds * 1000)); this.forceQuickPing(); } }
这里不细讲了,这里主要类为ShutdownEnabledTimer。采用定时调度的方式实现pingIntervalSeconds 为10S。
6)讲一下@LoadBalanced注解,了解Spring的应该都知道怎么去查找具体加入容器的类LoadBalancerAutoConfiguration(LoadBalanced自动装配的类)
@Bean public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) { return () -> { restTemplateCustomizers.ifAvailable((customizers) -> { Iterator var2 = this.restTemplates.iterator(); while(var2.hasNext()) { RestTemplate restTemplate = (RestTemplate)var2.next(); Iterator var4 = customizers.iterator(); while(var4.hasNext()) { RestTemplateCustomizer customizer = (RestTemplateCustomizer)var4.next(); customizer.customize(restTemplate); } } }); }; }
这里我们可以了解到这里维护了被修饰的RestTemplate类,通过customizer.customize(restTemplate);我们可以知道RestTemplate被LoadBalancerInterceptor拦截了
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); return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution)); }
这里的this.loadBalancer也就是前面的LoadBalancerClient了
7)总结:
1)Ribbon是通过LoadBalancerClient来实现负载均衡的。
2)LoadBalancerClient交给了ILoadBalancer处理
3)ILoadBalancer通过配置IRule,IPing等向Eureka-Server获取Eureka-Client列表。
4)并且没10秒ping一次Eureka-Client以保证生产者正常
5)最后在通过IRule进行负载均衡
六、源码地址:https://github.com/lilin409546297/spring-cloud/tree/master/ribbon