trpc.group/trpc-go/trpc-go@v1.0.3/docs/user_guide/server/flatbuffers.md (about)

     1  English | [中文](flatbuffers.zh_CN.md)
     2  
     3  # Background
     4  
     5  [flatbuffers](https://flatbuffers.dev/) introduction: A serialization library launched by Google, mainly used in gaming and mobile scenarios. Its function is similar to Protobuf, and its main advantages are:
     6  
     7  - Access to serialized data is incredibly fast (after serialization, data can be accessed without deserialization; Unmarshal only extracts the byte slice, and accessing fields is similar to virtual table mechanism i.e., looking up the offset and locating the data). In fact, both Marshal and Unmarshal operations of Flatbuffers are lightweight, and the actual serialization step is deferred until construction. Therefore, construction takes up a large proportion of the total time.
     8  - Since it can access fields without deserialization, it is suitable for cases where only a small number of fields need to be accessed, such as when only a few fields of a large message are required. Protobuf needs to deserialize the entire message to access these fields, while Flatbuffers does not.
     9  - Efficient use of memory, without frequent memory allocation: This is mainly compared with protobuf. When protobuf serializes and deserializes, it needs to allocate memory to store intermediate results. However, after initial construction, Flatbuffers does not need to allocate memory again during serialization and deserialization.
    10  - Performance tests have also shown that Flatbuffers outperforms Protobuf when dealing with large amounts of data.
    11  
    12  Summary:
    13  Pushing all operations to the construction stage makes the Marshal and Unmarshal operations very lightweight.  
    14  
    15  According to benchmark tests, the proportion of time taken is:  
    16  - For Protobuf, the construction stage accounts for about 20% (including construction, Marshal, and Unmarshal).  
    17  - For Flatbuffers, it accounts for 90%.  
    18  
    19  Drawbacks:
    20  
    21  - Modifying a constructed Flatbuffer is more cumbersome.
    22  - The API for constructing data is somewhat difficult to use.
    23  
    24  # Principle
    25  
    26  ![flatbuffers](/.resources/user_guide/server/flatbuffers/flatbuffers.png)
    27  
    28  # Example
    29  Firstly, install the latest version of the [trpc-cmdline](https://github.com/trpc-group/trpc-cmdline) tool.
    30  
    31  Next, use the tool to generate flatbuffers corresponding stub code, which currently supports single-send and single-receive, server/client streaming, bidirectional streaming, etc.
    32  
    33  We will walk through all the steps using a simple example.
    34  
    35  First, define an IDL file. Its syntax can be learned from the flatbuffers official website. The overall structure is very similar to protobuf. An example is shown below:
    36  
    37  ```idl
    38  namespace trpc.testapp.greeter; // Equivalent to the "package" in protobuf.
    39  
    40  // Equivalent to the "go_package" statement in protobuf.
    41  // Note: "attribute" is a standard syntax in flatbuffers, and the "go_package=xxx" syntax is a custom support implemented by trpc-cmdline.
    42  attribute "go_package=github.com/trpcprotocol/testapp/greeter";
    43  
    44  table HelloReply { // "table" is equivalent to "message" in protobuf.
    45    Message:string;
    46  }
    47  
    48  table HelloRequest {
    49    Message:string;
    50  }
    51  
    52  rpc_service Greeter {
    53    SayHello(HelloRequest):HelloReply; // Single-send and single-receive.
    54    SayHelloStreamClient(HelloRequest):HelloReply (streaming: "client"); // Client streaming.
    55    SayHelloStreamServer(HelloRequest):HelloReply (streaming: "server"); // Server streaming.
    56    SayHelloStreamBidi(HelloRequest):HelloReply (streaming: "bidi"); // Bidirectional streaming.
    57  }
    58  
    59  // Example with two services.
    60  rpc_service Greeter2 {
    61    SayHello(HelloRequest):HelloReply;
    62    SayHelloStreamClient(HelloRequest):HelloReply (streaming: "client");
    63    SayHelloStreamServer(HelloRequest):HelloReply (streaming: "server");
    64    SayHelloStreamBidi(HelloRequest):HelloReply (streaming: "bidi");
    65  }
    66  ```
    67  The meaning of the "go_package" field is similar to the corresponding field in protobuf,see https://developers.google.com/protocol-buffers/docs/reference/go-generated#package
    68  
    69  As shown in the above link. Note that in protobuf, the "package" and "go_package" fields are unrelated.
    70  
    71  *There is no correlation between the Go import path and the package specifier in the .proto file. The latter is only relevant to the protobuf namespace, while the former is only relevant to the Go namespace.*
    72  
    73  However, due to the limitations of flatc, the last segment of the namespace in the flatbuffers IDL file must be the same as the last segment of the "go_package" field. At least, the two bold sections below must be the same:
    74  
    75  - namespace trpc.testapp.greeter;
    76  - attribute "go_package=github.com/trpcprotocol/testapp/greeter";
    77  
    78  Then, use the following command to generate the corresponding stub code:
    79  
    80  ```sh
    81  $ trpc create --fbs greeter.fbs -o out-greeter --mod github.com/testapp/testgreeter
    82  ```
    83  The "--fbs" option specifies the file name of the flatbuffers file (with relative path), "-o" specifies the output path, "--mod" specifies the package content in the generated go.mod file. If "--mod" is not specified, it will look for the go.mod file in the current directory and use the package content in that file as the "--mod" content, which represents the module path identifier of the server itself. This is different from the "go_package" in the IDL file, which represents the module path identifier of the stub code.
    84  
    85  The directory structure of the generated code is as follows:
    86  
    87  ```sh
    88  ├── cmd/client/main.go # Client code.
    89  ├── go.mod
    90  ├── go.sum
    91  ├── greeter_2.go       # Server implementation for the second service.
    92  ├── greeter_2_test.go  # Testing for the server of the second service.
    93  ├── greeter.go         # Server implementation for the first service.
    94  ├── greeter_test.go    # Testing for the server of the first service.
    95  ├── main.go            # Service startup code.
    96  ├── stub/github.com/trpcprotocol/testapp/greeter # Stub code files.
    97  └── trpc_go.yaml       # Configuration files.
    98  ```
    99  In one terminal, compile and run the server:
   100  ```sh
   101  $ go build      # Compile
   102  $ ./testgreeter # Run
   103  ```
   104  In another terminal, run the client:
   105  ```sh
   106  $ go run cmd/client/main.go
   107  ```
   108  Then, you can view the messages sent between the two terminals in their respective logs.
   109  
   110  The main.go file for starting the server is shown below:
   111  ```go
   112  package main
   113  import (
   114      "flag"
   115      
   116      _ "trpc.group/trpc-go/trpc-filter/debuglog"
   117      _ "trpc.group/trpc-go/trpc-filter/recovery"
   118      trpc "trpc.group/trpc-go/trpc-go"
   119      "trpc.group/trpc-go/trpc-go/log"
   120      fb "github.com/trpcprotocol/testapp/greeter"
   121  )
   122  func main() {
   123      flag.Parse()
   124      s := trpc.NewServer()
   125      // If there are multiple services, the service name must be explicitly written as the first parameter, otherwise the stream will have issues.
   126      fb.RegisterGreeterService(s.Service("trpc.testapp.greeter.Greeter"), &greeterServiceImpl{})
   127      fb.RegisterGreeter2Service(s.Service("trpc.testapp.greeter.Greeter2"), &greeter2ServiceImpl{})
   128      if err := s.Serve(); err != nil {
   129          log.Fatal(err)
   130      }
   131  }
   132  ```
   133  The overall content is basically the same as the generated file for Protobuf, and the only thing to note is that "serverFBBuilderInitialSize" is used to set the initial size of "flatbuffers.NewBuilder" when constructing "rsp" in the server stub code for the service. Its default value is 1024. It is recommended to set the size to exactly the size required to construct all the data in order to achieve optimal performance. However, if the data size is variable, setting this size will become a burden. Therefore, it is recommended to keep the default value of 1024 until it becomes a performance bottleneck.
   134  
   135  Here is an example of server-side logic implementation:
   136  ```go
   137  func (s *greeterServiceImpl) SayHello(ctx context.Context, req *fb.HelloRequest) (*flatbuffers.Builder, error) {
   138      // Flatbuffers processing logic for single request and response scenario (for reference only, please modify as needed).
   139      log.Debugf("Simple server receive %v", req)
   140      // Replace "Message" with the name of the field you want to operate on.
   141      v := req.Message() // Get Message field of request.
   142      var m string
   143      if v == nil {
   144          m = "Unknown"
   145      } else {
   146          m = string(v)
   147      }
   148      // Example of adding a field:
   149      // Replace "String" in "CreateString" with the type of field you want to operate on.
   150      // Replace "Message" in "AddMessage" with the name of the field you want to operate on.
   151      idx := b.CreateString("welcome " + m) // Create a string in Flatbuffers.
   152      b := &flatbuffers.Builder{}
   153      fb.HelloReplyStart(b)
   154      fb.HelloReplyAddMessage(b, idx)
   155      b.Finish(fb.HelloReplyEnd(b))
   156      return b, nil
   157  }
   158  ```
   159  Here is a detailed explanation of each step in the construction process:
   160  ```go
   161  // Import the package containing the stub code.
   162  import fb "github.com/trpcprotocol/testapp/greeter"
   163  // Start by creating a *flatbuffers.Builder.
   164  b := flatbuffers.NewBuilder(0) 
   165  // To populate a field in a struct:
   166  // First create an object of the type that the field represents.
   167  // For example, if the field is of type String, 
   168  // you can call b.CreateString("a string") to create the string.
   169  // This method returns the index of the string in the flatbuffer.
   170  i := b.CreateString("GreeterSayHello")
   171  // To construct a HelloRequest struct:
   172  // Call the XXXXStart method provided in the stub code to indicate the start of constructing this struct.
   173  // The corresponding end method is fb.HelloRequestEnd.
   174  fb.HelloRequestStart(b)
   175  // If the field to be populated is called message, you can call fb.HelloRequestAddMessage(b, i) to construct the message field by passing the builder and the index of the previously constructed string.
   176  // Other fields can be constructed in a similar manner.
   177  fb.HelloRequestAddMessage(b, i)
   178  // Call the XXXEnd method when the struct is complete.
   179  // This method will return the index of the struct in the flatbuffer.
   180  // Then call b.Finish to complete the construction of the flatbuffer.
   181  b.Finish(fb.HelloRequestEnd(b))
   182  ```
   183  It's evident that the Flatbuffers construction API is significantly difficult to use, especially when constructing nested structures.
   184  
   185  To access a specific field in a received message, simply access it as follows:
   186  
   187  ```go
   188  req.Message() // Access the message field in req.
   189  ```
   190  
   191  # 性能对比
   192  ![performanceComparison](/.resources/user_guide/server/flatbuffers/performanceComparison.png)
   193  The load testing environment consisted of two machines with 8 cores, 2.5 GHz CPU, and 16 GB memory.
   194  - We implemented a client-side loop packet tool that can send packages serialized with either Protobuf or Flatbuffers.
   195  - We fixed the number of goroutines at 500 and tested for 50 seconds each time.
   196  - Each point on the graph represents the mean of three alternating tests of Flatbuffers and Protobuf (no standard deviation is shown because we found that the three values did not differ significantly).
   197  - The horizontal axis represents the number of fields, with each element in the vector treated as a separate field covering all basic field types.
   198  - The left y-axis represents QPS, and the right y-axis represents the p99 latency at different field numbers.
   199  - From this table, it can be seen that when there are no map fields, flatbuffers' performance is better than protobuf when the total number of fields increases.
   200  - The reason flatbuffers' performance is worse when there are fewer fields is that the initial builder in flatbuffers initializes the byte slice size uniformly to 1024, so even with fewer fields, this large space still needs to be allocated (protobuf does not do this), resulting in worse performance. This can be alleviated by adjusting the initial byte slice size beforehand, but this would add some burden to business. Therefore, a uniform initial size of 1024 was set during load testing.
   201  
   202  ![performanceComparison2](/.resources/user_guide/server/flatbuffers/performanceComparison2.png)
   203  
   204  - Protobuf has poor performance in map serialization and deserialization, as seen in the graph.
   205  - Since flatbuffers does not have a map type, it uses a vector of key-value pairs as a replacement, with the key-value types consistent with the key-value types in protobuf's map.
   206  - As can be seen, when the number of fields increases, flatbuffers' performance improves more significantly.
   207  
   208  ![performanceComparison3](/.resources/user_guide/server/flatbuffers/performanceComparison3.png)
   209  
   210  - From the graph, it can be seen that when the total number of fields is high, flatbuffers' performance is better than protobuf, especially when a map is present.
   211  - The horizontal axis is the number of fields without map. For the line with map, each point corresponds to a larger horizontal axis value.
   212  - These field numbers correspond to packet sizes in ascending order:
   213  
   214  | Whether there is a map | Serialization method |  |  |  |  |  |  |
   215  | --- | --- | --- | --- | --- | --- | --- | --- |
   216  | N | flatbuffers | 284 | 708 | 1124 | 1964 | 3644 | 7243 |
   217  | N | protobuf | 167 | 519 | 871 | 1573 | 2973 | 5834 |
   218  | Y | flatbuffers | 292 | 1084 | 1900 | 3540 | 6819 | 13619 |
   219  | Y | protobuf | 167 | 659 | 1171 | 2192 | 4232 | 8494 |
   220  
   221  
   222  # FAQ
   223  ##  Q1: How to generate stub code when other files are included in the .fbs file?
   224  
   225  Refer to the following usage examples on https://github.com/trpc-group/trpc-cmdline/tree/main/testcase/flatbuffers:
   226  
   227  - 2-multi-fb-same-namespace: Multiple .fbs files with the same namespace are in the same directory (namespace in flatbuffers is equivalent to the package statement in protobuf), and one of the main files includes the other .fbs files.
   228  - 3-multi-fb-diff-namespace: Multiple .fbs files are in the same directory with different namespaces. For example, the main file defining RPC references types in different namespaces.
   229  - 4.1-multi-fb-same-namespace-diff-dir: Multiple .fbs files have the same namespace but are in different directories. The main file helloworld.fbs uses a relative path when including other files, and --fbsdir is not needed to specify the search path, as shown in run4.1.sh.
   230  - 4.2-multi-fb-same-namespace-diff-dir: Except that the include statement in helloworld.fbs uses only the file name, everything else is the same as 4.1. To run this example correctly, specify the search path using --fbsdir, as shown in run4.2.sh:
   231    ```sh
   232    trpc create --fbsdir testcase/flatbuffers/4.2-multi-fb-same-namespace-diff-dir/request \
   233              --fbsdir testcase/flatbuffers/4.2-multi-fb-same-namespace-diff-dir/response \
   234              --fbs testcase/flatbuffers/4.2-multi-fb-same-namespace-diff-dir/helloworld.fbs \
   235              -o out-4-2 \
   236              --mod github.com/testapp/testserver42
   237    ```
   238    Therefore, to simplify the command line parameters as much as possible, it is recommended to write the relative path of the file in the include statement (if they are not in the same folder).
   239  - 5-multi-fb-diff-gopkg: Multiple .fbs files with include relationships, and their go_package is different. Note: due to the limitation of flatc, it currently does not support two files with the same namespace but different go_package, and requires that the last segment of namespace and go_package in a file must be the same (for example, trpc.testapp.testserver and github.com/testapp/testserver have the same last segment "testserver").