Spring Cloud 系列之 Consul 註冊中心(一)

Netflix Eureka 2.X https://github.com/Netflix/eureka/wiki 官方宣告停止開發,但其實對國內的用戶影響甚小,一方面國內大都使用的是 Eureka 1.X 系列,並且官方也在積極維護 1.X https://github.com/Netflix/eureka/releases。

The existing open source work on eureka 2.0 is discontinued. The code base and artifacts that were released as part of the existing repository of work on the 2.x branch is considered use at your own risk.

Eureka 1.x is a core part of Netflix’s service discovery system and is still an active project.

翻譯:

有關 eureka 2.0 的現有開源工作已停止。在 2.x 分支上作爲現有工作資料庫的一部分發布的代碼庫和工件被視爲使用後果自負。

Eureka 1.x 是 Netflix 服務發現系統的核心部分,仍然是一個活躍的項目。

雖然 Eureka,Hystrix 等不再繼續開發或維護,但是目前來說不影響使用,不管怎麼說感謝開源,向 Netflix 公司的開源致敬。

另一方面 Spring Cloud 支持很多服務發現的軟件,Eureka 只是其中之一,比如我們今天要講的主角 Consul。下面是 Spring Cloud 支持的服務發現軟件以及特性對比。

常見的註冊中心

  • Netflix Eureka
  • Alibaba Nacos
  • HashiCorp Consul
  • Apache ZooKeeper
  • CoreOS Etcd
  • CNCF CoreDNS

特性 Eureka Nacos Consul Zookeeper
CAP AP CP + AP CP CP
健康檢查 Client Beat TCP/HTTP/MYSQL/Client Beat TCP/HTTP/gRPC/Cmd Keep Alive
雪崩保護
自動註銷實例 支持 支持 不支持 支持
訪問協議 HTTP HTTP/DNS HTTP/DNS TCP
監聽支持 支持 支持 支持 支持
多數據中心 支持 支持 支持 不支持
跨註冊中心同步 不支持 支持 支持 不支持
SpringCloud集成 支持 支持 支持 支持

Consul 介紹

Consul 是 HashiCorp 公司推出的開源工具,用於實現分佈式系統的服務發現與配置。與其它分佈式服務註冊與發現的方案,Consul 的方案更“一站式”,內置了服務註冊與發現框架、分佈一致性協議實現、健康檢查、Key/Value 存儲(配置中心)、多數據中心方案,不再需要依賴其它工具(比如 ZooKeeper 等),使用起來也較爲簡單。

Consul 使用 Go 語言編寫,因此具有天然可移植性(支持Linux、Windows 和 Mac OS);安裝包僅包含一個可執行文件,方便部署,與 Docker 等輕量級容器可無縫配合。

Consul 特性

  • Raft 算法

  • 服務發現

  • 健康檢查

  • Key/Value 存儲(配置中心)

  • 多數據中心

  • 支持 http 和 dns 協議接口

  • 官方提供 web 管理界面

Consul 角色

點擊鏈接觀看:Consul 角色視頻(獲取更多請關注公衆號「哈嘍沃德先生」)

  • client:客戶端,無狀態,將 HTTP 和 DNS 接口請求轉發給局域網內的服務端集羣。
  • server:服務端,保存配置信息,高可用集羣,每個數據中心的 server 數量推薦爲 3 個或者 5 個。

首先,圖中有兩個數據中心,分別爲 Datacenter1 和 Datacenter2 。Consul 非常好的支持多個數據中心,每個數據中心內,有客戶端和服務器端,服務器一般爲 3~5 個,這樣可以在穩定和性能上達到平衡,因爲更多的機器會使數據同步很慢。不過客戶端是沒有限制的,可以有成千上萬個。

