目 录CONTENT

文章目录

服务发现

Sakura
2023-10-02 / 0 评论 / 0 点赞 / 62 阅读 / 25833 字 / 正在检测是否收录...

服务发现

1. 基本介绍

服务发现,Service Discovery : 若服务 A 需要与 服务 B 进行通信,那么如何知道服务 B 的地址

  • 作用 : 就是通过服务注册中心,来告知服务 A,服务B 的地址在哪里。地址通常为IP+Port/Path 的形式

服务发现机制由三个角色构成

  1. 服务的消费者,也就是服务A,其他服务的使用者。Consumer

  2. 服务的提供者,也就是服务B,为其他角色提供服务。Provider

  3. 服务注册中心,也称服务中介,存储已经注册的服务信息(地址),提供查找功能

服务提供者需要将自身的信息 ( 地址 ) 注册到服务注册中心 , 这样其他服务就可以在注册中心找到目标服务

注册中心的核心是存储系统,通常就是Key/Value结构的存储系统,存储服务标识与服务地址(或更详细的信息的映射。实操时,同一个服务可能存在多个提供者,那么一个服务标识,就会对应一个地址 ( 或信息 ) 列表,此时通常需要负载均衡算法来选择。

服务注册:将某个服务的信息存储到服务注册中心,是 SET 操作。服务提供者需要完成。

服务发现:从注册中心获取某个服务的信息,是 GET 操作。服务的消费者需要完成。

ServiceA 如果想要使用 ServiceB

首先会去服务注册中心查询 ServiceB , 服务注册中心返回 ServiceB 的地址

然后 ServiceA 通过地址请求 ServiceB 提供服务

2. 微服务需要的服务发现

  1. 分布式的服务注册中心 : 分布式的注册中心可以保证不会出现单点失效的严重问题。

  2. 服务的健康检查 : 因为微服务架构中的服务数量有很多 , 服务健康检查可以及时将无效服务从注册中心剔除

  3. 有服务管理工具 : 便于我们观察集群、服务状态等

目前有 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. 服务注册

有三种方式完成服务注册:

  1. consul services 命令完成服务的注册和注销

  2. consul agent 在启动时,同时完成服务的注册

  3. 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

步骤

  1. 采用 net/http 包定义服务

  2. 定义测试路由及处理器。/info

  3. 使用 consul/api 包完成服务注册

  4. 启动服务监听

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 提供了俩个方案,完成服务查询:

  1. HTTP API

  2. 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 检查

  1. 注册服务 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 服务健康状态查询

  1. DNS 方式

  1. HTTP API 方式

  • 不论该服务的健康检查是否通过都可以检索到

Method

Path

Produces

GET

/v1/agent/services

application/json

GET

/v1/agent/service/<service-id

application/json

  • 检索除服务以及该服务的健康状态

Method

Path ( 前面加 v1/agent... )

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

fromat=text 返回服务的状态,表示这组服务是否都通过了

0

评论区