golang 微服务 consul 配置3客户端 3服务端服务发现

服务发现

什么是服务发现?

服务发现,Service Discovery 指的是若服务 A 需要与 服务 B 进行通信,那么如何知道服务 B 的地址?服务发现的作用,就是通过服务注册中心,来告知服务A,服务B 的地址在哪里。这里说的地址,通常就是 IP:Port/path 的形式。

基于这段描述,服务发现机制由三个角色构成:

  1. 服务的消费者,也就是服务A,其他服务的使用者。Consumer
  2. 服务的提供者,也就是服务B,为其他角色提供服务。Provider
  3. 服务注册中心,也称服务中介,存储已经注册的服务信息(地址),提供查找功能。

其思想也很清晰:若服务B需要为其他角色提供服务,那么服务B要将自身的信息(地址)注册到服务注册中心,这样其他服务(A),就可以在注册中心找到目标服务(B)。

如图所示:

image.png

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

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

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

本例中,ServiceB 作为服务提供者,需要完成服务注册操作。之后 ServiceA 需要 Service B 的功能,需要三步走:

  1. 查询 ServiceB 的信息
  2. 注册中心告知 ServiceA:ServiceB 的信息
  3. ServiceA 请求 ServiceB 的服务。

以上就是服务发现的介绍。可见,只要支持 Key/Value 存储机制的产品,都可以作为服务中心来使用,来提供服务注册和发现功能。

早期,我就使用过 Redis 来实现服务注册中心。

  • 多个 zset 项存储服务信息
  • zset 的 key 为服务标识 ,zset 的成员为服务地址作,成员的 score 存储服务心跳时间戳,用于对服务做健康检测。
  • 服务注册基于 ZADD 命令实现
  • 服务发现基于 ZRANDMEMBER 命令实现
  • 服务移除基于 ZREM 命令实现
  • 还会使用一个集合记录全部的服务标识,可以是 List 或 Set。

如图所示:

image.png

除了存储之外,还要提供客户端供程序使用。客户端需要提供服务心跳、服务更新通知、负载均衡等功能。这里就不再深入了,大家如果对这个例子感兴趣,可以移步 https://github.com/han-joker/DiscoveryOnRedis.git。

现在有完善的注册中心产品,例如 Consul,Etcd,ZooKeeper 等,不需要我们自己来实现了。

微服务需要什么样的服务发现?

微服务系统需要一个分布式的服务注册中心来实现服务发现。分布式的注册中心可以保证不会出现单点失效的严重问题。

由于微服务架构的服务数量会很多,因此服务的健康检查就很重要,可以及时将无效服务从注册中心剔除。

最好有一个服务管理工具,便于我们观察集群、服务状态等。

基于以上原因,我们会从 Consul,Etcd,ZooKeeper 中做选择,因为以上三个,都是基于分布式存储系统构建的服务发现器。

Consul 作为服务发现

Consul 简介

安装

Consul 下载页

https://www.consul.io/downloads

image.png

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
image.png

Consul 的基本架构

整体架构

image.png

下面对 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)

服务注册

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

  1. consul services 命令完成服务的注册和注销
  2. consul agent 在启动时,同时完成服务的注册
  3. 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 agentconsul 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 。

产品服务示例代码,代码流程:

  1. 采用 net/http 包定义服务
  2. 定义测试路由及处理器。/info
  3. 使用 consul/api 包完成服务注册
  4. 启动服务监听
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 提供了俩个方案,完成服务查询:

  1. HTTP API
  2. 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 服务器,并将注册其中的服务都分配了对应的域名。

image.png

例如:

  • 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-ControlEtag/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 的。

你可能感兴趣的:(golang 微服务 consul 配置3客户端 3服务端服务发现)