数据迁移不停服设计借鉴concurrentHashMap chm 和 redis 渐进式rehash

人人都是java并发专家_个人渣记录仅为自己搜索用的博客-CSDN博客

本文会介绍如下

   jvm的gc , redis的rehash (双写) , concurrentHashMap resize (加分区锁) , codis的rehash(加slot锁)

业务系统数据迁移核心是update (无状态迁移,双写)

业务模型的重构和模型改造. 层级关系. 新建字段. 原来的还是写,但是不再读,和作业务判断. 同样也是双写的机制. 另外开一张表不合适,状态什么的可以不用新改. 不然后续更新也要双更新.

传统部分业务机房迁移_个人渣记录仅为自己搜索用的博客-CSDN博客

创建具备迁移属性/多机房属性 的分布式id , zgc 和迁移

梳理要点

        上游是迁移方上游迁,不然自己迁移 . 只要是流量进来了都要判断是否迁移. 

        流量入口:

                mq的统计. 基于订阅的topic join topic 的发起方的系统 join 系统类型

         流量出口:

停机粒度

        必须是系统和库粒度 (zgc不适合模仿, 原因是zgc仅单个, 无批量查询引用). 不然会打破这个系统的事务型. 除非系统代码都是非事务型的. 1. 先收单,再定时任务重试.    2.所有的sql 必须要有唯一id . 其他都是拒绝,先通过es等查询,再更新.  不然就需要数据库能实现迁移的能力. 还能实现事务锁控制. 这点我感觉OB有这个能力. 利用OB的分布式事务能力来实现. 单库也不能太大, 不然全局事务会非常的大. ( 需要给出各个表的切割粒度最好有路由位1024.  切割事务全局量. 业务上感知路由标识, 能理解事务的分裂. 现在OB无) OB指定把哪些数据 迁移到指定机房的数据库上. 确保事务型. 会导致事务变长, 资源消耗增加. 所有表都是Uid的分表位,统一路由规则.

        类似分布式事务,分布式事务框架一样.

改造点

  1. 系统一拆为二, 系统名,系统所在域都不同了. 

        回调相关/mq相关

          回调的应用如果根据pdCode / bizSceneId 配置, 改造成通过上下文自行获取到系统名称. 寻找回调地址. 调用外国是外部域, 就回调sofaGw.包括MQ也是, 跨域复制. 不能多复制过去. 新的系统接收方有业务字段上能区分过滤mq. 接受方可以通过业务字段来过滤. 但是回调方, 要通用,不感知业务. 感知系统 ,可以做到无缝对接, 像之前mq还需要根据业务字段来筛选,就很奇怪. (创建卡, 卡入金,发消息, 要给谁回调, 就需要感知业务字段, 不然真的不知道这个卡是哪个系统开的.  根据pdCode来区分回调哪个业务. 不同的业务点,粒度配置不同的字典表. 你有了最细粒度的业务标识了.  最好是配置回调系统. 迁移的时候, 老系统会自己迁移到新系统 ,mq配置不变. 等全部流量迁移掉后. 不太可能. 还是要改mq配置. 对不迁移的系统有侵入 ) 边界是什么很重要, 抽象,打交道的概念到底是什么.

  2. 系统一拆为二. 数据一拆为二.

不停服迁移的基本要求:

1.可遍历. 

   1.按顺序遍历

    2.不按顺序遍历

2.可拆解

 两种模式.

  1. 有状态.记录哪些迁移,哪些没迁移

  2.无状态, insert 单向. 双读,双更新.

选择后种.

*jvm的gc

  整体迁移时的diff log. 保存更改的log. stop the world. 迁移,切换.

  改进点: 将整体迁移改成逐步迁移, 实现方案是 利用hash 后获得slot 加锁迁移,, 逐步迁移. 缺点: 降低吞吐量.

* hashmap 的 rehash

   会修改原 entry 的实体, 调换链表顺序, 故并发下会死循环(1.7和1.8的 resize 方案不一致). 遍历操作会 fail-over .resize 关注的是槽位,而不是已插入的节点数.

* concurrentHashMap的 resize transfer

   1.迁移的时候每个节点加锁, 进行迁移 , 头尾互换  2.在整个table数组更换期间, 同时将原节点的查询和insert更换跳转到新的table数组上来. 

   所以分布式不停服迁移,也需要如此,对原有的写加锁,  基于某种规则,加分区锁, 遍历该分区锁下的所有数据,转移. 修改该分区锁的写/查逻辑,跳转到新的地址上去.

