網頁

2019/7/25

Spring Cloud Netflix Eureka + Ribbon 使用Load-balanced RestTemplate實作客戶端負載平衡服務溝通

本範例介紹使用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 RuleRound-robin 輪循演算法)來選擇導向的服務實例



切換效果。不斷重新整理頁面發送相同的請求,讓Member服務重複去呼叫Message服務。




運作方式如下。



不過有個問題我搞不清楚,為什麼Eureka Server UI的Message服務的實例名稱與MessageController中取得的instanceId不同?


參考:

沒有留言:

張貼留言