本範例介紹使用Spring Cloud Netflix Eureka建構Eureka服務發現(Service Discovery),多個Eureka Client服務實例(Service instances),搭配Spring Cloud Netflix Ribbon並透過RestTemplate
來進行服務間溝通的客戶端負載平衡(Client side load balancing)。(很饒口,看過就好)
本篇接續Spring Cloud Eureka 使用RestTemplate實作服務間溝通範例二這篇來做修改。
在前篇範例中已建立了一個Eureka Server專案 與 兩個Eureka Client專案:一是Message服務專案,一是Member服務專案。共三個專案。
前篇在Member服務實例中使用RestTemplate
與Message服務進行溝通。Member服務與Message服務的實例都只有一個;而為了顯示Ribbon的客戶端負載平衡效果,在本篇會啟動兩個Message服務實例。
因為要使用Ribbon實現負載平衡,專案必須引入Ribbon的Maven dependency spring-cloud-starter-netflix-ribbon
。不過Eureka Client的Maven dependency spring-cloud-starter-netflix-eureka-client
已經依賴了spring-cloud-starter-netflix-ribbon
,因此就不用在Member服務的pom.xml
進行設定了。
所以Member服務的pom.xml
內容跟之前一樣如下。
Member service - pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.abc</groupId>
<artifactId>member</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>member</name>
<description>Member Service</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR1</spring-cloud.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在前篇Member服務的SpringBootApplication類別MemberApplication
中設定了RestTemplate
的Bean,這邊加上@LoadBalanced
來讓RestTemplate
的Bean可使用LoadBalancerClient
的實例RibbonLoadBalancerClient
提供的負載平衡功能。
Member service - MemberApplication
package com.abc.member;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class MemberApplication {
public static void main(String[] args) {
SpringApplication.run(MemberApplication.class, args);
}
@Bean
@LoadBalanced // 使RestTemplate自動配置成支援Ribbon
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
}
前篇範例中,Member服務的MemberController
中使用注入的RestTemplate
實例與Message服務溝通是以Message服務的服務名稱(即pom.xml
中的spring.application.name
的設定值message-serivice
)透過EurekaClient.getNextServerFromEureka().getHomePageUrl()
取得Message的服務位址host name。
但本篇的RestTemplate
的Bean已配置Ribbon,所以當呼叫Message服務實例的API時,URI要改用virtual host name,即message-service
。
Member service - MemberController
package com.abc.member.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClient;
@RestController
@RequestMapping("members")
public class MemberController {
// @Autowired
// private EurekaClient eurekaClient;
@Autowired
private RestTemplate restTemplate;
@GetMapping(value = "/{memberId}", produces=MediaType.APPLICATION_JSON_UTF8_VALUE)
public String getMemberMessages(@PathVariable int memberId) throws JsonProcessingException {
String url = new StringBuilder("http://message-service") // 用 virtual host name "message-service"
.append("/messages/")
.append(memberId).toString();
final String response = restTemplate.getForObject(url, String.class);
return response;
}
// /**
// * 取得服務位址
// * @param serviceName 服務名稱
// * @return
// */
// public String getServiceUrl(String serviceName) {
// InstanceInfo instanceInfo = eurekaClient.getNextServerFromEureka(serviceName, false);
// return instanceInfo.getHomePageUrl();
// }
}
到此便完成了Member專案的修改。
下面接著修改Message專案。
將Message專案的application.yml
修改如下。
Message service - application.yml
# Spring properties
spring:
application:
name: message-service
# Discovery Server Access
eureka:
client:
service-url:
default-zone: http://localhost:8761/eureka/
instance:
instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}} # 隨機產生instance-id
# HTTP Server
server:
port: 0 # Use a Random Unassigned HTTP Port 使用未被使用的隨機port號
上面把Message服務的instanceId改為隨機產生。Eureka Client的每一個服務的實例都需要有唯一的識別id,本範例需要在本機(localhost)啟動兩個Message服務實例,實例的InstanceId不能重複,所以這裡instanceId名稱是用隨機產生。
除了服務的intanceId必須是唯一,又因為是在本機跑多個服務,每個服務的port號也不能相同,所以設servce.port=0
來隨機分派未使用的port號給服務使用。
修改MessageController
。這邊只多了取得instanceId的動作,讓我們能夠看到當Member服務呼叫Message服務的API時是在呼叫哪一個Message服務的實例。
Message service - MessageController
package com.abc.message.controller;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@RestController
@RequestMapping("messages")
public class MessageController {
@Value("${eureka.instance.instance-id}")
private String instanceId; // 注入application.yml的eureka.instance.instance-id的值
@GetMapping(value = "/{memberId}", produces=MediaType.APPLICATION_JSON_UTF8_VALUE)
public String getAllMessagesByMemberId(@PathVariable int memberId) throws JsonProcessingException {
List<String> messages = findMessagesByMemberId(memberId);
messages.add(instanceId); // 放入instanceId
String jsonString = new ObjectMapper().writeValueAsString(messages);
return jsonString;
}
private List<String> findMessagesByMemberId(int memberId) {
Map<Integer, List<String>> allMessages = new HashMap<>();
allMessages.put(1, new ArrayList<String>(Arrays.asList("謝謝大大無私地分享", "樓主一生平安")));
allMessages.put(2, new ArrayList<String>(Arrays.asList("樓主好人,純推不下", "祝大大平安喜樂")));
return allMessages.get(memberId);
}
}
到此便完成所有修改。
接著來測試Ribbon的效果。
依順序啟動Eureka Server專案 -> Message服務專案(1) -> Message服務專案(2) -> Member服務專案。
注意上面啟動了兩次Message專案,意思就是Message服務有兩個實例。
啟動後可以在Eureka Discover Server(http://localhost:8761/
)的UI看到註冊了三個Eureka Client;一個Member service,兩個Message service。
由於Message服務實例的instance-id是隨機產生的,所以名稱是message-serivce
後街一段隨機產生的字串。
在瀏覽器位址輸入http://localhost:2223/members/1
來呼叫Member服務的API,若成功會在畫面顯示如下結果。
如果再重新發送一次請求,可以看到實例的名稱改變了,這就是Ribbon負載平衡的效果。Ribbon預設使用Round Robin Rule(Round-robin 輪循演算法)來選擇導向的服務實例
切換效果。不斷重新整理頁面發送相同的請求,讓Member服務重複去呼叫Message服務。
運作方式如下。
不過有個問題我搞不清楚,為什麼Eureka Server UI的Message服務的實例名稱與MessageController
中取得的instanceId
不同?
參考:
- Client Side Load Balancing with Ribbon and Spring Cloud
- Spring RestTemplate as a Load Balancer Client
- Difference between @RibbonClient and @LoadBalanced
- Client Side Load Balancer: Ribbon
- Ribbon with Spring Cloud and Eureka: java.lang.IllegalStateException: No instances available for Samarths-MacBook-Pro.local
沒有留言:
張貼留言