*redis的rehash

  1. 单线程,无锁.增删改查,rehash迁移线程.

  2. 修改增删改查逻辑. 当迁移时需要先迁移,再进行操作. 最小粒度原则.

  3. 遍历所有的key.(通过hash槽位遍历)

  redis的rehash,利用了双hash表。属于迁移问题.

  rehash阶段的读操作 http://www.th7.cn/db/nosql/201602/176178.shtml  rehash阶段改变了原来的读操作,含写。

 insert 单插, 改删,双写 ,读双读.

 *codis的rehash

    1. 引入slot的概念,分层, zookeeper只记录slot集合信息(slot量可控),  如何遍历一个slot下所有的key ,一个key属于哪个slot有谁来记录?

     Slot模型层,代码位置在pkg/models/slot.go。

   原生Redis中并没有Slot这个概念,也就是说:虽然我们给Key分配好了Slot,但是一旦存入Redis后Key属于哪个Slot这个信息就丢失了。解决的方法有很多种,比如:

  • 1)在ZooKeeper中保存Key与Slot的对应关系,需要时就查询一下。这种方式类似HDFS中的NameNode,缺点是Key很多时会占用很多空间。
  • 2)数据迁移时遍历所有Key,用哈希函数现去算一下Key所属的Slot是否要迁移,缺点是迁移时计算量比较大,而且每个Slot迁移时都要去算可能有很多重复的计算量。
  • 3)保存到Redis之前在Key或Value中加入一些隐含信息,缺点是会改变业务的数据。
  • 4)修改Redis源码加入Slot的概念,在Redis中保存Key属于的Slot,并提供基于Slot的Migrate原子操作。Codis采取的是这种做法,缺点就是要修改Redis源码,以后升级Redis比较麻烦,尤其像Codis没有将改动封装到一个动态链接库则可能更为麻烦。
  • 5)利用Redis中database的概念替代Slot,GitHub上的xcodis采取的就是这种思想。缺点是每个Slot对应的Redis连接在使用前都要select到对应的数据库,否则就会修改到其他Slot的数据。

2.对于迁移中的Slot,如果恰好此时有客户端要访问该Slot中的某个Key该怎么办?Codis不是遇到这个问题的第一个中间件,像Taobao Tair (和jvm类似)中也有对此的解决方案:

发生迁移的时候data server如何对外提供服务?
当迁移发生的时候, 我们举个例子, 假设data server A 要把 桶 3,4,5 迁移给data server B. 因为迁移完成前, 客户端的路由表没有变化, 客户端对 3, 4, 5 的访问请求都会路由到A. 现在假设 3还没迁移, 4 正在迁移中, 5已经迁移完成. 那么如果是对3的访问, 则没什么特别, 跟以前一样. 如果是对5的访问, 则A会把该请求转发给B,并且将B的返回结果返回给客户, 如果是对4的访问, 在A处理, 同时如果是对4的修改操作, 会记录修改log.当桶4迁移完成的时候, 还要把log发送到B, 在B上应用这些log. 最终A B上对于桶4来说, 数据完全一致才是真正的迁移完成

    但Codis采取的是不同的策略。当迁移过程中发生数据访问时,Proxy会发送”slotsmgrttagone”迁移命令给Redis,强制将客户端要访问的Key立刻迁移,然后再处理客户端的请求。(ps: 需记录某个key是否已经迁移(基于slot两级隔离, 所以这个key总体不会太大). 可不用加锁, 大不了重复迁移,ps: 每个redis实例需要记录某个key是否已迁移(codis改造部分))

func (m *CodisSlotMigrator) Migrate(slot *models.Slot, fromGroup, toGroup int, task *MigrateTask, onProgress func(SlotMigrateProgress)) (err error) {
    groupFrom, err := models.GetGroup(task.zkConn, task.productName, fromGroup)
    groupTo, err := models.GetGroup(task.zkConn, task.productName, toGroup)
    fromMaster, err := groupFrom.Master(task.zkConn)
    toMaster, err := groupTo.Master(task.zkConn)

    c, err := redis.Dial("tcp", fromMaster.Addr)
    defer c.Close()

    _, remain, err := sendRedisMigrateCmd(c, slot.Id, toMaster.Addr)

    for remain > 0 {
        if task.Delay > 0 {
            time.Sleep(time.Duration(task.Delay) * time.Millisecond)
        }

        _, remain, err = sendRedisMigrateCmd(c, slot.Id, toMaster.Addr)
        if remain >= 0 {
            onProgress(SlotMigrateProgress{
                SlotId:    slot.Id,
                FromGroup: fromGroup,
                ToGroup:   toGroup,
                Remain:    remain,
            })
        }
    }
    return nil
}

func sendRedisMigrateCmd(c redis.Conn, slotId int, toAddr string) (int, int, error) {
    addrParts := strings.Split(toAddr, ":")
    if len(addrParts) != 2 {
        return -1, -1, ErrInvalidAddr
    }

    reply, err := redis.Values(c.Do("SLOTSMGRTTAGSLOT", addrParts[0], addrParts[1], MIGRATE_TIMEOUT, slotId))
    if err != nil {
        return -1, -1, err
    }

    var succ, remain int
    if _, err := redis.Scan(reply, &succ, &remain); err != nil {
        return -1, -1, err
    }
    return succ, remain, nil
}

* hashmap 的 rehash 会修改原 entry 的实体, 调换链表顺序,故并发下会死循环(1.7和1.8的 resize 方案不一致). 遍历操作会 fail-over .resize 关注的是槽位,而不是已插入的节点数.

* concurrentHashMap的 resize 不会,entrySet 是不可变的,next 值不会变. rehash 时生成新的 entrySet .故 enteySet. iterator遍历操作不加锁. 可能拿到的 不同 segment 下的 hashtable[] 是不同的.

*业务系统数据迁移核心是update

最好是记录每个id的迁移记录.

另外方案: 三级索引. 主开关,递增id号,新库初始id号.小与此都迁移过,大于此未迁移. (迁移开关一旦打开,老数据库中就不会新增数据).

    各分布式定时任务.获取迁移序列锁.迁移指定序列的数据. 最后验证下数据总量是否一致.id范围+hash进行验证排查.

两表共同迁移方案: ?

读: 根据序号判断去哪里读

写: 根据开关写入哪个新库

删: 根据开关删除哪里.

改:根据开关和序号,和加锁情况去哪里修改.



http://blog.csdn.net/dc_726/article/details/47355989?ref=myread 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值