当我们在使用需要连接的资源时,一般都应该想到可以通过池化的技术去做一定的性能优化。比如数据库连接池就是最常见的连接池。
在微服务中,服务与服务之间的通信也是需要建立连接的,如果需要频繁的交互,那么 建立连接池就可以避免每次交互都需要新建连接的性能消耗。
本案例就是要手写一个grpc的客户端连接池,整合到gin web
服务中,而这个web
服务需要频繁调用另外两个grpc
远程服务,分别是关键词匹配服务和敏感词过滤服务(当然这里不会有很复杂的匹配和过滤上的业务逻辑,毕竟主要演示的是调用链路),链路大致如下:
因为本博客主要学习的是连接池的实现方法、grpc服务的开发、gin web服务的开发、以及gin web 服务调用远程grpc服务
。此外,该案例会包含三个服务,工作中一般这三个服务会在不同的服务器上,这里为了演示,就在同一个代码包下,通过不同的端口号模拟多个服务。gin web
服务调用grpc
服务时,本案例中我们也没有用到服务注册与发现功能,而是在gin web
服务中写死了grpc
客户端该服务就一个接口,接收一段文本,然后输出是否包含敏感词,为了简便,我们不真的校验是否包含敏感词,直接返回true
即可。
syntax = "proto3";
package sensitive;
option go_package = "33-grpc-pool/sensitive/proto";
message ValidateRequest{
string input = 1;
}
message ValidateResponse {
bool ok = 1;
string word = 2;
}
service SensitiveFilter {
rpc Validate(ValidateRequest) returns (ValidateResponse);
}
protoc --proto_path=33-grpc-pool/sensitive/proto --go_out=. --go-grpc_out=. 33-grpc-pool/sensitive/proto/sensitive.proto
编写服务端代码server.go
package server
import (
"context"
"fmt"
"golang-trick/33-grpc-pool/sensitive/proto"
)
type SensitiveServer struct {
proto.UnimplementedSensitiveFilterServer
}
func (s SensitiveServer) Validate(ctx context.Context, request *proto.ValidateRequest) (*proto.ValidateResponse, error) {
fmt.Printf("%+v\n", request)
// 我们直接认为没有敏感词,直接返回true,敏感词为空
return &proto.ValidateResponse{
Ok: true,
Word: "",
}, nil
}
启动服务代码main.go
package main
import (
"flag"
"fmt"
"golang-trick/33-grpc-pool/sensitive/proto"
"golang-trick/33-grpc-pool/sensitive/sensitive-server/server"
"log"
"net"
"google.golang.org/grpc"
)
var (
port = flag.Int("port", 50051, "")
)
func main() {
flag.Parse()
// 监听端口
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatal(err)
}
// 建立rpc服务,并注册SensitiveServer
s := grpc.NewServer()
proto.RegisterSensitiveFilterServer(s, &server.SensitiveServer{})
// 启动服务
err = s.Serve(lis)
if err != nil {
log.Fatal(err)
}
}
关键词匹配 服务,编码与上面的敏感词过滤服务几乎一模一样,主要就是改了proto
文件以及服务的实现。但编码流程完全没变的。
我们写完后会发现,keywords服务
和sensitive服务
的代码结构一致。再次强调一下,这里为了演示,所以两个微服务写到了一起,通过端口分为两个微服务启动,实际一般是不同的两个微服务项目,部署到不同的机器上的。
syntax = "proto3";
package sensitive;
option go_package = "33-grpc-pool/keywords/proto";
message MatchRequest{
string input = 1;
}
message MatchResponse {
bool ok = 1;
string word = 2;
}
service KeyWordsMatch {
rpc Match(MatchRequest) returns (MatchResponse);
}
注意命令中的路径和sensitive
服务的有所不同,需要修改
protoc --proto_path=33-grpc-pool/keywords/proto --go_out=. --go-grpc_out=. 33-grpc-pool/keywords/proto/keywords.proto
编写服务端代码server.go
package server
import (
"context"
"fmt"
"golang-trick/33-grpc-pool/keywords/proto"
)
type KwServer struct {
proto.UnimplementedKeyWordsMatchServer
}
func (k KwServer) Match(ctx context.Context, request *proto.MatchRequest) (*proto.MatchResponse, error) {
fmt.Printf("%+v\n", request)
// 我们直接认为没有敏感词,直接返回true,敏感词为空
return &proto.MatchResponse{
Ok: true,
Word: "",
}, nil
}
服务启动代码main.go
注意端口换为了50052,sensitive服务的是50051
package main
import (
"flag"
"fmt"
"golang-trick/33-grpc-pool/keywords/keywords-server/server"
"golang-trick/33-grpc-pool/keywords/proto"
"log"
"net"
"google.golang.org/grpc"
)
var (
port = flag.Int("port", 50052, "")
)
func main() {
flag.Parse()
// 监听端口
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatal(err)
}
// 建立rpc服务,并注册SensitiveServer
s := grpc.NewServer()
proto.RegisterKeyWordsMatchServer(s, &server.KwServer{})
// 启动服务
err = s.Serve(lis)
if err != nil {
log.Fatal(err)
}
}
由于proto文件与上面两个服务的一样的,只是go_package路径需要改变一下,就不重复贴这里了,看下面目录结构即可
因为gin web
服务相当于路由服务,里面会通过rpc
调用远程的两个服务,所以那两个远程服务的客户端代码就是写到gin web
服务中的,因此也需要pb.go
文件存根。生成后如下:
keywords客户端存根生成命令
protoc --proto_path=33-grpc-pool/grpcpool/services/keywords/proto --go_out=. --go-grpc_out=. 33-grpc-pool/grpcpool/services/keywords/proto/keywords.proto
sensitive客户端存根生成命令
protoc --proto_path=33-grpc-pool/grpcpool/services/sensitive/proto --go_out=. --go-grpc_out=. 33-grpc-pool/grpcpool/services/sensitive/proto/sensitive.proto
sync.Pool 知识补充
**结构如下:**主要是关注New
字段,是一个方法,需要我们在初始化的时候提供,用于告知如何生成 池中的连接
Pool具有的方法: 主要关注Get
和Put
方法,用于获取和归还连接。与数据库连接池不太一样,数据库连接池一个连接用完了会自动返回池中,而sync.Pool
中的连接用完了,需要我们手动的放回去,故提供了一个Put
方法
定义grpc-client-pool.go文件实现连接池,内容如下
package services
import (
"log"
"sync"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
)
// 注意这里是大写开头,定义的是一个接口
type ClientPool interface {
Get() *grpc.ClientConn
Put(conn *grpc.ClientConn)
}
// 注意这里是小写开头,定义的是结构体,用于实现上面的ClientPool接口
type clientPool struct {
pool sync.Pool
}
// 获取连接池对象,并定义新建连接的方法,返回ClientPool接口类型
func GetPool(target string, opts ...grpc.DialOption) (ClientPool, error) {
return &clientPool{
pool: sync.Pool{
New: func() any {
conn, err := grpc.Dial(target, opts...)
if err != nil {
log.Fatal(err)
}
return conn
},
},
}, nil
}
// 从连接池中获取一个连接
func (c *clientPool) Get() *grpc.ClientConn {
conn := c.pool.Get().(*grpc.ClientConn)
// 当连接不可用时,关闭当前连接,并新建一个连接
if conn.GetState() == connectivity.Shutdown || conn.GetState() == connectivity.TransientFailure {
conn.Close()
conn = c.pool.New().(*grpc.ClientConn)
}
return conn
}
// 与数据库连接池不太一样,数据库连接池一个连接用完了会自动返回池中
// 而sync.Pool中的连接用完了,需要我们手动的放回去,故提供一个Put方法
func (c *clientPool) Put(conn *grpc.ClientConn) {
// 当连接不可用时,关闭当前连接,并不再放回池中
if conn.GetState() == connectivity.Shutdown || conn.GetState() == connectivity.TransientFailure {
conn.Close()
return
}
c.pool.Put(conn)
}
首先,由于我们gin web
服务需要调用多个不同rpc
服务,每个远程rpc
服务,我们都应该建立一个对应的客户端连接池,所以为了统一,建立一个ServiceClient
接口,并提供一个默认实现DefaultClient
。第二点,建立远程rpc
服务的客户端时(我们给sync.Pool
的New
字段传的函数grpc.Dial(target, opts...)
),可能想传入不同的可选项,所以我们提供了一个opts
文件,专门存放这些可选性,如安全连接校验等。
client.go
package client
import (
"golang-trick/33-grpc-pool/grpcpool/services"
"log"
)
type ServiceClient interface {
GetPool(addr string) services.ClientPool
}
type DefaultClient struct {
}
func (c *DefaultClient) GetPool(addr string) services.ClientPool {
pool, err := services.GetPool(addr, c.getOptions()...)
if err != nil {
log.Fatal(err)
}
return pool
}
// 还可以有很多其他的实现,比如KeywordsClient,SensitiveClient等,这里为了简单,就只写了DefaultClient
opts.go
package client
import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func (c *DefaultClient) getOptions() (opts []grpc.DialOption) {
opts = make([]grpc.DialOption, 0)
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
return opts
}
// 不同的实现可能有不同的opts,比较复杂的时候,还可以考虑使用函数式选项模式
现在可以分别创建keywords
和sensitive
服务对应的客户端连接池对象了,使用单例
33-grpc-pool/grpcpool/services/keywords/client.go
package keywords
import (
"golang-trick/33-grpc-pool/grpcpool/services"
"golang-trick/33-grpc-pool/grpcpool/services/client"
"sync"
)
// 注意是小写的,因为一个gin web服务,我们只希望它对一个grpc服务持有的连接池是一个单例
// 因此小写,避免其他地方可以构造这个结构体的对象。然后这里通过once控制是单例
type kwClient struct {
// 内嵌client.DefaultClient,从而实现了ServiceClient接口
// 如果有其他实现,比如KeywordsClient ,那么内嵌KeywordsClient即可
client.DefaultClient
}
var pool services.ClientPool
var once sync.Once
// 实际工作中,这里应该用服务的注册与发现机制,这里只是会了简单演示,所以写死了服务端的地址
var kwAddr = "localhost:50052"
func GetKwClientPool() services.ClientPool {
once.Do(func() {
c := &kwClient{}
// 实际调用的是内嵌的DefaultClient的GetPool
pool = c.GetPool(kwAddr)
})
return pool
}
33-grpc-pool/grpcpool/services/sensitive/client.go
package sensitive
import (
"golang-trick/33-grpc-pool/grpcpool/services"
"golang-trick/33-grpc-pool/grpcpool/services/client"
"sync"
)
// 注意是小写的,因为一个gin web服务,我们只希望它对一个grpc服务持有的连接池是一个单例
// 因此小写,避免其他地方可以构造这个结构体的对象。然后这里通过once控制是单例
type sensitiveClient struct {
// 内嵌client.DefaultClient,从而实现了ServiceClient接口
// 如果有其他实现,比如SensitiveClient ,那么内嵌SensitiveClient即可
client.DefaultClient
}
var pool services.ClientPool
var once sync.Once
// 实际工作中,这里应该用服务的注册与发现机制,这里只是会了简单演示,所以写死了服务端的地址
var sensitiveAddr = "localhost:50051"
func GetSensitiveClientPool() services.ClientPool {
once.Do(func() {
c := &sensitiveClient{}
// 实际调用的是内嵌的DefaultClient的GetPool
pool = c.GetPool(sensitiveAddr)
})
return pool
}
gin web
启动函数main.go
package main
import (
"golang-trick/33-grpc-pool/grpcpool/controllers"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", controllers.Ping)
r.Run()
}
在ping
函数中通过客户端连接池调用远程服务
package controllers
import (
"context"
"fmt"
"golang-trick/33-grpc-pool/grpcpool/services/keywords"
kwProto "golang-trick/33-grpc-pool/grpcpool/services/keywords/proto"
"golang-trick/33-grpc-pool/grpcpool/services/sensitive"
sensitiveProto "golang-trick/33-grpc-pool/grpcpool/services/sensitive/proto"
"net/http"
"github.com/gin-gonic/gin"
)
func Ping(ctx *gin.Context) {
// 建立一个sensitive服务的客户端单例连接,并调用sensitive远程rpc服务的Validate接口
spool := sensitive.GetSensitiveClientPool()
sconn := spool.Get()
// 注意用完后需要将连接手动放回连接池
defer spool.Put(sconn)
sensitiveClient := sensitiveProto.NewSensitiveFilterClient(sconn)
sIn := &sensitiveProto.ValidateRequest{Input: "今天天气很好"}
sensitiveRes, err := sensitiveClient.Validate(context.Background(), sIn)
fmt.Printf("%+v %+v \n", sensitiveRes, err)
// 建立一个keywords服务的客户端单例连接,并调用keywords远程rpc服务的Match接口
kpool := keywords.GetKwClientPool()
kconn := kpool.Get()
// 注意用完后需要将连接手动放回连接池
defer kpool.Put(kconn)
keywordsClient := kwProto.NewKeyWordsMatchClient(kconn)
kIn := &kwProto.MatchRequest{Input: "今天天气很好"}
keywordsRes, err := keywordsClient.Match(context.Background(), kIn)
fmt.Printf("%+v %+v \n", keywordsRes, err)
ctx.JSON(http.StatusOK, gin.H{
"message": "pong",
})
}
测试:
启动keywords
服务和sensitive
服务,以及gin web
服务,然后访问http://localhost:8080/ping