在本章节中,我们将之前介绍HTTP Client&Server的示例修改为GRPC微服务,并演示如何使用GoFrame框架开发一个简单的GRPC服务端和客户端,并且为GRPC微服务增加链路跟踪特性。

本章节的示例代码位于:https://github.com/gogf/gf/tree/master/example/trace/grpc_with_db

目录结构

链路跟踪-GRPC示例 - 图1

Protocol

  1. syntax = "proto3";
  2. package user;
  3. option go_package = "protobuf/user";
  4. import "github.com/gogo/protobuf/gogoproto/gogo.proto";
  5. // User service for tracing demo.
  6. service User {
  7. rpc Insert(InsertReq) returns (InsertRes) {}
  8. rpc Query(QueryReq) returns (QueryRes) {}
  9. rpc Delete(DeleteReq) returns (DeleteRes) {}
  10. }
  11. message InsertReq {
  12. string Name = 1 [(gogoproto.moretags) = 'v:"required#Please input user name."'];
  13. }
  14. message InsertRes {
  15. int32 Id = 1;
  16. }
  17. message QueryReq {
  18. int32 Id = 1 [(gogoproto.moretags) = 'v:"min:1#User id is required for querying."'];
  19. }
  20. message QueryRes {
  21. int32 Id = 1;
  22. string Name = 2;
  23. }
  24. message DeleteReq {
  25. int32 Id = 1 [(gogoproto.moretags) = 'v:"min:1#User id is required for deleting."'];
  26. }
  27. message DeleteRes {}

这里使用到了第三方的 github.com/gogo/protobuf 开源项目,用于注入自定义的Golang struct标签。这里不详细介绍,感兴趣的小伙伴可以自行了解。未来Katyusha微服务框架的官网文档也会做对这块详细介绍,包括GRPC工程目录、开发规范、开发工具、拦截器、注册发现、负载均衡等设计话题。

GRPC Server

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "time"
  6. "github.com/gogf/gf/contrib/trace/jaeger/v2"
  7. "github.com/gogf/gf/example/trace/grpc_with_db/protobuf/user"
  8. "github.com/gogf/gf/v2/database/gdb"
  9. "github.com/gogf/gf/v2/frame/g"
  10. "github.com/gogf/gf/v2/os/gcache"
  11. "github.com/gogf/gf/v2/os/gctx"
  12. "github.com/gogf/katyusha/krpc"
  13. )
  14. type server struct{}
  15. const (
  16. ServiceName = "grpc-server-with-db"
  17. JaegerUdpEndpoint = "localhost:6831"
  18. )
  19. func main() {
  20. var ctx = gctx.New()
  21. tp, err := jaeger.Init(ServiceName, JaegerUdpEndpoint)
  22. if err != nil {
  23. g.Log().Fatal(ctx, err)
  24. }
  25. defer tp.Shutdown(ctx)
  26. // Set ORM cache adapter with redis.
  27. g.DB().GetCache().SetAdapter(gcache.NewAdapterRedis(g.Redis()))
  28. s := krpc.Server.NewGrpcServer()
  29. user.RegisterUserServer(s.Server, &server{})
  30. s.Run()
  31. }
  32. // Insert is a route handler for inserting user info into database.
  33. func (s *server) Insert(ctx context.Context, req *user.InsertReq) (res *user.InsertRes, err error) {
  34. result, err := g.Model("user").Ctx(ctx).Insert(g.Map{
  35. "name": req.Name,
  36. })
  37. if err != nil {
  38. return nil, err
  39. }
  40. id, _ := result.LastInsertId()
  41. res = &user.InsertRes{
  42. Id: int32(id),
  43. }
  44. return
  45. }
  46. // Query is a route handler for querying user info. It firstly retrieves the info from redis,
  47. // if there's nothing in the redis, it then does db select.
  48. func (s *server) Query(ctx context.Context, req *user.QueryReq) (res *user.QueryRes, err error) {
  49. err = g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
  50. Duration: 5 * time.Second,
  51. Name: s.userCacheKey(req.Id),
  52. Force: false,
  53. }).WherePri(req.Id).Scan(&res)
  54. if err != nil {
  55. return nil, err
  56. }
  57. return
  58. }
  59. // Delete is a route handler for deleting specified user info.
  60. func (s *server) Delete(ctx context.Context, req *user.DeleteReq) (res *user.DeleteRes, err error) {
  61. err = g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
  62. Duration: -1,
  63. Name: s.userCacheKey(req.Id),
  64. Force: false,
  65. }).WherePri(req.Id).Scan(&res)
  66. return
  67. }
  68. func (s *server) userCacheKey(id int32) string {
  69. return fmt.Sprintf(`userInfo:%d`, id)
  70. }

服务端代码简要说明:

1、首先,服务端需要通过jaeger.Init方法初始化Jaeger

2、可以看到,业务逻辑和之前HTTP示例项目完全一致,只是接入层修改为了GRPC协议。

3、我们仍然通过缓存适配器的方式注入Redis缓存:

  1. g.DB().GetCache().SetAdapter(gcache.NewAdapterRedis(g.Redis()))

5、这里也是通过Cache方法启用ORM的缓存特性,之前已经做过介绍,这里不再赘述。

