服务发现
1. 基本介绍
服务发现,Service Discovery : 若服务 A 需要与 服务 B 进行通信,那么如何知道服务 B 的地址
作用 : 就是通过服务注册中心,来告知服务 A,服务B 的地址在哪里。地址通常为
IP+Port/Path
的形式
服务发现机制由三个角色构成
服务的消费者,也就是服务A,其他服务的使用者。Consumer
服务的提供者,也就是服务B,为其他角色提供服务。Provider
服务注册中心,也称服务中介,存储已经注册的服务信息(地址),提供查找功能
服务提供者需要将自身的信息 ( 地址 ) 注册到服务注册中心 , 这样其他服务就可以在注册中心找到目标服务
注册中心的核心是存储系统,通常就是Key/Value
结构的存储系统,存储服务标识与服务地址(或更详细的信息的映射。实操时,同一个服务可能存在多个提供者,那么一个服务标识,就会对应一个地址 ( 或信息 ) 列表,此时通常需要负载均衡算法来选择。
服务注册:将某个服务的信息存储到服务注册中心,是 SET 操作。服务提供者需要完成。
服务发现:从注册中心获取某个服务的信息,是 GET 操作。服务的消费者需要完成。
ServiceA 如果想要使用 ServiceB
首先会去服务注册中心查询 ServiceB , 服务注册中心返回 ServiceB 的地址
然后 ServiceA 通过地址请求 ServiceB 提供服务
2. 微服务需要的服务发现
分布式的服务注册中心 : 分布式的注册中心可以保证不会出现单点失效的严重问题。
服务的健康检查 : 因为微服务架构中的服务数量有很多 , 服务健康检查可以及时将无效服务从注册中心剔除
有服务管理工具 : 便于我们观察集群、服务状态等
目前有 Consul,Etcd,ZooKeeper ,以上三个都是基于分布式存储系统构建的服务发现器。
3. Consul 的安装与运行
官网 : Install | Consul | HashiCorp Developer
通过 Docker 启动 Consul
# 首先拉取镜像: 需要注意consul要加版本标签,默认latest无法拉取
docker pull consul:标签
# Docker run
# --rm 表示关闭容器会删除
# agent -dev 表示开发模式启动
# -client 表示哪个IP可以访问到consul
sudo docker run --rm -it -p 8500:8500 --name=ConsulDevServer consul agent -dev -client=0.0.0.0
sudo docker run --rm -it -d \
--net=host \
-p 8500:8500 \
-p 8600:8600 \
--name=ConsulDevServer \
consul:1.15.4 agent -dev -client=0.0.0.0
4. Consul 基本架构
4.1 主题架构
Consul 节点,Consul agent 命令启动一个 consul 分布式节点。consul agent 是 consul 的核心管理进程。服务负责完成维护成员信息、注册服务、运行检查、响应查询等工作。agent 分为客户端 client 和服务端 server 两种模式的节点。
服务端节点 , 是 consult 分布式集群的核心节点 , 数据存储在 Server , 功能全部由 Server 对外提供。Server 节点还需要负责分布式架构中一致性的实现。规模应该适中,建议奇数个,3,5,7 台,规模的增大,会导致共识一致性的效率降低,这个规模通常会在可用性和性能之间取得了平衡。
客户端节点 : consul 分布式集群的代理节点,负责将操作转发到 Server 节点上,本身不提供核心功能。客户端节点是构成集群大部分的轻量级进程,它们与服务器节点交互以进行大多数操作,并保持非常少的自身状态。客户端的主要目的与大量的外部请求进行交互,避免外部请求直接请求少量的Server,降低 Server 节点的 I/O 压力。规模任意,建议在任何的服务上都部署客户端节点,这样服务可以直接访问客户端节点完成服务发现。
全部节点间采用 Gossip 协议(八卦协议)进行消息扩散 ( 模拟消息在人群中是如何扩散的: 传给一个客户端 , 客户端互相传播 ) 。该协议主要负责下面几个功能:
客户端自动发现服务端
健康检查是分布式检查,不仅仅依赖于服务节点检查。
事件的高效传递,例如服务端选举产生了新 Leader,可以快速通知到全部的节点上
LAN Gossip 负责局域网内的消息传递
WAN Gossip 负责外网间的消息传递,也就是多个数据中心间的消息传递
服务节点基于 Raft 协议完成一致性,Raft 协议通过 Leader 选举和日志复制方案,快速达到一致性
4.2 端口说明
8300:集群内数据的读写和复制
8301:单个数据中心 gossip 协议通讯
8302:跨数据中心 gossip 协议通讯
8500:提供 HTTP API 服务;提供 UI 服务
8600:采用 DNS 协议提供服务发现功能
5. 部署 3 Servers 和 3 Clients(分布式部署,单数据中心)
5.1 Server
启动 ServerA
# 1.启动ServerA
# agent -server
# -bootstrap-expect=3 启动时候需要有三台服务器来构成Server集群
# -p 8500:8500,容器端口映射,8500 是 UI 服务端口
# -p 8600:8600, 服务发现的端口
# consul agent -ui,启动 UI 服务
# consul agent -node=ServerA,agent 节点的名字
# consul agent -server,Server 类型的 Agent 节点
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
启动之后 consul 会报 error="No cluster leader" , 是因为没有启动其他的两个 consul 服务器
另外启动之后 , 使用
docker inspect
查看 IP , 或者直接查看日志
启动 ServerB 和 ServerC
# consul agent -join=172.17.0.2,加入172.17.0.2 组成 cluster。使用任意已加入 Cluster 的 Server IP 即可。
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
可以看出在启动另外两个服务之后 , Leader 就被选举了出来 , 使用的是 Raft 协议中的算法
5.2 Client
命令和之前相比没有了
-server
, 就表示这是一个 client 节点 , 是 client 节点也就没有-bootstrap-expect=3
注意
-join
端口映射映射
# 启动ClientA
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
# 启动ClientB和ClientC
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
实际环境中 , client 数量要远远多 server 数量
可以通过
consul members
查看集群中的成员
sudo docker exec ConsulClient1 consul members
6. 服务注册
有三种方式完成服务注册:
consul services 命令完成服务的注册和注销
consul agent 在启动时,同时完成服务的注册
HTTP API 完成服务操作,包括注册和其他(查询、注销)
无论采用那种方案,我们需要对服务进行定义。
6.1 服务定义
# 首先创建目录
mkdir /consul
# vim /consult/service-some.json
# 1.基本服务定义
{
"service": {
"id": "someService-01",
"name": "someService",
"tags": ["someTag"],
"address": "127.0.0.1",
"port": 8080,
"meta": {
"info": "some service"
},
"checks": []
}
}
6.2 consul services register 注册服务
docker 环境下,我们需要将配置文件映射到容器中,再注册:
docker run --rm -it -p 8500:8500 -p 8605:8600 --name=ConsulDevServer -v ~/consul/:/consul/services consul:1.15.4 agent -dev -client=0.0.0.0
docker exec -it ConsulDevServer consul services register /consul/services/service-some.json
6.3 consul agent 启动时注册
启动时,通过指定配置文件,可以在启动时完成 service 的注册。
consul agent
命令的参数 -config-file
和 -config-dir
是用来指定配置文件的,-config-file
独立的配置文件,-config-dir
配置文件所在目录,可以同时加载目录中的多个配置文件。
sudo docker run --rm -it -p 8500:8500 -v ~/consul/services:/consul/config --name=ConsulDevServer consul agent -dev -client=0.0.0.0
6.4 HTTP API 注册服务
实际开发中更多的采用这种方式注册
注册服务的接口是:
PUT /agent/service/register
请求主体 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
}
}
6.5 go代码完成服务注册
HTTP API 的方式允许我们通过 PUT 请求的方案注册服务,那也就意味着我们研发的服务在启动时,可以直接注册到 Consul 中,便于其他服务发现使用。下面就编写 go 程序,将服务注册到 Consul 中
github.com/hashicorp/consul/api
包,是 consul 提供的对于其 HTTP API 操作的包,我们基于这个包,完成请求 HTTP API
步骤
采用 net/http 包定义服务
定义测试路由及处理器。/info
使用 consul/api 包完成服务注册
启动服务监听
func main() {
// 处理命令行参数
Addr := flag.String("addr", "127.0.0.1", "正在监听地址:127.0.0.1")
Port := flag.Int("port", 8090, "正在监听端口:8090")
flag.Parse()
// 创建HTTP请求
service := http.NewServeMux()
service.HandleFunc("/info", Default)
// 1.定义服务,得到AgentServiceRegistration
ServiceRegister := new(api.AgentServiceRegistration)
ServiceRegister.Name = "Product"
ServiceRegister.ID = "Product" + uuid.NewString() // 使用uuid包创建一个唯一字符串
ServiceRegister.Address = *Addr
ServiceRegister.Port = *Port
ServiceRegister.Tags = []string{"test"}
// 2.注册服务
// 2.1 配置consul 服务器地址
consulApiConfig := api.DefaultConfig()
consulApiConfig.Address = "154.8.197.123:8500" //地址为consult地址
consulClient, err := api.NewClient(consulApiConfig)
if err != nil {
log.Fatalln(err)
}
// 发出 put 注册请求
if err := consulClient.Agent().ServiceRegister(ServiceRegister); err != nil {
log.Fatalln(err)
}
// 启动监听
address := fmt.Sprintf("%s:%d", *Addr, *Port)
fmt.Printf("服务正在监听: %s", address)
log.Fatalln(http.ListenAndServe(address, service))
}
func Default(writer http.ResponseWriter, request *http.Request) {
_, err := fmt.Fprintf(writer, "Product Service")
if err != nil {
log.Fatal(err)
}
}
可以看到服务已经被注册进去了
7. 服务发现
当我们需要某个服务时,需要使用服务发现。核心就是在 consul 中查询目标服务的地址。consul 提供了俩个方案,完成服务查询:
HTTP API
DNS 查询
7.1 HTTP API
查询服务可以分为基于过滤条件的列表查询,和基于 ID 的单服务信息查询,接口分别:
列表查询 ( 多服务查询 ):GET /v1/agent/services
单服务查询:GET /v1/agent/service/:service_id
单服务查询
GET /v1/agent/services
通过 Go 编码进行查询
// 发出 GET 注册请求
// service 的第二个返回值表示QueryMeta,包含查询时间等等一系列的值
serviceRedis, _, err := consulClient.Agent().Service("redis1", nil)
if err != nil {
log.Fatalln(err)
}
log.Println(serviceRedis.Address, serviceRedis.Port)
多服务查询
通过 Go 编码进行查询
// 查询基于 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, 加权随机
7.2 DNS 查询
8. 服务注销
HTTP API
PUT /v1/agent/service/deregister/:service_id
func (a *Agent) ServiceDeregister(serviceID string)
命令行
# 支持通过IP注销
consul services deregister -id=web
# docker 注销
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
9. 服务健康检查
服务注册中的另一个主要的功能就是健康检查,健康检查可以针对服务,称为应用级别,也可以针对系统,称为系统级别,例如内存、CPU用量的检查。
一个服务可以定义多个检查,全部检查都通过,才意味着服务是健康的。
9.1 TCP 检查
注册服务 Redis,为 Redis 服务添加健康检查
PUT http://192.168.177.131:8500/v1/agent/service/register
{
"ID": "redis-01",
"Name": "Redis",
"Tags": ["primary"],
"Address": "IP地址",
"Port": 6379,
"Meta": {
"info": "Memory Cache by Redis."
},
"Checks": [
{
"CheckID": "redis-01-check",
"Name": "Redis-01-check",
"TCP": "IP地址:6379",
"Interval": "5s",
"Timeout": "1s"
}
]
}
注册服务
查看健康检查
9.2 HTTP 检查
如果容器是 host 方式启动的,ConsultAddres 要填服务器公网IP
const ConsulAddress = "服务器公网IP地址:8500"
func main() {
// 处理命令行参数
Addr := flag.String("addr", "127.0.0.1", "正在监听地址:127.0.0.1")
Port := flag.Int("port", 8090, "正在监听端口:8090")
// 拼接地址
address := fmt.Sprintf("%s:%d", *Addr, *Port)
flag.Parse()
// 创建HTTP请求
service := http.NewServeMux()
service.HandleFunc("/info", Default)
service.HandleFunc("/health", Health)
// 1.定义服务,得到AgentServiceRegistration
id := uuid.NewString() // 定义注册中心的服务ID
ProductService := new(api.AgentServiceRegistration)
ProductService.Name = "Product"
ProductService.ID = "Product" + id // 使用uuid包创建一个唯一字符串
ProductService.Address = *Addr
ProductService.Port = *Port
ProductService.Tags = []string{"Product"}
// 2.定义服务健康检查
ProductService.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: "",
},
}
// 2.注册服务
// 2.1 配置consul 服务器地址
consulApiConfig := api.DefaultConfig()
consulApiConfig.Address = ConsulAddress //地址为consult地址
consulClient, err := api.NewClient(consulApiConfig)
if err != nil {
log.Fatalln(err)
}
// 发出 put 注册请求
if err := consulClient.Agent().ServiceRegister(ProductService); err != nil {
log.Fatalln(err)
}
log.Println("服务注册成功")
// 启动监听
fmt.Printf("服务正在监听: %s", address)
log.Fatalln(http.ListenAndServe(address, service))
}
func Default(writer http.ResponseWriter, request *http.Request) {
_, err := fmt.Fprintf(writer, "Product Service")
if err != nil {
log.Fatal(err)
}
}
func Health(writer http.ResponseWriter, request *http.Request) {
log.Println("Health Check")
_, err := fmt.Fprintf(writer, "Product Service is Health")
if err != nil {
log.Fatal(err)
}
}
go run ServiceHealthCheck.go -addr 127.0.0.1 -port 8000
# 如果想要自己电脑访问,云服务器addr要填内网IP
可以看到每三秒进行一次健康检查
UI 界面也可以看到健康检查成功
9.3 服务健康状态
consul 对服务的健康状态有三种描述:
passing,检查通过
warning,警告状态 ( 服务占用过大... )
critical,危急状态,服务失效
另外还有三个配置是针对连续检查返回通过/关键后才变为通过/警告/关键
success_before_passing,通过前的成功次数 (成功几次才算成功)
failures_before_warning,警告前的失败次数 (失败几次才算警告)
failures_before_critical,危急前的失败次数 (服务失效几次才算失效)
在使用 Go 代码定义健康检查中也可以看到这三个配置项
9.4 服务健康状态查询
DNS 方式
HTTP API 方式
不论该服务的健康检查是否通过都可以检索到
Method | Path | Produces |
---|---|---|
GET |
|
|
GET |
|
|
检索除服务以及该服务的健康状态
Method | Path ( 前面加 v1/agent... ) | Produces |
---|---|---|
GET |
|
|
GET |
|
|
GET |
|
|
GET |
|
|
fromat=text 返回服务的状态,表示这组服务是否都通过了
评论区