1. 程式人生 > >微服務系列之 Consul 註冊中心

微服務系列之 Consul 註冊中心


原文連結:`https://mrhelloworld.com/posts/spring/spring-cloud/consul-service-registry/`
  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 角色    - client:客戶端,無狀態,將 HTTP 和 DNS 介面請求轉發給區域網內的服務端叢集。 - server:服務端,儲存配置資訊,高可用叢集,每個資料中心的 server 數量推薦為 3 個或者 5 個。    ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/consul-arch-420ce04a.png)   首先,圖中有兩個資料中心,分別為 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 工作原理    ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/consol_service.png)    ### 服務發現以及註冊      當服務 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 的最新版本。 ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578478694286.png)   支援多種環境安裝,截圖中只顯示了部分環境。 ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578478877282.png)    ### 安裝      為了讓大家學習到不同環境的安裝,單節點我們在 Windows 安裝,叢集環境在 Linux 安裝。    #### 單節點      壓縮包中就只有一個 `consul.exe` 的執行檔案。 ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/image-20200219123737599.png)   cd 到對應的目錄下,使用 cmd 啟動 Consul ``` # -dev表示開發模式執行,另外還有 -server 表示服務模式執行 consul agent -dev -client=0.0.0.0 ```   為了方便啟動,也可以在 consul.exe 同級目錄下建立一個指令碼來啟動,指令碼內容如下: ```shell consul agent -dev -client=0.0.0.0 pause ```   訪問管理後臺:http://localhost:8500/ 看到下圖意味著我們的 Consul 服務啟動成功了。 ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578481078411.png)    ## Consul 入門案例    ### 建立專案      我們建立聚合專案來講解 Consul,首先建立一個 pom 父工程。 ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/image-20200213134631692.png) ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578827910388.png) ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578827941332.png)    ### 新增依賴      pom.xml ```xml
