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結果。
參考
註冊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就知道要獲取哪個服務的實例信息
參考
使用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節點。
參考
負載均衡
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
}