數據中心內的所有節點都會加入到 Gossip (流言)協議。這就意味着有一個 Gossip 池,其中包含這個數據中心所有的節點。客戶端不需要去配置服務器地址信息,發現服務工作會自動完成。檢測故障節點的工作不是放在服務器端,而是分佈式的;這使得失敗檢測相對於本地化的心跳機制而言,更具可拓展性。在選擇 leader 這種重要的事情發生的時候,數據中心被用作消息層來做消息廣播。

每個數據中心內的服務器都是單個 Raft 中節點集的一部分。這意味着他們一起工作,選擇一個單一的領導者——一個具有額外職責的選定的服務器。leader 負責處理所有查詢和事物。事物也必須作爲同步協議的一部分複製到節點集中的所有節點。由於這個要求,當非 leader 服務器接收到 RPC 請求時,就會將請求其轉發給集羣 leader。

服務器端節點同時也作爲 WAN Gossip 池的一部分,WAN 池和 LAN 池不同的是,它針對網絡高延遲做了優化,而且只包含其他Consul 服務器的節點。這個池的目的是允許數據中心以最少的消耗方式發現對方。啓動新的數據中心與加入現有的 WAN Gossip 一樣簡單。因爲這些服務器都在這個池中運行,它還支持跨數據中心請求。當服務器收到對不同數據中心的請求時,它會將其轉發到正確數據中心中的隨機服務器。那個服務器可能會轉發給本地的 leader。

這樣會使數據中心的耦合非常低。但是由於故障檢測,連接緩存和複用,跨數據中心請求相對快速可靠。

總的來說,數據不會在不同的數據中心之間做複製備份。當收到一個請求處於別的數據中心的資源時,本地的 Consul 服務器會發一個 RPC 請求到遠端的 Consul 服務器,然後返回結果。如果遠端數據中心處於不可用狀態,那麼這麼資源也會不可用,但這不影響本地的數據中心。在一些特殊的情況下,有限的數據集會被跨數據中心複製備份,比如說 Consul 內置的 ACL 複製能力,或者像 consul-replicate 這樣的外部工具。

Consul 工作原理

服務發現以及註冊

當服務 Producer 啓動時,會將自己的 Ip/host 等信息通過發送請求告知 Consul,Consul 接收到 Producer 的註冊信息後,每隔 10s(默認)會向 Producer 發送一個健康檢查的請求,檢驗 Producer 是否健康。

服務調用

當 Consumer 請求 Product 時,會先從 Consul 中拿到存儲 Product 服務的 IP 和 Port 的臨時表(temp table),從temp table 表中任選一個· Producer 的 IP 和 Port, 然後根據這個 IP 和 Port,發送訪問請求;temp table 表只包含通過了健康檢查的 Producer 信息,並且每隔 10s(默認)更新。

Consul 安裝

Eureka 其實就是個 Servlet 程序,跑在 Servlet 容器中;Consul 則是用 go 語言編寫的第三方工具需要單獨安裝使用。

下載

訪問 Consul 官網:https://www.consul.io 下載 Consul 的最新版本。

支持多種環境安裝,截圖中只顯示了部分環境。

安裝

爲了讓大家學習到不同環境的安裝,單節點我們在 Windows 安裝,集羣環境在 Linux 安裝。

單節點

壓縮包中就只有一個 consul.exe 的執行文件。

cd 到對應的目錄下,使用 cmd 啓動 Consul

# -dev表示開發模式運行,另外還有 -server 表示服務模式運行
consul agent -dev -client=0.0.0.0

爲了方便啓動,也可以在 consul.exe 同級目錄下創建一個腳本來啓動,腳本內容如下:

consul agent -dev -client=0.0.0.0
pause

訪問管理後臺:http://localhost:8500/ 看到下圖意味着我們的 Consul 服務啓動成功了。

Consul 入門案例

點擊鏈接觀看:Consul 入門案例視頻(獲取更多請關注公衆號「哈嘍沃德先生」)

consul-demo 聚合工程。SpringBoot 2.2.4.RELEASESpring Cloud Hoxton.SR1

