Consul 初体验
Consul 是一个分布式的服务发现和配置中心系统。虽然我才刚刚开始学习,用的还不算非常深入,但是可以把目前学到的东西整理一下,希望可以给其他入门者一些帮助。
微服务面临的挑战
微服务架构有很多优势,这个话题已经被讨论地太多了:部署灵活、逻辑清晰、扩展方便、高可用…… 我就不一一深入了。
↑ 将一个庞大的服务拆成若干个微服务,每个微服务负责一个独立的模块。
但是天下没有没有免费的午餐,使用微服务也有很多挑战,而 Consul 就解决了微服务场景中很多痛点。下面我会解释一下其中两个比较关键的痛点以及 Consul 的解决方案。
↑ 如果不懂得如何正确地使用,那么任何架构没有价值。图片来自 Twitter@alvaro_sanchex
配置
以前我们在使用 Monolithic 服务的时候,整个服务使用的一个配置文件。但是在使用了微服务架构后,配置文件就会变得非常多。比如我需要修改一下数据库的地址,如果我有 20 个微服务,那么我就需要修改 20 个配置文件。
为了解决这个问题 Consul 提供了一个分布式的键值对储存系统,称之为 K/V Store。
所有配置都储存在 Consul 中,我们可以在一个地方查看和修改所有服务的配置。Consul 的 K/V Store 是基于 Raft 算法实现的分布式系统,所以整个系统没有单点依赖,一个节点(比如一台服务器)出现故障并不会导致配置无法访问。
服务发现
一个微服务可能有多个实例,共同提供微服务。这些服务的数量和 IP 地址可能会频繁地变化,导致调用方试图去请求微服务方的时候,并不知道应该请求哪个微服务实例。
为了解决这个问题,一种常见的方案是在每一个微服务集群前放一个负载均衡(Load Balancing),其他服务需要调用这个服务的时候,先去调用 LB,由 LB 负责将请求分发到不同的微服务实例上。LB 的地址是固定不变的,所以调用方总是知道应该找谁。
↑ 使用 LB 去分发流量
但是使用 LB 分发流量带来了其他问题,比如说 LB 成为了一个单点。如果 LB 挂了,那么无论它的背后有多少个微服务实例,这些实例都无法正常工作。另外一个问题是这样的调用增加了调用的链条,使得微服务之间的调用延迟变得更高。
Consul 可以提供了一个分布式服务于注册集群,每一个节点在创建或者销毁微服务的时候,可以通知这个集群,并将最新的微服务信息注册到这个集群中。通过分布式算法,每一个节点都可以获取到所有节点上的服务信息。
↑ 使用分布式注册与发现服务,让调用方可以直接知道微服务的日志,并直接去请求微服务的地址
使用 Docker 学习 Consul
我创建了一个 docker-compose 项目,使用多个 Docker 容器去模拟多台服务器,方便学习 Consul。
GitHub 地址:https://github.com/ocavue/consul-playground
使用前需要确保安装了 docker-compose 和 Docker。可以使用 which docker-compose docker
来确认。如果你安装 Docker 的平台是 Mac 或者 Windows,那么 docker-compose 已经默认安装好了。
下面的步骤根据你的配置可能需要 sudo
,我就不一一加上了。输入 make build
来构建镜像,第一次构建的时候可能需要几分钟的时间。在这个 docker-compose 中,我创建了 4 个容器,名字分别为 vm0
,vm1
,vm2
,vm3
。可以通过 docker exec -it vm0 bash
进入各个容器内部。
root@vm1:/# which consul
/usr/local/bin/consul
root@vm1:/# consul --version
Consul v1.4.4
Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)
root@vm1:/#
接下来所有的例子都可以直接在这个 docker-compose 中运行。
安装 Consul 以及注册节点
Consul 使用 Go 便写,所以最后打包出来的是一个拥有所有依赖的二进制包 以及 flag 包带来的奇怪的命令行参数格式。可以直接将二进制包下载到本地并软链到 PATH
中即可。这一步在创建 Docker 镜像的时候已经自动完成了。
配置中心
我们可以使用命令行工具、HTTP API 以及图形界面(本质上也是基于 HTTP API)来编辑这些配置。`
↑ Consul 的 Web 图形界面,URL 是 http://127.0.0.1:8500/ui/
Consul 的配置系统和 zookeeper 类似,使用的是类似文件目录的方式。Key config/order/db/redis/prof
是一个目录,Value 是具体的数据。所以可以做到嵌套的配置,方便分类管理
/config
/order
/host: "...."
/db
/redis
/prof: "...."
/mongodb: "...."
/mq
/rabbitmq: "...."
需要特别注意的是 Consul 限制了 value 的大小,最大是 512Kb。
服务注册与发现
我们假设现在有一个叫做 order 的微服务。我们想要要在 vm0 上注册这个服务。
在容器 vm0
中创建 /etc/consul.d/order-1.json
并写入下面的内容
{
"service": {
"id": "order-1",
"name": "order",
"port": 7100
}
}
/etc/consul.d
是存放 Consul 配置文件的目录。创建好配置后在 vm0 上执行 consul reload
,这个服务就注册完了。
我们可以在其他的机器上(比如 vm1)通过 Consul 的 API 参看刚刚注册的服务:
root@vm1:/# curl -s http://localhost:8500/v1/catalog/service/order | python3 -m json.tool
[
{
"ID": "ac54685b-3489-ca6a-59d6-fe50b9b292b4",
"Node": "vm0",
"Address": "172.18.0.4",
"Datacenter": "mydc",
"TaggedAddresses": {
"lan": "172.18.0.4",
"wan": "172.18.0.4"
},
"NodeMeta": {
"consul-network-segment": ""
},
"ServiceKind": "",
"ServiceID": "order-1",
"ServiceName": "order",
"ServiceTags": [],
"ServiceAddress": "",
"ServiceWeights": {
"Passing": 1,
"Warning": 1
},
"ServiceMeta": {},
"ServicePort": 7100,
"ServiceEnableTagOverride": false,
"ServiceProxyDestination": "",
"ServiceProxy": {},
"ServiceConnect": {},
"CreateIndex": 602,
"ModifyIndex": 602
}
]
可以看到在其他服务器上的已经可以找到这个 order 服务了。我们在配置文件中写的三个字段分别展示在 ServiceID
, ServiceName
, ServicePort
中。
在注册服务时候,我们在 json 文件中填写的 id 必须保证单台服务器上内唯一。
root@vm1:/# curl -s http://localhost:8500/v1/catalog/service/order | python3 -m json.tool
[
{
"Node": "vm0",
"ServiceID": "order-1",
"ServiceName": "order",
"ServicePort": 7100,
......
},
{
"Node": "vm0",
"ServiceID": "order-2",
"ServiceName": "order",
"ServicePort": 7200,
......
},
{
"Node": "vm1",
"ServiceID": "order-1",
"ServiceName": "order",
"ServicePort": 7100,
......
},
{
"Node": "vm2",
"ServiceID": "order-1",
"ServiceName": "order",
"ServicePort": 7100,
......
}
]
root@vm1:/#
↑ 多个节点中可以注册 ServiceID 相同的服务
这里需要特别提一点,服务的注册只是”注册”而已,相当于把这个 json 配置文件的内容放到了一个分布式储存系统中。Consul 本身并不会帮你在 7100 端口上启动你的 order 服务,这件事情需要你自己去做。
看到这里,我们已经简单地走完了一套服务注册与发现的流程。下面说一些其他比较常用的操作。
通过 API 注册服务
除了文件,consul 也支持通过 RESTful HTTP API 创建服务:
root@vm1:~# cat payload.json
{
"id": "order-3",
"name":"order",
"port":7300
}
root@vm1:~# curl --request PUT --data @payload.json http://127.0.0.1:8500/v1/agent/service/register
root@vm1:~#
通过 API 创建不需要执行 consul reload
。
值得注意 Consul 中配置项的写法。在配置文件中,只能使用 snake_case
,在 HTTP API 的请求中,snake_case
和 CamelCased
都可以使用,在 API 的响应中,Consul 返回的结果是 CamelCased
。在这篇文章中我在服务定义中使用的都是 snake_case
,因为这是在配置文件和 API 请求中都可以的写法。(相关文档)
自定义信息
在注册服务的时候,我们可能希望像这个服务写入一些自定义的信息,consul 提供了两种不同的配置项方式去完成这项任务:tags
和 meta
。
{
"id": "order-4",
"name": "order",
"port": 7400,
"tags": ["primary", "hotfix", "v2"],
"meta": {
"branch": "fix_order_count",
"commit": "e981106d2d84e",
"creator": "Alex"
}
}
简单来说,tags 是一个列表,meta 是一个字典。consul 并不关心这两个配置的数据具体是什么,这些数据都是由调用方去关心的。
出于性能和加密的考虑,meta
有一些限制,最多只能由 64 个键值对,key 只能使用特定的字符 (A-Z
a-z
0-9
_
和 -
),key 和 value 的最大长度分别是 128 和 512。
健康度检查
在部署 Consul 的时候,每台运行服务的机器都需要运行一个 agent 服务,原因之一是 Consul 提供了健康度检查的功能,这也是一个单纯的分布式存储系统所没有的。
{
"id": "order-4",
"name": "order",
"port": 7400,
"checks": [
{
"args": ["/bin/check_mem.py", "--limit", "256M"],
"interval": "10s"
}
]
}
在上面这个服务的定义中,我们增加了一个 checks
配置,这个配置让 Consul 每十秒钟运行一下 check_redis.py
脚本,然后获取这个脚本的 stdout 和 exit code。之后就可以在整个集群上的任意节点上获取这个服务的信息
root@vm2:~# curl -s http://127.0.0.1:8500/v1/health/checks/order | python3 -m json.tool
[
{
"Node": "vm3",
"CheckID": "service:order-4",
"Name": "Service 'order' check",
"Status": "warning",
"Notes": "",
"Output": "Something wrong\n",
"ServiceID": "order-8",
"ServiceName": "order",
"ServiceTags": [],
"Definition": {},
"CreateIndex": 1887,
"ModifyIndex": 1889
}
]
在上面这个这个例子中,"Something wrong\n"
是 /bin/check_mem.py
脚本的 stdout,"Status": "warning"
表示脚本的 exit code 为 1。exit code 和 status 具体的关系如下:
Exit code 0 - Check is passing
Exit code 1 - Check is warning
Any other code - Check is failing
除了上面展示的通过脚本来监测外,Consul 还内置了一些常用的检测方法,包括 HTTP、TCP、TTL、Docker 和 gRPC
{
"id": "order-5",
"name": "order",
"port": 7500,
"checks": [
{
"http": "https://localhost:7500/health",
"method": "POST",
"interval": "10s",
"timeout": "1s"
}
]
}
↑ 一个使用 HTTP 接口进行健康度检查的服务定义
通过 Consul 的健康度检查功能以及配合自己写的脚本,我们就可以根据健康度来动态地分配不同服务的流量
↑ A 服务了解每个 B 服务实例的健康度,从而可以避免将流量分配到特定 B 服务实例中
总结
这篇文章简单地介绍了一下 Consul 的两个核心特性:配置中心和服务发现。讲的不深,但是应该可以让大家对 Consul 有个清晰直观的认识。