文章目錄
- 一、簡介
- 前置說明
- 二、敏感詞過濾服務
- 1、定義sensitive.proto文件
- 2、protoc生成pb.go文件
- 3、sensitive服務端實現
- 三、關鍵詞匹配服務
- 1、編寫keywords.proto文件
- 2、生成pb.go文件
- 3、keywords服務端實現
- 四、gin web 路由服務
- 1、新建grpcpool服務作為gin web服務
- 2、根據proto文件,分別生成keywords服務和sensitive服務的pb.go文件
- 五、grpc 連接池實現
- 1、連接池的實現,通過sync.Pool實現
- 2、連接池的使用
代碼地址: https://gitee.com/lymgoforIT/golang-trick/tree/master/33-grpc-pool
一、簡介
當我們在使用需要連接的資源時,一般都應該想到可以通過池化的技術去做一定的性能優化。比如數據庫連接池就是最常見的連接池。
在微服務中,服務與服務之間的通信也是需要建立連接的,如果需要頻繁的交互,那么 建立連接池就可以避免每次交互都需要新建連接的性能消耗。
本案例就是要手寫一個grpc的客戶端連接池,整合到gin web
服務中,而這個web
服務需要頻繁調用另外兩個grpc
遠程服務,分別是關鍵詞匹配服務和敏感詞過濾服務(當然這里不會有很復雜的匹配和過濾上的業務邏輯,畢竟主要演示的是調用鏈路),鏈路大致如下:
前置說明
因為本博客主要學習的是連接池的實現方法、grpc服務的開發、gin web服務的開發、以及gin web 服務調用遠程grpc服務
。此外,該案例會包含三個服務,工作中一般這三個服務會在不同的服務器上,這里為了演示,就在同一個代碼包下,通過不同的端口號模擬多個服務。gin web
服務調用grpc
服務時,本案例中我們也沒有用到服務注冊與發現功能,而是在gin web
服務中寫死了grpc
客戶端
二、敏感詞過濾服務
該服務就一個接口,接收一段文本,然后輸出是否包含敏感詞,為了簡便,我們不真的校驗是否包含敏感詞,直接返回true
即可。
1、定義sensitive.proto文件
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);
}
2、protoc生成pb.go文件
protoc --proto_path=33-grpc-pool/sensitive/proto --go_out=. --go-grpc_out=. 33-grpc-pool/sensitive/proto/sensitive.proto
3、sensitive服務端實現
編寫服務端代碼server.go
package serverimport ("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 mainimport ("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服務,并注冊SensitiveServers := grpc.NewServer()proto.RegisterSensitiveFilterServer(s, &server.SensitiveServer{})// 啟動服務err = s.Serve(lis)if err != nil {log.Fatal(err)}
}
當前sensitive
整體代碼結構如下:
三、關鍵詞匹配服務
關鍵詞匹配 服務,編碼與上面的敏感詞過濾服務幾乎一模一樣,主要就是改了proto
文件以及服務的實現。但編碼流程完全沒變的。
我們寫完后會發現,keywords服務
和sensitive服務
的代碼結構一致。再次強調一下,這里為了演示,所以兩個微服務寫到了一起,通過端口分為兩個微服務啟動,實際一般是不同的兩個微服務項目,部署到不同的機器上的。
1、編寫keywords.proto文件
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);
}
2、生成pb.go文件
注意命令中的路徑和sensitive
服務的有所不同,需要修改
protoc --proto_path=33-grpc-pool/keywords/proto --go_out=. --go-grpc_out=. 33-grpc-pool/keywords/proto/keywords.proto
3、keywords服務端實現
編寫服務端代碼server.go
package serverimport ("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 mainimport ("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服務,并注冊SensitiveServers := grpc.NewServer()proto.RegisterKeyWordsMatchServer(s, &server.KwServer{})// 啟動服務err = s.Serve(lis)if err != nil {log.Fatal(err)}
}
四、gin web 路由服務
1、新建grpcpool服務作為gin web服務
由于proto文件與上面兩個服務的一樣的,只是go_package路徑需要改變一下,就不重復貼這里了,看下面目錄結構即可
2、根據proto文件,分別生成keywords服務和sensitive服務的pb.go文件
因為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
五、grpc 連接池實現
1、連接池的實現,通過sync.Pool實現
sync.Pool 知識補充
**結構如下:**主要是關注New
字段,是一個方法,需要我們在初始化的時候提供,用于告知如何生成 池中的連接
Pool具有的方法: 主要關注Get
和Put
方法,用于獲取和歸還連接。與數據庫連接池不太一樣,數據庫連接池一個連接用完了會自動返回池中,而sync.Pool
中的連接用完了,需要我們手動的放回去,故提供了一個Put
方法
定義grpc-client-pool.go文件實現連接池,內容如下
package servicesimport ("log""sync""google.golang.org/grpc""google.golang.org/grpc/connectivity"
)// 注意這里是大寫開頭,定義的是一個接口
type ClientPool interface {Get() *grpc.ClientConnPut(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)
}
2、連接池的使用
和連接池相關的代碼文件如下:
各個接口,類之間的關系如下:
首先,由于我們gin web
服務需要調用多個不同rpc
服務,每個遠程rpc
服務,我們都應該建立一個對應的客戶端連接池,所以為了統一,建立一個ServiceClient
接口,并提供一個默認實現DefaultClient
。第二點,建立遠程rpc
服務的客戶端時(我們給sync.Pool
的New
字段傳的函數grpc.Dial(target, opts...)
),可能想傳入不同的可選項,所以我們提供了一個opts
文件,專門存放這些可選性,如安全連接校驗等。
client.go
package clientimport ("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 clientimport ("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 keywordsimport ("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的GetPoolpool = c.GetPool(kwAddr)})return pool
}
33-grpc-pool/grpcpool/services/sensitive/client.go
package sensitiveimport ("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的GetPoolpool = c.GetPool(sensitiveAddr)})return pool
}
gin web
啟動函數main.go
package mainimport ("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 controllersimport ("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
終端也可以看到兩個遠程服務都調用成功啦