【后端开发】Consul服务与配置

Last updated on February 22, 2024 am

Consul介绍

Consul是hashicorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。 内置了服务注册与发现框架、分布一致性协议实现、健康检查、key/value存储、多数据中心方案,不再需要依赖其它工具。

Consul是一个服务网络解决方案,它使团队能够管理服务之间以及跨多云环境和运行时的安全网络连接。Consul提供服务发现、基于身份的授权、L7流量管理和服务到服务加密。

Consul示意图

Consul角色

服务发现和注册

  • dev:开发模式
  • client:客户端,接受请求转达给服务端集群,将http和dns接口请求转发给局域网内的服务端集群,它只是一个代理的角色
  • server:服务端,保存配置信息,高可用集群,每个数据中心的server数据推荐为3个或5个

Consul内部角色介绍

  • 不管是server还是client,统称为agent
  • consul client是相对无状态的,只负责转发rpcserver,资源开销很少
  • server是一个有一组扩展功能的代理,这些功能包括参与raft选举,维护集群状态,响应rpc查询,与其它数据中心交互wan gossip和转发查询给leader或远程数据中心。
  • 每个数据中心,clientserver是混合的,一般建有3-5台server

安装Consul

安装地址:https://www.consul.io/,这里建议安装终端版本

1
2
consul agent -dev -clinent=0.0.0.0
#-dev表示开发模式运行, -server表示服务模式运行
1
2
#默认端口
localhost:8500

登陆这个默认的端口就能看见可视化的界面 Consul的可视化界面

详细的运行指令解析

1
consul agent -server -ui -bootstrap-expect=1 -client=0.0.0.0 -bind 0.0.0.0 -data-dir /Users/lihaibin/Workspace/consuldata
  • -bootstrap-expect:设定一个数据中心需要的服务节点数,可以不设置,设置的数字必须和实际的服务节点数匹配。consul会等待直到数据中心下的服务节点满足设定才会启动集群服务。初始化leader选举,不能和bootstr- ap混用。必须配合-server配置。
  • -bind:绑定的内部通讯地址,默认0.0.0.0,即,所有的本地地址,会将第一个可用的ip地址散播到集群中,如果有多个可用的ipv4,则consul启动报错。[::]ipv6,TCP UDP协议,相同的端口。防火墙配置。
  • -client:客户端模式,http dns,默认127.0.0.1,回环令牌网址
  • -data-dir:状态数据存储文件夹,所有的节点都需要。文件夹位置需要不收consul节点重启影响,必须能够使用操作系统文件锁,unix-based系统下,文件夹文件权限为0600,注意做好账户权限控制
  • -dev:开发模式,去掉所有持久化选项,内存服务器模式。
  • -server:服务端节点模式。
  • -ui:内置web ui界面
  • -log-file:日志记录文件,如果没有提供文件名,则默认Consul-时间戳.log

Consul 中默认的端口号

  1. 服务器RPC(默认8300):由服务器用来处理来自其他代理的传入请求,仅限TCP。
  2. Serf LAN(默认8301):用来处理局域网中的八卦。所有代理都需要,TCP和UDP。
  3. Serf WAN(默认8302):被服务器用来在WAN上闲聊到其他服务器,TCP和UDP。从Consul 0.8开始,建议通过端口8302在LAN接口上为TCP和UDP启用服务器之间的连接,以及WAN加入泛滥功能。
  4. HTTP API(默认8500):被客户用来与HTTP API交谈,仅限TCP。
  5. DNS接口(默认8600):用于解析DNS查询,TCP和UDP。

Consul工作原理

producer:服务提供者

consumer:服务消费者

image-20240202230444248

服务发现与注册

  • producer启动时,会将自己的ip/host等信息通过发送请求告知consul
  • consul接收到producer的注册信息后,每隔10秒(默认)会向producer发送一个健康检查的请求,检查producer是否处于可用状态
  • post 服务注册 /health健康定期检查

