Consul
是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配置。Consul 是分布式的、高可用的、 可横向扩展的。
Key/Value
首先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 一致性算法
首先需要有一个正常的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,当然有些同学不赞同,后边会提到其他方案。
防止硬编码、容灾、水平扩缩容、提高运维效率等等,只要你想使用服务发现总能找到合适的理由。
一般的说法是因为使用微服务架构。传统的单体架构不够灵活不能很好的适应变化,从而向微服务架构进行转换,而伴随着大量服务的出现,管理运维十分不便,于是开始搞一些自动化的策略,服务发现应运而生。所以如果需要使用服务发现,你应该有一些对服务治理的痛点。
但是引入服务发现就可能引入一些技术栈,增加系统总体的复杂度,如果你只有很少的几个服务,比如10个以下,并且业务不怎么变化,吞吐量预计也很稳定,可能就没有必要使用服务发现。
名称 | 优点 | 缺点 | 接口 | 一致性算法 |
---|---|---|---|---|
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 |
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/ 复制代码
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 复制代码
:warning::启动后需要手动在其他两个节点手动加入 consul join 172.16.100.2
查看
[root@go2cloud_platform_pord conf.d]# supervisorctl status consul consul RUNNING pid 11838, uptime 0:13:28 复制代码
# 查看集群成员 consul members # 查看集群状态 consul info # 帮助 consul agent -h 复制代码
# 拉取镜像 docker pull consul 复制代码
然后就可以启动集群了,这里启动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发送给服务的请求不可达
可以自己去写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的简单配置中心
#!/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及端口使用。
在服务停止的时候去注销服务。
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的配置来实时更新应用中的配置,或者每次利用配置的时候去单独调用获取配置。