在前面几篇关于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多平台发布