服务调用

  • consumer请求product时,会先从consul中拿存储的producer服务的ip和port的临时表(temp table),从表中任选一个producer的ip和port
  • 根据这个ipport,发送访问请求
  • 此表只包含通过健康检查的producer信息,并且每隔10秒更新
  • Temp table 拉取服务列表 从临时表中拿producer的ip和端口发送请求

Consul+Go实现

下面给出一个例子对于在GO项目中是如何结合Consul进行使用的

consul中间件目录

首先给出consul中间件的结构体图,这里主要对consul进行了sdk的封装,包含了以下的内容:

  1. consulclient:服务注册与发现的板块
  2. consulconfig:主要是中心化配置的内容
  3. consulsdk:主要是对以上的两个板块进行了封装
  4. main_test:是给出了一个例子对上述的内容进行测试

Consulclient

这部分着重来写如何实现对服务注册和服务发现的功能

  • 定义 ConsulClient 结构体
1
2
3
4
5
goCopy code
type ConsulClient struct {
client *consulapi.Client
serverPort int
}

ConsulClient 结构体包含了一个 Consul 客户端指针和一个服务端口。

  • 创建新的 Consul 客户端
1
2
3
4
5
6
7
8
9
10
func NewConsulClient(consulAddress string, serverPort int) (*ConsulClient, error) {
config := consulapi.DefaultConfig()
config.Address = consulAddress
client, err := consulapi.NewClient(config)
if err != nil {
return nil, err
}
return &ConsulClient{client: client, serverPort: serverPort}, nil
}

NewConsulClient 函数用于创建一个新的 Consul 客户端实例。它接收 Consul 服务器的地址和服务端口作为参数,并返回一个 ConsulClient 实例以及可能的错误。

  • 注册服务
1
2
3
4
5
6
7
8
9
10
func (c *ConsulClient) RegisterService(serviceID, serviceName, serviceHost string, servicePort int) error {
service := &consulapi.AgentServiceRegistration{
ID: serviceID,
Name: serviceName,
Address: serviceHost,
Port: servicePort,
}
return c.client.Agent().ServiceRegister(service)
}

RegisterService 方法用于向 Consul 注册服务。它接收服务的ID、名称、主机和端口作为参数,并向 Consul 注册该服务。

  • 服务发现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func (c *ConsulClient) DiscoverService(serviceName string) (string, error) {
services, _, err := c.client.Health().Service(serviceName, "", true, nil)
if err != nil {
return "", err
}
if len(services) == 0 {
return "", fmt.Errorf("service not found")
}

// 随机选择一个服务实例
if len(services) > 0 {
index := rand.Intn(len(services))
service := services[index].Service
address := fmt.Sprintf("%v:%v", service.Address, service.Port)
return address, nil
}

return "", fmt.Errorf("no healthy instances found for service %s", serviceName)
}

DiscoverService 方法用于从 Consul 中发现服务实例。它接收服务名称作为参数,并返回一个服务实例的地址。在内部,它通过健康检查来获取可用的服务实例,并随机选择一个健康的实例返回其地址。

Consulconfig

这段代码实现了一个基本的 Consul 配置中心客户端,用于从 Consul 中获取、设置和删除键值对的配置信息。

  • 定义 ConsulConfigCenter 结构体
1
2
3
type ConsulConfigCenter struct {
client *consulapi.Client
}
  • 创建新的 Consul 配置中心客户端
1
2
3
4
5
6
7
8
9
func NewConsulConfigCenter(consulAddress string) (*ConsulConfigCenter, error) {
config := consulapi.DefaultConfig()
config.Address = consulAddress
client, err := consulapi.NewClient(config)
if err != nil {
return nil, err
}
return &ConsulConfigCenter{client: client}, nil
}

NewConsulConfigCenter 函数用于创建一个新的 Consul 配置中心客户端实例。它接收 Consul 服务器的地址作为参数,并返回一个 ConsulConfigCenter 实例以及可能的错误

  • 获取特定键对应的值
1
2
3
4
5
6
7
8
9
10
11
func (cc *ConsulConfigCenter) GetValue(key string) (string, error) {
kv := cc.client.KV()
pair, _, err := kv.Get(key, nil)
if err != nil {
return "", err
}
if pair == nil {
return "", fmt.Errorf("key '%s' not found", key)
}
return string(pair.Value), nil
}