創建項目

我們創建聚合項目來講解 Consul,首先創建一個 pom 父工程。

添加依賴

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>

    <!-- 項目座標地址 -->
    <groupId>com.example</groupId>
    <!-- 項目模塊名稱 -->
    <artifactId>consul-demo</artifactId>
    <!-- 項目版本名稱 快照版本SNAPSHOT、正式版本RELEASE -->
    <version>1.0-SNAPSHOT</version>

    <!-- 繼承 spring-boot-starter-parent 依賴 -->
    <!-- 使用繼承方式,實現複用,符合繼承的都可以被使用 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.4.RELEASE</version>
    </parent>

    <!--
        集中定義依賴組件版本號,但不引入,
        在子工程中用到聲明的依賴時,可以不加依賴的版本號,
        這樣可以統一管理工程中用到的依賴版本
     -->
    <properties>
        <!-- Spring Cloud Hoxton.SR1 依賴 -->
        <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
    </properties>

    <!-- 項目依賴管理 父項目只是聲明依賴,子項目需要寫明需要的依賴(可以省略版本信息) -->
    <dependencyManagement>
        <dependencies>
            <!-- spring cloud 依賴 -->
            <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>

</project>

服務提供者 service-provider

創建項目

在剛纔的父工程下創建一個 service-provider 服務提供者的項目。

添加依賴

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>

    <groupId>com.example</groupId>
    <artifactId>service-provider</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 繼承父依賴 -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>consul-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <!-- 項目依賴 -->
    <dependencies>
        <!-- spring cloud consul 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!-- spring boot actuator 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- spring boot web 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- lombok 依賴 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <!-- spring boot test 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
  
</project>

配置文件

application.yml

server:
  port: 7070 # 端口

spring:
  application:
    name: service-provider # 應用名稱
  # 配置 Consul 註冊中心
  cloud:
    consul:
      # 註冊中心的訪問地址
      host: localhost
      port: 8500
      # 服務提供者信息
      discovery:
        register: true                                # 是否需要註冊
        instance-id: ${spring.application.name}-01    # 註冊實例 id(必須唯一)
        service-name: ${spring.application.name}      # 服務名稱
        port: ${server.port}                          # 服務端口
        prefer-ip-address: true                       # 是否使用 ip 地址註冊
        ip-address: ${spring.cloud.client.ip-address} # 服務請求 ip

實體類

Product.java

package com.example.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {

    private Integer id;
    private String productName;
    private Integer productNum;
    private Double productPrice;

}

編寫服務

ProductService.java

package com.example.service;

import com.example.pojo.Product;

import java.util.List;

/**
 * 商品服務
 */
public interface ProductService {

    /**
     * 查詢商品列表
     *
     * @return
     */
    List<Product> selectProductList();

}

ProductServiceImpl.java

package com.example.service.impl;

import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;

/**
 * 商品服務
 */
@Service
public class ProductServiceImpl implements ProductService {

    /**
     * 查詢商品列表
     *
     * @return
     */
    @Override
    public List<Product> selectProductList() {
        return Arrays.asList(
                new Product(1, "華爲手機", 1, 5800D),
                new Product(2, "聯想筆記本", 1, 6888D),
                new Product(3, "小米平板", 5, 2020D)
        );
    }

}

控制層

ProductController.java

package com.example.controller;

import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    /**
     * 查詢商品列表
     *
     * @return
     */
    @GetMapping("/list")
    public List<Product> selectProductList() {
        return productService.selectProductList();
    }

}

該項目我們可以通過單元測試進行測試,也可以直接通過 url 使用 postman 或者瀏覽器來進行測試。

啓動類

ServiceProviderApplication.java

package com.example;

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

@SpringBootApplication
public class ServiceProviderApplication {

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

}

訪問

訪問管理後臺:http://localhost:8500/ 看到下圖意味着我們的服務註冊至註冊中心了。

