天天看點

consul php,用 Consul 來做服務注冊與服務發現

服務注冊與服務發現是在分布式服務架構中常常會涉及到的東西,業界常用的服務注冊與服務發現工具有 ZooKeeper、etcd、Consul 和 Eureka。Consul 的主要功能有服務發現、健康檢查、KV存儲、安全服務溝通和多資料中心。Consul 與其他幾個工具的差別可以在這裡檢視 Consul vs. Other Software。

為什麼需要有服務注冊與服務發現?

假設在分布式系統中有兩個服務 Service-A (下文以“S-A”代稱)和 Service-B(下文以“S-B”代稱),當 S-A 想調用 S-B 時,我們首先想到的時直接在 S-A 中請求 S-B 所在伺服器的 IP 位址和監聽的端口,這在服務規模很小的情況下是沒有任何問題的,但是在服務規模很大每個服務不止部署一個執行個體的情況下是存在一些問題的,比如 S-B 部署了三個執行個體 S-B-1、S-B-2 和 S-B-3,這時候 S-A 想調用 S-B 該請求哪一個服務執行個體的 IP 呢?還是将3個服務執行個體的 IP 都寫在 S-A 的代碼裡,每次調用 S-B 時選擇其中一個 IP?這樣做顯得很不靈活,這時我們想到了 Nginx 剛好就能很好的解決這個問題,引入 Nginx 後現在的架構變成了如下圖這樣:

consul php,用 Consul 來做服務注冊與服務發現

引入 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 要是有對服務的健康檢查以及能夠動态變更服務配置就是我們想要的工具,這就是服務注冊與服務發現工具的用處。下面是引入服務注冊與服務發現工具後的架構圖:

consul php,用 Consul 來做服務注冊與服務發現

在這個架構中:

首先 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 的服務資訊。

是以務注冊與服務發現工具除了服務本身的服務注冊和發現功能外至少還需要有健康檢查和狀态變更通知的功能。

Consul

Consul 作為一種分布式服務工具,為了避免單點故障常常以叢集的方式進行部署,在 Consul 叢集的節點中分為 Server 和 Client 兩種節點(所有的節點也被稱為Agent),Server 節點儲存資料,Client 節點負責健康檢查及轉發資料請求到 Server;Server 節點有一個 Leader 節點和多個 Follower 節點,Leader 節點會将資料同步到 Follower 節點,在 Leader 節點挂掉的時候會啟動選舉機制産生一個新的 Leader。

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

consul php,用 Consul 來做服務注冊與服務發現

Consule 的安裝和具體使用及其他詳細内容可浏覽官方文檔。

下面是我用 Docker 的方式搭建了一個有3個 Server 節點和1個 Client 節點的 Consul 叢集。

# 這是第一個 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 容器的指令換成下面這樣,也是一樣的效果。

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 有 Commands 和 HTTP API 兩種方式,進入任意一個容器執行 consul members 都可以有如下的輸出,說明 Consul 叢集就已經搭建成功了。

Node Address Status Type Build Protocol DC Segment

2dcf0c824cf0 172.17.0.7:8301 alive server 1.4.4 2 dc1

64746cffa116 172.17.0.6:8301 alive server 1.4.4 2 dc1

77af7d94a8ca 172.17.0.5:8301 alive server 1.4.4 2 dc1

6c71148f0307 172.17.0.8:8301 alive client 1.4.4 2 dc1

代碼實踐

假設現在有一個用 Node.js 寫的服務 node-server 需要通過 gRPC 的方式調用一個用 Go 寫的服務 go-server。

下面是用 Protobuf 定義的服務和資料類型檔案 hello.proto。

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 服務。

// 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 除了服務發現與注冊外,在配置中心、分布式鎖方面也有着很多的應用。