GetValue 方法用于从 Consul 中获取特定键对应的值。它接收键名作为参数,并返回键对应的值以及可能的错误。

  • 设置键值对
1
2
3
4
5
6
func (cc *ConsulConfigCenter) SetValue(key, value string) error {
kv := cc.client.KV()
p := &consulapi.KVPair{Key: key, Value: []byte(value)}
_, err := kv.Put(p, nil)
return err
}

SetValue 方法用于在 Consul 的键值存储中设置一个键值对。它接收键名和值作为参数,并将其设置到 Consul 中。

  • 删除键值对
1
2
3
4
5
func (cc *ConsulConfigCenter) DeleteValue(key string) error {
kv := cc.client.KV()
_, err := kv.Delete(key, nil)
return err
}

DeleteValue 方法用于从 Consul 的键值存储中删除指定的键值对。它接收键名作为参数,并将对应的键值对从 Consul 中删除。

ConsulSDK

实现了一个基于单例模式的 Consul SDK,用于管理 Consul 客户端和配置中心。让我们逐段解释代码的功能

  • 定义 ConsulSDK 结构体
1
2
3
4
type ConsulSDK struct {
Client *ConsulClient
ConfigCenter *ConsulConfigCenter
}

ConsulSDK 结构体包含了 Consul 客户端和配置中心的实例

  • 定义全局变量和 sync.Once 实例
1
2
3
4
var (
instance *ConsulSDK
once sync.Once
)

定义了 instance 变量用于保存 ConsulSDK 实例,once 变量用于确保 GetInstance() 函数只被执行一次

  • 实现 NewConsulSDK 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func NewConsulSDK(consulAddress string, serverPort int) (*ConsulSDK, error) {
// 创建 Consul 客户端
client, err := NewConsulClient(consulAddress, serverPort)
if err != nil {
return nil, err
}

// 创建 Consul 配置中心
configCenter, err := NewConsulConfigCenter(consulAddress)
if err != nil {
return nil, err
}

// 返回 ConsulSDK 实例
return &ConsulSDK{
Client: client,
ConfigCenter: configCenter,
}, nil
}

NewConsulSDK 函数用于创建一个新的 ConsulSDK 实例,它接收 Consul 服务器地址和端口作为参数,并返回一个 ConsulSDK 实例以及可能的错误

  • 实现 GetInstance() 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func GetInstance() *ConsulSDK {
// 保证只执行一次
once.Do(func() {
consulAddress := "127.0.0.1:8500"
serverPort := 8080

// 创建 Consul 客户端
client, err := NewConsulClient(consulAddress, serverPort)
if err != nil {
fmt.Println("Failed to create Consul client:", err)
os.Exit(1)
}

// 创建 Consul 配置中心
configCenter, err := NewConsulConfigCenter(consulAddress)
if err != nil {
fmt.Println("Failed to create Consul config center:", err)
os.Exit(1)
}

// 初始化 ConsulSDK 实例
instance = &ConsulSDK{
Client: client,
ConfigCenter: configCenter,
}
})
return instance
}

GetInstance() 函数用于获取 ConsulSDK 的单例实例。它通过 once.Do() 确保只执行一次,创建 Consul 客户端和配置中心,并将其保存到全局变量 instance 中,然后返回该实例

Main_test.go

这个部分主要对上述的sdk接口给出了一个具体的测试用例

注意在终端运行的时候的运行为:go test go默认对_test会进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package consul

import (
"fmt"
"os"
"testing"
)

func TestConsul(t *testing.T) {
//创建新的 consul sdk
sdk := GetInstance()

// 注册服务
serviceID := "my_service"
serviceName := "my_service"
serviceHost := "127.0.0.1"
servicePort := 8080

err := sdk.Client.RegisterService(serviceID, serviceName, serviceHost, servicePort)
if err != nil {
fmt.Printf("Error registering service: %v\n", err)
os.Exit(1)
}

// 发现服务
serviceAddress, err := sdk.Client.DiscoverService(serviceName)
if err != nil {
fmt.Printf("Error discovering service: %v\n", err)
os.Exit(1)
}
fmt.Printf("Discovered service address: %s\n", serviceAddress)

//使用 ConsulConfig 获取键对应的值
value, err := sdk.ConfigCenter.GetValue("test")
if err != nil {
fmt.Println("Failed to get value:", err)
return
}
fmt.Println("Value:", value)
}

