Consul实现http或者grpc服务的注册与发现,Golang、Python的使用记录

consul官网

Consul是什么

Consul包含多个组件,但是作为一个整体,为你的基础设施提供服务发现和服务配置的工具.他提供以下关键特性:

  • 服务发现 Consul的客户端可用提供一个服务,比如 api 或者mysql ,另外一些客户端可用使用Consul去发现一个指定服务的提供者.通过DNS或者HTTP应用程序可用很容易的找到他所依赖的服务.

  • 健康检查 Consul客户端可用提供任意数量的健康检查,指定一个服务(比如:webserver是否返回了200 OK 状态码)或者使用本地节点(比如:内存使用是否大于90%). 这个信息可由operator用来监视集群的健康.被服务发现组件用来避免将流量发送到不健康的主机.

  • Key/Value存储 应用程序可用根据自己的需要使用Consul的层级的Key/Value存储.比如动态配置,功能标记,协调,领袖选举等等,简单的HTTP API让他更易于使用.

  • 多数据中心: Consul支持开箱即用的多数据中心.这意味着用户不需要担心需要建立额外的抽象层让业务扩展到多个区域.

Consul面向DevOps和应用开发者友好.是他适合现代的弹性的基础设

架构图

在这里插入图片描述
让我们把这幅图分解描述。首先,我们可以看到有两个数据中心,分别标记为“1”和“2”。Consul拥有对多个数据中心的一流支持,这是比较常见的情况。

在每个数据中心中,我们都有客户机和服务器。预计将有三到五台服务器。这在故障情况下的可用性和性能之间取得了平衡,因为随着添加更多的机器,一致性会逐渐变慢。但是,客户端的数量没有限制,可以很容易地扩展到数千或数万。

Consul 实现多个数据中心都依赖于gossip protocol协议。这样做有几个目的:首先,不需要使用服务器的地址来配置客户端;服务发现是自动完成的。其次,健康检查故障的工作不是放在服务器上,而是分布式的。这使得故障检测比单纯的心跳模式更具可伸缩性。为节点提供故障检测;如果无法访问代理,则节点可能经历了故障。

每个数据中心中的服务器都是一个筏对等集的一部分。这意味着它们一起工作来选举单个leader,一个被选中的服务器有额外的职责。领导负责处理所有的查询和事务。事务还必须作为协商一致协议的一部分复制到所有对等方。由于这个需求,当非leader服务器接收到RPC请求时,它会将其转发给集群leader。

Consul的使用场景

Consul的应用场景包括服务发现、服务隔离、服务配置:

  • 服务发现场景中consul作为注册中心,服务地址被注册到consul中以后,可以使用consul提供的dns、http接口查询,consul支持health check。
  • 服务隔离场景中consul支持以服务为单位设置访问策略,能同时支持经典的平台和新兴的平台,支持tls证书分发,service-to-service加密。
  • 服务配置场景中consul提供key-value数据存储功能,并且能将变动迅速地通知出去,借助Consul可以实现配置共享,需要读取配置的服务可以从Consul中读取到准确的配置信息。
  • Consul可以帮助系统管理者更清晰的了解复杂系统内部的系统架构,运维人员可以将Consul看成一种监控软件,也可以看成一种资产(资源)管理系统。
    比如:docker实例的注册与配置共享、coreos实例的注册与配置共享、vitess集群、SaaS应用的配置共享、Consul与confd服务集成,动态生成nginx和haproxy配置文件或者Consul结合nginx构建高可用可扩展的Web服务。

启动Consul服务端

wget https://releases.hashicorp.com/consul/1.10.4/consul_1.10.4_linux_amd64.zip
unzip consul_1.10.4_linux_amd64.zip
./consul agent -server -bootstrap-expect 1 -data-dir /root/consul/data-dir -node=jatel -bind=192.168.235.154 -ui-dir /root/consul/consul-ui -rejoin -config-dir=/root/consul/config-dir -client 0.0.0.0

