#0329 Consul

和 etcd 的对比

Consul 除了实现了分布式 kv 存储的原语之外,还原生支持了服务发现、健康检查等高级功能。

cloudflare 如何部署

Cluster Architecture

As I mentioned, we have around 200 locations where we have servers. And importantly, we want those locations to be separate from each other. We want them to be part of different failure domains. We don’t want a failure in — let’s say — Amsterdam impacting the London location. With that in mind, we deployed a different Consul cluster in each of those locations — so the failure of a Consul cluster or anything else wouldn’t impact other locations.

https://www.hashicorp.com/resources/how-nomad-and-consul-are-being-used-at-cloudflare

一个边缘机房一个独立的 Consul。

安装

方法一:从 https://www.consul.io/downloads.html 下载预编译好的可执行文件,将下载得到的 consul 可执行文件直接放到 PATH 目录中。

方法二:使用包管理系统(CentOS)。

# yum install -y yum-utils
# yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
# yum -y install consul

国内服务器可能无法直接通过包管理直接安装 consul,可以手工下载安装。

# 在国外服务器上下载 rpm 包。
# consul 最新版本号可以在 https://www.consul.io/downloads 页面查看到。
wget https://rpm.releases.hashicorp.com/RHEL/7/x86_64/stable/consul-1.9.5-1.x86_64.rpm

# 将 consul-1.9.5-1.x86_64.rpm 复制拷贝到国内服务器上
# ...

# 安装
rpm -i consul-1.9.5-1.x86_64.rpm

其它:https://learn.hashicorp.com/tutorials/consul/get-started-install#install-consul

基本使用(使用命令行工具描述)

https://learn.hashicorp.com/collections/consul/getting-started

启动开发测试用 agent

agent 是 consul 的核心服务进程。

$ consul agent -dev

更多命令参见:https://www.consul.io/commands

开发测试用本地集群

多个 agent 可以组成集群,集群中的 agent 有 server 和 client 两种角色,server 参与 raft 共识算法,client 只转发请求给 server 处理,相当于一层代理。

mkdir /tmp/consul

# 在 root 中启动一个 server 角色的 agent,并配置集群中只需要一个 server 角色的 agent
# 主要是图简单,生产环境中需要至少配置 3 个或以上 server
consul agent -server --bootstrap-expect=1 -node=root -bind 172.17.2.1 \
    -data-dir=/tmp/consul/root -log-file=/tmp/consul/root >/dev/null 2>&1 &

# 在 ns 中各自分别启动一个 client 角色的 agent,并加入集群
for i in {2..4}; do
    NS=t$i$i
    ip netns exec $NS consul agent -node=$NS -bind 172.17.$i.$i -retry-join 172.17.2.1 \
        -data-dir=/tmp/consul/$NS -log-file=/tmp/consul/$NS >/dev/null 2>&1 &
done

查看集群中的节点:

# consul members
Node  Address          Status  Type    Build  Protocol  DC   Segment
root  172.17.2.1:8301  alive   server  1.9.4  2         dc1  <all>
t22   172.17.2.2:8301  alive   client  1.9.4  2         dc1  <default>
t33   172.17.3.3:8301  alive   client  1.9.4  2         dc1  <default>
t44   172.17.4.4:8301  alive   client  1.9.4  2         dc1  <default>

查看集群的 leader:

# curl localhost:8500/v1/status/leader
"172.17.2.1:8300"

其它常见运维操作

consul 参数也可以通过配置文件配置,见:https://www.consul.io/docs/agent/options#configuration_files

生产环境一般会配置 3 或者 5 个 server 角色的 consul,这样不会一个 server 挂了就无法提供服务了。https://www.consul.io/docs/architecture/consensus#deployment_table

生产环境中有些时候需要添加删除 server,常见场景:

  • 如果有 server 节点故障无法恢复,首先调用 consul force-leave <node-name> 删除该节点,然后再添加一个新的节点。

  • 如果要下线某台 server 的节点,一般是先添加一个节点,待新添加的节点状态变为健康后,再在要下线的机器上调用 consul leave 将其脱离集群。

添加节点的方法和前面安装配置启动服务一样,另外把新的节点 IP 加到 retry_join 中,bootstrap_expect 不用变(这个参数只在第一次部署集群的时候才有意思,其它时候没有意义)。

查看 server 角色机器的健康状态:

# consul operator autopilot state