Consul持久性

在Consul中,持久性是指存储的数据在服务重启或者集群重新选举等情况下仍然能够保持不变的特性。Consul提供了持久化存储功能,允许用户将重要的键值对数据存储在集群中,并确保==这些数据即使在Consul服务重启后也能被保留==

  1. Catalog数据持久化: Consul的Catalog存储了有关服务、节点和数据中心成员的信息。这些数据被用于服务发现、健康检查和路由等功能。Catalog数据的持久化确保了在Consul服务重启后这些关键信息不会丢失。
  2. 健康检查状态持久化: Consul支持对注册的服务进行健康检查,以确保服务的可用性。健康检查状态也是Consul的持久化数据之一。持久化健康检查状态确保了即使Consul服务重启,之前的健康检查状态仍然有效。
  3. ACL(Access Control List)配置持久化: ACL是Consul中用于访问控制的重要功能。ACL配置包括用户、角色、权限和令牌等信息。这些配置的持久化确保了在Consul重启后ACL配置仍然有效,保障了安全访问。
  4. 事件日志持久化:Consul记录了各种事件日志,包括服务注册、健康检查状态变化、成员变化等。事件日志的持久化确保了即使Consul服务重启,之前的事件记录不会丢失

持久性存储通常是通过在Consul服务器上将数据写入磁盘来实现的,这样即使Consul服务停止运行,数据也不会丢失。

  • 第一步需要指定存储数据的磁盘文件夹位置

    1
    consul agent -server -ui -bootstrap-expect=1 -client=0.0.0.0 -bind 0.0.0.0 -data-dir /Users/lihaibin/Workspace/consuldata

    其中 -data-dir /Users/lihaibin/Workspace/consuldata就是在指定需要持久性存储数据的文件夹位置,事实上只需要对data-dir位置进行了指定就能实现对数据的持久性存储关键就在于在启动consul的时候有没有指定

  • 第二步代码指定数据存储

这里给出一个持久性存储的实例代码(KV存储):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
consulapi "github.com/hashicorp/consul/api"
)
func main() {
// 创建 Consul 客户端
config := consulapi.DefaultConfig()
config.Address = "localhost:8500"
client, err := consulapi.NewClient(config)
if err != nil {
fmt.Println("Failed to create Consul client:", err)
return
}
// 获取 Consul KV 存储
kv := client.KV()

KV 存储是 Consul 中用于持久化存储键值对数据的一种机制,而这种持久化存储正是与 Consul 的持久性密切相关的

1
2
3
4
5
6
7
8
9
// 写入数据到 Consul
key := "example/key"
value := []byte("example value")
p := &consulapi.KVPair{Key: key, Value: value}
_, err = kv.Put(p, nil)
if err != nil {
fmt.Println("Failed to write data to Consul:", err)
return
}

这段代码的作用是将指定的键值对写入到Consul的键值存储中,从而实现数据的持久化存储

KV存储的示意图

1
2
3
4
5
6
7
8
9
10
11
12
// 从 Consul 读取数据
pair, _, err := kv.Get(key, nil)
if err != nil {
fmt.Println("Failed to read data from Consul:", err)
return
}
if pair == nil {
fmt.Println("Key not found in Consul")
return
}
fmt.Printf("Key: %s, Value: %s\n", pair.Key, pair.Value)
}

这段代码的功能是从Consul中读取指定键的值,并将其打印出来

通过实验也可以看出关闭了Consul之后重新打开仍然能够让设置的KV数据能存储在指定的位置上

持久性保持


【后端开发】Consul服务与配置
https://lihaibineric.github.io/2024/02/02/develop_consul/
Author
Haibin Li
Posted on
February 2, 2024
Updated on
February 22, 2024
Licensed under