gh-ost使用问题记录

因为 pt-osc 对数据库性能影响较大,且容易造成死锁问题,目前我们在线更改表结构都使用 gh-ost 工具进行修改,这里记录一下使用 gh-ost 过程中的问题,以作记录;首先先复习一下gh-ost的基本实现,gh-ost的基本实现原理如下图所示:

gh-ost使用问题记录

根据源码,核心步骤如下:

  1. initiateStreaming: 初始化 binlog events streaming
  2. initiateApplier: 初始化 applier
  3. addDMLEventsListener: 添加对指定表的binlog event过滤
  4. ReadMigrationRangeValues: 获取对应表唯一索引的 min & max 值
  5. executeWriteFuncs: 通过applier向ghost表写入数据, binlog event 相比 copy rows具有更高优先级
  6. iterateChunks: 根据 min & max的值, 批量插入数据到 ghost 表
  7. cutOver: rename & drop新旧表

问题一:gh-ost导致最新一次写操作丢失

原因分析:

在 initiateStreaming 的过程中通过 show master status 获取主节点当前的 binlog name & pos & Executed_Gtid_Set,然后通过 binlog name & pos 和当前的数据库节点建立复制通道,而后在 ReadMigrationRangeValues 的过程中通过 select min(unique_key) 和 select max(unique_key) 快照读的方式获取原表数据的范围。

问题就出在这里,根据事务的提交流程,如果sync_binlog != 1,那么 binlog name & pos 是在binlog flush阶段之后进行更新;如果 sync_binlog = 1,那么 binlog name & pos 是在 binlog sync 阶段之后进行更新,这时事务还没有在 Innodb 中完成 commit。因此,最新的一次事务对于 select min() & max() 这样的快照读是不可见的,最终造成了写操作的丢失。

如何修复:

这里有两种解决办法:1. 虽然 binlog name & pos 的信息是在 Innodb memory commit 之前进行更新,但是show master status 的 Executed_Gtid_Set 是在 Innodb memory commit 完成之后进行更新的,因此 gh-ost 可以使用 Executed_Gtid_Set 来与数据库节点建立复制通道来解决这个问题。

  1. 在 ReadMigrationRangeValues 的过程中使用 select min() & max() lock in share mode 当前读来解决这个问题;

问题二:添加唯一索引时有可能导致数据丢失:

在使用 gh-ost 做 “add column field1 int not null, add unique index uniq_idx_field1(field1)” 或 “add column field1 int not null default 0, add unique index uniq_idx_field1(field1)” 这样的操作时,会导致整张表只剩下一条数据;

在执行 “add unique index uniq_idx_field1(field1)” 这样的操作时,如果表中的 field1字段存在重复数据,会导致第 2~n 条重复数据丢失。

相比之下,pt-osc 工具提供的 –check-unique-key-change 参数可以在出现以上情况时进行 warning 退出,有补救的可能。

问题三:高并发写入时gh-ost无法结束:

gh-ost使用问题记录

如截图所示,Applied一直在增大,而 Copy保持不变。这是因为在通过 Applier 向 ghost 表中写数据时,binlog events apply 相比rows copy 具有更高的优先级;同时,由于 gh-ost 是通过监听 mysql binlog的方式获取增量写操作,对源MySQL节点的侵入较小;因此,在MySQL实例高并发写入时,gh-ost会忙于 apply binlog events而无法结束。

扩展一:gh-ost的cutover过程:

gh-ost使用问题记录

如上图所示,根据gh-ost -cut-over参数的不同会选择不同的 cur-over 算法,默认是github的 atomic算法,也可以选择 facebook的 OSC算法。

atomic算法// atomicCutOver

gh-ost使用问题记录

这里为什么需要 magic_old表呢?

是为了防止lockSessionId被意外关闭后,可以阻塞rename操作。lockSessionId被意外关闭后,original table可以被写入,会造成数据不一致。至于为何要对 magic_old表加锁,我个人认为是防止magic_old表被意外删除。

源码实现如下:

go;gutter:true; func (this *Migrator) atomicCutOver() (err error) {      // 设置 cutover 标记, 限流函数会使用此标记 atomic.StoreInt64(&this.migrationContext.InCutOverCriticalSectionFlag, 1) defer atomic.StoreInt64(&this.migrationContext.InCutOverCriticalSectionFlag, 0)      // 通知释放锁的通道 okToUnlockTable := make(chan bool, 4)      // 最后删除 magic old table defer func() { okToUnlockTable RENAME released -> queries on original are unblocked.</p> <pre><code> // 阻塞等待锁被释放 if err := </code></pre> <pre><code> facebook OSC算法// cutOverTwoStep cutOverTwoStep很巧妙地利用MySQL不同 session 下 alter table x rename to x1; 和 rename table x1 to x; 不同的锁机制进行 cutover,值得深入研究。 ![gh-ost使用问题记录](https://johngo-pic.oss-cn-beijing.aliyuncs.com/articles/20230605/727246-20220330114826899-1784650786.png) 源码如下: ;gutter:true;
/*
* cutOverTwoStep() 将阻塞原始表,等待原始表上的binlog events全部应用到 ghost 表,然后进行非原子的表rename操作,original->old, then new->original;
* 在rename过程中,原始表不存在,查询操作将失败。
*/
func (this *Migrator) cutOverTwoStep() (err error) {
atomic.StoreInt64(&this.migrationContext.InCutOverCriticalSectionFlag, 1)
defer atomic.StoreInt64(&this.migrationContext.InCutOverCriticalSectionFlag, 0)
atomic.StoreInt64(&this.migrationContext.AllEventsUpToLockProcessedInjectedFlag, 0)
     // 首先通过 lock tables write; 对原始表加锁
if err := this.retryOperation(this.applier.LockOriginalTable); err != nil {
return err
}
// 等待原始表上的 binlog events 全部应用到 ghost 表
if err := this.retryOperation(this.waitForEventsUpToLock); err != nil {
return err
}
     // 1. 这里使用和 LockOriginalTable 操作相同的session来执行 alter original_table rename magic_old_table; 来重
// 命名原表,该操作不会被 LockOriginalTable 操作加的锁阻塞,相同的 session 执行 rename table original to magic 将阻塞。
// 2. 将 ghost 表重命名为 original 表; 这里采用在另一个 session 上执行 rename table ghost to original;来进行,因此在
// 原 session 上执行将被锁阻塞。
if err := this.retryOperation(this.applier.SwapTablesQuickAndBumpy); err != nil {
return err
}
// 对原表解锁
if err := this.retryOperation(this.applier.UnlockTables); err != nil {
return err
}

lockAndRenameDuration := this.migrationContext.RenameTablesEndTime.Sub(this.migrationContext.LockTablesStartTime)
renameDuration := this.migrationContext.RenameTablesEndTime.Sub(this.migrationContext.RenameTablesStartTime)
log.Debugf("Lock & rename duration: %s (rename only: %s). During this time, queries on %s were locked or failing", lockAndRenameDuration, renameDuration, sql.EscapeName(this.migrationContext.OriginalTableName))
return nil
}

Original: https://www.cnblogs.com/juanmaofeifei/p/16070998.html
Author: 卷毛狒狒
Title: gh-ost使用问题记录

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/590943/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球