GRPC Client

  1. package main
  2. import (
  3. "github.com/gogf/gf/contrib/trace/jaeger/v2"
  4. "github.com/gogf/gf/example/trace/grpc_with_db/protobuf/user"
  5. "github.com/gogf/gf/v2/frame/g"
  6. "github.com/gogf/gf/v2/net/gtrace"
  7. "github.com/gogf/gf/v2/os/gctx"
  8. )
  9. const (
  10. ServiceName = "grpc-client-with-db"
  11. JaegerUdpEndpoint = "localhost:6831"
  12. )
  13. func main() {
  14. var ctx = gctx.New()
  15. tp, err := jaeger.Init(ServiceName, JaegerUdpEndpoint)
  16. if err != nil {
  17. g.Log().Fatal(ctx, err)
  18. }
  19. defer tp.Shutdown(ctx)
  20. StartRequests()
  21. }
  22. func StartRequests() {
  23. ctx, span := gtrace.NewSpan(gctx.New(), "StartRequests")
  24. defer span.End()
  25. var client, err = user.NewClient()
  26. if err != nil {
  27. g.Log().Fatalf(ctx, `%+v`, err)
  28. }
  29. // Baggage.
  30. ctx = gtrace.SetBaggageValue(ctx, "uid", 100)
  31. // Insert.
  32. insertRes, err := client.User().Insert(ctx, &user.InsertReq{
  33. Name: "john",
  34. })
  35. if err != nil {
  36. g.Log().Fatalf(ctx, `%+v`, err)
  37. }
  38. g.Log().Info(ctx, "insert id:", insertRes.Id)
  39. // Query.
  40. queryRes, err := client.User().Query(ctx, &user.QueryReq{
  41. Id: insertRes.Id,
  42. })
  43. if err != nil {
  44. g.Log().Errorf(ctx, `%+v`, err)
  45. return
  46. }
  47. g.Log().Info(ctx, "query result:", queryRes)
  48. // Delete.
  49. _, err = client.User().Delete(ctx, &user.DeleteReq{
  50. Id: insertRes.Id,
  51. })
  52. if err != nil {
  53. g.Log().Errorf(ctx, `%+v`, err)
  54. return
  55. }
  56. g.Log().Info(ctx, "delete id:", insertRes.Id)
  57. // Delete with error.
  58. _, err = client.User().Delete(ctx, &user.DeleteReq{
  59. Id: -1,
  60. })
  61. if err != nil {
  62. g.Log().Errorf(ctx, `%+v`, err)
  63. return
  64. }
  65. g.Log().Info(ctx, "delete id:", -1)
  66. }

客户端代码简要说明:

1、首先,客户端也是需要通过jaeger.Init方法初始化Jaeger

2、客户端非常简单,内部初始化以及默认拦截器的设置已经由Katyusha框架封装好了,开发者只需要关心业务逻辑实现即可,

效果查看

启动服务端:

链路跟踪-GRPC示例 - 图2

启动客户端:

链路跟踪-GRPC示例 - 图3

这里客户端的执行最后报了一个错误,那是我们故意为之,目的是演示GRPC报错时的链路信息展示。我们打开jaeger查看一下链路跟踪信息:

链路跟踪-GRPC示例 - 图4

可以看到本次请求涉及到两个服务:tracing-grpc-clienttracing-grpc-server,即客户端和服务端。整个请求链路涉及到17span,客户端5span,服务端12span,并且产生了2个错误。我们点击查看详情:

链路跟踪-GRPC示例 - 图5

我们点击查看一下最后接口调用错误的span情况:

链路跟踪-GRPC示例 - 图6

看起来像个参数校验错误,点击查看Events/Logs中的请求参数:

链路跟踪-GRPC示例 - 图7

查看Process中的Log信息可以看到,是由于传递的参数为-1,不满足校验规则,因此在数据校验的时候报错返回了。

GRPC Client

由于ormredislogging组件在之前的章节中已经介绍过链路信息,因此我们这里主要介绍GRPC Client&Server的链路信息。

Attributes

链路跟踪-GRPC示例 - 图8

Attribute/Tag说明
net.peer.ip请求的目标IP。
net.peer.port请求的目标端口。
rpc.grpc.status_codeGRPC的内部状态码,0表示成功,非0表示失败。
rpc.serviceRPC的服务名称,注意这里是RPC而不是GRPC,因为这里是通用定义,客户端支持多种RPC通信协议,GRPC只是其中一种。
rpc.methodRPC的方法名称。
rpc.systemRPC协议类型,如:grpc, thrift等。

Events/Logs

链路跟踪-GRPC示例 - 图9

Event/Log说明
grpc.metadata.outgoingGRPC客户端请求提交的Metadata信息,可能会比较大。
grpc.request.baggageGRPC客户端请求提交的Baggage信息,用于服务间链路信息传递。
grpc.request.message

GRPC客户端请求提交的Message数据,可能会比较大,最大只记录512KB,如果超过该大小则忽略。仅对Unary请求类型有效。

grpc.response.messageGRPC客户端请求接收返回的的Message信息,可能会比较大。仅对Unary请求类型有效。

GRPC Server

Attributes

链路跟踪-GRPC示例 - 图10

GRPC Server端的Attributes含义同GRPC Client,在同一请求中,打印的数据基本一致。

Events

链路跟踪-GRPC示例 - 图11

GRPC Server端的EventsGRPC Client不同的是,在同一请求中,服务端接收到的metadatagrpc.metadata.incoming,其他同GRPC Client