gRPC入门系列之4-consul服务注册与发现

在前面几篇关于gRPC的文章中,我们已经实现了简单的gRPC接口,但是这些接口都是本地调用,这在现实生产环境中几乎不可能。在生产环境中,除非是直直接在固定ip的物理机/虚拟机上运行,否则ip地址是不固定的,而且服务的数量也是不固定的,这时候就需要服务注册与发现了。

在微服务架构中,服务的注册与发现是一个非常重要的组件,它可以让服务之间的调用更加简单,也可以让服务更加健壮。服务注册发现中间件有很多,比如etcd、consul、zookeeper等,这里我们选择consul。

在本章中,我们将实现如下功能:

  • 服务端启动时,将服务注册到consul中,并实现健康检查接口。
  • 客户端通过consul发现服务,获取服务列表,并根据从consul获取到的信息,调用服务。

本文示例环境及版本信息:

  • 操作系统:macOS。
  • Go版本:1.21.4。

准备工作

在开始之前,我们需要先安装consul。本机是mac,直接使用brew安装:

bash 复制代码
brew install consul

可以如下命令启动或停止consul:

bash 复制代码
# 启动consul
brew services start consul
# 停止consul
brew services stop consul

成功启动consul后,在浏览器中访问http://localhost:8500/ui/,可以看到consul的web界面,如下图所示:

更多信息,大家可以访问consul官方网站查看:developer.hashicorp.com/consul/docs...

服务端开发

为了方便,我们新建一个目录,复制之前gRPC系列之一-Unary模式中的代码,在其基础上进行开发。

下面,我们讲解一下主要逻辑:

初始化consul客户端

我们新建一个pkg目录,用于存放一些中间件代码,在pkg目录下新建consul/consul.go文件,内容如下:

go 复制代码
package consul

import (
	"github.com/hashicorp/consul/api"
	"github.com/pkg/errors"
)

var client *api.Client

// InitConsulClient 初始化consul客户端
func InitConsulClient(address string) error {
	var err error

	config := api.DefaultConfig()
	config.Address = address

	client, err = api.NewClient(config)

	if err != nil {
		return errors.Wrap(err, "init consul client error")
	}

	_, err = client.Status().Leader()

	if err != nil {
		return errors.Wrap(err, "get consul leader error")
	}

	return nil
}

// GetConsulClient 获取consul客户端
func GetConsulClient() *api.Client {
	return client
}

在上面的例子中,我们初始化了consul客户端,并且提供了获取consul客户端的方法。在初始化时,我们判断是否成功连接到consul,如果连接失败,会返回错误。

服务主要逻辑开发

在服务端的main函数中,我们将实现如下逻辑:

  • 启动gRPC服务。
  • 注册到consul。
  • 注册健康检查接口。
  • 实现优化关闭。

main函数代码如下:

go 复制代码
func main() {
	// consul地址,此处为本地地址
	consulAddress := "localhost:8500"
	// 服务名称
	serviceName := "service-grpcdemo"

	logger := zap.NewExample()
	// 实现服务端逻辑
	lis, err := net.Listen("tcp", ":5630")

	if err != nil {
		panic(err)
	}

	s := grpc.NewServer()

	pb.RegisterGreetServiceServer(s, &Server{})

	reflection.Register(s)

	logger.Info("grpc server start")

	// 启动相关服务
	var gg errgroup.Group

	gg.Go(func() error {
		logger.Debug("grpc server start")

		go func() {
			if err := s.Serve(lis); err != nil {
				logger.Fatal("grpc server start failed", zap.Error(err))
			}
		}()

		return nil
	})

	gg.Go(func() error {
		if err := consul.InitConsulClient(consulAddress); err != nil {
			return err
		}

		logger.Debug("consul client start")

		return nil
	})

	if err = gg.Wait(); err != nil {
		logger.Fatal("server start failed", zap.Error(err))
		return
	}

	// 注册服务

	// 健康检查
	go func() {
		mux := http.NewServeMux()

		mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
			_, _ = w.Write([]byte("ok"))
		})

		err = http.ListenAndServe(":8081", mux)

		if err != nil {
			logger.Fatal("health check start failed", zap.Error(err))
		}

	}()

	check := &api.AgentServiceCheck{
		Interval:                       "3s",
		Timeout:                        "2s",
		DeregisterCriticalServiceAfter: "60s",
		HTTP:                           "http://127.0.0.1:8081/health",
		Method:                         "GET",
	}

	err = consul.GetConsulClient().Agent().ServiceRegister(&api.AgentServiceRegistration{
		Name:  serviceName,
		Port:  5630,
		Check: check,
		ID:    "1001",
		Tags:  []string{"grpc.port=5630"},
	})

	if err != nil {
		logger.Fatal("register service failed", zap.Error(err))
		return
	}

	// 优雅关闭
	ch := make(chan os.Signal, 1)

	// 监听信号
	signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL, syscall.SIGHUP, syscall.SIGQUIT)

	// 阻塞等待信号
	sig := <-ch

	logger.Info("receive signal", zap.Any("signal", sig))

	// 关闭服务
	s.GracefulStop()

	logger.Debug("server stop")
}

