go-kratos 微服務框架 warden模塊使用

pb文件

創建項目成功後,進入api目錄下可以看到api.proto文件:

option go_package = "api";
option (gogoproto.goproto_getters_all) = false;

service RPCDemo {
  rpc Ping(.google.protobuf.Empty) returns (.google.protobuf.Empty);
  rpc SayHello(HelloReq) returns (.google.protobuf.Empty);
  rpc SayHelloURL(HelloReq) returns (HelloResp) {
    option (google.api.http) = {
      get: "/kratos-demo/say_hello"
    };
  };
}

message HelloReq {
  string name = 1 [(gogoproto.moretags) = 'form:"name" validate:"required"'];
}

message HelloResp {
  string Content = 1 [(gogoproto.jsontag) = 'content'];
}

運行:

kratos tool protoc --grpc --bm api.proto

命令可以得到api.pb.go 和 api.bm.go

api.proto是gRPC server的描述文件
api.pb.go是基於api.proto生成的代碼文件,用於rpc調用,具體邏輯可在internal/service/serevice.go 內實現
api.bm.go是基於api.proto生成的代碼文件,用於http調用,將參數綁定後,調用serevice.go中方法,並返回json結果。

參考

gogo/protobuf

註冊server

進入internal/server/grpc目錄打開server.go文件,可以看到以下代碼,只需要替換以下注釋內容就可以啓動一個gRPC服務。

package grpc

import (
	"github.com/luslin/tools/kratos-demo/api"

	"github.com/go-kratos/kratos/pkg/conf/paladin"
	"github.com/go-kratos/kratos/pkg/net/rpc/warden"
)

// New new a grpc server.
func New(svc api.RPCDemoServer) (ws *warden.Server, err error) {
	var (
		cfg warden.ServerConfig
		ct paladin.TOML
	)
	if err = paladin.Get("grpc.toml").Unmarshal(&ct); err != nil {
		return
	}
	if err = ct.Get("Server").UnmarshalTOML(&cfg); err != nil {
		return
	}
	ws = warden.NewServer(&cfg)
	// 替換這裏   RegisterRPCDemoServer 在 api.pb.go 中
	api.RegisterRPCDemoServer(ws.Server(), svc)
	ws, err = ws.Start()
	return
}

註冊方法 internal/service/service.go

var Provider = wire.NewSet(New, wire.Bind(new(api.RPCDemoServer), new(*Service)))

// Service service.
type Service struct {
	ac  *paladin.Map
	dao dao.Dao
}

// New new a service and return.
func New(d dao.Dao) (s *Service, cf func(), err error) {
	s = &Service{
		ac:  &paladin.TOML{},
		dao: d,
	}
	cf = s.Close
	err = paladin.Watch("application.toml", s.ac)
	return
}

// SayHello grpc demo func.
func (s *Service) SayHello(ctx context.Context, req *api.HelloReq) (reply *empty.Empty, err error) {
	reply = new(empty.Empty)
	fmt.Printf("hello %s", req.Name)
	return
}

// SayHelloURL bm demo func.
func (s *Service) SayHelloURL(ctx context.Context, req *api.HelloReq) (reply *api.HelloResp, err error) {
	reply = &api.HelloResp{
		Content: "hello " + req.Name,
	}
	fmt.Printf("hello url %s", req.Name)
	return
}

// Ping ping the resource.
func (s *Service) Ping(ctx context.Context, e *empty.Empty) (*empty.Empty, error) {
	return &empty.Empty{}, s.dao.Ping(ctx)
}

// Close close the resource.
func (s *Service) Close() {
}

請進入internal/service內找到SayHello方法,注意方法的入參和出參,都是按照gRPC的方法聲明對應的:

第一個參數必須是context.Context,第二個必須是proto內定義的message對應生成的結構體
第一個返回值必須是proto內定義的message對應生成的結構體,第二個參數必須是error
在http框架bm中,如果共用proto文件生成bm代碼,那麼也可以直接使用該service方法
建議service嚴格按照此格式聲明方法使其能夠在bm和warden內共用。

client調用

請進入internal/dao方法內,一般對資源的處理都會在這一層封裝。
對於client端,前提必須有對應proto文件生成的代碼,那麼有兩種選擇:

拷貝proto文件到自己項目下並且執行代碼生成
直接import服務端的api package
這也是業務代碼我們加了一層internal的關係,服務對外暴露的只有接口

