Spring Cloud Netflix服務治理:Eureka+OpenFeign(Ribbon+Hystrix)

目錄

1 服務註冊中心

1.1 Maven依賴

1.2 配置文件

1.3 啓動類

1.4 host文件

1.5 運行及結果

2 服務提供者

2.1 Maven依賴

2.2 配置文件

2.3 啓動類

2.4 Controller

2.5 運行及結果

3 服務消費者

3.1 Maven依賴

3.2 配置文件

3.3 啓動類

3.4 Controller

3.5 Service

3.6 Model

3.7 運行及結果


Spring Cloud Eureka是Spring Cloud的服務治理組件,本文演示其服務註冊中心、服務提供者和服務消費者的搭建過程。註冊中心是Eureka的服務端,而服務提供者和消費者則是Eureka的客戶端。

Spring Cloud Ribbon是一個基於HTTP和TCP的客戶端負載均衡器,它可以負載均衡地調用服務。而Spring Cloud Hystrix是容錯保護的斷路器。當服務因爲某種原因關閉或者服務阻塞等原因而變得不能訪問的情況下,該組件可以立即返回一個錯誤響應,避免整個系統的故障蔓延。

其中的消費者採用Spring Cloud OpenFeign來實現,因爲在實際使用時,Ribbon和Hystrix幾乎是同時使用的,所以OpenFeign整合了Ribbon和Hystrix。同時提供了一種聲明式、註解式的服務調用,不用再像之前那樣使用Ribbon需要寫一個RestTemplate的封裝方法,同時加上@LoadBalanced註解開啓負載均衡。OpenFeign自己做了一些封裝,以此來簡化開發量。

筆者使用的Java版本是jdk-8u201,IDE使用的是IntelliJ IDEA 2019.2 x64,Spring Boot的版本是2.1.7.RELEASE,Spring Cloud的版本是Greenwich.SR2。


1 服務註冊中心

創建一個Spring Boot項目,命名爲eureka-server。

1.1 Maven依賴

<?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.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.hys</groupId>
    <artifactId>eureka-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>eureka-server</name>
    <description>Demo project for Spring Cloud</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</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>

1.2 配置文件

註冊中心的搭建過程可以是單點搭建,也可以是集羣搭建。單點搭建的配置文件示例代碼如下所示:

server.port=1111
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/

注意其中eureka.client.register-with-eureka和eureka.client.fetch-registry配置項一定要配置成false,前者表示不向註冊中心註冊自己,後者表示不需要檢索服務。

本文采用模擬三個節點的集羣搭建方式來確保高可用,當一個註冊中心掛了的時候,另外兩個可以繼續使用。分別創建三個配置文件application-peer1.properties、application-peer2.properties和application-peer3.properties:

spring.application.name=eureka-server
server.port=1111
eureka.instance.hostname=peer1
eureka.client.service-url.defaultZone=http://peer2:1112/eureka/,http://peer3:1113/eureka/
spring.application.name=eureka-server
server.port=1112
eureka.instance.hostname=peer2
eureka.client.service-url.defaultZone=http://peer1:1111/eureka/,http://peer3:1113/eureka/
spring.application.name=eureka-server
server.port=1113
eureka.instance.hostname=peer3
eureka.client.service-url.defaultZone=http://peer1:1111/eureka/,http://peer2:1112/eureka/

其中每個節點的eureka.client.service-url.defaultZone項都配置成其他兩個節點的服務地址,用逗號分隔。需要注意的是,和單點的搭建方式不同,這裏的eureka.client.register-with-eureka和eureka.client.fetch-registry配置項一定要配置成true,或者不寫,默認值就爲true。

1.3 啓動類

package com.hys.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }

}

其中@EnableEurekaServer註解表示這是一個Eureka的服務註冊中心,提供給其他應用進行對話。

1.4 host文件

接着在本地的host文件(C:\Windows\System32\drivers\etc)中加上下面的三行代碼,使得serviceUrl可以正確訪問:

127.0.0.1 peer1
127.0.0.1 peer2
127.0.0.1 peer3

1.5 運行及結果

然後將該項目打包成jar,通過以下的三條命令來啓動:

java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1
java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2
java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer3

