Consul 详解
近期在微服务业务中用的注册中心,在此简单记录下以备后用。
一 概述
1.1 概念
Consul
是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配置。Consul 是分布式的、高可用的、 可横向扩展的。
1.2 特征
服务发现: Consul 提供了通过 DNS 或者 HTTP 接口的方式来注册服务和发现服务。一些外部的服务通过 Consul 很容易的找到它所依赖的服务。
健康检测: Consul 的 Client 提供了健康检查的机制,可以通过用来避免流量被转发到有故障的服务上。
Key/Value
存储: 应用程序可以根据自己的需要使用 Consul 提供的 Key/Value 存储。 Consul 提供了简单易用的 HTTP 接口,结合其他工具可以实现动态配置、功能标记、领袖选举等等功能。
多数据中心: Consul 支持开箱即用的多数据中心. 这意味着用户不需要担心需要建立额外的抽象层让业务扩展到多个区域。
1.3 架构图及解析
1.3.1 内部架构及原理
1.3.1.1 架构图
1.2.1.2 图解
首先 Consul 支持多数据中心,在上图中有两个 DataCenter,他们通过 Internet 互联,同时请注意为了提高通信效率,只有 Server 节点才加入跨数据中心的通信。
在单个数据中心中,Consul 分为 Client 和 Server 两种节点(所有的节点也被称为 Agent),Server 节点保存数据,Client 负责健康检查及转发数据请求到 Server;Server 节点有一个 Leader 和多个 Follower,Leader 节点会将数据同步到 Follower,Server 的数量推荐是 3 个或者 5 个,在 Leader 挂掉的时候会启动选举机制产生一个新的 Leader。
集群内的 Consul 节点通过 gossip 协议(流言协议)维护成员关系,也就是说某个节点了解集群内现在还有哪些节点,这些节点是 Client 还是 Server。单个数据中心的流言协议同时使用 TCP 和 UDP 通信,并且都使用 8301 端口。跨数据中心的流言协议也同时使用 TCP 和 UDP 通信,端口使用 8302。
集群内数据的读写请求既可以直接发到 Server,也可以通过 Client 使用 RPC 转发到 Server,请求最终会到达 Leader 节点,在允许数据轻微陈旧的情况下,读请求也可以在普通的 Server 节点完成,集群内数据的读写和复制都是通过 TCP 的 8300 端口完成。
Consul 集群间使用了 Gossip
协议通信和 raft 一致性算法
Gossip —— Gossip protocol 也叫 Epidemic Protocol (流行病协议),实际上它还有很多别名,比如:“流言算法”、“疫情传播算法”等。 这个协议的作用就像其名字表示的意思一样,非常容易理解,它的方式其实在我们日常生活中也很常见,比如电脑病毒的传播,森林大火,细胞扩散等等。
Client —— 一个 Client 是一个转发所有 RPC 到 server 的代理。这个 client 是相对无状态的。client 唯一执行的后台活动是加入 LAN gossip 池。这有一个最低的资源开销并且仅消耗少量的网络带宽。
Server —— 一个 server 是一个有一组扩展功能的代理,这些功能包括参与 Raft 选举,维护集群状态,响应 RPC 查询,与其他数据中心交互 WAN gossip 和转发查询给 leader 或者远程数据中心。
DataCenter —— 虽然数据中心的定义是显而易见的,但是有一些细微的细节必须考虑。例如,在 EC2 中,多个可用区域被认为组成一个数据中心。我们定义数据中心为一个私有的,低延迟和高带宽的一个网络环境。这不包括访问公共网络,但是对于我们而言,同一个 EC2 中的多个可用区域可以被认为是一个数据中心的一部分。
Consensus —— 一致性,使用 Consensus 来表明就 leader 选举和事务的顺序达成一致。为了以容错方式达成一致,一般有超过半数一致则可以认为整体一致。Consul 使用 Raft 实现一致性,进行 leader 选举,在 consul 中的使用 bootstrap 时,可以进行自选,其他 server 加入进来后 bootstrap 就可以取消。
LAN Gossip —— 它包含所有位于同一个局域网或者数据中心的所有节点。
WAN Gossip —— 它只包含 Server。这些 server 主要分布在不同的数据中心并且通常通过因特网或者广域网通信。
RPC——远程过程调用。这是一个允许 client 请求 server 的请求/响应机制。
1.3.2 Consul 服务发现原理
1.3.2.1 原理图
1.3.2.2 图解
首先需要有一个正常的 Consul 集群,有 Server,有 Leader。这里在服务器 Server1、Server2、Server3 上分别部署了 Consul Server,假设他们选举了 Server2 上的 Consul Server 节点为 Leader。这些服务器上最好只部署 Consul 程序,以尽量维护 Consul Server 的稳定。
然后在服务器 Server4 和 Server5 上通过 Consul Client 分别注册 Service A、B、C,这里每个 Service 分别部署在了两个服务器上,这样可以避免 Service 的单点问题。服务注册到 Consul 可以通过 HTTP API(8500 端口)的方式,也可以通过 Consul 配置文件的方式。Consul Client 可以认为是无状态的,它将注册信息通过 RPC 转发到 Consul Server,服务信息保存在 Server 的各个节点中,并且通过 Raft 实现了强一致性。
最后在服务器 Server6 中 Program D 需要访问 Service B,这时候 Program D 首先访问本机 Consul Client 提供的 HTTP API,本机 Client 会将请求转发到 Consul Server,Consul Server 查询到 Service B 当前的信息返回,最终 Program D 拿到了 Service B 的所有部署的 IP 和端口,然后就可以选择 Service B 的其中一个部署并向其发起请求了。如果服务发现采用的是 DNS 方式,则 Program D 中直接使用 Service B 的服务发现域名,域名解析请求首先到达本机 DNS 代理,然后转发到本机 Consul Client,本机 Client 会将请求转发到 Consul Server,Consul Server 查询到 Service B 当前的信息返回,最终 Program D 拿到了 Service B 的某个部署的 IP 和端口。
图中描述的部署架构笔者认为是最普适最简单的方案,从某些默认配置或设计上看也是官方希望使用者采用的方案,比如 8500 端口默认监听 127.0.0.1,当然有些同学不赞同,后边会提到其他方案。
1.4 为什么使用服务发现
防止硬编码、容灾、水平扩缩容、提高运维效率等等,只要你想使用服务发现总能找到合适的理由。
一般的说法是因为使用微服务架构。传统的单体架构不够灵活不能很好的适应变化,从而向微服务架构进行转换,而伴随着大量服务的出现,管理运维十分不便,于是开始搞一些自动化的策略,服务发现应运而生。所以如果需要使用服务发现,你应该有一些对服务治理的痛点。
但是引入服务发现就可能引入一些技术栈,增加系统总体的复杂度,如果你只有很少的几个服务,比如 10 个以下,并且业务不怎么变化,吞吐量预计也很稳定,可能就没有必要使用服务发现。
二 consul 与其他框架差异
| 名称 | 优点 | 缺点 | 接口 | 一致性算法 |
| --------- | ------------------------------------------------------------ | ------------------------------------------------------------ | -------- | -------------- |
| zookeeper | 1.功能强大,不仅仅只是服务发现 2.提供 watcher 机制能实时获取服务提供者的状态 3.dubbo 等框架支持 | 1.没有健康检查 2.需在服务中集成 sdk,复杂度高 3.不支持多数据中心 | sdk | Paxos |
| consul | 1.简单易用,不需要集成 sdk 2.自带健康检查 3.支持多数据中心 4.提供 web 管理界面 | 1.不能实时获取服务信息的变化通知 | http/dns | Raft |
| etcd | 1.简单易用,不需要集成 sdk 2.可配置性强 | 1.没有健康检查 2.需配合第三方工具一起完成服务发现 3.不支持多数据中心 | http | Raft |
三 安装部署
3.1 物理服务器安装部署
3.1.1 linux 安装
wget https://releases.hashicorp.com/consul/1.5.1/consul_1.5.1_linux_amd64.zip
unzip consul_0.8.1_linux_amd64.zip
mv consul /usr/local/bin/
复制代码
3.1.2 superviosr 启动
mkdir -p /data/consul/{data,logs}
cat < /etc/supervisord.d/consul.conf >EOF
[program:consul]
command=consul agent -server -bootstrap-expect 3 -data-dir /data/consul/data -bind=172.16.100.2 -ui -client 0.0.0.0 -advertise=172.16.100.2 -node=go2cloud-platform-test -rejoin
user=root
stdout_logfile=/data/consul/logs/consul.log
autostart=true
autorestart=true
startsecs=60
stopasgroup=true
ikillasgroup=true
startretries=1
redirect_stderr=true
EOF
复制代码
[root@go2cloud_platform_pord conf.d]# supervisorctl status consul
consul RUNNING pid 11838, uptime 0:13:28
复制代码
# 查看集群成员
consul members
# 查看集群状态
consul info
# 帮助
consul agent -h
复制代码
3.2 Docker 部署
然后就可以启动集群了,这里启动 4 个 Consul Agent,3 个 Server(会选举出一个 leader),1 个 Client。
#启动第1个Server节点,集群要求要有3个Server,将容器8500端口映射到主机8900端口,同时开启管理界面
docker run -d --name=consul1 -p 8500:8500 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=true --bootstrap-expect=3 --client=0.0.0.0 -ui
#启动第2个Server节点,并加入集群
docker run -d --name=consul2 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=true --client=0.0.0.0 --join 172.17.0.2
#启动第3个Server节点,并加入集群
docker run -d --name=consul3 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=true --client=0.0.0.0 --join 172.17.0.2
#启动第4个Client节点,并加入集群
docker run -d --name=consul4 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=false --client=0.0.0.0 --join 172.17.0.2
复制代码
第 1 个启动容器的 IP 一般是 172.17.0.2,后边启动的几个容器 IP 会排着来:172.17.0.3、172.17.0.4、172.17.0.5。
这些 Consul 节点在 Docker 的容器内是互通的,他们通过桥接的模式通信。但是如果主机要访问容器内的网络,需要做端口映射。在启动第一个容器时,将 Consul 的 8500 端口映射到了主机的 8900 端口,这样就可以方便的通过主机的浏览器查看集群信息。
# 编写service.json
{
"services": [
{
"id": "hello1",
"name": "hello",
"tags": [
"primary"
],
"address": "172.17.0.5",
"port": 5000,
"checks": [
{
"http": "http://localhost:5000/",
"tls_skip_verify": false,
"method": "Get",
"interval": "10s",
"timeout": "1s"
}
]
}
]
}
复制代码
# 将json文件拷贝进容器内
docker cp myservice.json consul1:/consul/config
# 重载配置文件
docker exec consul1 consul reload
复制代码
此时已经有了服务,只是服务不可用,consul 发送给服务的请求不可达
3.3 k8s 中部署
可以自己去写 yaml 资源清单文件或者利用官方提供好的 helm 的 charts 来安装
# 查询helm
[root@master opt]# helm search consul
NAME CHART VERSION APP VERSION DESCRIPTION
apphub/consul 5.3.3 1.6.0 Highly available and distributed service discovery and ke...
apphub/prometheus-consul-exporter 0.1.4 0.4.0 A Helm chart for the Prometheus Consul Exporter
bitnami/consul 5.3.3 1.6.0 Highly available and distributed service discovery and ke...
incubator/ack-consul 0.5.0 0.5.0 Install and configure Consul on Kubernetes.
stable/consul 3.8.1 1.5.3 Highly available and distributed service discovery and ke...
stable/prometheus-consul-exporter 0.1.4 0.4.0 A Helm chart for the Prometheus Consul Exporter
# 安装consul
[root@master opt]# helm install stable/consul -n consul
NAME: consul
LAST DEPLOYED: Fri Nov 22 09:52:52 2019
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/ConfigMap
NAME DATA AGE
consul-tests 1 3m4s
==> v1/Pod(related)
NAME READY STATUS RESTARTS AGE
consul-0 0/1 ContainerCreating 0 3m4s
==> v1/Secret
NAME TYPE DATA AGE
consul-gossip-key Opaque 1 3m4s
==> v1/Service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
consul ClusterIP None <none> 8500/TCP,8400/TCP,8301/TCP,8301/UDP,8302/TCP,8302/UDP,8300/TCP,8600/TCP,8600/UDP 3m4s
consul-ui NodePort 10.107.180.193 <none> 8500:30082/TCP 3m4s
==> v1beta1/PodDisruptionBudget
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
consul-pdb N/A 1 0 3m4s
==> v1beta1/StatefulSet
NAME READY AGE
consul 0/3 3m4s
NOTES:
1. Watch all cluster members come up.
$ kubectl get pods --namespace=default -w
2. Test cluster health using Helm test.
$ helm test consul
3. (Optional) Manually confirm consul cluster is healthy.
$ CONSUL_POD=$(kubectl get pods -l='release=consul' --output=jsonpath={.items[0].metadata.name})
$ kubectl exec $CONSUL_POD consul members --namespace=default | grep server
复制代码
我们可以看到 helm 安装 consul 使用的是 statefulset,服务使用的是 NodePort 方式
# 查看运行状态
[root@master opt]# kubectl get pods --show-labels -l release=consul
NAME READY STATUS RESTARTS AGE LABELS
consul-0 1/1 Running 0 3m41s chart=consul-3.8.1,component=consul-consul,controller-revision-hash=consul-6969c79b5c,heritage=Tiller,release=consul,statefulset.kubernetes.io/pod-name=consul-0
consul-1 1/1 Running 0 2m56s chart=consul-3.8.1,component=consul-consul,controller-revision-hash=consul-6969c79b5c,heritage=Tiller,release=consul,statefulset.kubernetes.io/pod-name=consul-1
consul-2 1/1 Running 0 2m17s chart=consul-3.8.1,component=consul-consul,controller-revision-hash=consul-6969c79b5c,heritage=Tiller,release=consul,statefulset.kubernetes.io/pod-name=consul-2
# 查看svc
[root@master opt]# kubectl get svc -l release=consul
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
consul ClusterIP None <none> 8500/TCP,8400/TCP,8301/TCP,8301/UDP,8302/TCP,8302/UDP,8300/TCP,8600/TCP,8600/UDP 5m23s
consul-ui NodePort 10.107.180.193 <none> 8500:30082/TCP 5m23s
# 查看server
[root@master opt]# CONSUL_POD=$(kubectl get pods -l='release=consul' --output=jsonpath={.items[0].metadata.name})
[root@master opt]# kubectl exec $CONSUL_POD consul members --namespace=default | grep server
consul-0 10.244.1.67:8301 alive server 1.5.3 2 dc1 <all>
consul-1 10.244.2.193:8301 alive server 1.5.3 2 dc1 <all>
consul-2 10.244.1.68:8301 alive server 1.5.3 2 dc1 <all>
复制代码
四 使用
在此演示利用 python 来使用 consul 的服务器发现与注册,已经 consul 的简单配置中心
4.1 服务发现与注册
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import json
import requests
from consul import Consul, Check
from random import randint
# consul 操作类
class ConsulClient():
def __init__(self, host=None, port=None, token=None): # 初始化,指定consul主机,端口,和token
self.host = host # consul 主机
self.port = port # consul 端口
self.token = token
self.consul = Consul(host=host, port=port)
def register(self, name, service_id, local_ip, local_port, consul_health_url, tags=None, interval=None): # 注册服务 注册服务的服务名 端口 以及 健康监测端口
# 心跳检测url
health_check_url = ''.join(["http://", local_ip, ":", str(local_port), consul_health_url])
# 健康检查的ip,端口,检查时间
http_check = Check.http(health_check_url, "10s")
return self.consul.agent.service.register(name, service_id=service_id, address=local_ip, port=int(local_port),
check=http_check, tags=tags, interval=interval)
def deregister(self, service_id):
# 此处有坑,源代码用的get方法是不对的,改成put,两个方法都得改
de_result = self.consul.agent.service.deregister(service_id)
check_result = self.consul.agent.check.deregister(service_id)
return de_result, check_result
def getService(self, name): # 负债均衡获取服务实例
self.port = str(self.port)
url = 'http://' + self.host + ':' + self.port + '/v1/catalog/service/' + name # 获取 相应服务下的DataCenter
dataCenterResp = requests.get(url)
if dataCenterResp.status_code != 200:
raise Exception('can not connect to consul ')
listData = json.loads(dataCenterResp.text)
dcset = set() # DataCenter 集合 初始化
for service in listData:
dcset.add(service.get('Datacenter'))
serviceList = [] # 服务列表 初始化
for dc in dcset:
if self.token:
url = 'http://' + self.host + ':' + self.port + '/v1/health/service/' + name + '?dc=' + dc + '&token=' + self.token
else:
url = 'http://' + self.host + ':' + self.port + '/v1/health/service/' + name + '?dc=' + dc + '&token='
resp = requests.get(url)
if resp.status_code != 200:
raise Exception('can not connect to consul ')
text = resp.text
serviceListData = json.loads(text)
for serv in serviceListData:
status = serv.get('Checks')[1].get('Status')
if status == 'passing': # 选取成功的节点
address = serv.get('Service').get('Address')
port = serv.get('Service').get('Port')
serviceList.append({'port': port, 'address': address})
if len(serviceList) == 0:
raise Exception('no serveice can be used')
else:
service = serviceList[randint(0, len(serviceList) - 1)] # 随机获取一个可用的服务实例
return service['address'], int(service['port'])
def getServices(self):
return self.consul.agent.services()
if __name__ == '__main__':
host = '10.234.2.204'
port = '30082'
server_name = 'myapp'
server_id = server_name + '-8500'
c = ConsulClient(host, port)
# print(c.deregister(server_id))
# print(c.register(server_name, server_id, 'x.x.x.x', 8012, '/ops-audit/health'))
print(c.consul.agent.services())
print(c.getService(server_name))
from apps.jumpserver.conf import get_consul_server
print(get_consul_server('cmp', 'SMARTOPS_API_URL'))
# server_name2 = 'myconsulapp'
# local_port = '8000'
# server_id2 = server_name2 + '-' + local_port
# print(c.register(server_name2, server_id2, '10.234.2.186', '8000', '/ops-audit/health'))
复制代码
注意,需要写在服务启动的时候去注册,在利用到服务发现的地方查询可用的后端服务器来获取服务的 ip 及端口使用。
在服务停止的时候去注销服务。
4.2 配置中心
def get_config(CONSUL_HOST, CONSUL_PORT, KEY_NAME):
c = consul.Consul(CONSUL_HOST, CONSUL_PORT)
index = None
index, data = c.kv.get(KEY_NAME, index=index)
return yaml.safe_load(data.get("Value").decode(encoding='utf-8'))
复制代码
一般情况下需要去启一个进程一直 watch consul 的配置来实时更新应用中的配置,或者每次利用配置的时候去单独调用获取配置。
参考链接
https://www.consul.io/
https://elfgzp.cn/2019/03/11/consul-template-python-micro-service.html
https://blog.51cto.com/tianshili/1758566
http://blog.didispace.com/consul-service-discovery-exp/
评论