一、負載均衡是指將負載分攤到多個執行單元上,常見的負載均衡有兩種方式。一種是獨立進程單元,通過負載均衡策略,將請求轉發到不同的執行單元上,例如 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進行負載均衡