HTTP

这里我们可用Python的http.server模块来模拟两个http服务

Python

import consul

c = consul.Consul(host='192.168.235.154')
check = consul.Check().http(
    url='http://192.168.21.188:8098',
    interval='3s',
    timeout='5s',
    deregister='5s'
)

if c.agent.service.register(
        name='test_http',
        service_id='http_id',
        address='192.168.21.188',
        tags=['http_consul', 'test_http_python', 'http_server'],
        port=8098,
        check=check
):
    for _, value in c.agent.services().items():
        print(value)

通过以上Python代码注册后,我们可以看到我们的http服务如下图
在这里插入图片描述

Golang

package main

import (
	"fmt"
	consulapi "github.com/hashicorp/consul/api"
)

const (
	consulAddress = "192.168.235.154:8500"
	localIp       = "192.168.21.188"
	localPort     = 50051
)

func consulRegister() {
	// 创建连接consul服务配置
	config := consulapi.DefaultConfig()
	config.Address = consulAddress
	client, err := consulapi.NewClient(config)
	if err != nil {
		fmt.Println("consul client error : ", err)
	}
	//
	// 创建注册信息
	registration := new(consulapi.AgentServiceRegistration)
	registration.ID = "337grpc"
	registration.Name = "service337grpc"
	registration.Port = localPort
	registration.Tags = []string{"testServicegrpc", "viagrpc", "wanggrpc"}
	registration.Address = localIp
	
	// 增加consul健康检查回调函数
	check := new(consulapi.AgentServiceCheck)
	check.GRPC = fmt.Sprintf("%s:%d", registration.Address, registration.Port)
	check.Timeout = "5s"
	check.Interval = "2s"
	check.DeregisterCriticalServiceAfter = "30s" // 故障检查失败30s后 consul自动将注册服务删除
	registration.Check = check
	
	// 注册服务到consul
	err = client.Agent().ServiceRegister(registration)
	if err != nil {
		fmt.Println("consul ServiceRegister error : ", err)
	}
	
	// 过滤服务
	var serviceTag = "python_consul"
	data, err := client.Agent().ServicesWithFilter(fmt.Sprintf("%s in Tags", serviceTag))
	if err != nil {
		fmt.Println("consul ServicesWithFilter error : ", err)
	}
	
	for key, _ := range data {
		fmt.Println(key)
	}
	kv := client.KV()

	// PUT a new KV pair
	p := &consulapi.KVPair{Key: "metis/via_ip_port", Value: []byte("192.168.10.111_12345")}
	_, err = kv.Put(p, nil)
	if err != nil {
		panic(err)
	}

	// Lookup the pair
	pair, _, err := kv.Get("metis/via_ip_port", nil)
	if err != nil {
		panic(err)
	}
	fmt.Println(pair)
}

func main() {
	consulRegister()
}

在这里插入图片描述

GRPC

grpc health介绍

Python

目录结构如下图
在这里插入图片描述
health_grpc_check.py代码如下

from consul_client.health import health_pb2
from consul_client.health import health_pb2_grpc

class DataServiceHealth(health_pb2_grpc.HealthServicer):
    def Check(self, request, context):
        print(f"DataService Health Check...{request}")
        return health_pb2.HealthCheckResponse(status=health_pb2.HealthCheckResponse.ServingStatus.SERVING)

    def Watch(self, request, context):
        return


class JobServiceHealth(health_pb2_grpc.HealthServicer):
    def Check(self, request, context):
        print(f"JobServiceHealth Health Check...{request}")
        return health_pb2.HealthCheckResponse(status=health_pb2.HealthCheckResponse.ServingStatus.SERVING)

    def Watch(self, request, context):
        return


def add_service(server, _type):
    if _type == 'dataNode':
        health_pb2_grpc.add_HealthServicer_to_server(DataServiceHealth(), server)
    elif _type == 'jobNode':
        health_pb2_grpc.add_HealthServicer_to_server(JobServiceHealth(), server)
    else:
        raise Exception(f'Unknown service type {_type}')