4.0.0 com.example consul-demo 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.2.4.RELEASE Hoxton.SR1 org.springframework.cloud
spring-cloud-dependencies ${spring-cloud.version} pom import
```    ### 服務提供者 service-provider    #### 建立專案      在剛才的父工程下建立一個 `service-provider` 服務提供者的專案。 ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578836677985.png) ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/image-20200213134753954.png) ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578481759596.png) ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/image-20200213134830875.png) ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578828160188.png)    #### 新增依賴      pom.xml ```xml
4.0.0 com.example service-provider 1.0-SNAPSHOT com.example consul-demo 1.0-SNAPSHOT org.springframework.cloud spring-cloud-starter-consul-discovery org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-web org.projectlombok lombok provided org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine
```    #### 配置檔案      application.yml ```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 ```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 ```java package com.example.service; import com.example.pojo.Product; import java.util.List; /** * 商品服務 */ public interface ProductService { /** * 查詢商品列表 * * @return */ List selectProductList(); } ```   ProductServiceImpl.java ```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 selectProductList() { return Arrays.asList( new Product(1, "華為手機", 1, 5800D), new Product(2, "聯想筆記本", 1, 6888D), new Product(3, "小米平板", 5, 2020D) ); } } ```    #### 控制層      ProductController.java ```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 selectProductList() { return productService.selectProductList(); } } ``` > 該專案我們可以通過單元測試進行測試,也可以直接通過 url 使用 postman 或者瀏覽器來進行測試。    #### 啟動類      ServiceProviderApplication.java ```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/ 看到下圖意味著我們的服務註冊至註冊中心了。 ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578484723997.png) ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578484737902.jpg)      將 `service-provider` 專案複製一份修改埠為 `7071` ,註冊例項 id 為 02。 ```yml 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` 結果如下: ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578485552226.png) ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578485572069.jpg)    ### 服務消費者 service-consumer    #### 建立專案      在剛才的父工程下建立一個 `service-consumer` 服務消費者的專案。 ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578836536923.png) ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/image-20200213134753954.png) ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578485715853.png) ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/image-20200213134830875.png) ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578828621621.png)    #### 新增依賴      pom.xml ```xml 4.0.0 com.example service-consumer 1.0-SNAPSHOT com.example consul-demo 1.0-SNAPSHOT org.springframework.cloud spring-cloud-starter-consul-discovery org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-web org.projectlombok lombok provided org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine ```    #### 配置檔案      application.yml ```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 ```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 ```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 productList; } ```    #### 消費服務      OrderService.java ```java package com.example.service; import com.example.pojo.Order; public interface OrderService { /** * 根據主鍵查詢訂單 * * @param id * @return */ Order selectOrderById(Integer id); } ```   OrderServiceImpl.java ```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 selectProductListByLoadBalancerAnnotation() { // ResponseEntity: 封裝了返回資料 ResponseEntity> response = restTemplate.exchange( "http://service-provider/product/list", HttpMethod.GET, null, new ParameterizedTypeReference>() { }); return response.getBody(); } } ```    #### 控制層      OrderController.java ```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 ```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 結果如下: ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/image-20200213142910699.png)    ## Consul 叢集    ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/u=2902221230,3748556624&fm=26&gp=0.jpg)   上圖是一個簡單的 Consul Cluster 架構,Consul Cluster 有 Server 和 Client 兩種角色。不管是 Server 還是 Client,統稱為 Agent,Consul Client 是相對無狀態的,只負責轉發 RPC 到 Server 資源開銷很少。Server 是一個有一組擴充套件功能的代理,這些功能包括參與 Raft 選舉,維護叢集狀態,響應 RPC 查詢,與其他資料中心互動 WAN Gossip 和轉發查詢給 leader 或者遠端資料中心。   每個資料中心,Client 和 Server 是混合的。一般建議有 3~5 臺 Server。這是基於有故障情況下的可用性和效能之間的權衡結果,因為越多的機器加入達成共識越慢,Server 之間會選舉出一個 Leader。然而並不限制 Client 的數量,一般建議一個服務對應一個 Client,它們可以很容易的擴充套件到數千或者數萬臺 。在開發時我們繫結一組服務註冊中心中的客戶端即可。    ### 環境準備    | 伺服器 IP | Consul 型別 | Node 節點 | | -------------- | ----------- | --------- | | 192.168.10.101 | server | server-01 | | 192.168.10.102 | server | server-02 | | 192.168.10.103 | server | server-03 | | 192.168.10.1 | client | client-01 |    ### 安裝      將安裝包上傳至伺服器。   安裝 unzip 命令,建立 consul 目錄,將 consul 解壓至指定目錄。 ```shell yum -y install unzip # 安裝 unzip mkdir -p /usr/local/consul # 建立 consul 目錄 unzip consul_1.7.0_linux_amd64.zip -d /usr/local/consul/ # 解壓至 consul 目錄 mkdir -p /usr/local/consul/data # 建立 consul 資料目錄 ```    ### 啟動    #### 註冊中心服務端      以 server 服務模式執行三臺註冊中心。 ```shell # node-01 ./consul agent -server -bind=192.168.10.101 -client=0.0.0.0 -ui -bootstrap-expect=3 -data-dir=/usr/local/consul/data/ -node=server-01 # node-02 ./consul agent -server -bind=192.168.10.102 -client=0.0.0.0 -ui -bootstrap-expect=3 -data-dir=/usr/local/consul/data/ -node=server-02 # node-03 ./consul agent -server -bind=192.168.10.103 -client=0.0.0.0 -ui -bootstrap-expect=3 -data-dir=/usr/local/consul/data/ -node=server-03 ```   引數含義如下: - `-server`:以服務端身份啟動(註冊中心) - `-bind`:表示繫結到哪個 ip - `-client`:指定客戶端訪問的 ip,0.0.0.0 表示不限客戶端 ip - `-ui`:開啟 web 介面訪問 - `-bootstrap-expect=3`:表示 server 叢集最低節點數為 3,低於這個值將工作不正常(注:類似 ZooKeeper一樣,通常叢集數為奇數方便選舉,Consul 採用的是 Raft 演算法) - `-data-dir`:表示指定資料的存放目錄(該目錄必須存在,需提前建立好) - `-node`:表示節點在 web ui 中顯示的名稱    #### 註冊中心客戶端    ```shell consul agent -client=0.0.0.0 -bind=192.168.10.1 -data-dir=D:\Example\consol\data -node=client-01 ```    #### 關聯叢集      在 server-02 和 server-03 和 client-01 節點中輸入以下命令建立叢集關係。 ```shell ./consul join 192.168.10.101 ``` ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578490529277.png)    #### 叢集狀態      在任意一臺伺服器中輸入以下命令可檢視叢集中所有節點資訊。 ```shell ./consul members ``` ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578490802748.png)    #### 訪問      訪問:http://192.168.10.101:8500/ 或者 http://192.168.10.102:8500/ 或者 http://192.168.10.103:8500/ 叢集任意節點都可看到以下介面說明叢集環境搭建成功。 ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578491407807.png) ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578491551393.png)    ### 測試      service-provider 和 service-consumer 專案的配置檔案和程式碼都無需更改,直接啟動測試。   訪問:http://192.168.10.101:8500/,結果如下: ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/1578492067194.png)   訪問:http://localhost:9090/order/1 結果如下: ![](https://mrhelloworld.com/resources/articles/spring/spring-cloud/consul/image-20200213142910699.png)   經過測試服務正常可用,至此 Consul 服務註冊中心所有的知識點就講解結束了。 ![](https://mrhelloworld.com/resources/articles/articles_bottom/end02.gif)   本文采用 `知識共享「署名-非商業性使用-禁止演繹 4.0 國際」許可協議`。   大家可以通過 `分類` 檢視更多關於 `Spring Cloud` 的文章。