Ocavue's Blog
Consul 初体验(上) 2019-05-12

Consul 是一个分布式的服务发现配置中心系统。虽然我才刚刚开始学习,用的还不算非常深入,但是可以把目前学到的东西整理一下,希望可以给其他入门者一些帮助。

# 微服务面临的挑战

微服务架构有很多优势,这个话题已经被讨论地太多了:部署灵活、逻辑清晰、扩展方便、高可用…… 我就不一一深入了。

↑ 将一个庞大的服务拆成若干个微服务,每个微服务负责一个独立的模块。

但是天下没有没有免费的午餐,使用微服务也有很多挑战,而 Consul 就解决了微服务场景中很多痛点。下面我会解释一下其中两个比较关键的痛点以及 Consul 的解决方案。

monolith-microservices

↑ 如果不懂得如何正确地使用,那么任何架构没有价值。图片来自 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 镜像的时候已经自动完成了。

# 配置中心

我们可以使用命令行工具 (opens new window)HTTP API (opens new window) 以及图形界面(本质上也是基于 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_caseCamelCased 都可以使用,在 API 的响应中,Consul 返回的结果是 CamelCased。在这篇文章中我在服务定义中使用的都是 snake_case,因为这是在配置文件和 API 请求中都可以的写法。(相关文档) (opens new window)

# 自定义信息

在注册服务的时候,我们可能希望像这个服务写入一些自定义的信息,consul 提供了两种不同的配置项方式去完成这项任务:tagsmeta

{
    "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 还内置了一些常用的检测方法 (opens new window),包括 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 的健康度检查功能以及配合自己写的脚本,我们就可以根据健康度来动态地分配不同服务的流量

Service discovery with health check

↑ A 服务了解每个 B 服务实例的健康度,从而可以避免将流量分配到特定 B 服务实例中

# 总结

这篇文章简单地介绍了一下 Consul 的两个核心特性:配置中心和服务发现。讲的不深,但是应该可以让大家对 Consul 有个清晰直观的认识。

# 参考资料