服务发现
什么是服务发现?
服务发现,Service Discovery 指的是若服务 A 需要与 服务 B 进行通信,那么如何知道服务 B 的地址?服务发现的作用,就是通过服务注册中心,来告知服务A,服务B 的地址在哪里。这里说的地址,通常就是 IP:Port/path
的形式。
基于这段描述,服务发现机制由三个角色构成:
- 服务的消费者,也就是服务A,其他服务的使用者。Consumer
- 服务的提供者,也就是服务B,为其他角色提供服务。Provider
- 服务注册中心,也称服务中介,存储已经注册的服务信息(地址),提供查找功能。
其思想也很清晰:若服务B需要为其他角色提供服务,那么服务B要将自身的信息(地址)注册到服务注册中心,这样其他服务(A),就可以在注册中心找到目标服务(B)。
如图所示:
注册中心的核心是存储系统,通常就是 Key/Value 结构的存储系统,存储服务标识与服务地址(或更详细的信息的映射。实操时,同一个服务可能存在多个提供者,那么一个服务标识,就会对应一个地址(或信息)列表,此时通常需要负载均衡算法来选择。
服务注册:将某个服务的信息存储到服务注册中心,是 SET 操作。服务提供者需要完成。
服务发现:从注册中心获取某个服务的信息,是 GET 操作。服务的消费者需要完成。
本例中,ServiceB 作为服务提供者,需要完成服务注册操作。之后 ServiceA 需要 Service B 的功能,需要三步走:
- 查询 ServiceB 的信息
- 注册中心告知 ServiceA:ServiceB 的信息
- ServiceA 请求 ServiceB 的服务。
以上就是服务发现的介绍。可见,只要支持 Key/Value 存储机制的产品,都可以作为服务中心来使用,来提供服务注册和发现功能。
早期,我就使用过 Redis 来实现服务注册中心。
- 多个 zset 项存储服务信息
- zset 的 key 为服务标识 ,zset 的成员为服务地址作,成员的 score 存储服务心跳时间戳,用于对服务做健康检测。
- 服务注册基于 ZADD 命令实现
- 服务发现基于 ZRANDMEMBER 命令实现
- 服务移除基于 ZREM 命令实现
- 还会使用一个集合记录全部的服务标识,可以是 List 或 Set。
如图所示:
除了存储之外,还要提供客户端供程序使用。客户端需要提供服务心跳、服务更新通知、负载均衡等功能。这里就不再深入了,大家如果对这个例子感兴趣,可以移步 https://github.com/han-joker/DiscoveryOnRedis.git。
现在有完善的注册中心产品,例如 Consul,Etcd,ZooKeeper 等,不需要我们自己来实现了。
微服务需要什么样的服务发现?
微服务系统需要一个分布式的服务注册中心来实现服务发现。分布式的注册中心可以保证不会出现单点失效的严重问题。
由于微服务架构的服务数量会很多,因此服务的健康检查就很重要,可以及时将无效服务从注册中心剔除。
最好有一个服务管理工具,便于我们观察集群、服务状态等。
基于以上原因,我们会从 Consul,Etcd,ZooKeeper 中做选择,因为以上三个,都是基于分布式存储系统构建的服务发现器。
Consul 作为服务发现
Consul 简介
安装
Consul 下载页
https://www.consul.io/downloads
CentOS/RHEL yum
- 安装 yum 工具包
- 配置yum增加consul(hashicorp)镜像源
- 安装consul
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install consul
测试安装结果
#### Ubuntu/Debian apt
```shell
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install consul
Homebrew
brew tap hashicorp/tap
brew install hashicorp/tap/consul
https://github.com/hashicorp/consul
git clone https://github.com/hashicorp/consul
cd consul
make tool
make linux
macOS
brew
brew tap hashicorp/tap
brew install hashicorp/tap/consul
二进制
选择合适的版本:
https://releases.hashicorp.com/consul/1.12.2/consul_1.12.2_darwin_amd64.zip
https://releases.hashicorp.com/consul/1.12.2/consul_1.12.2_darwin_arm64.zip
FreeBSD
选择合适的二进制版本:
https://releases.hashicorp.com/consul/1.12.2/consul_1.12.2_freebsd_386.zip
https://releases.hashicorp.com/consul/1.12.2/consul_1.12.2_freebsd_amd64.zip
Solaris
选择合适的二进制版本:
https://releases.hashicorp.com/consul/1.12.2/consul_1.12.2_solaris_amd64.zip
Windows
选择32或64位下载:
https://releases.hashicorp.com/consul/1.12.2/consul_1.12.2_windows_386.zip
https://releases.hashicorp.com/consul/1.12.2/consul_1.12.2_windows_amd64.zip
下载后,解压即可。内包含直接可运行的执行程序:
consul_1.12.2_windows_amd64> dir
Mode LastWriteTime Length Name
---- ------------- ------ ----
------ 6/3/2022 7:53 PM 118531040 consul.exe
Docker 镜像 (课堂)
docker 拉取 consul 镜像
sudo docker pull consul
$ sudo docker pull consul
Using default tag: latest
latest: Pulling from library/consul
df9b9388f04a: Pull complete
7aa48d4bd8bb: Pull complete
fa3ef9b012a5: Pull complete
d239fc798a4c: Pull complete
199124be58be: Pull complete
5c3ccfe93b8b: Pull complete
Digest: sha256:ee0735e34f80030c46002f71bc594f25e3f586202da8784b43b4050993ef2445
Status: Downloaded newer image for consul:latest
docker.io/library/consul:latest
运行
示例:开发模式
sudo docker run --rm -it -p 8500:8500 --name=ConsulDevServer consul agent -dev -client=0.0.0.0
访问 UI :
http://:8500/ui
Consul 的基本架构
整体架构
下面对 consul 架构做一个介绍:
- Consul 节点,Consul agent 命令启动一个 consul 分布式节点。consul agent 是 consul 的核心管理进程。服务负责完成维护成员信息、注册服务、运行检查、响应查询等工作。agent 分为客户端 client 和服务端 server 两种模式的节点。其中:
- 服务端节点,consul 分布式集群的核心节点,数据存储在 Server 上,功能全部由 Server 对外提供。Server 节点还需要负责分布式架构中一致性的实现。规模应该适中,建议奇数个,3,5,7 台,规模的增大,会导致共识一致性的效率降低,这个规模通常会在可用性和性能之间取得了平衡。
- 客户端节点,consul 分布式集群的代理节点,负责将操作转发到 Server 节点上,本身不提供核心功能。客户端节点是构成集群大部分的轻量级进程,它们与服务器节点交互以进行大多数操作,并保持非常少的自身状态。客户端的主要目的与大量的外部请求进行交互,避免外部请求直接请求少量的Server,降低 Server 节点的 I/O 压力。规模任意,建议在任何的服务上都部署客户端节点,这样服务可以直接访问客户端节点完成服务发现。
- 全部节点间采用 Gossip 协议(八卦协议)进行消息扩散。该协议主要负责下面几个功能:
- 客户端自动发现服务端
- 健康检查是分布式检查,不仅仅依赖于服务节点检查。
- 事件的高效传递,例如服务端选举产生了新 Leader,可以快速通知到全部的节点上
- LAN Gossip 负责局域网内的消息传递
- WAN Gossip 负责外网间的消息传递,也就是多个数据中心间的消息传递
- 服务节点基于 Raft 协议完成一致性,Raft 协议通过 Leader 选举和日志复制方案,快速达到一致性
- Consul 支持多数据中心的部署
端口说明
- 8300:集群内数据的读写和复制
- 8301:单个数据中心 gossip 协议通讯
- 8302:跨数据中心 gossip 协议通讯
- 8500:提供 HTTP API 服务;提供 UI 服务
- 8600:采用 DNS 协议提供服务发现功能
示例:部署 3 Servers 和 3 Clients(分布式部署,单数据中心)
快速命令:
sudo docker run --rm -d -p 8500:8500 -p 8600:8600 --name=ConsulServerA consul agent -server -ui -node=ServerA -bootstrap-expect=3 -client=0.0.0.0
sudo docker run --rm -d -p 8501:8500 -p 8601:8600 --name=ConsulServerB consul agent -server -ui -node=ServerB -bootstrap-expect=3 -client=0.0.0.0 -join=172.17.0.2
sudo docker run --rm -d -p 8502:8500 -p 8602:8600 --name=ConsulServerC consul agent -server -ui -node=ServerC -bootstrap-expect=3 -client=0.0.0.0 -join=172.17.0.2
sudo docker run --rm -d -p 8503:8500 -p 8603:8600 --name=ConsulClient1 consul agent -node=Client1 -ui -client=0.0.0.0 -join=172.17.0.2
sudo docker run --rm -d -p 8504:8500 -p 8604:8600 --name=ConsulClient2 consul agent -node=Client2 -ui -client=0.0.0.0 -join=172.17.0.3
sudo docker run --rm -d -p 8505:8500 -p 8605:8600 --name=ConsulClient3 consul agent -node=Client3 -ui -client=0.0.0.0 -join=172.17.0.4
启动 ServerA
sudo docker run --rm -it -p 8500:8500 -p 8600:8600 --name=ConsulServerA consul agent -server -ui -node=ServerA -bootstrap-expect=3 -client=0.0.0.0
- docker run --rm,容器退出时自动删除容器。便于我们测试,可以重复执行上面的命令
- docker run -it, 容器以交互模式运行,便于我们观察服务日志
- docker run -p 8500:8500,容器端口映射,8500 是 UI 服务端口
- docker run -p 8600:8600
- consul agent -server,Server 类型的 Agent 节点
- consul agent -ui,启动 UI 服务
- consul agent -node=ServerA,agent 节点的名字
- consul agent -bootstrap-expect=3,需要3个节点才能启动
- consul agent -client=0.0.0.0,允许任意客户端连接
$ sudo docker run -it -p 8500:8500 --name=ConsulServerA consul agent -server -ui -node=ServerA -bootstrap-expect=3 -client=0.0.0.0
留意 ServerA 的 IP 是 172.17.0.2,在开启其他 Server 或 Client 时需要。
若以 -d 的方式启动容器,可以通过 `docker inspect <container-id>` 的方式查看网络信息。
以 docker 方式运行的,需要找到容器的 IP 才可以。docker inspect <Container>
$ sudo docker inspact
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "4aa470fd3c2904d86072b47b3cb702fcda69ee3c328f3cc109fac0fbe29c0fa0",
"EndpointID": "3dd695784883439e21f11b6e7c8ed44459fda85e4b89793ef372d7a2cdf2e03a",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:11:00:02",
"DriverOpts": null
}
##### 启动 ServerB 和 ServerC,并加入Cluster
```shell
sudo docker run --rm -d -p 8501:8500 -p 8601:8600 --name=ConsulServerB consul agent -server -ui -node=ServerB -bootstrap-expect=3 -client=0.0.0.0 -join=172.17.0.2
sudo docker run --rm -d -p 8502:8500 -p 8602:8600 --name=ConsulServerC consul agent -server -ui -node=ServerC -bootstrap-expect=3 -client=0.0.0.0 -join=172.17.0.2
- docker run -d,容器以守护进程方式后台执行
- consul agent -join=172.17.0.2,加入172.17.0.2 组成 cluster。使用任意已加入 Cluster 的 Server IP 即可。
启动 Cient1
sudo docker run --rm -it -p 8503:8500 -p 8603:8600 --name=ConsulClient1 consul agent -node=Client1 -ui -client=0.0.0.0 -join=172.17.0.2
使用 client1 的 -it 交换,便于观察日志。
启动 Client2 和 Client 3
sudo docker run --rm -d -p 8504:8500 -p 8604:8600 --name=ConsulClient2 consul agent -node=Client2 -ui -client=0.0.0.0 -join=172.17.0.3
sudo docker run --rm -d -p 8505:8500 -p 8605:8600 --name=ConsulClient3 consul agent -node=Client3 -ui -client=0.0.0.0 -join=172.17.0.4
加入集群,指定任意的节点 IP 即可。
检查结果
UI,我们在六个节点上都使用了 -ui,因此以下任意 URL 都可以访问:
http://:8500/ui
http://:8501/ui
http://:8502/ui
http://:8503/ui
http://:8504/ui
http://:8505/ui
Command,命令 consule members
可以查看集群中的成员:
$ sudo docker exec 1a5f4ee5325dc9 consul members
Node Address Status Type Build Protocol DC Partition Segment
ServerA 172.17.0.2:8301 alive server 1.12.2 2 dc1 default
ServerB 172.17.0.3:8301 alive server 1.12.2 2 dc1 default
ServerC 172.17.0.4:8301 alive server 1.12.2 2 dc1 default
Client1 172.17.0.5:8301 alive client 1.12.2 2 dc1 default
Client2 172.17.0.6:8301 alive client 1.12.2 2 dc1 default
Client3 172.17.0.7:8301 alive client 1.12.2 2 dc1 default
停止由 consul 镜像创建的容器
sudo docker stop $(sudo docker ps -aq --no-trunc -f ancestor=consul)
服务注册
有三种方式完成服务注册:
- consul services 命令完成服务的注册和注销
- consul agent 在启动时,同时完成服务的注册
- HTTP API 完成服务操作,包括注册和其他(查询、注销)
无论采用那种方案,我们需要对服务进行定义。
服务定义
服务定义,指的是对服务的熟悉进行配置,例如名字、ID、地址、标签等。
一个基本的服务定义示例:
~/consul/config/service-some.json
{
"service": {
"id": "someService-01",
"name": "someService",
"tags": ["someTag"],
"address": "127.0.0.1",
"port": 8080,
"meta": {
"info": "some service"
},
"checks": []
}
}
一个完整的服务定义文件如下,JSON 格式。
{
"service": {
"id": "redis",
"name": "redis",
"tags": ["primary"],
"address": "",
"meta": {
"meta": "for my service"
},
"tagged_addresses": {
"lan": {
"address": "192.168.0.55",
"port": 8000,
},
"wan": {
"address": "198.18.0.23",
"port": 80
}
},
"port": 8000,
"socket_path": "/tmp/redis.sock",
"enable_tag_override": false,
"checks": [
{
"args": ["/usr/local/bin/check_redis.py"],
"interval": "10s"
}
],
"kind": "connect-proxy",
"proxy_destination": "redis", // Deprecated
"proxy": {
"destination_service_name": "redis",
"destination_service_id": "redis1",
"local_service_address": "127.0.0.1",
"local_service_port": 9090,
"local_service_socket_path": "/tmp/redis.sock",
"mode": "transparent",
"transparent_proxy": {
"outbound_listener_port": 22500
},
"config": {},
"upstreams": [],
"mesh_gateway": {
"mode": "local"
},
"expose": {
"checks": true,
"paths": [
{
"path": "/healthz",
"local_path_port": 8080,
"listener_port": 21500,
"protocol": "http2"
}
]
}
},
"connect": {
"native": false,
"sidecar_service": {}
"proxy": { // Deprecated
"command": [],
"config": {}
}
},
"weights": {
"passing": 5,
"warning": 1
},
"token": "233b604b-b92e-48c8-a253-5f11514e4b50",
"namespace": "foo"
}
}
也支持 HCL 格式。
几个常用的属性:
属性 | 必须 or 可选 | 意义 | 类型 | 默认值 |
---|---|---|---|---|
name | 必须 | 服务名称 | string | None |
id | 可选 | 服务 ID | string | id = name |
tags | 可选 | 标签 | []string | [] |
address | 可选 | IP 地址或主机名 | string | 节点的 IP 地址 |
port | 可选,但指定 address 应该同时指定 port | 端口 | int | None |
meta | 可选 | 服务 k/v 型元数据 | object | none |
socket_path | 可选,当服务监听 Unix Domain Socket 时指定 | Unix socket 地址 | string | None |
checks | 可选 | 服务的健康检查定义 | []Object | none |
weights |
完整选项说明:https://www.consul.io/docs/discovery/services#service
通常我们将服务配置在 .json
文件中,利用 consul agent
或 consul services
的参数指定配置文件。
consul services register 注册服务
consul agent 启动后,通过 CLI 注册即可,命令如下:
consul services register
docker 环境下,我们需要将配置文件映射到容器中,再注册:
编辑配置文件:
$ mkdir consul/services -p
$ vi consul/services/service-some.json
# 将配置文件目录映射到容器中
sudo docker run --rm -it -p 8500:8500 -p 8605:8600 --name=ConsulDevServer -v ~/consul/services:/consul/services consul agent -dev -client=0.0.0.0
sudo docker exec -it ConsulDevServer consul services register /consul/services/service-some.json
consul agent 启动时注册
启动时,通过指定配置文件,可以在启动时完成 service 的注册。
consul agent
命令的参数 -config-file
和 -config-dir
是用来指定配置文件的,-config-file
独立的配置文件,-config-dir
配置文件所在目录,可以同时加载目录中的多个配置文件。
命令:
consul agent -config-file= -config-dir=
docker 环境下,会自动加载容器中 /consul/config 中的配置文件,我们需要将配置卷映射到容器中:
sudo docker run --rm -it -p 8500:8500 -v ~/consul/services:/consul/config --name=ConsulDevServer consul agent -dev -client=0.0.0.0
Tip: 除了服务的配置文件,agent 启动时其他选项也可以在配置文件中配置。详细参考 Consul 课程。
HTTP API 注册服务
consul 暴露的 8500 端口负责接收 HTTP API 请求。
注册服务的接口是:
PUT /agent/service/register
查询字符串 Query String:
replace-existing-check:替换已经存在的健康检查
请求主体荷载 JSON 数据,Body Payload:
{
"ID": "redis1",
"Name": "redis",
"Tags": ["primary", "v1"],
"Address": "127.0.0.1",
"Port": 8000,
"Meta": {
"redis_version": "4.0"
},
"EnableTagOverride": false,
"Check": {
"DeregisterCriticalServiceAfter": "90m",
"Args": ["/usr/local/bin/check_redis.py"],
"Interval": "10s",
"Timeout": "5s"
},
"Weights": {
"Passing": 10,
"Warning": 1
}
}
内容服务定义一致。
演示:postman
自定义服务通过 API 注册
HTTP API 的方式允许我们通过 PUT 请求的方案注册服务,那也就意味着我们研发的服务在启动时,可以直接注册到 Consul 中,便于其他服务发现使用。下面就编写 go 程序,将服务注册到 Consul 中。
github.com/hashicorp/consul/api
包,是 consul 提供的对于其 HTTP API 操作的包,我们基于这个包,完成请求 HTTP API 。
产品服务示例代码,代码流程:
- 采用 net/http 包定义服务
- 定义测试路由及处理器。/info
- 使用 consul/api 包完成服务注册
- 启动服务监听
package main
import (
"flag"
"fmt"
"github.com/google/uuid"
"github.com/hashicorp/consul/api"
"log"
"net/http"
)
//main
func main() {
// 接收命令行参数作为服务对外的地址和端口
addr := flag.String("addr", "127.0.0.1", "The address of the listen. The default is 127.0.0.1.")
port := flag.Int("port", 8080, "The port of the listen. The default is 8080.")
flag.Parse()
// 定义服务
server := http.NewServeMux()
// 服务的第一个接口, /info
server.HandleFunc("/info", func(writer http.ResponseWriter, request *http.Request) {
_, err := fmt.Fprintf(writer, "Product Service.")
if err != nil {
log.Fatalln(err)
}
})
// consul 客户端初始化
config := api.DefaultConfig()
config.Address = "192.168.177.131:8500"
client, err := api.NewClient(config)
if err != nil {
log.Fatalln(err)
}
// 定义服务
serviceRegistration := new(api.AgentServiceRegistration)
serviceRegistration.Name = "product"
serviceRegistration.ID = "product-" + uuid.NewString()
serviceRegistration.Address = *addr
serviceRegistration.Port = *port
serviceRegistration.Tags = []string{"product"}
// 注册服务
if err := client.Agent().ServiceRegister(serviceRegistration); err != nil {
log.Fatalln(err)
}
log.Println("Service register was completed.")
// 产品服务启动
address := fmt.Sprintf("%s:%d", *addr, *port)
log.Printf("Service is listening on %s.\n", address)
log.Fatalln(http.ListenAndServe(address, server))
}
服务发现
当我们需要某个服务时,需要使用服务发现。核心就是在 consul 中查询目标服务的地址。consul 提供了俩个方案,完成服务查询:
- HTTP API
- DNS 查询
HTTP API
查询服务可以分为基于过滤条件的列表查询,和基于 ID 的单服务信息查询,接口分别:
- 列表查询:GET /v1/agent/services
- 单服务查询:GET /v1/agent/service/:service_id
其中,单服务查询,仅提供服务ID即可,而列表查询需要通过查询字符串filter参数进行过滤,最常见的基于服务的名字查询多个该服务,之后选择其中一个实例使用。
单服务查询 API
GET /v1/agent/service/:service_id
postman 演示
GET http://192.168.177.131:8500/v1/agent/service/redis1
{
"ID": "redis1",
"Service": "redis",
"Tags": [
"primary",
"v1"
],
"Meta": {
"redis_version": "4.0"
},
"Port": 8000,
"Address": "127.0.0.1",
"TaggedAddresses": {
"lan_ipv4": {
"Address": "127.0.0.1",
"Port": 8000
},
"wan_ipv4": {
"Address": "127.0.0.1",
"Port": 8000
}
},
"Weights": {
"Passing": 1,
"Warning": 1
},
"EnableTagOverride": false,
"ContentHash": "6cfc2fbe8597402a",
"Datacenter": "dc1"
}
Go 编码演示
orderService.go
package main
import (
"flag"
"fmt"
"github.com/hashicorp/consul/api"
"log"
"net/http"
)
func main() {
// 处理命令行参数
addr := flag.String("addr", "127.0.0.1", "The Address for listen. Default is 127.0.0.1")
port := flag.Int("port", 8080, "The Port for listen. Default is 8080.")
flag.Parse()
// 定义业务逻辑服务,假设为产品服务
service := http.NewServeMux()
service.HandleFunc("/info", func(writer http.ResponseWriter, request *http.Request) {
_, err := fmt.Fprintf(writer, "Order Service.")
if err != nil {
log.Fatalln(err)
}
})
// 连接 consul ,作为客户端连接 consul
consulApiConfig := api.DefaultConfig()
consulApiConfig.Address = "192.168.177.131:8500"
consulClient, err := api.NewClient(consulApiConfig)
if err != nil {
log.Fatalln(err)
}
// 发出 GET 注册请求
serviceRedis, _, err := consulClient.Agent().Service("redis1", nil)
if err != nil {
log.Fatalln(err)
}
log.Println(serviceRedis.Address, serviceRedis.Port)
// 启动监听
address := fmt.Sprintf("%s:%d", *addr, *port)
fmt.Printf("Order service is listening on %s.\n", address)
log.Fatalln(http.ListenAndServe(address, service))
}
服务信息列表
接口为:GET /v1/agent/services
查询字符串 filter 的格式为字符串,常用的检索支持:
选择器 | 支持的运算 | ||
---|---|---|---|
Address |
地址 | Equal, Not Equal, In, Not In, Matches, Not Matches | ==, !=, |
Port |
端口 | Equal, Not Equal | |
Service |
服务名 | Equal, Not Equal, In, Not In, Matches, Not Matches | |
Tags |
标签 | In, Not In, Is Empty, Is Not Empty | |
ID |
ID | Equal, Not Equal, In, Not In, Matches, Not Matches | |
Kind | 类型 | Equal, Not Equal, In, Not In, Matches, Not Matches | |
Meta | 元信息 | Is Empty, Is Not Empty, In, Not In | |
Meta. |
具体的元信息 | Equal, Not Equal, In, Not In, Matches, Not Matches |
完整的 filter 支持请参考:https://www.consul.io/api-docs/agent/service#filtering
其中运算符语法为:// 是否相等
== ""
!= ""
// 是否为空
is empty
is not empty
// 包含 // 子串检查
"" in
"" not in
contains ""
not contains ""
// 正则匹配
matches ""
not matches ""
同时支持使用逻辑运算连接多个条件:
// Or
or
// And
and
// Not
not
// 分组
( )
// Inspects data to check for a match
Postman 演示
基于名字查找:
GET http://192.168.177.131:8500/v1/agent/services?filter=Service==Product
{
"product-c5d3ced5-b733-4ecf-a762-526aeee55177": {
"ID": "product-c5d3ced5-b733-4ecf-a762-526aeee55177",
"Service": "Product",
"Tags": [
"test"
],
"Meta": {},
"Port": 8081,
"Address": "127.0.0.1",
"TaggedAddresses": {
"lan_ipv4": {
"Address": "127.0.0.1",
"Port": 8081
},
"wan_ipv4": {
"Address": "127.0.0.1",
"Port": 8081
}
},
"Weights": {
"Passing": 1,
"Warning": 1
},
"EnableTagOverride": false,
"Datacenter": "dc1"
},
"product-e771836e-0622-4309-ba7e-b366f1ea2944": {
"ID": "product-e771836e-0622-4309-ba7e-b366f1ea2944",
"Service": "Product",
"Tags": [
"test"
],
"Meta": {},
"Port": 8080,
"Address": "127.0.0.1",
"TaggedAddresses": {
"lan_ipv4": {
"Address": "127.0.0.1",
"Port": 8080
},
"wan_ipv4": {
"Address": "127.0.0.1",
"Port": 8080
}
},
"Weights": {
"Passing": 1,
"Warning": 1
},
"EnableTagOverride": false,
"Datacenter": "dc1"
}
}
ID 包含:
GET http://192.168.177.131:8500/v1/agent/services?filter=ID contains "product-"
ID 前缀:
GET http://192.168.177.131:8500/v1/agent/services?filter=ID matches "^product-"
Tag 包含:
GET http://192.168.177.131:8500/v1/agent/services?filter="test" in Tags
逻辑运算:
GET http://192.168.177.131:8500/v1/agent/services?filter="test" in Tags or Tags is empty
Go 编码演示
其他部分与单服务信息一致,调用的方法为 Agent().ServicesWithFilter()
:
// 查询基于 filter 过滤的多个服务信息
filter := "Service==Product"
services, err := consulClient.Agent().ServicesWithFilter(filter)
if err != nil {
log.Fatalln(err)
}
for id, sev := range services {
log.Println(id, sev.Address, sev.Port)
}
Agent().Services()
查询全部。
查询到一组服务,是ID对应服务信息的结构。查询之后,通常需要使用负载均衡策略,选择其中之一。常见的负载均衡策略为:
- rr:Round Robin, 循环
- wrr : Weighted round robin,加权循环
- p2c : Power of two choices,随机选2个,再从中选1个效率高的
- random : Random,随机
- wr: Weighted Random, 加权随机
DNS 查询
另一种方案就是使用 DNS 查询。consul 实现了一个 DNS 服务器,并将注册其中的服务都分配了对应的域名。
例如:
- consul.service.consul
- product.service.consul
**格式就是 **<service-name>.service[.datacenter-name].consul
。其中数据中心可以缺省,表示当前数据中心。
**默认情况下,consul 的 DNS 服务监听在 127.0.0.1:8600 上。也就意味着,只要我们解析域名时,指定 DNS 服务为 consul 的地址和端口,就可以完成注册服务的域名解析了。 **
以 dig 为例,完成注册服务的域名解析工作。( Dig是一个在类Unix命令行模式下查询DNS包括NS记录,A记录,MX记录等相关信息的工具。安装过程见附件。)
dig 语法:
dig @DNS服务器地址 -p DNS服务器端口 带查询的域名
dns 服务的默认端口为:53。而 consul 暴露的端口是 8600。
演示的时候,为了方便在教师机(老师的widows)上也可以访问到该DNS服务器,将docker容器的网络与宿主机网络绑定:--net=host
:
$ sudo docker run --rm -it --net=host --name=ConsulDevServer consul agent -dev -client=0.0.0.0
示例,默认 A 类型(IP地址)查询:
$ dig @192.168.177.131 -p 8600 consul.service.consul
# 综述,一共几个查询,有几个回复
; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.el7_9.9 <<>> @192.168.177.131 -p 8600 consul.service.consul
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 62462
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available
# 查询部分
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;consul.service.consul. IN A
# 响应部分
;; ANSWER SECTION:
consul.service.consul. 0 IN A 127.0.0.1
# 查询信息
;; Query time: 2 msec
;; SERVER: 192.168.177.131#8600(192.168.177.131)
;; WHEN: Sun Jul 10 15:19:57 EDT 2022
;; MSG SIZE rcvd: 66
+short 表示 获取基本短信息:
$ dig @192.168.177.131 -p 8600 +short consul.service.consul
127.0.0.1
带有端口信息,SRV 类型查询
$ dig @192.168.177.131 -p 8600 +short consul.service.consul SRV
1 1 8300 localhost.localdomain.node.dc1.consul.
多个地址的域名解析:
$ dig @192.168.177.131 -p 8600 +short product.service.consul SRV
1 1 8081 7f000001.addr.dc1.consul.
1 1 8083 7f000001.addr.dc1.consul.
1 1 8082 7f000001.addr.dc1.consul.
$ dig @192.168.177.131 -p 8600 +short product.service.consul SRV
1 1 8081 7f000001.addr.dc1.consul.
1 1 8082 7f000001.addr.dc1.consul.
1 1 8083 7f000001.addr.dc1.consul.
$ dig @192.168.177.131 -p 8600 +short product.service.consul SRV
1 1 8083 7f000001.addr.dc1.consul.
1 1 8082 7f000001.addr.dc1.consul.
1 1 8081 7f000001.addr.dc1.consul.
多次执行,大家会发现响应的结果是随机排序的,这也是 consul DNS 服务给我们实现的简易负载均衡器。
支持 DNS 直接查询域名,也就是我们在使用某个服务地址时,直接使用域名即可,而不是必须要使用 IP 或其他信息了。例如:
product:
address: 192.168.1.123:8081
对比
product:
address: product.service.consul
域名这种配置,就几乎不用改变。
注意,要支持 DNS 方式,就需要将服务查询所在机器的 DNS 地址,指向 consul 的服务地址8600端口。例如可以通过转发 cousul.
域下的全部域名解析。
服务注销
服务支持 HTTP API 和命令行的方式注销。
HTTP API
Method | Path | Produces |
---|---|---|
PUT |
/v1/agent/service/deregister/:service_id |
application/json |
postman 演示:
服务不存在也没有关系,不会发生任何操作。
API 代码:
func (a *Agent) ServiceDeregister(serviceID string)
consul services deregister
支持 ID 注销
$ consul services deregister -id=web
$ sudo docker exec -it ConsulDevServer consul services deregister -id=product-f7ceb87d-9658-4d91-aaa9-04da54f7d12c
Deregistered service: product-f7ceb87d-9658-4d91-aaa9-04da54f7d12c
支持配置文件注销:
$ cat web.json
{
"Service": {
"Name": "web"
}
}
$ consul services deregister web.json
services deregister 命令只能主要 service register 注册的服务,而不能注销 agent 通过配置文件加载的服务。agent 配置文件加载的服务需要通过修改配置文件,重新加载配置文件来实现。
健康检查
服务注册中的另一个主要的功能就是健康检查,健康检查可以针对服务,称为应用级别,也可以针对系统,称为系统级别,例如内存、CPU用量的检查。
Consul 支持多种类型的检查:
- Script + Interval,周期性脚本
- HTTP + Interval,周期性 HTTP
- TCP + Interval,周期性 TCP
- Time to Live (TTL),TTL
- Docker + Interval,周期性 Docker
- gRPC + Interval,周期性 gRPC
- H2ping + Interval, 周期性 H2
- Alias,别名
我们常常通过周期性脚本检查来监控系统的状态;通过周期性 HTTP、gRPC、TCP 来检查服务的状态,这取决于我们提供何种类型的服务。
一个服务可以定义多个检查,全部检查都通过,才意味着服务是健康的。
TCP 检查
示例:对 redis 服务做 tcp 检测。
我们通过服务的配置文件完成该示例,首先准备好 redis,我们采用 Docker 的方式部署 redis:
sudo docker pull redis
sudo docker run --rm --name RedisDev --net=host -d redis
redis 默认的暴露的是:127.0.0.1:6379
我们配置 redis 服务,注册到 consul,并同时增加健康检测:
redis 服务的配置 json:
{
"service": {
"id": "redis-01",
"name": "Redis",
"tags": ["primary"],
"address": "127.0.0.1",
"port": 6379,
"meta": {
"info": "Memory Cache by Redis."
},
"checks": [
{
"id": "redis-01-check",
"name": "Redis-01-check",
"tcp": "127.0.0.1:6379",
"interval": "5s",
"timeout": "1s"
}
]
}
}
可以使用 consul services register
命令或 HTTP API 完成注册:
HTTP API 示例,postman,注意 Body 的 Payload 要大小写问题:
PUT http://192.168.177.131:8500/v1/agent/service/register
{
"ID": "redis-01",
"Name": "Redis",
"Tags": ["primary"],
"Address": "127.0.0.1",
"Port": 6379,
"Meta": {
"info": "Memory Cache by Redis."
},
"Checks": [
{
"CheckID": "redis-01-check",
"Name": "Redis-01-check",
"TCP": "127.0.0.1:6379",
"Interval": "5s",
"Timeout": "1s"
}
]
}
服务的 HTTP 检查
我们以周期性 HTTP 为例,定义一个检查:
{
"check": {
"id": "check-id",
"name": "The Name of Health Check on HTTP Service",
"http": "https://localhost:8080/health",
"tls_server_name": "",
"tls_skip_verify": false,
"method": "GET",
"header": { "Content-Type": ["application/json"] },
"body": "{\"method\":\"health\"}",
"interval": "5s",
"timeout": "1s"
}
}
可以在 consul services register
注册服务时,在服务是配置文件中指定,也可以在 HTTP API 注册服务时设置。甚至可以利用 HTTP API 单独注册健康检查。
我们以最常用的定义服务时,同时定义健康检查为例,演示:
Product 服务,需要注册时提供健康检查:
productService.go 其他代码与之前保持一致:
func main() {
// 定义 http 检测接口,响应 2xx 都表示检测通过,其他状态码,表示失败
service.HandleFunc("/health", func(writer http.ResponseWriter, request *http.Request) {
log.Println("Consul Check.")
_, err := fmt.Fprintf(writer, "Product Service is health")
if err != nil {
log.Fatalln(err)
}
})
// 定义注册中心的服务
id := uuid.NewString()
serviceReg := new(api.AgentServiceRegistration)
serviceReg.Name = "Product"
serviceReg.ID = "product-" + id
serviceReg.Address = *addr
serviceReg.Port = *port
serviceReg.Tags = []string{"test"}
// 定义服务检测
serviceReg.Checks = api.AgentServiceChecks{
&api.AgentServiceCheck{
CheckID: "product-check-" + id,
Name: "Product-Check",
Interval: "3s",
Timeout: "1s",
HTTP: fmt.Sprintf("http://%s/health", address),
Method: "GET",
SuccessBeforePassing: 0,
FailuresBeforeWarning: 0,
FailuresBeforeCritical: 0,
DeregisterCriticalServiceAfter: "",
},
}
}
通过 consul ui 查看健康检查状态!
HTTP 检查根据响应状态码判定结果,2xx
表示通过,health 为 passing 状态;429 Too ManyRequests
表示请求过多, health 为 warning 状态,其他表示未通过,health 为 critical 状态。
再看一个 gRPC 的健康检查示例:
{
"check": {
"id": "mem-util",
"name": "Service health status",
"grpc": "127.0.0.1:12345",
"grpc_use_tls": true,
"interval": "10s"
}
}
若使用脚本检查系统状态,根据脚本的返回值来确定健康状态。也是三种:
- Exit code 0 - passing
- Exit code 1 - warning
- Any other code - failing
健康状态
consul 对服务的健康状态有三种描述:
- passing,检查通过
- warning,警告状态
- critical,危急状态,服务失效
为了防止健康检查的抖动,进而限制它们对集群造成的负载,健康检查可以配置为仅在指定数量的连续检查返回通过/关键后才变为通过/警告/关键。在达到配置的阈值之前,状态不会转换状态。默认都是0,表示状态立即改变。有三个配置:
- success_before_passing,通过前的成功次数
- failures_before_warning,警告前的失败次数
- failures_before_critical,危急前的失败次数
定义的健康检测的初始状态默认为 critical,这可以有效防止无效服务的注册。若需要更改,可以通过选项 status 来调整初始状态。
处于 critical 状态的服务,可能被 consul 自动注销。当服务的状态超过 deregister_critical_service_after
所指定的时长后,consul 会自动注销该服务。
服务健康状态查询
当我们使用服务发现查询服务时,DNS 方式会自动过滤状态未通过的全部服务,而 HTTP API 方式需要我们主动去查询。
DNS 方式:
dig @192.168.177.131 -p 8600 +short product.service.consul
192.168.177.1
127.0.0.1
仅仅列出了 check 通过的服务。
HTTP API 方式:
GET /v1/agent/services
的方式和 GET /v1/agent/service/<service-id>
的方式都可以获取服务信息,不论该服务的check是否通过。
我们需要接口:
Method | Path | Produces |
---|---|---|
GET |
/agent/health/service/name/:service_name |
application/json |
GET |
/agent/health/service/name/:service_name?format=text |
text/plain |
GET |
/agent/health/service/id/:service_id |
application/json |
GET |
/agent/health/service/id/:service_id?format=text |
text/plain |
来基于名字或ID获取服务的健康状态。
注意,基于名字来获取服务状态如果使用 text 格式,那么必须全部服务都通过,状态才为通过,否则为紧急。
postman 测试
HTTP 之 H2 vs H1
现阶段,我们所说的 HTTP 请求,通常表示 HTTP/1.1 版本的请求。HTTP/1.1 于1997年1月发布,目前最流行的版本。
HTTP/1.1 的典型特点:
- Host 标头,通过 Host 标头可以区分虚拟主机
- 支持持久连接,persistent connection,默认开启
Connection: keep-alive
,即 TCP 连接默认不关闭,可以被多个请求复用 - 范围请求,在请求头引入了
range
头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接,支持断点续传 - 缓存处理,引入了更多的缓存控制策略:
Cache-Control
、Etag/If-None-Match
等
2015年5月HTTP/2标准正式发表,就是 RFC 7540。H2 标准带来了如下的特征:
- 二进制分帧,frame,HTTP/1.1的头信息是文本(ASCII编码),数据体可以是文本,也可以是二进制;HTTP/2 头信息和数据体都是二进制,统称为“帧”:头信息帧和数据帧。帧是 HTTP/2 数据通信的最小单位。
- 数据流,stream,HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的请求或响应。HTTP/2 将每个请求或响应的所有数据包,称为一个数据流(stream)。每个数据流都有一个独一无二的编号。数据包发送的时候,都必须标记数据流 ID,用来区分它属于哪个数据流。
- 多路复用,双工通信,通过单一的 HTTP/2 连接发起多重的请求-响应消息,即在一个连接里,客户端可以同时发送和接收多个请求和响应
- HTTP/2 不再依赖多 TCP 连接实现多流并行
- 同域名下所有通信都在单个连接上完成,同个域名只需要占用一个 TCP 连接,消除了因多个 TCP 连接而带来的延时和内存消耗
- 单个连接可以承载任意数量的双向数据流,单个连接上可以并行交错的请求和响应,之间互不干扰
- 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装。
- 首部压缩,HTTP/2对消息头采用 HPACK 算法进行压缩传输,能够节省消息头占用的网络的流量。压缩是使用了首部表策略
- 服务端推送,server push,HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送
当我们的服务支持 H2 后,意味着我们可以高效的在服务间进行基于 HTTP 的数据传递了。Go 中最常用的 RPC 实现 gRPC 底层也是基于 HTTP/2 的。