在上面的代码中,我们启动了gRPC服务,监听端口是5630,并用8081端口提供健康检查接口。

接下来,为了方便操作,我们再来修改一下Makefile文件,添加如下内容:

makefile 复制代码
consuldemo-build:
	go build -o ./consuldemo/bin/server ./consuldemo/server/
    # go build -o ./consuldemo/bin/client ./consuldemo/client/
.PHONY: tidy greet-gen greet-build greet-evans greet-evans-reflect calculator-gen calculator-build calculator-evans-reflect consuldemo-build

执行如下命令,编译服务端和客户端:

bash 复制代码
make consuldemo-build

现在,我们执行./consuldemo/bin/server启动服务端,可以看到如下输出:

我们再来看一下consul的web界面,可以看到服务已经注册到consul中了:

好了,服务端开发完成,接下来我们来实现客户端。

客户端开发

客户端的开发比服务逻辑简单很多,大致逻辑是:

  • 从consul中获取服务列表。
  • 根据服务列表返回的节点,随机选择一个节点,调用服务。

代码如下:

go 复制代码
package main

import (
	"context"
	"fmt"
	"git.gqnotes.com/guoqiang/grpcexercises/consuldemo/pb"
	"git.gqnotes.com/guoqiang/grpcexercises/consuldemo/pkg/consul"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"log"
	"math/rand"
)

func main() {
	// 通过consul服务发现,调用grpc接口
	consulAddress := "localhost:8500"
	serviceName := "service-grpcdemo"

	// 初始化consul客户端
	if err := consul.InitConsulClient(consulAddress); err != nil {
		log.Fatalf("init consul client failed:%+v", err)
	}

	// 获取consul客户端
	client := consul.GetConsulClient()

	// 获取服务
	services, _, err := client.Health().Service(serviceName, "", true, nil)

	if err != nil {
		log.Fatalf("get service failed:%+v", err)
	}

	// 随机获取一个服务
	service := services[rand.Intn(len(services))]

	// 拼接服务地址
	address := fmt.Sprintf("%s:%d", service.Service.Address, service.Service.Port)

	log.Printf("grpc client call address:%s", address)

	// 连接grpc服务
	conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()))

	if err != nil {
		log.Fatalf("grpc client dial failed:%+v", err)
	}

	defer conn.Close()

	// 创建grpc客户端
	grpcClient := pb.NewGreetServiceClient(conn)

	// 调用grpc接口
	resp, err := grpcClient.Greet(context.Background(), &pb.GreetRequest{
		Greeting: "zhangsan",
	})

	if err != nil {
		log.Fatalf("grpc client call greet failed:%+v", err)
		return
	}

	log.Printf("grpc client call greet success:%+v", resp)
}

在上面的代码中,我们通过consul客户端获取服务列表,然后随机选择一个服务节点,调用服务。

我们执行上面的代码,可以看到如下输出:

小结

今天,我们完善了我们的gRPC服务,通过consul实现了服务注册与发现。需要说明的是:在现实中,consul服务的地址可能是通过配置文件,或者配置中心获取的,不会写死在代码中。而一个服务的服务节点可能有很多,每个节点的ip都不一样,在注册时需要带上ip地址。

注:此文原载于本人个人网站,链接地址

本文由mdnice多平台发布

相关推荐
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠3 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
AskHarries3 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_3 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平5 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
BiteCode_咬一口代码6 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
齐 飞6 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb