用 Consul 來作服務註冊與服務發現

服務註冊與服務發現是在分佈式服務架構中經常會涉及到的東西,業界經常使用的服務註冊與服務發現工具備 ZooKeeperetcdConsulEureka。Consul 的主要功能有服務發現、健康檢查、KV存儲、安全服務溝通和多數據中心。Consul 與其餘幾個工具的區別能夠在這裏查看 Consul vs. Other Softwarejavascript

爲何須要有服務註冊與服務發現?

假設在分佈式系統中有兩個服務 Service-A (下文以「S-A」代稱)和 Service-B(下文以「S-B」代稱),當 S-A 想調用 S-B 時,咱們首先想到的時直接在 S-A 中請求 S-B 所在服務器的 IP 地址和監聽的端口,這在服務規模很小的狀況下是沒有任何問題的,可是在服務規模很大每一個服務不止部署一個實例的狀況下是存在一些問題的,好比 S-B 部署了三個實例 S-B-一、S-B-2 和 S-B-3,這時候 S-A 想調用 S-B 該請求哪個服務實例的 IP 呢?仍是將3個服務實例的 IP 都寫在 S-A 的代碼裏,每次調用 S-B 時選擇其中一個 IP?這樣作顯得很不靈活,這時咱們想到了 Nginx 恰好就能很好的解決這個問題,引入 Nginx 後如今的架構變成了以下圖這樣:

引入 Nginx 後就解決了 S-B 部署多個實例的問題,還作了 S-B 實例間的負載均衡。但如今的架構又面臨了新的問題,分佈式系統每每要保證高可用以及能作到動態伸縮,在引入 Nginx 的架構中,假如當 S-B-1 服務實例不可用時,Nginx 仍然會向 S-B-1 分配請求,這樣服務就不可用,咱們想要的是 S-B-1 掛掉後 Nginx 就再也不向其分配請求,以及當咱們新部署了 S-B-4 和 S-B-5 後,Nginx 也能將請求分配到 S-B-4 和 S-B-5,Nginx 要作到這樣就要在每次有服務實例變更時去更新配置文件再重啓 Nginx。這樣看彷佛用了 Nginx 也很不舒服以及還須要人工去觀察哪些服務有沒有掛掉,Nginx 要是有對服務的健康檢查以及可以動態變動服務配置就是咱們想要的工具,這就是服務註冊與服務發現工具的用處。下面是引入服務註冊與服務發現工具後的架構圖:
html

在這個架構中:java

  • 首先 S-B 的實例啓動後將自身的服務信息(主要是服務所在的 IP 地址和端口號)註冊到註冊工具中。不一樣註冊工具服務的註冊方式各不相同,後文會講 Consul 的具體註冊方式。
  • 服務將服務信息註冊到註冊工具後,註冊工具就能夠對服務作健康檢查,以此來肯定哪些服務實例可用哪些不可用。
  • S-A 啓動後就能夠經過服務註冊和服務發現工具獲取到全部健康的 S-B 實例的 IP 和端口,並將這些信息放入本身的內存中,S-A 就可用經過這些信息來調用 S-B。
  • S-A 能夠經過監聽(Watch)註冊工具來更新存入內存中的 S-B 的服務信息。好比 S-B-1 掛了,健康檢查機制就會將其標爲不可用,這樣的信息變更就被 S-A 監聽到了,S-A 就更新本身內存中 S-B-1 的服務信息。

因此務註冊與服務發現工具除了服務自己的服務註冊和發現功能外至少還須要有健康檢查和狀態變動通知的功能。node

Consul

Consul 做爲一種分佈式服務工具,爲了不單點故障經常以集羣的方式進行部署,在 Consul 集羣的節點中分爲 Server 和 Client 兩種節點(全部的節點也被稱爲Agent),Server 節點保存數據,Client 節點負責健康檢查及轉發數據請求到 Server;Server 節點有一個 Leader 節點和多個 Follower 節點,Leader 節點會將數據同步到 Follower 節點,在 Leader 節點掛掉的時候會啓動選舉機制產生一個新的 Leader。ios

Client 節點很輕量且無狀態,它以 RPC 的方式向 Server 節點作讀寫請求的轉發,此外也能夠直接向 Server 節點發送讀寫請求。下面是 Consul 的架構圖:

Consule 的安裝和具體使用及其餘詳細內容可瀏覽官方文檔
下面是我用 Docker 的方式搭建了一個有3個 Server 節點和1個 Client 節點的 Consul 集羣。git

# 這是第一個 Consul 容器,其啓動後的 IP 爲172.17.0.5
docker run -d --name=c1 -p 8500:8500 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=true --bootstrap-expect=3 --client=0.0.0.0 -ui

docker run -d --name=c2 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=true --client=0.0.0.0 --join 172.17.0.5

docker run -d --name=c3 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=true --client=0.0.0.0 --join 172.17.0.5

#下面是啓動 Client 節點
docker run -d --name=c4 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=false --client=0.0.0.0 --join 172.17.0.5

啓動容器時指定的環境變量 CONSUL_BIND_INTERFACE 其實就是至關於指定了 Consul 啓動時 --bind 變量的參數,好比能夠把啓動 c1 容器的命令換成下面這樣,也是同樣的效果。github

docker run -d --name=c1 -p 8500:8500 -e consul agent --server=true --bootstrap-expect=3 --client=0.0.0.0 --bind='{{ GetInterfaceIP "eth0" }}' -ui

操做 Consul 有 CommandsHTTP API 兩種方式,進入任意一個容器執行 consul members 均可以有以下的輸出,說明 Consul 集羣就已經搭建成功了。golang

Node          Address          Status  Type    Build  Protocol  DC   Segment
2dcf0c824cf0  172.17.0.7:8301  alive   server  1.4.4  2         dc1  <all>
64746cffa116  172.17.0.6:8301  alive   server  1.4.4  2         dc1  <all>
77af7d94a8ca  172.17.0.5:8301  alive   server  1.4.4  2         dc1  <all>
6c71148f0307  172.17.0.8:8301  alive   client  1.4.4  2         dc1  <default>

代碼實踐

假設如今有一個用 Node.js 寫的服務 node-server 須要經過 gRPC 的方式調用一個用 Go 寫的服務 go-server。
下面是用 Protobuf 定義的服務和數據類型文件 hello.protodocker

syntax = "proto3";

package hello;
option go_package = "hello";

// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (stream HelloRequest) returns (stream HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

用命令經過 Protobuf 的定義生成 Go 語言的代碼:protoc --go_out=plugins=grpc:./hello ./*.proto 會在 hello 目錄下獲得 hello.pb.go 文件,而後在 hello.go 文件中實現咱們定義的 RPC 服務。apache

// hello.go
package hello

import "context"

type GreeterServerImpl struct {}

func (g *GreeterServerImpl) SayHello(c context.Context, h *HelloRequest) (*HelloReply, error)  {
    result := &HelloReply{
        Message: "hello" + h.GetName(),
    }
    return result, nil
}

下面是入口文件 main.go,主要是將咱們定義的服務註冊到 gRPC 中,並建了一個 /ping 接口用於以後 Consul 的健康檢查。

package main

import (
    "go-server/hello"
    "google.golang.org/grpc"
    "net"
    "net/http"
)

func main() {
    lis1, _ := net.Listen("tcp", ":8888")
    lis2, _ := net.Listen("tcp", ":8889")
    grpcServer := grpc.NewServer()
    hello.RegisterGreeterServer(grpcServer, &hello.GreeterServerImpl{})
    go grpcServer.Serve(lis1)
    go grpcServer.Serve(lis2)
    
    http.HandleFunc("/ping", func(res http.ResponseWriter, req *http.Request){
        res.Write([]byte("pong"))
    })
    http.ListenAndServe(":8080", nil)
}

至此 go-server 端的代碼就所有編寫完了,能夠看出代碼裏面沒有任何涉及到 Consul 的地方,用 Consul 作服務註冊是能夠作到對項目代碼沒有任何侵入性的。下面要作的是將 go-server 註冊到 Consul 中。將服務註冊到 Consul 能夠經過直接調用 Consul 提供的 REST API 進行註冊,還有一種對項目沒有侵入的配置文件進行註冊。Consul 服務配置文件的詳細內容能夠在此查看。下面是咱們經過配置文件進行服務註冊的配置文件 services.json

{
  "services": [
    {
      "id": "hello1",
      "name": "hello",
      "tags": [
        "primary"
      ],
      "address": "172.17.0.9",
      "port": 8888,
      "checks": [
        {
          "http": "http://172.17.0.9:8080/ping",
          "tls_skip_verify": false,
          "method": "GET",
          "interval": "10s",
          "timeout": "1s"
        }
      ]
    },{
      "id": "hello2",
      "name": "hello",
      "tags": [
        "second"
      ],
      "address": "172.17.0.9",
      "port": 8889,
      "checks": [
        {
          "http": "http://172.17.0.9:8080/ping",
          "tls_skip_verify": false,
          "method": "GET",
          "interval": "10s",
          "timeout": "1s"
        }
      ]
    }
  ]
}

配置文件中的 172.17.0.9 表明的是 go-server 所在服務器的 IP 地址,port 就是服務監聽的不一樣端口,check 部分定義的就是健康檢查,Consul 會每隔 10秒鐘請求一下 /ping 接口以此來判斷服務是否健康。將這個配置文件複製到 c4 容器的 /consul/config 目錄,而後執行consul reload 命令後配置文件中的 hello 服務就註冊到 Consul 中去了。經過在宿主機執行curl http://localhost:8500/v1/catalog/services\?pretty就能看到咱們註冊的 hello 服務。
下面是 node-server 服務的代碼:

const grpc = require('grpc');
const axios = require('axios');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync(
  './hello.proto',
  {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true
  });
const hello_proto = grpc.loadPackageDefinition(packageDefinition).hello;

function getRandNum (min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min;
};

const urls = []
async function getUrl() {
  if (urls.length) return urls[getRandNum(0, urls.length-1)];
  const { data } = await axios.get('http://172.17.0.5:8500/v1/health/service/hello');
  for (const item of data) {
    for (const check of item.Checks) {
      if (check.ServiceName === 'hello' && check.Status === 'passing') {
        urls.push(`${item.Service.Address}:${item.Service.Port}`)
      }
    }
  }
  return urls[getRandNum(0, urls.length - 1)];
}

async function main() {
  const url = await getUrl();
  const client = new hello_proto.Greeter(url, grpc.credentials.createInsecure());
   
  client.sayHello({name: 'jack'}, function (err, response) {
    console.log('Greeting:', response.message);
  }); 
}

main()

代碼中 172.17.0.5 地址爲 c1 容器的 IP 地址,node-server 項目中直接經過 Consul 提供的 API 得到了 hello 服務的地址,拿到服務後咱們須要過濾出健康的服務的地址,再隨機從全部得到的地址中選擇一個進行調用。代碼中沒有作對 Consul 的監聽,監聽的實現能夠經過不斷的輪詢上面的那個 API 過濾出健康服務的地址去更新 urls 數組來作到。如今啓動 node-server 就能夠調用到 go-server 服務。服務註冊與發現給服務帶來了動態伸縮的能力,也給架構增長了必定的複雜度。Consul 除了服務發現與註冊外,在配置中心、分佈式鎖方面也有着不少的應用。

相關文章
相關標籤/搜索