autopilot 判定的节点健康的标准大概就是节点状态是 alive,并且 raft 日志没有落后 leader 太多,详细可见:https://learn.hashicorp.com/tutorials/consul/autopilot-datacenter-operations#server-health-checking

更多可以参见官方文档以及:https://imaginea.gitbooks.io/consul-devops-handbook/content/

故障处理

autopilot 处理机制

Consul 集群并不是超过 N/2 的 server 节点挂就一定会导致整个集群不可用,而是 一次性同时挂 的 server 节点不能超过节点总数的 1/2,否则会导致集群不可用。

Consul 的 autopilot 会定时将挂掉的节点从 raft server 节点里移除。这个配置由 autopilot 的 CleanupDeadServers 配置控制,默认开启。

# consul operator autopilot get-config
CleanupDeadServers = true
LastContactThreshold = 200ms
MaxTrailingLogs = 250
MinQuorum = 0
ServerStabilizationTime = 10s
RedundancyZoneTag = ""
DisableUpgradeMigration = false
UpgradeVersionTag = ""
# consul operator autopilot set-config -cleanup-dead-servers=false
Configuration updated!

比如一开始有 5 个 server 节点,如果同时挂掉 3 个节点,此时集群会不可用,但是如果是先挂 2 个节点,然后 autopilot 清理掉挂掉的节点,此时就变成了 3 个 server 节点,此时再挂 1 个节点,就不影响服务。最好不要动 CleanupDeadServers 这个配置。

https://learn.hashicorp.com/tutorials/consul/autopilot-datacenter-operations

集群故障为什么 consul members 命令还正常显示

consul members 命令展示的是 serf 节点发现的节点,serf 是基于 gossip 的协议,即使 raft 实例有问题,也能正常展示。

集群故障处理

如果集群故障导致命令执行出现 Unexpected response code: 500 (No cluster leader) 这样的错误信息,此时 raft 配置变更命令 consul leaveconsul force-leave 也就无法使用了(需要有 leader 才能执行),也就没办法移除故障节点让集群恢复服务。

此时,只能通过 raft/peers.json 文件来恢复集群。

找一个健康的 server 节点,在其 consul 数据目录的 raft 子目录下,添加一个 peers.json 文件,文件内容如下:

[
  {
    "id": "adf4238a-882b-9ddc-4a9d-5b6758e4159e",
    "address": "10.1.0.1:8300",
    "non_voter": false
  }
]

其中 id 为节点的 node-id,可以参见数据目录下的 node-id 文件,address 是节点的 IP 和端口,端口为节点的 RPC 服务端口。

添加完成后保存,然后重启 consul 服务,就可以恢复服务了。重启完成完成 peers.json 文件会被删除。

修改配置文件中 Gossip 加密的 Key 为什么没生效

修改配置文件中的 encrypt 为新 Key 后并重启了 Consul,但是 Key 没有生效,原因是 Consul 会在 serf/local.keyring 中缓存当前使用的 Key,这个 Key 后续只能通过 consul keyring 命令来更新,直接改配置文件重启 Consul 没有用。

# cat /opt/consul/serf/local.keyring
["Atmv2ENPBeGGR9/neXPIaIkbf+JSKe+Q2nIxYhnawsA="]

可以通过配置 disable_keyring_file 设置为 true 改变默认行为(但没必要)。

https://discuss.hashicorp.com/t/failed-to-join-no-installed-keys-could-decrypt-the-message/33324/2

consul keyring 的使用方法参见:https://learn.hashicorp.com/tutorials/consul/gossip-encryption-rotate

Raft has a leader but other tracking of the node would indicate that the node is unhealthy or does not exist

错误信息如下:

agent.server: Raft has a leader but other tracking of the node would indicate that the node is unhealthy or does not exist.  \
The network may be misconfigured.: leader=183.3.203.134:28300

这个错误一般出现在从一个 consul 集群中取出一部分节点构建一个新的 consul 集群(历史遗留特色场景),新集群配置了不同的 serf encrypt key 。这样新集群的节点无法通过 serf 节点发现 join 进老的集群, consul members 看老集群中这些节点处于 fail 状态。看新集群只有新的节点。这种时候就会报出上面的错误。

因为虽然 serf 节点发现加密了,但是 raft 是没有加密的,新集群认老的 raft leader,但是 leader 在新集群的成员列表里找不到,就会报这个错误。

https://github.com/hashicorp/consul/blob/v1.14.2/agent/consul/rpc.go#L774

处理方法就是使用 consul force-leave -prune <hostname> 从老的集群中删除这些 fail 的节点,然后再恢复新集群。

