基础知识
-
pg中b+树索引的节点,保留了向左,向右,向下的索引
-
每个节点是一个页
-
每个节点的第一个key是highkey( 表示此节点或者此节点的子节点的最大值 ),除了每层最右节点(没有highkey,第一个key用firstkey表示)
-
索引的第0页是元数据页
-
pg的btree 不限制一页的索引数量,但限制每条索引的长度,这是为了支持可变长度变量而做的考虑。
-
为了平衡每页的占用量,pg在计算页占用时会把要插入的索引的长度也算进去,以避免可能找到了合适的插入页的位置,但这页的空间不够这个索引插入的尴尬
-
pg没有实现在节点从超过半满到降回半满时的merge操作,因为太复杂了
对于相同key的处理
- 当要插入一个索引,发现key与这页(叶子节点)的 highkey 一样,这时要看下一页(右叶子节点)与这一页哪页的空闲位置多,就插在哪一页
btree上的锁
- pg的加的锁是页级的读锁,这会影响其它进程写的效率,因而当扫描到要的索引后,会先把这些索引对应的id拷贝一份,然后释放锁,不必一直持有锁,增加效率、
- pg为了支持后向扫描而加入了左 sibling 的指针,因而在节点分裂时,相比不需要维护左指针的b树而言,需要额外锁 右sibling 的节点,以防返向读的那个进程能访问到这个正在分裂的页。
- 在顺序扫描叶子节点时要对左叶子节点加pin,以防止左叶子节点被删除,而使后向扫描失败。
- 同样由于支持后向扫描,在做后向扫描时要先找到左sibling,加锁,这样可以防止左节点分裂。
- 分裂的存在使得,扫描右页时检查一下当前页是不是分裂了一半的状态,如果是说明父节点的downlink还没有指向这个右页就挂了,这时要去重新加上父节点向右页的downlink。
- 当向左扫描一页时,是扫描到边界时才去锁左页,过早锁页可能因左页分裂而遗漏一些元组
BTree节点的分裂
-
一般节点:分裂时,先标记自己为half-dead,创建新左右节点,把原节点的一半复制到左节点,一半到右节点上,再链接进原有的链中。第二步要建立父节点到右节点的downlink,一切结束后才把这个half-dead标记清除(相当于是一个原子操作的标记,如果这两步中间挂了,下一次恢复后,扫描一页发现是half-dead,则要去补充父节点到右节点的downlink,为什么在扫描时而不在recovery的vacuum扫描阶段呢:最后中途崩溃的恢复有说明)
-
在顺序扫描叶子节点时,pg 会记住右节点的页号,如果当前页扫描完发现下一页的页号变了,说明当前页可能发生了分裂,要跳过下一页的一些元组以防止重复扫描。
-
根的处理有一点点不同:先分裂出右页,再创建根,最后将元数据的pointer指向新根(元数据要最后更新)
BTree节点的删除
-
vacuum在回归元组空间时会先删除索引,后删除元组,可能存在正有进程访问索引对应的元组,回来发现自己的索引没了,为了避免这种冲突,pg引入super-exclusive 锁,要求上锁的页不能有 pin。
-
由于最右节点不会删除,树的高度不会减少:标记最低的那个根为快根,每次分裂和删除时要检查和更新快根
-
删除叶子页有两步:
-
从parent上移除指向自己的downlink,标记自己为half-dead(扫描遇到half-dead页时会跳过它)
-
锁定父节点和自己
-
使自己的downlink指向右sibling,相当于变成了一个中间页,没有downlink指向自己,而自己的downlink指向右兄弟节点(这里相当于一个trick,当它与右节点不能合并时可以保证顺序扫描依然能进行)
-
如果它与右sibling节点有同一个parent,就将它与右节点合并,更新highkey和父节点的downlink。如果不是,考虑到非叶子节点的合并不是原子操作,因而不去做合并。(这样的结果是,如果父节点有多于一个的子节点,那么这个最右节点不会合并范围,直到只剩它自己时再,删除父节点,过程见下面。)
-
-
与右节点合并
- 上锁:左sibling,自己,右sibling
- 更新 sidelink
- 标记自己为删除(之后vacuum时会被放在FSM中)
-
- 当孩子节点只剩一个时,删除最后一个孩子节点:
- 断开父与爷之间downlink
- 如果父也是爷的惟一的孩子,则向上找有多于一个孩子的父节点,移除downlink
- 把自己标记为half-dead
- 从上向下,将每一层中节点与左右sibling之间的 link 删除
向左(逆序)扫描算法
- 先向左,如果左节点是活的,那就是它
- 否则这个左节点可能分裂了,从这个左节点开始向右找自己的邻接左节点
- 找到左节点后判断自己是不是还活着,如果dead了,就向右移动至一个活节点,重复第一步
空间的回收 vacuum
- 空间回收使用的是最强最简单的条件:没有快照引用这个页了
- 回收的页会放在FSM中
- 回收是按照页号进行顺序扫描的,当发现有一个页在回收开始后分裂了,且分裂的右节点比当前页号小,为了覆盖的完全,需要先去回收那个分裂的小页号的页,再接着回收当前页
- 具体实现:在扫一页前,要判断是否这个叶在本次vacuum进行时分裂了(即页上标记的分裂时 cycle id 是本次vacuum的cycle id,如果分裂则要看看它的右页页号是不是小页号,如果是,则这个右页没被vacuum扫描过,就要先扫描右页,然后才回到这个页号,按页号继续向前。
一致性的保证(WAL日志)
- 写 write ahead log 日志的时间点有:
- 插入索引
- 分裂一个节点
- 生成新的根
- 叶节点标记half-dead并移除downlink
- 移除左右 sibling 向自己的 link
中途崩溃的恢复
-
在插入时发现某个节点标记正在分裂,说明还没有建立 父节点的 downlink,重新建立
-
之所以选择在插入时而不在vacuum扫描时重新建立downlink,是因为建立downlink时有可能导致分裂页,需要占用磁盘,而之前崩溃有可能就是因为磁盘满了才导致的,将会使vacuum失败,磁盘永远满下去。