學習筆記
作用:Ribbon主要提供客戶端的軟件負載均衡算法
簡介:Spring Cloud Ribbon是一個基於HTTP和TCP的客戶端負載均衡工具,它基於Netflix Ribbon實現。通過Spring Cloud的封裝,可以讓我們輕松地將面向服務的REST模板請求自動轉換成客戶端負載均衡的服務調用。
通過Spring Cloud Ribbon的封裝,我們在微服務架構中使用客戶端負載均衡調用非常簡單,只需要如下兩步:
1.服務提供者只需要啟動多個服務實例並注冊到一個注冊中心或是多個相關聯的服務注冊中心。
2.服務消費者直接通過調用被@LoadBalanced注解修飾過的RestTemplate來實現面向服務的接口調用。
@Bean //把RestTemplate注冊到SpringBoot容器中,如果使用rest方式以別名方式進行調用,依賴ribbon負載均衡器@LoadBalanced @LoadBalanced //@LoadBalanced開啟以別名方式去Eureka讀取注冊信息,然后本地實現rpc遠程調用 RestTemplate restTemplate() { return new RestTemplate(); }
修改負載均衡策略配置
服務提供者別名:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
內置負載均衡規則類 | 規則描述 |
RoundRobinRule | 簡單輪詢服務列表來選擇服務器。它是Ribbon默認的負載均衡規則。 |
AvailabilityFilteringRule | 對以下兩種服務器進行忽略: (1)在默認情況下,這台服務器如果3次連接失敗,這台服務器就會被設置為“短路”狀態。短路狀態將持續30秒,如果再次連接失敗,短路的持續時間就會幾何級地增加。 (2)並發數過高的服務器。如果一個服務器的並發連接數過高,配置了AvailabilityFilteringRule規則的客戶端也會將其忽略。並發連接數的上線,可以由客戶端的進行配置。 |
WeightedResponseTimeRule | 為每一個服務器賦予一個權重值。服務器響應時間越長,這個服務器的權重就越小。這個規則會隨機選擇服務器,這個權重值會影響服務器的選擇。 |
ZoneAvoidanceRule | 以區域可用的服務器為基礎進行服務器的選擇。使用Zone對服務器進行分類,這個Zone可以理解為一個機房、一個機架等。 |
BestAvailableRule | 忽略那些短路的服務器,並選擇並發數較低的服務器。 |
RandomRule | 隨機選擇一個可用的服務器。 |
Retry | 重試機制的選擇邏輯 |
Ribbon源碼分析
LoadBalancerAutoConfiguration.java為實現客戶端負載均衡器的自動化配置類。
@Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { return restTemplate -> { List<ClientHttpRequestInterceptor> list = new ArrayList<>( restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); }; }
在自動化配置中主要做三件事:
- 創建一個LoadBalancerInterceptor的Bean,用於實現對客戶端發起請求時進行攔截,以實現客戶端負載均衡。
- 創建一個RestTemplateCustomizer的Bean,用於給RestTemplate增加LoadBalancerInterceptor攔截器。
- 維護了一個被@LoadBalanced注解修飾的RestTemplate對象列表,並在這里進行初始化,通過調用RestTemplateCustomizer的實例來給需要客戶端負載均衡的RestTemplate增加LoadBalancerInterceptor攔截器。
從@LoadBalanced注解碼的注釋中,可以知道該注解用來給RestTemplate標記,以使用負載均衡的客戶端(LoadBalancerClient)來配置它。
LoadBalancerClient
public interface LoadBalancerClient extends ServiceInstanceChooser { <T> T execute(String var1, LoadBalancerRequest<T> var2) throws IOException; // 從負載均衡器中挑選出的服務實例來執行請求內容。 <T> T execute(String var1, ServiceInstance var2, LoadBalancerRequest<T> var3) throws IOException; // 為了給一些系統使用,創建一個帶有真實host和port的URI。 // 一些系統使用帶有原服務名代替host的URI,比如http://myservice/path/to/service。 // 該方法會從服務實例中取出host:port來替換這個服務名。 URI reconstructURI(ServiceInstance var1, URI var2); }
父接口ServiceInstanceChooser
public interface ServiceInstanceChooser { /** * Choose a ServiceInstance from the LoadBalancer for the specified service * @param serviceId the service id to look up the LoadBalancer * @return a ServiceInstance that matches the serviceId */ // 根據傳入的服務名serviceId,從負載均衡器中挑選一個對應服務的實例。 ServiceInstance choose(String serviceId); }
RibbonLoadBalancerClient 實現類代碼
@Override public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException { // 獲取負載均衡策略 ILoadBalancer loadBalancer = getLoadBalancer(serviceId); // 根據負載均衡策略,獲取一個服務器 Server server = getServer(loadBalancer); if (server == null) { throw new IllegalStateException("No instances available for " + serviceId); } RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server)); return execute(serviceId, ribbonServer, request); }
ILoadBalancer接口
public interface ILoadBalancer { //向負載均衡器的實例列表中增加實例 public void addServers(List<Server> newServers); //通過某種策略,從負載均衡器中選擇一個具體的實例 public Server chooseServer(Object key); //用來通知和標識負載均衡器中某個具體實例已經停止服務,不然負載均衡器在下一次獲取服務實例清單前都會認為服務實例均是正常服務的。 public void markServerDown(Server server); //獲取正常服務列表 public List<Server> getReachableServers(); //所有已知實例列表 public List<Server> getAllServers(); }
LoadBalancerContext類中實現
// 轉換host:port形式的請求地址。 public URI reconstructURIWithServer(Server server, URI original) { String host = server.getHost(); int port = server.getPort(); String scheme = server.getScheme(); if (host.equals(original.getHost()) && port == original.getPort() && scheme == original.getScheme()) { return original; } if (scheme == null) { scheme = original.getScheme(); } if (scheme == null) { scheme = deriveSchemeAndPortFromPartialUri(original).first(); } try { StringBuilder sb = new StringBuilder(); sb.append(scheme).append("://"); if (!Strings.isNullOrEmpty(original.getRawUserInfo())) { sb.append(original.getRawUserInfo()).append("@"); } sb.append(host); if (port >= 0) { sb.append(":").append(port); } sb.append(original.getRawPath()); if (!Strings.isNullOrEmpty(original.getRawQuery())) { sb.append("?").append(original.getRawQuery()); } if (!Strings.isNullOrEmpty(original.getRawFragment())) { sb.append("#").append(original.getRawFragment()); } URI newURI = new URI(sb.toString()); return newURI; } catch (URISyntaxException e) { throw new RuntimeException(e); } }
模仿Ribbon本地負載均衡效果
注冊中心
pom.xml
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> </parent> <!-- 管理依賴 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.M7</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- SpringCloud eureka-server --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies> <!-- 注意:這里必須要添加,否則各種依賴有問題 --> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/libs-milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>
application.yml
###服務端口號
server:
port: 8100
eureka:
instance:
###注冊中心ip地址
hostname: 127.0.0.1
client:
service-url:
###注冊中心地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
###因為自己是注冊中心,是否需要將自己注冊給自己的注冊中心(集群的時候是需要是為true)
register-with-eureka: false
###因為自己是注冊中心,不需要去檢索服務信息
fetch-registry: false
server:
###測試時關閉自我保護機制,保證不可用服務及時踢出
enable-self-preservation: false
###剔除失效服務間隔
eviction-interval-timer-in-ms: 2000
主類
@SpringBootApplication @EnableEurekaServer //表示開啟EurekaServer服務,開啟注冊中心 public class AppEureka { public static void main(String[] args) { SpringApplication.run(AppEureka.class, args); } }
服務提供者
pom.xml
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> </parent> <!-- 管理依賴 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.M7</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- SpringBoot整合Web組件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- SpringBoot整合eureka客戶端 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> <!-- 注意:這里必須要添加,否則各種依賴有問題 --> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/libs-milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>
application.yml
###會員項目的端口號
server:
port: 8040
###服務別名(服務注冊到注冊中心名稱)
spring:
application:
name: app-hclz-member
eureka:
client:
service-url:
###當前會員服務注冊到eureka服務地址
defaultZone: http://localhost:8100/eureka
###需要將我的服務注冊到eureka上
register-with-eureka: true
###需要檢索服務
fetch-registry: true
主類
@SpringBootApplication @EnableEurekaClient //將當前服務注冊到eureka上 public class AppMember { public static void main(String[] args) { SpringApplication.run(AppMember.class, args); } }
控制類
@RestController public class MemberApiController { @Value("${server.port}") private String serverPort; @RequestMapping("/getMember") public String getMember() { return "this is member,我是會員服務,springcloud2.0版本! 會員項目端口號:"+serverPort; } }
服務消費者
pom.xml(與服務提供者一致)
application.yml
###訂單服務的端口號
server:
port: 8001
###服務別名(服務注冊到注冊中心名稱)
spring:
application:
name: app-hclz-order
eureka:
client:
service-url:
###當前會員服務注冊到eureka服務地址
defaultZone: http://localhost:8100/eureka
###需要將我的服務注冊到eureka上
register-with-eureka: true
###需要檢索服務
fetch-registry: true
主類
@SpringBootApplication @EnableEurekaClient public class AppOrder { public static void main(String[] args) { SpringApplication.run(AppOrder.class, args); } @Bean //把RestTemplate注冊到SpringBoot容器中,如果使用rest方式以別名方式進行調用,依賴ribbon負載均衡器@LoadBalanced //@LoadBalanced //模仿Ribbon本地負載均衡效果將此注解注釋掉 RestTemplate restTemplate() { return new RestTemplate(); } }
//模仿Ribbon本地負載均衡效果 @RestController public class MockRibbonController { //可以獲取注冊中心上的服務列表 @Autowired private DiscoveryClient discoveryClient; @Autowired private RestTemplate restTemplate; //初始化接口的請求總數 private int reqCount = 1; @RequestMapping("/ribbonMember") public String ribbonMember() { //1.獲取對應服務器遠程調用地址 String instancesUrl = getInstances() + "/getMember"; System.out.println("************************instancesUrl:" + instancesUrl); //2.可以直接使用httpClient技術實現遠程調用 String result = restTemplate.getForObject(instancesUrl,String.class); System.out.println("-----------------------"+result); return result; } private String getInstances() { List<ServiceInstance> instances = discoveryClient.getInstances("app-hclz-member"); if(instances == null || instances.size() <= 0) { return null; } //獲取服務器集群個數 int instanceSize = instances.size(); //請求總數 % 服務器集群數量 得到下標 int serviceIndex = reqCount % instanceSize; //將請求總數增加1 reqCount++; return instances.get(serviceIndex).getUri().toString(); } }
分別啟動注冊中心,多個端口的服務提供者,服務消費者
Ribbon與Nginx區別
Ribbon本地負載均衡,原理:在調用接口的時候,會在eureka注冊中心上獲取注冊信息服務列表,獲取到之后,緩存在jvm本地,使用本地實現rpc遠程調用技術進行調用。即客戶端實現負載均衡。
Nginx服務器負載均衡,客戶端所有請求都會交給nginx,然后再由nginx實現轉發請求。即服務端實現負載均衡。
應用場景:
Ribbon本地負載均衡器適合在微服務rpc遠程調用,比如Dubbo、SpringCloud
Nginx服務負載均衡器適合針對服務器端,比如Tomcat、Jetty