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").