注意,一開始啓動第一個jar包的時候會因爲其他兩個節點還沒有啓動而報一些錯,這都是正常的,只要先後啓動這三個jar包就可以了。在啓動第三個jar包的時候就可以發現不會報相關錯誤了。

除了上述打成jar包的方式啓動之外,也可以在IDE中啓動並通過不同的參數來區分。當三個節點都運行起來後,分別訪問http://localhost:1111/http://localhost:1112/http://localhost:1113/,可以看到Eureka的相關信息面板:

由上可以看到,每個節點都可以看到另外兩個註冊的節點,並且這些節點都在可用分片(available-replicas)之中。


2 服務提供者

創建一個Spring Boot項目,命名爲hello-service。

2.1 Maven依賴

<?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.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.hys</groupId>
    <artifactId>hello-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>hello-service</name>
    <description>Demo project for Spring Cloud</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.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</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</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>

2.2 配置文件

spring.application.name=hello-service
eureka.client.service-url.defaultZone=http://peer1:1111/eureka/,http://peer2:1112/eureka/,http://peer3:1113/eureka/

2.3 啓動類

package com.hys.helloservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HelloServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(HelloServiceApplication.class, args);
    }

}

在新版本中,不用再配置@EnableDiscoveryClient註解了。只需要引入以spring-cloud-starter-netflix爲前綴的依賴庫之後,服務發現就自動開啓了。

2.4 Controller

package com.hys.helloservice.controller;

import com.hys.helloservice.entity.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import java.util.Random;

@RestController
public class HelloController {

    @Value("${server.port}")
    private String port;

    @GetMapping("/hello")
    public String index() {
        return "port:" + port;
    }

    @GetMapping("/hello2")
    public String index2() throws InterruptedException {
        Thread.sleep(new Random().nextInt(10000));
        return "port:" + port;
    }

    @GetMapping("/hello3")
    public String hello3(@RequestParam("name") String name) {
        return name;
    }

    @GetMapping("/hello4")
    public User hello4(@RequestHeader("name") String name, @RequestHeader("age") Integer age) {
        User user = new User();
        user.setName(name);
        user.setAge(age);
        return user;
    }

    @PostMapping("/hello5")
    public String hello5(@RequestBody User user) {
        return user.getName() + user.getAge();
    }
}

其中index方法是用來測試負載均衡的方法,通過刷新頁面,查看端口號是否發生改變即可看出有沒有實現負載均衡。

而index2方法不僅是用來測試負載均衡,更多的是用來測試斷路器的效果。因爲Hystrix的默認超時時間是1秒,所以通過休眠一個小於10秒的隨機時間,如果當前休眠的時間小於1秒,則消費者能成功訪問服務;如果當前休眠的時間大於1秒,則過了1秒後,會立即返回錯誤反饋,而不會等到休眠時間到了爲止才響應(亦或是會一直阻塞下去)。

index3、index4和index5方法是用來模擬帶參數綁定的調用效果。其中需要的User代碼和下面第3.6節的User代碼是一致的,在hello-service項目中再寫一份同樣的代碼即可,也可以考慮抽取出來單獨放在一個Maven中去調用。

2.5 運行及結果

和上面相似,可以將該項目打成jar包或者直接運行IDE的方式來運行,打成jar包的命令如下所示:

java -jar hello-service-0.0.1-SNAPSHOT.jar --server.port=8081
java -jar hello-service-0.0.1-SNAPSHOT.jar --server.port=8082

啓動兩個hello-service項目,通過端口號來區分。Eureka的信息頁面可以看到相關的變化:


3 服務消費者

消費者通過OpenFeign來實現消費,創建一個Spring Boot項目,命名爲feign-consumer。

3.1 Maven依賴

<?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.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.hys</groupId>
    <artifactId>feign-consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>feign-consumer</name>
    <description>Demo project for Spring Cloud</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.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>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</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>

其中OpenFeign是在Netflix Feign的基礎上擴展了對Spring MVC的註解支持,在筆者所使用版本的Spring Cloud中已經沒有了對spring-cloud-starter-feign的依賴。

3.2 配置文件

spring.application.name=feign-consumer
server.port=9001
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/,http://localhost:1112/eureka/,http://localhost:1113/eureka/
feign.hystrix.enabled=true

