0.12 wal的写入与flush流程

详见WAL 0.11 和 0.12 版本对比

通过对现有wal写入与flush流程的分析,我们发现两个潜在的问题

  • 因为wal在刷盘时使用FileChannel,所以会涉及多一次的内存拷贝,FileChannel在调用native的write方法前,会将HeapByteBuffer的内容拷贝到DirectByteBuffer中,然后将DirectByteBuffer作为参数调用native的write,这里会导致一次多余的内存拷贝操作。
  • 虽然0.12使用了线程池异步刷盘,在一定程度上解决了0.11的wal导致的堆外内存激增的问题,但是使用线程池,依旧将堆外内存何时释放交给了jvm(依赖于jvm的gc何时进行),jvm无法得知哪些堆外内存应该及时被释放,所以也可能会导致堆外内存释放不及时而我们是清楚的。


构建堆外内存池,解决上述两个问题

  • 使用堆外内存后,wal在写入时,就直接序列化到DirectByteBuffer中,而不是HeapByteBuffer,所以FileChannel在写入时,判断是DirectByteBuffer,就无需多做一个内存拷贝了。
  • 构建堆外内存池后,我们就掌握了何时创建、何时销毁堆外内存的主动权,无需依赖jvm的gc去清理堆外内存。


具体设计

堆外内存池的设计原则有如下四点

  • 为了减少不同线程对堆外内存池同时申请时的竞态,我们将池的粒度放在storage group内部,也就是每个storage group维护自己的堆外内存池
  • 为了减少不必要的堆外内存创建,我们采用懒加载的方式,只有真正需要的时候,才会去创建新的堆外内存
  • 偶发乱序数据可能会导致堆外内存池扩张,但是扩张后,等待乱序数据落盘后,扩张的堆外内存却可能很长时间都不会被用到,我们需要定时去检查非活跃的堆外内存,并及时释放
  • 堆外内存池的大小不能无限扩张,需要对每个storage group的总大小做限制,达到限制后,后续的申请需要阻塞。

设计实现

在StorageGroupProcessor中维护一个双向链表作为pool,pool最大可分配数量等于 允许最大的可并行写入的分区数量 * 4(4表示,顺序2个wal buffer,乱序2个wal buffer)

  • 申请时(对应StorageGroupProcessor.getWalDirectByteBuffer())
    1. 若pool中有空闲的堆外内存,则从队首pop出两个;
    2. 若pool为空,并且当前已分配的DirectByteBuffer数量已大于最大的可分配数量则阻塞等待;
    3. 若pool为空,但是当前已分配的DirectByteBuffer数量已小于最大的可分配数量则创建新的两个DirectByteBuffer,并将已分配数量+2
  • 归还时,则将堆外内存放入队尾。(对应StorageGroupProcessor.releaseWalBuffer())

同时,在StorageGroup被创建时,启动后台定时线程去检查非活跃的堆外内存,并及时释放(对应StorageGroupProcessor.trimTask())

  • 我们期望pool中只保留活跃的DirectByteBuffer,而活跃的DirectByteBuffer数量应该等于 (当前活跃的顺序分区数量 + 当前活跃的乱序分区数量) * 2
  • 如果定时线程发现,当前已分配数量大于上面计算出来期待的活跃数量,并且pool不为空,则不断移除pool中的DirectByteBuffer(每次移除时,需要将已分配数量同时减少),直到pool为空,或者已分配数量等于期待的活跃数量


理论分析

顺序文件与乱序文件刷盘的机理并无差别,所以下面的分析考虑只有顺序写入,乱序写入无非就是将所有分析结果乘以2.

符号定义

假设关闭一个tsfile需要耗时T_closing, 从打开一个新的tsfile开始写入到这个tsfile开始关闭耗时T_writing.

下面考虑两种情况

T_closing <= T_writing

这种情况下,新的tsfile需要关闭前,前一个tsfile就已经关闭结束,前一个tsfile占用的wal buffer已经归还到pool中,下一次写入无需阻塞等待。

所以这种情况,只需要2个buffer即可。

如果只提供1个buffer,那么在前一个tsfile关闭的过程中,新的写入就需要阻塞等待T_closing这么长时间。即每次tsfile close时,写入都会被阻塞T_closing

T_closing > T_writing

这种情况下,tsfile的关闭耗时大于新的tsfile写满的耗时,这样就会导致系统内部会有多于1个正在closing的tsfile,如果要保证写入不会阻塞,则必须保证系统为每个SG预留了足够多的wal buffer,即wal buffer的数量要大于等于系统内最大可能同时存在的closing tsfile数量。

系统内最大可能同时存在的closing tsfile数量 = (T_closing - T_writing) / T_writing + 1

结论

如果要保证写入不会被阻塞,只考虑顺序写入的情况下,max_wal_bytebuffer_num_for_each_partition应该设置为

max_wal_bytebuffer_num_for_each_partition  = Math.max(0, (T_closing - T_writing) / T_writing) + 2

  • No labels