Q1ngbo commented on PR #3196:
URL: https://github.com/apache/brpc/pull/3196#issuecomment-3753397291
为了便于理解,我想对flatbuffers构造消息的基本流程进行简要补充说明,该示例程序的Scheme文件(类似于.proto文件)如下。
```c++
table EchoRequest {
opcode:int;
attachment:int;
}
```
Message需要通过MessageBuilder创建(定义在flatbuffers_impl.h中),该类继承了::flatbuffers::FlatBufferBuilder,它才是实际起作用的类,MessageBuilder的主要作用是向它传递自定义的内存分配器(SlabAllocator)。应用在创建message时,需要先定义一个MessageBuilder,然后基于它来添加元素,最终调用ReleaseMessage方法来获取完整消息,以下是一段示例代码。
```c++
brpc::flatbuffers::MessageBuilder mb;
auto message = mb.CreateString(g_request);
auto req = test::CreateBenchmarkRequest(mb, 123, 333, 1111, 2222, 0,
message);
mb.Finish(req);
brpc::flatbuffers::Message request = mb.ReleaseMessage();
```
在构造message过程中,flatbuffers会使用一段连续buffer存储数据和metadata,在代码里,该buffer使用vector_downward表示,由FlatBufferBuilder管理。该Buffer维护了scratch_和cur_两个指针。在初次向buffer中添加数据时,会申请max(需要长度,1024)字节的内存,之后会使用cur_将数据添加在buffer末尾,将偏移量信息(FieldLoc)添加在buffer起始位置,如下图所示。如果在添加过程中buffer不够了,则会重新分配一个足够大的buffer,并将数据按序拷贝过去,所以或许可以配置一个GFlag来调整初次分配buffer的大小(**TODO**)。
<img width="1024" height="323" alt="image"
src="https://github.com/user-attachments/assets/045a3fdb-ffb5-42f1-b736-bc3ceb375faf"
/>
brpc只需要定义好内存分配器SlabAllocator,在添加元素的过程中,当内存不足时flatbuffers会调用allocate或reallocate_downward,这实际上会调用SingleIOBuf中的相关方法。我们发现将rpc请求的header部分与message连续存储可获得明显性能提升。因此,在申请buffer时我们在Buffer前面预留出了一部分空间,其大小由宏DEFAULT_RESERVE_SIZE指定,默认为64B(也可通过GFlag配置,TODO)。这部分内存的消费者是brpc中的协议处理逻辑,在创建message过程中不会被使用到。
在完成数据添加后,应用需调用Finish方法完成创建,这一步flatbuffers会创建vtable,其主要执行逻辑为:1.
根据data大小为vtable申请空间;2. 从buffer起始处取出FieldLoc,将实际数据相对于"Table
Data"起始地址的偏移量写入到vtable中;3. 在table data部分开头记录该位置相对于vtable开头的偏移量;4.
填充vtable中的vtable总长度、data总长度;5. 将"Table
Data"相对于有效起始地址的偏移写入开头。vtable不是从buffer起始地址、而是从cur_左侧构建的,所以vtable和table
data仍然是连续的,vtable左侧多余空间会被释放掉。以上面提到的EchoRequest为例,最终创建的完整内存布局如下所示(图中没画出为header部分预留的空间):
<img width="1024" height="373" alt="image"
src="https://github.com/user-attachments/assets/0842827d-e659-4f15-b283-6e07de2f3980"
/>
应用调用ReleaseMessage所获取到的Message中包含了该连续buffer对应的SingleIOBuf,该函数的执行逻辑为:1.
从vector_downward(buffer)中获取前面构造的msg地址;2.
从SingleIOBuf中获取分配该buffer所使用的block起始地址;3. 计算msg在block中的偏移,基于它创建一个BlockRef;4.
通过BlockRef创建SingleIOBuf,它会作为Message的一个成员变量。至此,创建message的过程就结束了。
flatbuffers读取消息的本质是进行间接寻址。参考上图,当读取EchoRequest中的opcode时,首先从buffer起始地址出读取Root
Offset,获取Table Data相对于buffer起始地址的偏移量;然后从Table
Data起始处获取Vtable的相对偏移量,再次跳转;然后从预先生成的FlatBuffersVTableOffset中获取opcode在vtable中的位置,访问它获取实际偏移量;当字段合法时,通过(Table
Data + 偏移量)获取到实际值。该过程需要3次间接寻址。
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]