Consul模板
Consul模板工具可以使用编程的方式从包括Consul KV数据集在内的各种位置渲染配置文件,模板工具基于Go模板,并有许多相同的属性。
Consul模板具有多种用途,我们将重点介绍其中的两个:
-
更新配置文件
: Consul模板工具可用于更新服务配置文件,一种常见的用法是管理负载均衡配置文件,这些文件需要在许多无法直接连接到Consul群集的计算机上定期动态更新; -
发现有关Consul群集和服务的数据
:可以收集有关Consul群集中服务的信息。例如,用户可以收集群集上运行的所有服务的列表,或者可以发现Redis所有服务的地址。注意,这个操作在生产环境有所限制。
在本章节中,我们将简要讨论Consul模板的工作原理,安装方法以及两个用例,在阅读本章节前,我们假设您对Consul KV和Go模板有所了解。
Consul模板简介
Consul模板是一个简单但功能强大的工具。启动后,它将读取一个到多个模板文件,并向Consul发出查询,加载它们所需的所有数据。通常,consul-template
作为守护程序运行,一开始获取初始值,之后持续监控更新,在集群中发生任何相关更改时都会重新加载模板。用户也可以使用-once
标志仅加载一次模板,这在测试或者由其他脚本触发时比较有用。最后,更新过程完成后,模板还可以运行后置命令。例如,它可以在进行配置更改后将HUP信号发送到负载均衡器服务。
Consul模板工具非常灵活,可以适应许多不同的环境和工作流程。根据使用情况,用户可能在少数几个主机上只有一个consul-template实例,或者也可能需要在每个主机上运行多个实例。每个Consul模板过程都可以管理多个不相关的文件,如果这些文件共享数据依赖项,则将根据需要对提取的内容进行重复数据删除,这样可以减少Consul服务器上可能共享的负载。
安装Consul模板
在本章节中,我们在开发者模式下使用本地Consul代理,先执行consul agent -dev
,Consul代理正常运行后才能进行以下其他步骤。
Consul模板工具本身不包含在Consul二进制文件中,需要单独安装。用户可以直接安装预编译的二进制文件,也可以下载源代码自行编译,我们将安装预编译的二进制文件,首先,先下载consul-template
二进制文件:
$ curl -O https://releases.hashicorp.com/consul-template/0.19.5/consul-template<_version_OS>.tgz
接下来,将二进制文件加入$PATH
路径中:
$ tar -zxf consul-template<_version_OS>.tgz
用例:Consul KV数据集
在第一个用例中,我们将渲染一个模板,该模板会从Consul KV数据集
中提取HashiCorp地址。我们需要创建一个包含HashiCorp地址的简单模板,运行consul-template
,为Consul KV数据集添加一个HashiCorp地址的值,最后查看渲染的文件。
首先,我们需要创建一个模板文件find_address.tpl
来查询Consul KV数据集:
{{ key "/hashicorp/street_address" }}
运行consul-template
命令,同时指定要使用的模板和需要更新的文件:
$ consul-template -template "find_address.tpl:hashicorp_address.txt"
consul-template
命令会持续运行,用户使用CTRL+c
可以停止其运行,然后我们打开一个新终端,使用命令行指令将数据写入Consul:
$ consul kv put hashicorp/street_address "101 2nd St"
Success! Data written to: hashicorp/street_address
我们可以通过查看hashicorp_address.txt
文件来确保有数据写入文件:
$ cat hashicorp_address.txt
101 2nd St
如果用户执行命令consul kv put hashicorp/street_address "22b Baker ST"
更新hashicorp/street_address
值,可以看到该文件立即更新。这个过程虽然很简单,但是意义很强大,比如用户可以用相同的过程来更新HAProxy负载均衡器配置。
用例:发现所有服务
在此示例中,我们将发现Consul群集中运行的所有服务,我们使用上一个示例中的开发环境接着开发。
首先,我们将需要创建一个新模板all-services.tpl
来查询所有服务。
{{range services}}# {{.Name}}{{range service .Name}}
{{.Address}}{{end}}
{{end}}
接下来,运行consul-template
命令指定我们刚刚创建的模板,并使用-once
标志,表示仅运行一次:
$ consul-template -template="all-services.tpl:all-services.txt" -once
如果您在本地代理上完成此操作,则可以通过all-services.txt
文件查看consul服务:
# consul
127.0.0.7
在开发或生产集群上,用户会看到所有服务的列表:
# consul
104.131.121.232
# redis
104.131.86.92
104.131.109.224
104.131.59.59
# web
104.131.86.92
104.131.109.224
104.131.59.59
使用会话实现应用程序领导者选举
对于某些应用程序,例如HDFS
,有必要将一个实例设置为领导者,这样可以确保应用程序数据是最新且稳定的,本章节介绍了如何使用Consul为服务实例构建面向客户端(client-side)
的领导者选举,Consul对会话的支持可帮助用户构建一个能够正常处理故障的系统。
这篇章节的内容与Consul本身的领导者选举无关,如果对该部分内容感兴趣,可以参考consensus
协议。
竞争服务实例
想象一下,用户有一组服务实例正在争夺给定服务的领导地位,所有参与的服务实例都应商定一个给定的密钥进行协调,一个好的模式就是:
service/<service name>/leader
在本章节中,我们的完整密钥可能是service/dbservice/leader
,为了简化行文,我们将密钥定义为lead
。
创建会话
首先使用HTTP API创建一个会话:
$ curl -X PUT -d '{"Name": "dbservice"}' http://localhost:8500/v1/session/create
这将返回一个包含会话ID的JSON对象:
{
"ID": "4ca8e74b-6350-7587-addf-a18084928f3c"
}
获取会话
下一步是某个服务实例使用PUT方法获取给定密钥的会话,请求需要拼接?acquire=<session>
查询参数。
PUT方法中的<body>
应该是代表本地实例的JSON对象。该值对Consul不透明,但应包含客户端与用户应用程序进行通信所需的必要信息(例如,它可以是包含节点名称和应用程序端口的JSON对象)。
$ curl -X PUT -d <body> http://localhost:8500/v1/kv/lead?acquire=4ca8e74b-6350-7587-addf-a18084928f3c
响应结果为true
或false
,如果为true,则表示已经获取到锁
,本地服务实例现在为领导者,如果返回值为false
,则说明其他某个节点获得了锁
。
监听会话
现在,所有其他实例都处于空闲等待状态。在这种状态下,它们会监听密钥lead
的变化,锁可能随时会被释放,实例也可能失效。领导者也必须注意变化,因为运维人员可能手动释放锁,或者系统故障检查误报自动释放锁。
默认情况下,会话仅使用Gossip故障
检测器,也就是说,只要默认的Serf健康检查
未声明该节点不健康,就认为该节点保留着会话。如果需要,也可以指定其他检查。
通过对<key>
使用阻塞查询(blocking query)
来监听变化。如果它们察觉到<key>
的会话为空,则说明此时没有领导者,然后会重试获取锁。每次尝试获取密钥都需要有一段时间间隔,Consul强制使用了lock-delay
。
释放会话
领导者可以解除锁来释放资源:
$ curl -X PUT http://localhost:8500/v1/kv/lead?release=4ca8e74b-6350-7587-addf-a18084928f3c
发现领导者
关于领导者选举,另一种常见的场景是,非领导者实例希望在实例集群中辨认出哪个实例是领导者,与选举领导者一样,所有参与的实例都应就用于协调的密钥达成一致,该密钥将被称为公正密钥(just key)
。
获取密钥
服务实例需要做的非常简单,它们仅需读取密钥就能找到当前的领导者,如果密钥有关联的会话,则说明已经有领导者了。
$ curl -X GET http://localhost:8500/v1/kv/lead
[
{
"Session": "4ca8e74b-6350-7587-addf-a18084928f3c",
"Value": "Ym9keQ==",
"Flags": 0,
"Key": "dbservice",
"LockIndex": 1,
"ModifyIndex": 29,
"CreateIndex": 29
}
]
如果有领导者,则value
字段会显示与应用程序相关的信息,信息使用Base64
编码。
获取会话信息
用户可以查询/v1/session/info
端点获取会话的详细信息:
$ curl -X GET http://localhost:8500/v1/session/info/4ca8e74b-6350-7587-addf-a18084928f3c
[
{
"LockDelay": 1.5e+10,
"Checks": [
"serfHealth"
],
"Node": "consul-primary-bjsiobmvdij6-node-lhe5ihreel7y",
"Name": "dbservice",
"ID": "4ca8e74b-6350-7587-addf-a18084928f3c",
"CreateIndex": 28
}
]
使用会话实现分布式信号量
当用户想要调度许多服务,同时某些资源有访问限制时,分布式信号量(distributed semaphore)
可能会很有用。在本章节中,我们将重点介绍如何使用Consul对会话的支持以及利用Consul KV数据集来构建分布式信号量。有多种方式可以构建信号量,本章节不会全部覆盖。
在阅读本章节前,用户应该熟悉Consul KV数据集
和Consul会话
。
信号量中的竞争节点
想象一下,我们有一组试图获取信号量中插槽(slot)
的节点,所有参与竞争的节点都应达成以下三个共识:
- KV数据集中用于调度的前缀;
- 单独的
键值(Key)
作为锁; - 持有插槽数量的限制。
会话
第一步是为每个竞争节点创建一个会话,会话使我们能够构建可以正常处理故障的系统。
$ curl -X PUT -d '{"Name": "db-semaphore"}' \
http://localhost:8500/v1/session/create
响应值是包含会话ID的JSON对象。
{
"ID": "4ca8e74b-6350-7587-addf-a18084928f3c"
}
创建竞争锁
接下来,我们创建一个竞争锁(lock contender)
。每个竞争者都会创建一个与会话相关联的KV条目(KV entry)
。这样做的目的是,如果竞争者持有某个插槽并发生故障,则其会话会与键值分离 ,然后其他竞争者可以检测到该会话,用户可以发送PUT
请求创建竞争锁:
curl -X PUT -d <body> http://localhost:8500/v1/kv/<prefix>/<session>?acquire=<session>
body
的值一般为与该竞争锁相关的有意义的内容 ,比如节点的名称,Consul本身不关注body
的内容,这个值对运维人员有用,<session>
的值是/v1/session/create
请求的返回值。
该调用结果返回true
或者false
。如果为true
,则说明创建竞争锁创建成功。如果为false
,则表明创建失败,这通常意味着会话失效。
创建键值(Key)
下一步是创建一个键值(Key)
,用来协调插槽的持有者。 <prefix>/.lock
是该键值的一个不错选择,我们称这个特殊的键值为<lock>
。
$ curl -X PUT -d <body> http://localhost:8500/v1/kv/<lock>?cas=0
由于正在创建锁,此处cas
索引值为0
,仅当该值不存在时才放入,body
的内容为信号量的插槽限制和当前所有者的会话ID,内容如下所示:
{
"Limit": 2,
"Holders": ["<session>"]
}
信号量管理
对<prefix>
发送GET
请求可以读取信号量当前状态:
$ curl http://localhost:8500/v1/kv/<prefix>?recurse
在返回值中,我们应该重点关注两个值:<lock>
和<session>
:
[
{
"LockIndex": 0,
"Key": "<lock>",
"Flags": 0,
"Value": "eyJMaW1pdCI6IDIsIkhvbGRlcnMiOlsiPHNlc3Npb24+Il19",
"Session": "",
"CreateIndex": 898,
"ModifyIndex": 901
},
{
"LockIndex": 1,
"Key": "<prefix>/<session>",
"Flags": 0,
"Value": null,
"Session": "<session>",
"CreateIndex": 897,
"ModifyIndex": 897
}
]
返回的响应中,<lock>
字段使用的是Base64编码。
读取<lock>
数据并将其解码后,我们可以验证Limit
与Holders
计数是否一致,这用于检测潜在冲突。下一步是确定当前哪些插槽(slot)
仍在运行。查询响应结果返回了所有竞争者条目,通过扫描这些条目,我们可以创建了一组会话
值,不在该组中的所有Holders
都会被修剪。实际上,我们将根据列表结果创建一组实时竞争者,并与Holder
进行一定的区别以检测和修剪任何可能失败的Holder。在此示例中,<session>
存在于Holders
中,并附加在键<prefix>/<session>
上,因此不需要修剪。
如果修剪后的持有者数量少于限制数量,则竞争者会尝试通过将其自己的会话添加到持有者(Holders)
列表中,并对<lock>
进行Check-And-Set
操作,这会执行乐观更新。
我们可以通过以下方式获取:
$ curl -X PUT -d <Updated Lock Body> http://localhost:8500/v1/kv/<lock>?cas=<lock-modify-index>
lock-modify-index
是<lock>
已知的最新ModifyIndex
值,在此示例中为901
。
如果该请求返回true
,竞争者将在信号量中保留一个插槽,如果返回false
,则可能是与另一个竞争者发生了争夺。
当重新尝试进行数据采集时,我们会注意到<prefix>
的更新。这是因为可能会释放插槽,或者节点发生故障等等。通过对/kv/<prefix>recurse
接口进行阻塞查询可以监控更新。
插槽持有者必须不断关注<prefix>
的更新,因为运维人员可以释放插槽,或者由于故障检测器误报而自动释放插槽。在更改<prefix>
时,必须重新检查锁的持有人(Holders)
列表,以确保仍保留该插槽。另外,如果监听器无法连接插槽,则应视为丢失。
此信号量系统纯粹是建议性的。因此,还是应该由客户端来确定在执行某些关键操作之前(和期间)是否已保留插槽。
最后,如果插槽持有者希望自愿释放插槽,则应通过对<lock>
进行检查并设置(Check-And-Set)
操作以从Holders
对象中删除其会话。完成后,竞争者密钥<prefix>/<session>
和会话都会被删除。