service-provider 項目複製一份修改端口爲 7071 ,註冊實例 id 爲 02。

spring:
  application:
    name: service-provider # 應用名稱
  # 配置 Consul 註冊中心
  cloud:
    consul:
      # 註冊中心的訪問地址
      host: localhost
      port: 8500
      # 服務提供者信息
      discovery:
        register: true                                # 是否需要註冊
        instance-id: ${spring.application.name}-02    # 註冊實例 id(必須唯一)
        service-name: ${spring.application.name}      # 服務名稱
        port: ${server.port}                          # 服務端口
        prefer-ip-address: true                       # 是否使用 ip 地址註冊
        ip-address: ${spring.cloud.client.ip-address} # 服務請求 ip

# 端口
server:
  port: 8602

啓動 service-provider02 結果如下:

服務消費者 service-consumer

創建項目

在剛纔的父工程下創建一個 service-consumer 服務消費者的項目。

添加依賴

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>

    <groupId>com.example</groupId>
    <artifactId>service-consumer</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 繼承父依賴 -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>consul-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <!-- 項目依賴 -->
    <dependencies>
        <!-- spring cloud consul 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!-- spring boot actuator 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- spring boot web 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- lombok 依賴 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <!-- spring boot test 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

</project>

配置文件

application.yml

server:
  port: 9090 # 端口

spring:
  application:
    name: service-consumer # 應用名稱
  # 配置 Consul 註冊中心
  cloud:
    consul:
      # 註冊中心的訪問地址
      host: localhost
      port: 8500
      # 服務提供者信息
      discovery:
        register: false                               # 是否需要註冊
        instance-id: ${spring.application.name}-01    # 註冊實例 id(必須唯一)
        service-name: ${spring.application.name}      # 服務名稱
        port: ${server.port}                          # 服務端口
        prefer-ip-address: true                       # 是否使用 ip 地址註冊
        ip-address: ${spring.cloud.client.ip-address} # 服務請求 ip

實體類

Product.java

package com.example.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {

    private Integer id;
    private String productName;
    private Integer productNum;
    private Double productPrice;

}

Order.java

package com.example.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order implements Serializable {

    private Integer id;
    private String orderNo;
    private String orderAddress;
    private Double totalPrice;
    private List<Product> productList;

}

消費服務

OrderService.java

package com.example.service;

import com.example.pojo.Order;

public interface OrderService {

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    Order selectOrderById(Integer id);

}

OrderServiceImpl.java

package com.example.service.impl;

import com.example.pojo.Order;
import com.example.pojo.Product;
import com.example.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    @Override
    public Order selectOrderById(Integer id) {
        return new Order(id, "order-001", "中國", 22788D,
                selectProductListByLoadBalancerAnnotation());
    }

    private List<Product> selectProductListByLoadBalancerAnnotation() {
        // ResponseEntity: 封裝了返回數據
        ResponseEntity<List<Product>> response = restTemplate.exchange(
                "http://service-provider/product/list",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {
                });
        return response.getBody();
    }

}

控制層

OrderController.java

package com.example.controller;

import com.example.pojo.Order;
import com.example.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
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;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public Order selectOrderById(@PathVariable("id") Integer id) {
        return orderService.selectOrderById(id);
    }

}

啓動類

ServiceConsumerApplication.java

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class ServiceConsumerApplication {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

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

}

訪問

訪問:http://localhost:9090/order/1 結果如下:

下一篇我們講解 Consul 集羣環境的搭建。記得關注噢~

本文采用 知識共享「署名-非商業性使用-禁止演繹 4.0 國際」許可協議

大家可以通過 分類 查看更多關於 Spring Cloud 的文章。


🤗 您的點贊轉發是對我最大的支持。

📢 掃碼關注 哈嘍沃德先生「文檔 + 視頻」每篇文章都配有專門視頻講解,學習更輕鬆噢 ~

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