服務註冊與服務發現是在分佈式服務架構中經常會涉及到的東西,業界經常使用的服務註冊與服務發現工具備 ZooKeeper、etcd、Consul 和 Eureka。Consul 的主要功能有服務發現、健康檢查、KV存儲、安全服務溝通和多數據中心。Consul 與其餘幾個工具的區別能夠在這裏查看 Consul vs. Other Software。javascript
假設在分佈式系統中有兩個服務 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
後如今的架構變成了以下圖這樣: html
在這個架構中:java
因此務註冊與服務發現工具除了服務自己的服務註冊和發現功能外至少還須要有健康檢查和狀態變動通知的功能。node
Consul 做爲一種分佈式服務工具,爲了不單點故障經常以集羣的方式進行部署,在 Consul 集羣的節點中分爲 Server 和 Client 兩種節點(全部的節點也被稱爲Agent),Server 節點保存數據,Client 節點負責健康檢查及轉發數據請求到 Server;Server 節點有一個 Leader 節點和多個 Follower 節點,Leader 節點會將數據同步到 Follower 節點,在 Leader 節點掛掉的時候會啓動選舉機制產生一個新的 Leader。ios
Client 節點很輕量且無狀態,它以 RPC 的方式向 Server 節點作讀寫請求的轉發,此外也能夠直接向 Server 節點發送讀寫請求。下面是 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 有 Commands 和 HTTP 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.proto
。docker
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 除了服務發現與註冊外,在配置中心、分佈式鎖方面也有着不少的應用。