不管哪一種方式,以下初始化gRPC client的代碼建議伴隨生成的代碼存放在統一目錄下:

api/client_test.go

package api

import (
	"context"
	"fmt"
	"github.com/go-kratos/kratos/pkg/net/rpc/warden"
	"github.com/golang/protobuf/ptypes/empty"
	"google.golang.org/grpc"
	"log"
	"testing"
)
const (
	address     = "localhost:9000"
)
// 傳統rpc調用
func TestNewClient(t *testing.T) {
	conn, err := grpc.Dial(address, grpc.WithInsecure())
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()
	c := NewRPCDemoClient(conn)

	r, err := c.Ping(context.TODO(), &empty.Empty{})
	if err != nil {
		log.Fatal( err)
	}
	fmt.Println(r)
	rep, err := c.SayHelloURL(context.TODO(),&HelloReq{Name: "lin"})
	if err != nil {
		log.Fatal( err)
	}
	fmt.Println(rep.Content)

}

// kratos 封裝的rpc調用
func TestClient2(t *testing.T) {
	client := warden.NewClient(&warden.ClientConfig{})
	cc, err := client.Dial(context.Background(), fmt.Sprintf("direct://default/%s", address))
	if err != nil {
		panic(err)
	}
	rpc_cli :=  NewRPCDemoClient(cc)
	rep, err := rpc_cli.SayHelloURL(context.TODO(),&HelloReq{Name: "lin"})
	if err != nil {
		log.Fatal( err)
	}
	fmt.Println(rep.Content)
}

服務註冊與發現

服務註冊與發現最簡單的就是direct固定服務端地址的直連方式。也就是服務端正常監聽端口啓動不進行額外操作,客戶端使用如下target:

direct://default/127.0.0.1:9000,127.0.0.1:9091

其中direct爲協議類型,此處表示直接使用該URL內提供的地址127.0.0.1:9000,127.0.0.1:9091進行連接,而default在此處無意義僅當做佔位符。

使用discovery

服務註冊

要將本項目註冊到discovery中:

func DiscoveryRegister() func(){
	hn, _ := os.Hostname()
	dis := discovery.New(nil)
	ins := &naming.Instance{
		Zone:     "sh001",
		Env:      "dev",
		AppID:    "kratos_grpc",
		Hostname: hn,
		Addrs: []string{
			"grpc://192.168.1.88:9000",
			"http://192.168.1.88:9056",
		},
	}
	cancel, err := dis.Register(context.Background(), ins)
	if err != nil {
		panic(err)
	}

	// 省略...

	// 特別注意!!!
	// cancel必須在進程退出時執行!!!
	return cancel
}

在服務退出時,調用cancel從discovery中去掉本服務
可以在discovery中看到本服務:

"kratos_grpc":[
            {
                "region":"sh",
                "zone":"sh001",
                "env":"dev",
                "appid":"kratos_grpc",
                "hostname":"local",
                "addrs":[
                    "grpc://192.168.1.88:9000"
                ],
                "version":"",
                "metadata":null,
                "status":1,
                "reg_timestamp":1589962982365217546,
                "up_timestamp":1589962982365217546,
                "renew_timestamp":1589962982365217546,
                "dirty_timestamp":1589962982358288851,
                "latest_timestamp":1589962982365217546
            }

要使用discovery需要在業務的NewClient前進行註冊,代碼如下:

package api
import (
	"context"
	"fmt"
	"github.com/go-kratos/kratos/pkg/naming/discovery"
	"github.com/go-kratos/kratos/pkg/net/rpc/warden/resolver"
	"github.com/go-kratos/kratos/pkg/net/rpc/warden"
	"google.golang.org/grpc"
)

// AppID .
const AppID = "kratos_grpc"

func init()  {
	// NOTE: 注意這段代碼,表示要使用discovery進行服務發現
	// NOTE: 還需注意的是,resolver.Register是全局生效的,所以建議該代碼放在進程初始化的時候執行
	// NOTE: !!!切記不要在一個進程內進行多個不同中間件的Register!!!
	// NOTE: 在啓動應用時,可以通過flag(-discovery.nodes) 或者 環境配置(DISCOVERY_NODES)指定discovery節點
	resolver.Register(discovery.Builder())
}
// NewClient new grpc client
func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (RPCDemoClient, error) {
	client := warden.NewClient(cfg, opts...)
	cc, err := client.Dial(context.Background(), fmt.Sprintf("discovery://default/%s", AppID))
	if err != nil {
		return nil, err
	}
	// 注意替換這裏:
	// NewDemoClient方法是在"api"目錄下代碼生成的
	// 對應proto文件內自定義的service名字,請使用正確方法名替換
	return NewRPCDemoClient(cc), nil
}

測試

func TestClient2(t *testing.T) {
	client := warden.NewClient(&warden.ClientConfig{})
	cc, err := client.Dial(context.Background(), fmt.Sprintf("discovery://default/%s", "kratos_grpc"))
	if err != nil {
		panic(err)
	}
	rpc_cli :=  NewRPCDemoClient(cc)
	rep, err := rpc_cli.SayHelloURL(context.TODO(),&HelloReq{Name: "lin"})
	if err != nil {
		log.Fatal( err)
	}
	fmt.Println(rep.Content)
}
結果:
INFO 05/20-16:28:56.596 grpc-access-log ts=0.002694286 path=/demo.service.v1.RPCDemo/SayHelloURL args=name:"lin"  ret=0 ip=192.168.1.88:9000 
hello lin

target是discovery://default/${appid},當gRPC內進行解析後會得到scheme=discovery和appid,然後進行以下邏輯:

  • warden/resolver.Builder會通過scheme獲取到naming/discovery.Builder對象(靠resolver.Register註冊過的)
  • 拿到naming/discovery.Builder後執行Build(appid)構造naming/discovery.Discovery
  • naming/discovery.Discovery對象基於appid就知道要獲取哪個服務的實例信息
參考

discovery部署與使用

使用ETCD

和使用discovery類似,只需要在註冊時使用etcd naming即可

func init(){
   // NOTE: 注意這段代碼,表示要使用etcd進行服務發現 ,其他事項參考discovery的說明
   // NOTE: 在啓動應用時,可以通過flag(-etcd.endpoints) 或者 環境配置(ETCD_ENDPOINTS)指定etcd節點
   // NOTE: 如果需要自己指定配置時 需要同時設置DialTimeout 與 DialOptions: []grpc.DialOption{grpc.WithBlock()}
   resolver.Register(etcd.Builder(nil))
}

etcd的服務註冊與discovery基本相同,可以傳入詳細的etcd配置項, 或者傳入nil後通過flag(-etcd.endpoints)/環境配置(ETCD_ENDPOINTS)來指定etcd節點。

參考

etcd使用

負載均衡

grpc-go內置了round-robin輪詢,但由於自帶的輪詢算法不支持權重,也不支持color篩選等需求,故需要重新實現一個負載均衡算法。

WRR (Weighted Round Robin)

該算法在加權輪詢法基礎上增加了動態調節權重值,用戶可以在爲每一個節點先配置一個初始的權重分,之後算法會根據節點cpu、延遲、服務端錯誤率、客戶端錯誤率動態打分,在將打分乘用戶自定義的初始權重分得到最後的權重值。

P2C (Pick of two choices)

本算法通過隨機選擇兩個node選擇優勝者來避免羊羣效應,並通過ewma儘量獲取服務端的實時狀態。

服務端: 服務端獲取最近500ms內的CPU使用率(需要將cgroup設置的限制考慮進去,併除於CPU核心數),並將CPU使用率乘與1000後塞入每次grpc請求中的的Trailer中夾帶返回: cpu_usage uint64 encoded with string cpu_usage : 1000

客戶端: 主要參數:

  • server_cpu:通過每次請求中服務端塞在trailer中的cpu_usage拿到服務端最近500ms內的cpu使用率
  • inflight:當前客戶端正在發送並等待response的請求數(pending request)
  • latency: 加權移動平均算法計算出的接口延遲
  • client_success:加權移動平均算法計算出的請求成功率(只記錄grpc內部錯誤,比如context deadline)
    目前客戶端,已經默認使用p2c負載均衡算法grpc.WithBalancerName(p2c.Name):
// NewClient returns a new blank Client instance with a default client interceptor.
// opt can be used to add grpc dial options.
func NewClient(conf *ClientConfig, opt ...grpc.DialOption) *Client {
	c := new(Client)
	if err := c.SetConfig(conf); err != nil {
		panic(err)
	}
	c.UseOpt(grpc.WithBalancerName(p2c.Name))
	c.UseOpt(opt...)
	c.Use(c.recovery(), clientLogging(), c.handle())
	return c
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章