其中feign.hystrix.enabled配置項默認爲false,也就是不開啓Hystrix熔斷器,需要設置成true才能開啓該功能。

3.3 啓動類

package com.hys.feignconsumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients
@SpringBootApplication
public class FeignConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(FeignConsumerApplication.class, args);
    }

}

其中@EnableFeignClients註解是用來開啓Spring Cloud Feign的支持功能。

3.4 Controller

package com.hys.feignconsumer.controller;

import com.hys.feignconsumer.entity.User;
import com.hys.feignconsumer.service.IHelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ConsumerController {

    @Autowired
    private IHelloService helloService;

    @GetMapping("/feign-consumer")
    public String helloConsumer() {
        return helloService.hello();
    }

    @GetMapping("/feign-consumer2")
    public String helloConsumer2() {
        return helloService.hello2();
    }

    @GetMapping("/feign-consumer3")
    public String helloConsumer3() {
        String hello3 = helloService.hello3("Tom");
        User hello4 = helloService.hello4("Robert Hou", 24);
        User user = new User();
        user.setName("Jerry");
        user.setAge(79);
        String hello5 = helloService.hello5(user);
        StringBuilder sb = new StringBuilder();
        String s = sb.append(hello3).append(" ").append(hello4).append(" ").append(hello5).append(" ").toString();
        return s;
    }
}

3.5 Service

package com.hys.feignconsumer.service;

import com.hys.feignconsumer.entity.User;
import com.hys.feignconsumer.service.impl.HelloServiceImplFallback;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

@FeignClient(name = "hello-service", fallback = HelloServiceImplFallback.class)
public interface IHelloService {

    @GetMapping("/hello")
    String hello();

    @GetMapping("/hello2")
    String hello2();

    @GetMapping("hello3")
    String hello3(@RequestParam("name") String name);

    @GetMapping("/hello4")
    User hello4(@RequestHeader("name") String name, @RequestHeader("age") Integer age);

    @PostMapping("/hello5")
    String hello5(@RequestBody User user);
}

其中@FeignClient註解是通過指定服務名來綁定服務,不區分大小寫,而fallback則是指定Hystrix的降級配置實現類,其代碼如下所示:

package com.hys.feignconsumer.service.impl;

import com.hys.feignconsumer.entity.User;
import com.hys.feignconsumer.service.IHelloService;
import org.springframework.stereotype.Component;

@Component
public class HelloServiceImplFallback implements IHelloService {

    @Override
    public String hello() {
        return "訪問超時,請重新再試!";
    }

    @Override
    public String hello2() {
        return "訪問超時,請重新再試!";
    }

    @Override
    public String hello3(String name) {
        return "訪問超時,請重新再試!";
    }

    @Override
    public User hello4(String name, Integer age) {
        User user = new User();
        user.setName("未知");
        user.setAge(-1);
        return user;
    }

    @Override
    public String hello5(User user) {
        return "訪問超時,請重新再試!";
    }
}

3.6 Model

package com.hys.feignconsumer.entity;

public class User {

    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

3.7 運行及結果

運行該項目,Eureka的信息頁面中可以看到該項目已經註冊進去了:

同時頁面訪問http://localhost:9001/feign-consumer,頁面結果如下所示:

不斷刷新該頁面,可以看到port一直在8081和8082之間切換(因爲默認的負載均衡是通過輪詢的方式來實現),可以驗證負載均衡是成功的。

而Hystrix斷路器的演示通過訪問http://localhost:9001/feign-consumer2,不斷刷新該頁面,當隨機的時間小於1秒的時候,頁面訪問的結果如下所示:

而如果隨機的時間大於1秒的時候,頁面的訪問結果如下所示:

頁面等了1秒左右後會出提示,由此可以看出Hystrix斷路器是可以及時得到錯誤響應的。

頁面訪問http://localhost:9001/feign-consumer3,即可看到帶參數調用的效果:

如果將其中的hello-service項目手工停掉,再來訪問該頁面,效果如下:

由上可見,Hystrix的降級配置在此依然起了作用,顯示的是我們在HelloServiceImplFallback類中配置的降級代碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章