需要注册的grpc服务代码

import grpc

import hello_pb2
import hello_pb2_grpc
from consul_client.health import health_grpc_check
from concurrent import futures


class Greeter(hello_pb2_grpc.GreeterServicer):
    def SayHello(self, request, context):
        return hello_pb2.HelloReply(message='Hello abc, %s!' % request.name)


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
    health_grpc_check.add_service(server, 'dataNode')

    server.add_insecure_port('[::]:50053')
    server.start()
    server.wait_for_termination()


if __name__ == '__main__':
    serve()

服务注册与发现api.py代码如下

import os
import re
import consul
import base64
from common.utils import load_cfg


class ConsulApi(object):
    def __init__(self,
                 host='127.0.0.1',
                 port=8500,
                 token=None,
                 scheme='http',
                 consistency='default',
                 dc=None,
                 verify=True,
                 cert=None,
                 **kwargs):
        self.c = consul.Consul(
            host, port, token, scheme, consistency, dc, verify, cert, **kwargs
        )
        self.config_file_path = ''

    def headers(self):
        token = None
        headers = {}
        token = token or self.c.token
        if token:
            headers['X-Consul-Token'] = token
        return headers

    def discovery(self, filter_str):
        """
        filter_str detail please see https://www.consul.io/api-docs/agent/service#ns
        filter_str example:
                          Service==jobNode
                          via in Tags
                          ID=="jobNode_192.168.21.188_50053
        """
        result = self.c.http.get(consul.base.CB.json(),
                                 path='/v1/agent/services',
                                 params=[('filter', filter_str)],
                                 headers=self.headers())
        if len(result) > 1:
            print(f'The number of query results is greater than one, and the query condition is {filter_str}')
            return None
        if len(result) == 0:
            print(f'According to the query conditions, {filter_str} is useless to query relevant information.')
            return None
        result, *_ = list(result.values())
        return f'{result["Address"]}:{result["Port"]}'

    def get_via_external_connection(self):
        result = self.c.http.get(consul.base.CB.json(),
                                 path='/v1/kv/metis/via_ip_port',
                                 headers=self.headers())
        connection, *_ = result
        return base64.b64decode(connection["Value"]).decode()

    def check_service_config(self):
        if not os.path.isfile(self.config_file_path):
            raise Exception(f'file {self.config_file_path} not exits')
        try:
            config = load_cfg(self.config_file_path)
            consul_config = config['consul']
            ip, port = config['bind_ip'], int(config['port'])
            name, tag = consul_config['name'], consul_config['tag']
            interval, deregister = consul_config['interval'], consul_config['deregister']
            str_re = '^(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|[1-9])\.(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|\d)$'
            if not re.compile(str_re).match(ip):
                raise Exception(f'IP address {ip} is illegal')
            if not 1024 < port <= 65535:
                raise Exception(f'PORT {port} illegal,port 1024~65535')
            if name not in ['dataNode', 'jobNode']:
                raise Exception(f'Found the wrong service type {name}, which can only be data or compute')
            return ip, port, name, tag, interval, deregister
        except Exception as e:
            raise Exception(f'check_service_config exception is:{e}')

    def register(self, config_file_path):
        self.config_file_path = config_file_path
        ip, port, name, tag, interval, deregister = self.check_service_config()
        check = consul.Check().grpc(
            grpc=f'{ip}:{port}/{name}',
            interval=interval,
            deregister=deregister
        )

        params = {
            'name': name,
            'service_id': f'{name}_{ip}_{port}',
            'address': ip,
            'tags': [tag],
            'port': port,
            'check': check
        }
        if self.c.agent.service.register(**params):
            print(f'register service successful,service detail info:{params}')

运行结果如下图
在这里插入图片描述

Golang

demo的GitHub地址

参考

参考1
参考2
参考3

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值