服务发现

什么是服务发现?

IP:Port/path

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

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

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

如图所示:

注册中心的核心是存储系统,通常就是 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。

如图所示:

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

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

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

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

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

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

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

Consul 作为服务发现

Consul 简介

安装

Consul 下载页

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
git clone https://github.com/hashicorp/consul
cd consul
make tool
make linux

macOS

brew
brew tap hashicorp/tap
brew install hashicorp/tap/consul
二进制

选择合适的版本:

FreeBSD

选择合适的二进制版本:

Solaris

选择合适的二进制版本:

Windows

选择32或64位下载:

下载后,解压即可。内包含直接可运行的执行程序:

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://<IP>: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 &#x3c;container-id>` 的方式查看网络信息。

以 docker 方式运行的,需要找到容器的 IP 才可以。docker inspect &#x3c;Container>

$ sudo docker inspact <container>
"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://<IP>:8500/ui
http://<IP>:8501/ui
http://<IP>:8502/ui
http://<IP>:8503/ui
http://<IP>:8504/ui
http://<IP>:8505/ui
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    <all>
ServerB  172.17.0.3:8301  alive   server  1.12.2  2         dc1  default    <all>
ServerC  172.17.0.4:8301  alive   server  1.12.2  2         dc1  default    <all>
Client1  172.17.0.5:8301  alive   client  1.12.2  2         dc1  default    <default>
Client2  172.17.0.6:8301  alive   client  1.12.2  2         dc1  default    <default>
Client3  172.17.0.7:8301  alive   client  1.12.2  2         dc1  default    <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
.jsonconsul agentconsul services

consul services register 注册服务

consul agent 启动后,通过 CLI 注册即可,命令如下:

consul services register <service-config.json>

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.json> -config-dir=<configDir>

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

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

  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 的格式为字符串,常用的检索支持:

AddressPortServiceTagsID

其中运算符语法为:// 是否相等


<Selector> == "<Value>"

<Selector> != "<Value>"

// 是否为空

<Selector> is empty

<Selector> is not empty

// 包含 // 子串检查

"<Value>" in <Selector>

"<Value>" not in <Selector>

<Selector> contains "<Value>"

<Selector> not contains "<Value>"

// 正则匹配

<Selector> matches "<Value>"

<Selector> not matches "<Value>"

同时支持使用逻辑运算连接多个条件:

// Or

<Expression 1> or <Expression 2>

// And

<Expression 1 > and <Expression 2>

// Not

not <Expression 1>

// 分组

( <Expression 1> )

// Inspects data to check for a match

<Matching Expression 1>
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。

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

域名这种配置,就几乎不用改变。

cousul.

服务注销

服务支持 HTTP API命令行的方式注销。

HTTP API

PUT/v1/agent/service/deregister/:service_idapplication/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 示例,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

我们以最常用的定义服务时,同时定义健康检查为例,演示:

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 查看健康检查状态!

2xx429 Too ManyRequests

再看一个 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 来调整初始状态。

deregister_critical_service_after

服务健康状态查询

当我们使用服务发现查询服务时,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/servicesGET /v1/agent/service/<service-id>

我们需要接口:

GET/agent/health/service/name/:service_nameapplication/jsonGET/agent/health/service/name/:service_name?format=texttext/plainGET/agent/health/service/id/:service_idapplication/jsonGET/agent/health/service/id/:service_id?format=texttext/plain

来基于名字或ID获取服务的健康状态。

注意,基于名字来获取服务状态如果使用 text 格式,那么必须全部服务都通过,状态才为通过,否则为紧急。

postman 测试

HTTP 之 H2 vs H1

现阶段,我们所说的 HTTP 请求,通常表示 HTTP/1.1 版本的请求。HTTP/1.1 于1997年1月发布,目前最流行的版本。

HTTP/1.1 的典型特点:

Connection: keep-aliverangeCache-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 的。