Go API Examples

导入包

import (
    "log"
    "time"

    "github.com/hashicorp/consul/api"
)
client, err := api.NewClient(api.DefaultConfig())

KV 存储

读写:

kv := client.KV()
p := &api.KVPair{Key: "KEY", Value: []byte("1000")}
_, err = kv.Put(p, nil)

pair, _, err := kv.Get("KEY", nil)
fmt.Printf("KV: %v %s\n", pair.Key, pair.Value)
// KEY 不存在的时候 pair 为 nil

事务:

ok, response, _, err := kv.Txn(api.KVTxnOps{
    &api.KVTxnOp{
        Verb: api.KVSet,
        Key: "KEY1",
        Value: []byte("1000"),
    },
    &api.KVTxnOp{
        Verb: api.KVSet,
        Key: "KEY2",
        Value: []byte("2000"),
    },
}, nil)

列取某一个前缀的所有 KEY:

keys, _, err := kv.Keys("KEY", "", nil)

列取某一个前缀的所有 KEY 和 VALUE:

kvs, _, err := kv.List("KEY", nil)

https://www.consul.io/api-docs/kv


kv 存储的值最大大小为 512KB。

https://www.consul.io/docs/troubleshoot/faq#q-what-is-the-per-key-value-size-limitation-for-consul-s-key-value-store

服务注册 & 健康检查

注册一个服务 & 健康检查,健康检查类型为 TTL,应用程序自行上报健康信息,如果 TTL 时间内没有上报,就标记服务为 fail:

ttl := 10*time.Second
agent := client.Agent()
serviceDef := &api.AgentServiceRegistration{
    Name: "myservice",
    Check: &api.AgentServiceCheck{
        TTL: ttl.String(),
        // 检查失败后多长时间后从 consul 中自动注销
        DeregisterCriticalServiceAfter: "1h",
    },
}
if err := agent.ServiceRegister(serviceDef); err != nil {
    log.Println("register service failed: ", err)
}

agent.UpdateTTL("service:myservice", "", "pass")
// 最后一个参数为服务状态,可以为 pass/warn/fail
// 刚注册服务状态为 fail,且需要每间隔一段时间(< TTL)上报一次状态

if err := agent.ServiceDeregister("myservice"); err != nil {
    log.Println("deregister service failed: ", err)
}

获取某一服务 & 健康检查的信息:

hs := client.Health()
serviceEntries, _, err := hs.Service("myservice", "", false, nil)
for _, e := range serviceEntries {
    log.Println(e.Node.Address, e.Checks.AggregatedStatus())
}

监控(watch)

使用 QueryOptions 中的 WaitTime 参数可以监控服务注册信息或者 KV 存储值的变化 。大致逻辑如下:

h := client.Health()
opts := &api.QueryOptions{WaitTime: 5*time.Second}
for {
    serviceEntries, meta, err := h.Service("myservice", "", false, opts)
    if err != nil {
        log.Error(err)
        continue
    }

    if opts.WaitIndex == meta.LastIndex {
        log.Info("wait timeout but key got no change")
        continue
    }
    opts.WaitIndex = meta.LastIndex

    log.Info("event:", serviceEntries)
}

分布式锁

lock, err := client.LockKey("mylock")
if err != nil {
    log.Fatal(err)
}

stopCh := make(chan struct{})

for {
    lostCh, err := lock.Lock(stopCh)
    if err != nil {
        log.Println("error:", err)
        continue
    }
    if lostCh == nil {
        lock.Unlock()
        break
    }

    log.Println("got lock")

    // go dosomething(lostCh)
    // 如果操作 kv 需要先检查 lostCh 看是否丢失了锁,如果是取消操作返回。

    <-lostCh
    log.Println("lost lock")
    lock.Unlock()
}

默认情况下,持有锁的应用进程如果挂了 / 网络不通,其它正在等待同一把锁的应用进程需要 15s 后才能获得锁,这个参数由 LockOptions 中的 LockDelay 参数控制,默认为 15s,作用在于:当持有锁的应用进程是因为比如网络抖动等问题导致锁丢失了而不是进程挂了,该应用进程可能正在写持有锁才能操作的 kv,给其一定的时间退出,防止多个应用进程同时写导致状态不一致。详细可以见:https://www.consul.io/docs/dynamic-app-config/sessions

锁底层是使用 KV + Session 实现的,详细可见:https://learn.hashicorp.com/tutorials/consul/application-leader-elections