• Btree并发内存回收


      在并发写Btree原理剖析 一文中提到,节点内存回收有可能导致内存突增以及影响写性能。本文将阐述最近对内存回收的改进,多线程可并行回收内存。

    回收策略

      采用基于版本的机制,Btree全局维护一个版本号V,一个全局数组A,每个线程一个槽位,每个线程读写操作之前获取全局版本号V,记作V0,并且记录到全局数组A中自己所属的槽位,表明对应线程不会再访问版本号V0之前的节点。写操作结束后,获取全局版本号V记录到这次写操作的TRecycleNode中,然后将V原子加1,这样来推进Btree的版本号递增,最后将这个TRecycleNode挂在线程局部的TRecycleNode的链表尾部。回收内存时,只需要遍历全局数组A,找到最小版本号记作minV,每个线程只回收线程局部的版本号小于minV的所有的TRecycleNode及其内部的TBtreeNode。可以看出,在目前这种回收策略中,不存在全局的TRecycleNode链表,线程不用抢锁,各自回收自己局部内存即可。

      这里有一个问题:如果某个线程由于某些原因,很少对Btree进行读写,那么全局数组A的最小值永远不变,导致其他线程也不能回收内存。针对这种情况,可以在每次读写操作结束后,将线程在全局数组中对应的槽位置为无穷大。

      这就带来了另外一个问题:一个线程刚拿到全局版本号V,记作V0,还没有来得及放入全局数组A对应槽位,随后一些写操作完成全局版本号V被递增,然后某个线程开始回收,扫描了全局数组并且已经计算出了最小版本V1,这时V1 > V0, 代表V0可以被回收,这时,第一个线程读V0就会出现问题。解决这个问题,可以使用类似于hazard pointer的机制,读写开始获取版本号和获取回收版本号的过程分别如下:

      读写开始拿可用版本号的过程:

         1. 拿全局版本号V放入局部变量V0:    V0 <- V

         2. 将V0写入全局数组中对应的槽位中:  A[slot] <- V0

         3. 如果 V0 = V 则可以开始安全的读写,版本号为V0,否则重试,跳第一步。

       写线程回收时拿到可回收版本号的过程:

         1.  拿全局版本号V放入局部变量V1:  V1 <- V

         2.  V2 <- min(A)

         3.  V3 <- min(V1,V2) V3之前的版本即为可以回收的版本

          证明:

          1.  如果V1 <= V0:则V3 = min(V1,V2) <= V1 <= V0,读写拿到的版本号V0 大于等于回收版本号

          2.  如果V1 > V0:如果读写过程的第三步成立V0 = V,那么,回收过程的第二步V2 <- min(A)计算最小值时已经看到了读写过程写入全局数组A中的版本V0,即V3 = min(V1,V2) <= V0

        可以看出,这种方式对于正常的Btree读写流程来说,可能第3步需要进行重试: 在第一步和第三步之间的最多的几十ns期间全局版本号V被其它写线程递增。当然,这种情况还算是比较少。为了进一步的避免这个问题,可以采用如下方法:

        维护一个全局可回收版本号recycleV,它比V小一些,比如100个版本,每个线程回收时只会回收小于recycleV的TRecycleNode,读写过程和回收过程算法如下:

       读写开始拿可用版本号的过程:

           1. 拿全局版本号V放入局部变量V0:    V0 <- V

           2. 将V0写入全局数组中对应的槽位中:  A[slot] <- V0

           3. 如果 V0 > recycleV 则可以开始安全的读写,版本号为V0,否则重试,跳第一步。

          写线程回收时拿到可回收版本号的过程:

           1. recycleV <- V – 100  多线程需要保证recycleV递增

           2. V2 <- min(A)

           3. V3 <- min(recycleV,V2)   小于V3的版本即为可以回收的版本

      这种优化相当于让回收线程滞后100个版本回收。

    还有其它一些小优化,比如全局数组A每个元素cache line对齐,避免false sharing,线程局部可用节点太多,归还一些到全局内存池等。

    分析

    从读写操作开始拿版本号的过程可以看出,实际上这种方法就是hazard  pointer !! 看这篇:lock free数据结构内存回收技术-hazard pointer 

    读写开始拿可用版本号的过程:

           1. 拿全局版本号V放入局部变量V0:    V0 <- V

           2. 将V0写入全局数组中对应的槽位中:  A[slot] <- V0  // 这一步相当于将pointer给保护起来

           3. 如果 V0 > recycleV 则可以开始安全的读写,版本号为V0,否则重试,跳第一步。 // 这一步相当于check一下上一步是否保护成功

    测试结果

    在2路超线程32core机器上,起32个线程,int64_t作为KV,TPS由原来的120W左右提升到240W左右,QPS由原来的680W左右提升到1900W左右。

  • 相关阅读:
    MySQL优化
    数据库之事务
    浮动与定位的区别
    CSS-画三角
    CSS(中)篇
    CSS(前)篇
    html篇
    定位真机运行能用但是打包成apk就不能用的解决方法
    定位与权限
    activity与fragment之间的传递数据
  • 原文地址:https://www.cnblogs.com/foxmailed/p/3505773.html
Copyright © 2020-2023  润新知