因为 pt-osc 对数据库性能影响较大,且容易造成死锁问题,目前我们在线更改表结构都使用 gh-ost 工具进行修改,这里记录一下使用 gh-ost 过程中的问题,以作记录;首先先复习一下gh-ost的基本实现,gh-ost的基本实现原理如下图所示:
根据源码,核心步骤如下:
- initiateStreaming: 初始化 binlog events streaming
- initiateApplier: 初始化 applier
- addDMLEventsListener: 添加对指定表的binlog event过滤
- ReadMigrationRangeValues: 获取对应表唯一索引的 min & max 值
- executeWriteFuncs: 通过applier向ghost表写入数据, binlog event 相比 copy rows具有更高优先级
- iterateChunks: 根据 min & max的值, 批量插入数据到 ghost 表
- 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 来与数据库节点建立复制通道来解决这个问题。
- 在 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无法结束:
如截图所示,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 -cut-over参数的不同会选择不同的 cur-over 算法,默认是github的 atomic算法,也可以选择 facebook的 OSC算法。
atomic算法// atomicCutOver
这里为什么需要 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/
转载文章受原作者版权保护。转载请注明原作者出处!