基于consul实现服务注册与发现
Consul是一个分布式、高可用性和数据中心感知的解决方案,用于跨动态、分布式基础设施连接和配置应用程序。
Consul介绍
Consul是一个分布式、高可用性和数据中心感知的解决方案,用于跨动态、分布式基础设施连接和配置应用程序。
Consul提供了几个关键特性:
- 多数据中心——Consul被构建为数据中心感知的,可以支持任意数量的区域,而不需要复杂的配置。
- 服务网格/服务细分——使用自动 TLS 加密和基于身份的授权实现安全的服务对服务通信。应用程序可以在服务网格配置中使用 sidecar 代理为入站和出站连接建立 TLS 连接,而完全不知道连接方。
- 服务发现——Consul使服务注册自己和通过 DNS 或 HTTP 接口发现其他服务变得简单。也可以注册外部服务,如 SaaS 提供者。
- 健康检查——健康检查使得领事能够就集群中的任何问题快速向操作员发出警报。与服务发现的集成可以防止将流量路由到不健康的主机,并启用服务级断路器。
- Key/Value存储——一个灵活的键/值存储允许存储动态配置、特性标记、协调、领导选举等。简单的 HTTP API 使得在任何地方都可以轻松使用。
Consul安装
推荐按官方docker-compose-datacenter教程使用docker-compose一键搭建consul环境。
推荐使用 Docker Compose 快速搭建学习使用的 Consul 环境。
执行下面的命令,从官方 github 仓库中获取的配置和资源。
git clone https://github.com/hashicorp/learn-consul-docker.git
这个仓库中有很多定义好的各式 consul 环境 docker-compose 配置,我们这里使用的是服务发现的环境,进入到 datacenter-deploy-service-discovery
目录。
cd datacenter-deploy-service-discovery
执行以下命令,启动容器。
❯ docker-compose up -d
Creating network "datacenter-deploy-service-discovery_consul" with driver "bridge"
Creating consul-server ... done
Creating consul-client ... done
这样一个用于服务发现的Consul环境就搭建好了。
使用浏览器打开 http:127.0.0.1:8500 就可以看到Consul的管理界面了。
服务注册与服务发现
详细用法和参数请参照 Agent HTTP API文档。
查询服务
Method | Path | Produces |
---|---|---|
GET |
/agent/services |
application/json |
注册服务
Method | Path | Produces |
---|---|---|
PUT |
/agent/service/register |
application/json |
注销服务
Method | Path | Produces |
---|---|---|
PUT |
/agent/service/deregister/:service_id |
application/json |
注意: 实际Path路径前需要加v1
版本前缀。
Go SDK
在代码中导入官方 api 包
import "github.com/hashicorp/consul/api"
连接consul
type consul struct {
client *api.Client
}
// NewConsul 连接至consul服务返回一个consul对象
func NewConsul(addr string) (*consul, error) {
cfg := api.DefaultConfig()
cfg.Address = addr
c, err := api.NewClient(cfg)
if err != nil {
return nil, err
}
return &consul{c}, nil
}
获取本机出口IP
这里补充一个获取本机出口IP的方法。
// GetOutboundIP 获取本机的出口IP
func GetOutboundIP() (net.IP, error) {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return nil, err
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP, nil
}
服务注册
将我们 grpc 服务注册到 consul。
// RegisterService 将gRPC服务注册到consul
func (c *consul) RegisterService(serviceName string, ip string, port int) error {
srv := &api.AgentServiceRegistration{
ID: fmt.Sprintf("%s-%s-%d", serviceName, ip, port), // 服务唯一ID
Name: serviceName, // 服务名称
Tags: []string{"q1mi", "hello"}, // 为服务打标签
Address: ip,
Port: port,
}
return c.client.Agent().ServiceRegister(srv)
}
健康检查
consul支持健康检查。
gRPC服务支持健康检查
gRPC 服务要支持健康检查需要先导入相关依赖包。
import "google.golang.org/grpc/health"
import healthpb "google.golang.org/grpc/health/grpc_health_v1"
注册健康检查服务。
s := grpc.NewServer() // 创建gRPC服务器
healthcheck := health.NewServer()
healthpb.RegisterHealthServer(s, healthcheck)
consul添加健康检查
在向 consul 注册我们的 gRPC 服务时,可以指定健康检查的配置。
// RegisterService 将gRPC服务注册到consul
func (c *consul) RegisterService(serviceName string, ip string, port int) error {
// 健康检查
check := &api.AgentServiceCheck{
GRPC: fmt.Sprintf("%s:%d", ip, port), // 这里一定是外部可以访问的地址
Timeout: "10s",
Interval: "10s",
DeregisterCriticalServiceAfter: "20s",
}
srv := &api.AgentServiceRegistration{
ID: fmt.Sprintf("%s-%s-%d", serviceName, ip, port), // 服务唯一ID
Name: serviceName, // 服务名称
Tags: []string{"q1mi", "hello"}, // 为服务打标签
Address: ip,
Port: port,
Check: check,
}
return c.client.Agent().ServiceRegister(srv)
}
服务发现
通常我们会选择使用filter查询我们想要的服务,具体支持的filter策略可查看文档。
// ListService 服务发现
func (c *consul) ListService() (map[string]*api.AgentService, error) {
// c.client.Agent().Service("hello-127.0.0.1-8972")
return c.client.Agent().ServicesWithFilter("Service==`hello`")
}
注销服务
将服务从 consul 注销可以使用ServiceDeregister
函数。
// Deregister 注销服务
func (c *consul) Deregister(serviceID string) error {
return c.client.Agent().ServiceDeregister(serviceID)
}
在现实场景中我们通常需要在某个server节点退出时将当前节点从 consul 注销。
这里搭配chan os.Signal
实现程序退出前执行注销服务操作。
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
fmt.Printf("failed to listen: %v", err)
return
}
s := grpc.NewServer() // 创建gRPC服务器
// 开启健康检查
healthcheck := health.NewServer()
healthpb.RegisterHealthServer(s, healthcheck)
pb.RegisterGreeterServer(s, &server{}) // 在gRPC服务端注册服务
// 注册服务
consul, err := NewConsul("127.0.0.1:8500")
if err != nil {
log.Fatalf("NewConsul failed, err:%v\n", err)
}
ipObj, _ := GetOutboundIP()
ip := ipObj.String()
serviceId := fmt.Sprintf("%s-%s-%d", "hello", ip, port)
consul.RegisterService(serviceId, ip, port)
// 启动服务
go func() {
err = s.Serve(lis)
if err != nil {
log.Printf("failed to serve: %v", err)
return
}
}()
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
<-quit
// 退出时注销服务
consul.Deregister(serviceId)
负载均衡
下面的示例演示了 gRPC 客户端使用 consul 作为注册中心,round_robin
作为负载均衡策略建立 gRPC 连接的示例。其中使用了第三方的grpc-consul-resolver库。
package main
import _ "github.com/mbobakov/grpc-consul-resolver"
// ...
conn, err := grpc.Dial(
// consul服务
"consul://192.168.1.11:8500/hello?wait=14s",
// 指定round_robin策略
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)