PolarDB 物理复制刷脏约束问题和解决
目前物理复制到了 ro 开始刷 120s apply_lsn 不推进的信息以后, 即使压力停下来也无法恢复, 为什么?
如下图所示:
这里最极端的场景是如果 rw 上面最老的 page1, 也就是在 flush list 上根据 oldest_modification_lsn 排在最老的位置 page_lsn 已经大于 ro 上面的 apply_lsn 了, 那么刷脏是无法进行的, 因为物理复制需要保证 page 已经被解析到 ro parse buffer 才可以进行刷脏. 另外想 Page2 这样的 Page 虽然 newest_modification 和 oldest_modification 没有差很多也无法进行刷脏了. 因为 Parse buffer 已经满了.
但是这个时候 ro 节点的 apply_lsn 已经不推进了, 因为上面的 parse buffer 已经满了, parse buffer 推进需要等 rw 节点把老的 page 刷下去, 老的 parse buffer chunk 才可以释放. 但是由于上面 rw 节点已经最老的 page 都无法刷脏, 那么 parse buffer chunk 肯定就没机会释放了.
那么此时就形成了死循环了. 即使写入压力停下来, ro 也是无法恢复的.
所以只要 rw 上面最老 page 超过了 parse buffer 的大小, 也就是最老 page newest_modification_page lsn > ro apply_lsn 之时, 那么死锁就已经形成, 后续都无法避免了
这里 copy_page 为何没有生效?
目前 copy_page 的机制是刷脏的时候进行的, 在下图中 copy page copy 出来的 page newest_modification 也是大于 ro apply_lsn 的, 所以也是无法刷脏的, 所以这个时候其实这个 copy_page 机制是无效的机制.
正确的做法是: 在发现 Page newest_modification 有可能超过一定的大小, 那么就应该让该 page 进行 copy page 强行刷脏, 否则到后面在进行刷脏就来不及了.
开启了多版本 LogIndex 版本为什么可以规避这个问题?
在因为 parse buffer 满导致的刷脏约束中, 如上图所示, Page1, Page2 无法进行刷脏, 但是其他的 Page 如果 newest_modification < ro apply_lsn 是可以刷脏的, 因此 rw 节点 buffer pool 里面脏页其实不多.
开启了 LogIndex 以后, ro 就可以随意丢弃自己的 parse buffer 了, 当然也就不会 crash.
但是依然有一个问题是如果 Page1 一直修改, 这个 Page1 的 newest_modification lsn 一直在更新, 那么即使开启 LogIndex 也无法将该 Page 刷下去, 带来的问题是 rw checkpoint 是无法推进, 但是由于有了 LogIndex, 其他 page 可以随意刷脏, 所以不会出现 rw 脏页数不够的问题. 那 Page1 刷脏如何解决呢?
通过 copy page 解决.
如果 rw 开启了 copy page 以后, 虽然上图中的 Page1 刚刚被 copy 出来的时候无法 flush, 但是因为开启 LogIndex, ro apply_lsn 可以随意推进, 随着 ro apply_lsn 的推进, 过一段时间一定可以刷这个 copy page, 也就避免了这个问题了.
所以目前版本答案是 LogIndex + copy page 解决了几乎所有问题
另外验证了刷脏约束两种场景
大量写入场景
有热点页场景
其实大量写入场景即使导致了刷脏约束, 后面还是可以恢复的, 只有热点页场景才无法恢复. 很多时候热点页不一定是用户修改的 page, 而是 Btree 上面的一些其他 page, 比如 root page 等等, 我们很难发现的.
另外验证了如果 page 以及 redo log 写入延迟都升高, 是不会特别出现刷脏约束问题, 只有出现热点页的场景才会有问题.
上图可以看到
ro parse buffer = ro appply_lsn - rw flush_lsn
apply_lsn 是 ro 节点读取 redo 并应用推进的速度
flush_lsn 是 rw 节点 page 刷脏推进的速度
由于 IO 延迟同时影响了 redo 和 page, 从公式可以看到, 那么 ro parse buffer 不会快速增长的.
从公式里面可以看到, 如果 redo 推进速度加快, page 刷脏速度减慢, 那么是最容易出现刷脏约束的. 也就是 redo IO 速度不变, Page IO 速度变慢, 就容易出现把 RO parse buffer 打满的情况, 但是一样需要出现热点页才能出现 parse buffer 被打满的死锁.
如果没有热点页, 这个时候由于 parse buffer 还是再推进, 所以不会自动 crash, 反而会出现 rw 由于被限制了刷脏, buffer pool 里面大量的脏页, 最后找不到空闲 Page 的情况. rw crash 的情况.
多版本或者 Aurora 如何解决这个问题?
刚才上面的分析有两个链条互相依赖
约束 1: rw 的刷脏依赖 ro 节点 apply_lsn 的推进
约束 2: ro 节点释放 old parse buffer 依赖 rw 节点刷脏
多版本和 Aurora 都把约束 2 给去掉了, ro 节点可以随意释放 old parse buffer. 那么就不会有 parse buffer 满的问题, 那么如果 ro 节点访问到 rw 还未刷下去 page, 但是 ro 节点已经把 Parse buffer 释放了, 那么会通过磁盘上的 logIndex + 磁盘上 page 生成想要的版本.
但是这里依然还要去解决约束 1 的问题, rw 的刷脏会被 ro 给限制. PolarDB rw 刷脏的时候需要判断 page newest_modification_lsn > ro apply_lsn, 才可以进行刷脏.
在 Aurora 里面这种情况的行为是 Page 在 Page Server 上无法进行 Page Apply. 但是 Aurora 和 PolarDB 区别在于 Aurora 可以把这个 Page 丢出 buffer pool, 需要访问的时候通过 Old Page + LogIndex 去获得指定版本的 Page.
目前对于热点页场景 PolarDB 已经通过 Copy Page 机制去规避这种场景, 也就是 page 的 newest_modification_lsn 在某一时刻可以 copy 出来, 不再增长, 那么随着 RO apply_lsn 的增长, 总是会超过 RO apply_lsn 的.
但是这种场景唯一存在缺陷的情况是, 如果 RO 节点 Hang 住了, 那么这个时候 RO apply_lsn 就不会增长, 那么 Copy Page 也就没有任何效果了, 那么就 RW 就无法刷脏, 就是出现 RW 自己 crash 了. 这个时候 PolarDB 通过叫 Delay flush(LogIndex +Old Page 读取)机制, 去解决这个常见的问题.
PolarDB 和 Aurora 类似, 把 dirty page 丢出 Buffer Pool, 访问的时候和 RO 节点类似的方法通过 LogIndex + Old Page 进行访问, 但是这样会造成访问性能急剧下降,Checkpoint 无法推进等等一系列问题, 所以目前这个策略在 PolarDB 上还没有默认打开.
超过一定时间以后, PolarDB 和 Aurora 都一样, 认为只读节点延迟太大, 将这个只读节点 kickout.
版权声明: 本文为 InfoQ 作者【ba0tiao】的原创文章。
原文链接:【http://xie.infoq.cn/article/532139ce36717646481a67acb】。文章转载请联系作者。
评论