目錄
1.簡介
負載均衡是指將負載分攤至多個執行單元上,常見的負載均衡有如下兩種
1.服務器負載均衡.如Nginx:通過Nginx負載均衡策略,將請求轉發至後端服務,如下圖所示
2.客戶端負載均衡:以代碼的形式封裝至服務消費者服務上,消費者維護一份服務提供者信息列表,通過負載均衡策略將分攤給多個服務提供者,從而達到負載均衡目的
2.使用RestTemplate與Ribbon進行消費服務
在上一篇基礎上完成該案例演示,服務具體信息如下表所示
服務名 | 服務端口 | 作用 |
---|---|---|
eureka-server | 8761 | 註冊中心 |
eureka-client | 8762,8763 | 服務提供者 |
eureka-ribbon-client | 8764 | 負載均衡客戶端 |
-
新建服務
eureka-ribbon-client
-
引入依賴
<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>
-
配置
application.yml
spring: application: name: eureka-ribbon-client server: port: 8764 eureka: client: service-url: defaultZone: http://localhost:8761/eureka/
-
書寫啓動類
注意啓動類中需加載
RestTemplate
類@EnableEurekaClient @SpringBootApplication public class EurekaRibbonClientApplication { public static void main(String[] args) { SpringApplication.run(EurekaRibbonClientApplication.class, args); } @LoadBalanced @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } }
-
寫一個Restful API接口,用於遠程調用
8762
和8763
兩個服務@Service public class RibbonService { @Resource private RestTemplate restTemplate; public String hi(String name){ return restTemplate.getForObject("http://eureka-client/hi?name={1}", String.class, name); } }
-
啓動註冊中心、兩個服務提供者以及ribbon-client-service服務,查看註冊中心
-
訪問ribbon-client-service服務,會輪流輸出兩個服務提供者信息,表示負載均衡起作用了
3.LoadBalancerClient介紹
負載均衡器的核心類爲LoadBalancerClient,我們可以通過LoadBalancerClient控制訪問的服務,在此,新增一接口"/testLoadBalancer",通過LoadBalancerClient訪問服務提供者。
-
新增接口
@RestController public class EurekaRibbonClient { @Resource private LoadBalancerClient loadBalancerClient; @GetMapping("/testLoadBalancer") public String testRibbon() { ServiceInstance instance = loadBalancerClient.choose("eureka-client"); return instance.getHost() + ":" + instance.getPort(); } }
-
啓動服務,訪問
http://localhost:8764/testLoadBalancer
,輸出結果如下127.0.0.1:8762 127.0.0.1:8763
-
Ribbon禁止從Eureka註冊中心獲取服務註冊信息,而是自己維護服務實例列表
(1)配置application.yml
ribbon: # 禁止Ribbon從Euraka獲取註冊信息 eureka: enabled: false stores: # 設置本地服務註冊信息 ribbon: listOfServers: example.com,goole.com
(2)書寫程序
@RestController public class EurekaRibbonClient { @Resource private LoadBalancerClient loadBalancerClient; @GetMapping("/testLoadBalancer") public String testRibbon() { ServiceInstance instance = loadBalancerClient.choose("stores"); return instance.getHost() + ":" + instance.getPort(); } }
(3) 啓動工程,訪問
http://localhost:8764/testLoadBalancer
,結果展示如下example.com:80 google.com:80
-
結論
(1) Ribbon通過LoadBalancerClient從註冊中心獲取所有註冊服務信息,並緩存至Ribbon本地
(2) LoadBalancerClient的choose根據傳入的serviceId從註冊服務列表中獲取服務實例信息(ip及端口)
(3) 如果禁止Ribbon從Eureka獲取註冊列表信息,則需自己維護一份服務註冊列表信息,根據自己維護的註冊列表信息,實現負載均衡
4.Ribbon源碼簡單分析
-
Ribbon負載均衡過程
(1) 通過
LoadBalancerAutoConfiguration
類中如下代碼,維護一個RestTemplate列表,同時初始化時,給每個restTemplate對象增加一個攔截器@LoadBalanced @Autowired(required = false) private List<RestTemplate> restTemplates = Collections.emptyList();
(2) 攔截器主要對每個請求路徑進行解析,最後將解析出的serviceId(serviceName)將給LoadBalancerClient處理
@Override public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { final URI originalUri = request.getURI(); String serviceName = originalUri.getHost(); Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri); return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution)); }
(3) LoadBalancerClient則會根據傳入的serviceId獲取服務註冊列表、緩存服務註冊列表、檢測服務註冊列表是否變化、ping下游服務是否可用、根據配置的負載均衡策略進行服務調用
-
LoadBalancerClient實現功能過程
(1) 核心類
LoadBalancerClient
的choose()最終通過ILoadBalancer
的實現類DynamicServerListLoadBalancer
實現(2)
DynamicServerListLoadBalancer
構造方法中需要初始:IClientConfig
、IRule
、IPing
、ServerList
、ServerListFilter
五個屬性屬性類 作用 IClientConfig 獲取配置負載均衡客戶端 IRule 配置負載均衡策略 IPing 檢測負載均衡的服務是否可用 ServerList 獲取註冊中心服務註冊列表 ServerListFilter 根據配置去過濾或動態獲取符合條件的server列表方法 (3) 負載均衡策略
負載均衡策略的選擇是根據IRule子類完成的,默認走的是輪詢策略,常見的幾個實現類如下表所示:
類名 作用 BestAvailableRule 選擇最小請求數 ClientConfigEnabledRoundRobinRule 輪詢 RandomRule 隨機 RetryRule 根據輪詢方式重試 WeightedResponseTimeRule 根據響應時間分配權重,權重越低,被選擇可能性越低 ZoneAvoidanceRule 根據server的zone區域和可用性來輪訓選擇 IRule有3個方法,分別是choose(serviceId),setLoadBalancer(),getLoadBalancer()三個方法,分別是根據servcieId獲取服務實例信息,設置和獲取ILoadBalancer
(4) 檢測要負載均衡的服務是否可用(IPing),Iping通過其子類完成服務檢測,主要由如下幾個子類實現:
類名 作用 PingUrl 真實地去ping某個url PingConstant 固定返回某個服務是否可用,默認爲true NoOpPing 不去ping,直接返回true,即可用 DummyPing 直接返回true,並實現了initWithNiwsConfig方法 NIWSDiscoveryPing 根據DiscoveryEnabledServer的instanceInfo的InstanceStatus去判斷,如果InstanceStatus.UP,則可用,否則不可用 (5) 獲取註冊服務列表(serverList)
方法名 作用 DynamicServerListLoadBalancer構造函數 初始化上述所需屬性 DynamicServerListLoadBalancer->initWithNiwsConfig() 初始化信息 initWithNiwsConfig->restOfInit->updateListOfServers() 獲取服務註冊列表 updateListOfServers->serverListImpl.getUpdatedListOfServers() 具體獲取註冊列表服務實現 serverListImpl實現ServerList,實現類DiscoveryEnabledServer->obtainServersViaDiscovery->eurekaClientProvider.get() 獲取Eureka中註冊的服務 LegacyEurekaClientProvider->get() 具體實現 最終獲取服務列表代碼
class LegacyEurekaClientProvider implements Provider<EurekaClient> { private volatile EurekaClient eurekaClient; @Override public synchronized EurekaClient get() { if (eurekaClient == null) { eurekaClient = DiscoveryManager.getInstance().getDiscoveryClient(); } return eurekaClient; } }
(6) 負載均衡獲取服務服務註冊時間
BaseLoadBalancer構造函數中有一方法setupPingTask()
,方法具體實現如下,根據如下代碼可知,每隔10秒向EurekaClient發送一次心跳檢測。void setupPingTask() { if (canSkipPing()) { return; } if (lbTimer != null) { lbTimer.cancel(); } lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name, true); lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000); forceQuickPing(); }
(7) 查看定時任務代碼,分析心跳檢測具體實現
查看pingTask()方法,主要實現邏輯在runPinger(),而在runPinger()方法中通過pingerStrategy.pingServers(ping, allServers)
獲取服務可用性,檢測是否與之前相同,如果相同則不拉取,如果不同則調用notifyServerStatusChangeListener(changedServers);
向註冊中心拉取服務列表,最終實現本地服務註冊列表的更新class PingTask extends TimerTask { public void run() { try { new Pinger(pingStrategy).runPinger(); } catch (Exception e) { logger.error("LoadBalancer [{}]: Error pinging", name, e); } } } ---------------------------------------------------------------------------------------- public void runPinger() throws Exception { ...省略 results = pingerStrategy.pingServers(ping, allServers); final List<Server> newUpList = new ArrayList<Server>(); final List<Server> changedServers = new ArrayList<Server>(); for (int i = 0; i < numCandidates; i++) { boolean isAlive = results[i]; Server svr = allServers[i]; boolean oldIsAlive = svr.isAlive(); svr.setAlive(isAlive); if (oldIsAlive != isAlive) { changedServers.add(svr); logger.debug("LoadBalancer [{}]: Server [{}] status changed to {}", name, svr.getId(), (isAlive ? "ALIVE" : "DEAD")); } if (isAlive) { newUpList.add(svr); } } upLock = upServerLock.writeLock(); upLock.lock(); upServerList = newUpList; upLock.unlock(); notifyServerStatusChangeListener(changedServers); } finally { pingInProgress.set(false); } }
5.Ribbon配置
5.1 設置全局策略
1.創建配置類
2.根據需求創建所需策略類
public class RibbonConfiguration{
@Bean
public IRule ribbonRule(){
return new RandomRule();
}
}
5.2 定製化策略
1.創建配置類
2.自定義註解
3.啓動類中排除自定義配置類
- 配置類
@Configuration @AvoidScan public class RibbonConfiguration { @Bean public IRule ribbonRule(){ return new RandomRule(); } }
- 啓動類,排除自定義配置
@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients @RibbonClient(name="provider-service",configuration = RibbonConfiguration.class) @ComponentScan(excludeFilters = {@ComponentScan.Filter(type= FilterType.ANNOTATION,value = {AvoidScan.class})}) public class RibbonFeignApplication { public static void main(String[] args) { SpringApplication.run(RibbonFeignApplication.class, args); } }
5.3 配置方式配置策略
下表爲配置策略相關類
配置項 | 說明 |
---|---|
clientName.ribbon.NFLoadBalancerClassName | 指定ILoadBalancer的實現類 |
clientName.ribbon.NFLoadBalancerRuleClassName | 指定IRule的實現類 |
clientName.ribbon.NFLoadBalancerPingClassName | 指定IPing的實現類 |
clientName.ribbon.NFWSServerListClassName | 指定ServerList的實現類 |
clientName.ribbon.NIWSServerListFilterClassName | 指定ServerListFilter的實現類 |
provider-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
ConnectTimeout: 30000
ReadTimeout: 30000
MaxAutoRetries: 1 #對第一次請求的服務重試次數
MaxAutoRetriesNextServer: 1 #要重試的下一個服務的最大數量(不包括第一個服務)
OkToRetryOnAllOperations: true
ribbon:
eager-load: # ribbon的飢餓加載,應對第一次調用加載時間長,超時而調用不成功情況
enabled: true
clients: provider-service
6.參考資料
- 《重新定義Springcloud實戰》
- 《Springcloud微服務實戰》
- 《深入理解Spring Cloud與微服務構建》