1整體框架的說明
在本案例的框架里,我們將配置一個Eureka服務器,搭建三個提供相同服務的Eureka服務提供者,同時在Eureka服務調用者里引入Ribbon組件,這樣,當有多個url向服務調用者發起調用請求時,整個框架能按配置在IRule和IPing中的“負載均衡策略“和“判斷服務器是否可用的策略”,把這些url請求合理地分攤到多台機器上。
在下圖里,我們能看到本系統的結構圖,在其中,三個服務提供者向Eureka服務器注冊服務,而基於Ribbon的負載均衡器能有效地把請求分攤到不同的服務器上。
為了讓大家更方便地跑通這個案例,我們將講解全部的服務器、服務提供者和服務調用者部分的代碼。在下表里,列出了本架構中的所有項目。
項目名 |
說明 |
EurekaRibbonDemo-Server |
Eureka服務器 |
EurekaRibbonDemo-ServiceProviderOne EurekaRibbonDemo-ServiceProviderTwo EurekaRibbonDemo-ServiceProviderThree |
在這三個項目里,分別部署着一個相同的服務提供者 |
EurekaRibbonDemo-ServiceCaller |
服務調用者 |
2 編寫Eureka服務器
第一,在pom.xml里編寫本項目需要用到的依賴包,在其中,是通過如下的代碼引入了Eureka服務器所必需的包,關鍵代碼如下。
1 <dependency> 2 <groupId>org.springframework.cloud</groupId> 3 <artifactId>spring-cloud-starter-eureka-server</artifactId> 4 </dependency>
第二,在application.yml這個文件里,指定了針對Eureka服務器的配置,關鍵代碼如下。
1 server: 2 port: 8888 3 eureka: 4 instance: 5 hostname: localhost 6 client: 7 serviceUrl: 8 defaultZone: http://localhost:8888/eureka/
在第2和第5行里,指定了本服務器所在的主機地址和端口號是localhost:8888,在第8行里,指定了默認的url是http://localhost:8888/eureka/。
第三步,在RegisterCenterApp這個服務啟動程序里編寫啟動代碼。
1 //省略必要的package和import代碼 2 @EnableEurekaServer 3 @SpringBoot 4 Application 5 public class RegisterCenterApp 6 { 7 public static void main( String[] args ) 8 { SpringApplication.run(RegisterCenterApp.class, args); } 9 }
啟動該程序后,能在http://localhost:8888/看到該服務器的相關信息。
3 編寫Eureka服務提供者
這里有三個服務提供者,它們均是根據之前博文中的案例EurekaBasicDemo-ServiceProvider改寫而來。我們就拿EurekaRibbonDemo-ServiceProviderOne來舉例,看下其中包含的關鍵要素。
第一,同樣是在pom.xml里,引入了服務提供者程序所需的jar包,不過在其中需要適當地修改項目名。
第二,同樣是在ServiceProviderApp.java里,編寫了啟動程序,代碼不變。
第三,在application.yml里,編寫了針對這個服務提供者的配置信息,關鍵代碼如下。
1 server: 2 port: 1111 3 spring: 4 application: 5 name: sayHello 6 eureka: 7 client: 8 serviceUrl: 9 defaultZone: http://localhost:8888/eureka/
在第2行里,指定了本服務是運行在1111端口上,在另外的兩個服務提供者程序里,我們分別指定了它們的工作端口是2222和3333。
在第5行里,我們指定了服務提供者的名字是sayHello,另外兩個服務器提供者的名字同樣是sayHello,正因為它們的名字都一樣,所以服務調用者在請求服務時,負載均衡組件才能有效地分攤流量。
第四,在Controller這個控制器類里,編寫了處理url請求的邏輯,關鍵代碼如下。
1 //省略了必要的package和import的代碼 2 @RestController 3 public class Controller { 4 @RequestMapping(value = "/sayHello/{username}", method = RequestMethod.GET ) 5 public String hello(@PathVariable("username") String username) { 6 System.out.println("This is ServerProvider1"); 7 return "Hello Ribbon, this is Server1, my name is:" + username; 8 } 9 }
在第2行里,我們通過@RestController注解來說明本類承擔着“控制器”的角色。在第4行里,我們定義了觸發hello方法的url格式和Http請求的方式。在第5到第8行的hello方法里我們返回了一個字符串。請大家注意,在第6行和第7行的代碼里,我們能明顯看出輸出和返回信息是來自於1號服務提供者。
EurekaRibbonDemo-ServiceProviderTwo和EurekaRibbonDemo-ServiceProviderOne項目很相似,改動點有如下三個。
第一,在pom.xml里,把項目名修改成EurekaRibbonDemo-ServiceProviderTwo。
第二,在application.yml里,把端口號修改成2222,關鍵代碼如下所示。
1 server:
2 port: 2222
第三,在Controller.java的hello方法里,在輸出和返回信息里,打上出“Server2“的標記,關鍵代碼如下。
1 @RequestMapping(value = "/sayHello/{username}", method = RequestMethod.GET ) 2 public String hello(@PathVariable("username") String username) { 3 System.out.println("This is ServerProvider2"); 4 return "Hello Ribbon, this is Server2, my name is:" + username; 5 }
在EurekaRibbonDemo-ServiceProviderThree里,同樣在EurekaRibbonDemo-ServiceProviderOne基礎上做上述3個改動。這里需要在application.yml里,把端口號修改成3333,在Controller類中,同樣需要在輸出和返回信息中打上“Server3”的標記。
4 在Eureka服務調用者里引入Ribbon
EurekaRibbonDemo-ServiceCaller項目是根據第三章的EurekaBasicDemo-ServiceCaller改寫而來,其中的關鍵代碼如下。
第一,在pom.xml里,只是適當地修改項目名字,沒有修改其它代碼。
第二,沒有修改啟動類ServiceCallerApp.java里的代碼。
第三,在application.yml了, 添加了描述服務器列表的listOfServers屬性,代碼如下。
1 spring: 2 application: 3 name: callHello 4 server: 5 port: 8080 6 eureka: 7 client: 8 serviceUrl: 9 defaultZone: http://localhost:8888/eureka/ 10 sayHello: 11 ribbon: 12 listOfServers: 13 http://localhost:1111/,http://localhost:2222/,http://localhost:3333
在第3行,我們指定了服務調用者本身的服務名是callHello,在第5里,指定了這個微服務的是運行在8080端口上。由於服務調用者本身也能對外界提供服務,所以外部程序能根據這個服務名和端口號,以url的形式調用其中的hello方法。
這里的關鍵是在第12和13行,我們通過ribbon.listOfServers,指定了該服務調用者能獲得服務的三個url地址,請注意,這里的三個地址和上文里服務提供者發布服務的三個地址是一致的。
第四,在控制器類里,用RestTemplate對象,以負載均衡的方式調用服務,代碼如下。
1 //省略必要的package和import的代碼 2 @RestController 3 @Configuration 4 public class Controller { 5 @Bean 6 @LoadBalanced 7 public RestTemplate getRestTemplate() 8 { return new RestTemplate(); } 9 //提供服務的hello方法 10 @RequestMapping(value = "/hello", method = RequestMethod.GET ) 11 public String hello() { 12 RestTemplate template = getRestTemplate(); 13 String retVal = template.getForEntity("http://sayHello/sayHello/Eureka", String.class).getBody(); 14 return "In Caller, " + retVal; 15 } 16 }
在這個控制器類的第7行里,我們通過getRestTemplate方法返回一個RestTemplate類型對象。
RestTemplate是Spring提供的能以Rest形式訪問服務的對象,本身不具備負載均衡的能力,所以我們需要在第6行通過@LoadBalanced注解賦予它這個能力。
在第11行的hello方法里,我們首先在第12行通過getRestTemplate方法得到了template對象,隨后通過第13行的代碼,用template對象提供的getForEntity方法,訪問之前Eureka服務提供者提供的“http://sayHello/sayHello/Eureka“服務,並得到String類型的結果,最后在第14行,根據調用結果返回一個字符串。
由於在框架里,我們模擬了在三台機器上部署服務的場景,而在上述服務調用者的代碼里,我們又在template對象上加入了@LoadBalanced注解,所以在上述第13行代碼里發起的請求會被均攤到三台服務器上。
需要注意的是,這里我們沒有重寫IRule和IPing接口,所以這里是采用了默認的RoundRobbin(也就是輪詢)的訪問策略,同時將默認所有的服務器都處於可用狀態。
依次啟動本框架中的Eureka服務器,三台服務提供者和服務器調用者的服務之后,在瀏覽器里輸入http://localhost:8888/,我們能看到如下圖所示的效果。
在其中,我們能看到有三個提供服務的SAYHELLO應用實例,它們分別運行在1111,2222和3333端口上,同時,服務調用者CALLHELLO則運行在8080端口上。
如果我們不斷在瀏覽器里輸入http://localhost:8080/hello,那么能依次看到如下所示的輸出。
1 In Caller, Hello Ribbon, this is Server2, my name is:Eureka 2 In Caller, Hello Ribbon, this is Server1, my name is:Eureka 3 In Caller, Hello Ribbon, this is Server3, my name is:Eureka 4 In Caller, Hello Ribbon, this is Server2, my name is:Eureka 5 In Caller, Hello Ribbon, this is Server1, my name is:Eureka 6 In Caller, Hello Ribbon, this is Server3, my name is:Eureka 7 …
從上述輸出來看,請求是以Server2,Server1和Server3的次序被均攤到三台服務器上。在每次啟動服務后,可能承接請求的服務器次序會有所變化,可能下次是按Server1,Server2和Server3的次序,但每次都能看到“負載均衡”的效果。
5 重寫IRule和IPing接口
這里,我們將在上述案例的基礎上,重寫IRule和IPing接口里的方法,從而實現自定義負載均衡和判斷服務器是否可用的規則。
請注意,由於我們是在客戶端,也就是EurekaRibbonDemo-ServiceCaller這個項目調用服務,所以本部分的所有代碼是寫在這個項目里的。
步驟一,編寫包含負載均衡規則的MyRule.java,代碼如下。
1 package com.controller; //請注意這個package路徑 2 省略必要的import語句 3 public class MyRule implements IRule {//實現IRule類 4 private ILoadBalancer lb; 5 //必須要重寫這個choose方法 6 public Server choose(Object key) { 7 //得到0到3的一個隨機數,但不包括3 8 int number = (int)(Math.random() * 3); 9 System.out.println("Choose the number is:" + number); 10 //得到所有的服務器對象 11 List<Server> servers = lb.getAllServers(); 12 //根據隨機數,返回一個服務器 13 return servers.get(number); 14 } 15 省略必要的get和set方法 16 }
在上述代碼的第3行里,我們實現了IRule類,並在其中得第6行里,重寫了choose方法。
在這個方法里,我們在第8行通過Math.random方法,得到了0到3之間的一個隨機數,包括0,但不包括3,並用這個隨機數在第13行返回了一個Server對象,以此實現隨機選擇的效果。在實際的項目里,還可以根據具體的業務邏輯choose方法,以實現其它的“選擇服務器”的策略。
第二步,編寫判斷服務器是否可用的MyPing.java,代碼如下。
1 package com.controller; //也請注意這個package的路徑 2 省略import語句 3 public class MyPing implements IPing { //這里是實現IPing類 4 //重寫了判斷服務器是否可用的isAlive方法 5 public boolean isAlive(Server server) { 6 //這里是生成一個隨機數,以此來判斷該服務器是否可用 7 //還可以根據服務器的相應時間等依據,判斷服務器是否可用 8 double data = Math.random(); 9 if (data > 0.6) { 10 System.out.println("Current Server is available, Name:" + server.getHost() + ", Port is:" + server.getHostPort()); 11 return true; 12 } else { 13 System.out.println("Current Server is not available, Name:" + server.getHost() + ", Port is:" + server.getHostPort()); 14 return false; 15 } 16 } 17 }
在第3行里,我們是實現了IPing這個接口,並在第5行重寫了其中的isAlive方法。
在這個方法里,我們是根據一個隨機數,來判斷該服務器是否可用,如果可用,則返回true,如反之則返回false。請注意,這僅僅是個演示的案例,在實際項目里,我們基本上是不會重寫isAlive方法的。
第三步,改寫application.yml,在其中添加關於MyPing和MyRule的配置,代碼如下。
1 spring: 2 application: 3 name: callHello 4 server: 5 port: 8080 6 eureka: 7 client: 8 serviceUrl: 9 defaultZone: http://localhost:8888/eureka/ 10 sayHello: 11 ribbon: 12 NFLoadBalancerRuleClassName: com.controller.MyRule 13 NFLoadBalancerPingClassName: com.controller.MyPing 14 listOfServers: 15 http://localhost:1111/,http://localhost:2222/,http://localhost:3333
改動點是從第10行到第13行,請注意這里的SayHello需要和服務提供者給出的“服務名”一致,在第12和13行里,分別定義了本程序(也就是服務調用者)所用到的IRule和IPing類,配置時需要包含包名和文件名。
第四步,改寫Controller.java和這個控制器類,代碼如下。
1 省略必要的package和import代碼 2 @RestController 3 @Configuration 4 public class Controller { 5 //以Autowired的方式引入loadBalanceerClient對象 6 @Autowired 7 private LoadBalancerClient loadBalancerClient; 8 //給RestTemplate對象加入@LoadBalanced注解 9 //以此賦予該對象負載均衡的能力 10 @Bean 11 @LoadBalanced 12 public RestTemplate getRestTemplate() 13 { return new RestTemplate(); } 14 @Bean //引入MyRule 15 public IRule ribbonRule() 16 { return new MyRule();} 17 @Bean //引入MyPing 18 public IPing ribbonpIng() 19 { return new MyPing();} 20 //編寫提供服務的hello方法 21 @RequestMapping(value = "/hello", method = RequestMethod.GET ) 22 public String hello() { 23 //引入策略,這里的sayHello需要和application.yml 24 //第10行的sayHello一致,這樣才能引入MyPing和MyRule 25 loadBalancerClient.choose("sayHello"); 26 RestTemplate template = getRestTemplate(); 27 String retVal = template.getForEntity("http://sayHello/sayHello/Eureka", String.class).getBody(); 28 return "In Caller, " + retVal; 29 } 30 }
和之前的代碼相比,我們添加了第15行和第18行的兩個方法,以此引入自定義的MyRule和MyPing兩個方法。
而且,在hello方法的第15行里,我們通過choose方法,為loadBalancerClient這個負載均衡對象選擇了MyRule和MyPing這兩個規則。
如果依次啟動Eureka服務器,注冊在Eureka里的三個服務提供者和服務調用者之后,在瀏覽器里輸入http://localhost:8080/hello,那么能在EurekaRibbonDemo-ServiceCaller的控制台里看到類似於如下的輸出。
1 Choose the number is:1 2 Choose the number is:0 3 Current Server is not available, Name:192.168.42.1, Port is:192.168.42.1:2222 4 Current Server is available, Name:192.168.42.1, Port is:192.168.42.1:3333 5 Current Server is not available, Name:192.168.42.1, Port is:192.168.42.1:1111
第1和第2行是MyRule里的輸出,而第3到第5行是MyPing里的輸出,由於這些輸出是和隨機數有關,所以每次輸出的內容未必一致,但至少能說明,我們在MyRule和MyPing里配置的相關策略是生效的,服務調用者(EurekaRibbonDemo-ServiceCaller)的多次請求在以“負載均衡”的方式分發到各服務提供者時,會引入我們定義在上述兩個類里的策略。
本文所給出的代碼均可運行,此外,本文內容摘自本人所寫的專業書籍,轉載時請同時引入該版權申明,請勿用於商業用途。