• 那些年跟领导聊过的数据归档【DB篇】:从梳理到落地-DB单表千万级归档详细流程讲解


    知人论世

    无论何种需求的出现都是因为某种迫切解决的问题契机,它是业务发展中定数也是劫数,近期DBA反馈磁盘存储空间超过80%不足以支撑未来业务发展,勉强能够承受1年存储;随后TL高度重视,计划一整个Q完成,一场渡劫之路拉开帷幕。。。。。

    公司内部调整到核心部门不久的我和另一个partner临危受命,对业务不熟悉的我从0开始,随机整理一份数据现状供给分析

    其实分析维度还有很多,如唯一索引使用情况、涉及服务、数据更新场景等没有一一列举。

    表名单表数据量分表数量分表维度单表月平均增幅描述
    xxA千万+64订单编号100w+核心
    xxB千万+64订单编号100w+核心
    xxC千万+-用户id同用户增长1:1核心

    DB磁盘占用:80.5%     ES磁盘占用76%
    磁盘容量和单表压力日渐明显, 迫切需要优化方案并落地,而后便开始漫长的方案设计,长路漫漫,上下求索


    执笔蓝图

    老实说这是我第一次处理这类技术需求,所以第一反应就是借鉴前人思路,公司wiki、Google大厂优化案例扒了几遍,有了大致思路,然后执笔挥斥方遒。大约用了一周制定了第一版方案

    V1 - 浅尝辄止

    第一种方案基本1:1是借鉴其它部门处理方式:在原来基础上进行水平扩容分表,即由64扩到128,这个是基于订单号构造想的一个快速方案,由于前六位是系统生成的时间戳,所以只需要在路由算法处进行升级即可,如下示意图

    在这里插入图片描述

    核心思想: 根据设定时间点进行切分,在时间点以前的现有表不会新增,只做查询和更新,时间点以后继续按hash策略路由新表处理读写
    方案优势:

    1. 改造代码成本极低,只需要更改路由算法
    2. 仅需要同步数仓部门进行binlog感知【当时并没有注意这一点】

    方案劣势:

    1. 仅解决数据增量问题,并没有解决现有单表的查询压力
    2. 数据库磁盘存储容量压力仍然存在
      实施上这个方案想的过于简单,可行性极低,且没有解决实际问题,有人会说,如果加上定期归档是否可性,当时会议确实提过,但我的TL随机而来五连问:
    序号问题
    1当前表影响面是怎样的,是否影响C端核心链路?
    2只有本部门在使用改表吗?归档后会影响其它业务方使用?
    3归档粒度你如何界定,产品是否有思考?
    4归档后历史数据查询怎么办,现有接口是否做了容错和兼容?
    5归档表有数据能确定是最终态吗?比如未支付状态

    毫无准备的我瞬间懵逼,当时基于对业务初步梳理,准确说没有深入理解业务,就开始激扬文字般地开始设计,注定产物是不成熟的,或者说缺乏实践;了解到自己的认知不足后,迅速开始深入业务理解,在一周后开启v2版本制定


    V2 - 初窥门镜

    经过第一次和领导的沟通后认识到很多地方欠缺考虑,也随机带着回答问题方式做了如下努力:
    业务梳理:

    1. 从普罗米修斯监控上查询影响的所有服务的所有接口的一个月日志API调用

      看自己公司日志维护情况,至少2个周的调用日志保证是需要的

    2. 从go和java等服务dao 层往上梳理所有接口,确定使用场景、读写分类

    业务沟通

    1. 和产品确定归档的边界,即业务方能接受的存储时间维度

      实施上当时产品给的意见尽量全量保留,那势必技术方面就要划分冷热数据源,暂定1年为界限;而这也为前端和客服分别带来了改造成本和使用成本,这个过程撕逼不断。。。

    2. 确定表的使用方

      确实也有其他部门直连我们业务表,推动其改造,道阻且长;有的时候你认为的高优先级对方未必觉得,组织一致性这时就显得很重要

    3. 和数仓部门确定归档操作对其无感知

      作为业务部门我们对表的任何DDL操作 这个是沟通次数最多,也是最费劲的,不过他们给我们的结论也是最坑的【后面会提到】,当时沟通是等待其内部新老系统改造完毕后即无感知操作

    方案设计

    • 基于梳理认知构建归档流程
    • 细化归档节奏
    • 先入为主,思考归档前后可能出现的潜在问题

    整体设计分为两部走:

    1. 用户维度数据: 进行散表64张表

      • 根据user_id 进行hash % 64取余
        • hash算法对比【减少碰撞】
      • 增量同步
        • 开关控制,一键回切
        • 双写事务保障数据一致性【可选】
        • 无则新增,有则修改
      • 全量同步
        • 无则新增,有则跳过
      • 读切换
        • 开关控制,离线比对
        • 比对期间返回老数据
        • 比对无误全切新
      • 原功能下线
        • 原读写代码下沉
        • 原单表数据delete

    事实上该方案理论可行,单当前的数据库磁盘存储压力明显,因为是物理机的原因在原基础已经无法继续扩容,所以此刻水平扩表失去了先机,但思想是可以借鉴的,后续的真实落地方案也是基于此设计而成。

    1. 订单维度数据根据产品要求保留的时间范围之外进行归档
      设计归档策略时也和领导聊了很多,最终罗列如下:
    • 归档策略
      • 旧库 -> 新库,旧库直接物理删除,新库按年分库,分表同旧【短时间数据double,读写都需要改造,长期维护】
      • 旧库 -> 数仓 ,旧库直接物理删除,查询依赖数仓【历史全量数据依托BI,时间排期、优先级不可控】
      • 旧库 -> ES, 旧库直接物理删除,查询依赖 ES 【内部闭环,改造全部查询接口】
      • 旧库 -> MongoDB 【无使用经验,新增经济成本,疫情当下需要降本增效】

    当时领导决策决定采用旧库 -> 数仓 方案,从数据维护合理性上考虑,业务侧确实无法支撑历史全量数据的查询,这的确是一个快速且稳妥的方案,但事与愿违:

    数仓TL反馈不在一个体系,短时间满足需求可以,但其组织变更后未必能持续满足,言外之意深重,而后经过几次会议同步,一次比一次会议的沟通结果和最开始目标相去甚远,索性另辟蹊径,另一方面即便下沉到数仓,有些底层查询sql势必要下沉,从业务合理性数仓确实应该业务无关性。
    最终归档策略定为:旧库 -> ES,这里值得关注为啥一开始不选取该方案,因为在数据库优化的时候,ES磁盘占用也来到了75%,当时双重压力现在也不得不去面对和解决


    V3 - 木已成舟

    确定了归档策略,我们还要从全局执行节奏上考虑发现还有几处遗漏:

    • 行动纲领

      这就是后续所有开发节奏和操作内容的总纲领。

    在这里插入图片描述

    • 数据同步能力

      技术选型
      1. 采用现成组件logstash | binlog
      优点:开箱即用
      缺点:黑盒,需要申请资源部署、全量同步能力弱
      2. 手动编写脚本
      优点:白盒,增量和全量同步可以自定义
      缺点:开发成本较高【choose

    • 故障修复能力

      如果归档期间出现问题影响了使用方,那么需要回滚,如果采用数据库方式回滚,那么大批的delete这个过程是及其漫长,有锁表风险。只能另辟蹊径:从ES|数仓进行紧急情况下的数据恢复,依旧需要踩坑

    • 数据比对能力

      接口改造后不可能直接进行读切换,应该在具备校验数据一致性前提下,应该经历比对、灰度最后全量的操作,因此需要补足比对脚本和开关控制

    紧接着做了开发前的最后信息同步和确认:

    1. 产品、前端、客服、上游业务部门沟通当前全量查询变更为冷热方式

      这是一个相互理解的过程,技术改造理应对上游无感知,尽最大努力闭环,冷热划分后我们对一些C端场景采取限制,B端可以支持全量,毕竟有迹可查,但需要上游配合约定一个标识让我们感知到冷热边界以便减轻查询压力。但与此同时带来一些使用成本,在这由衷感谢客服一线人员对技改的理解。

    2. 同步SRE 当前方案,评估DB、ES磁盘是否可以支撑到改造结束

    3. 产品、业务方进行方案宣贯

      这个很重要:确保各个业务部门感知到,以便撕逼时可以据理力争

    整体执行分为2期项目

    • 订单维度表
      1. 先对订单相关表查询接口进行改造 【DB不存在 -》ES】
      2. 相关脚本开发
        • 数据逆向恢复脚本【ES | 数仓 -> DB】
        • 数据同步脚本【DB -> ES】
        • 数据比对 【DB with ES】
        • 归档脚本 【delete from xx where order_sn=?】
      3. 接口上线,脚本执行
      4. 物理归档,释放磁盘空间

    在这里插入图片描述

    • 用户维度表

      从业务领域划分考虑,有几个表不归属于订单,所以下沉给用户部门,具体步骤大同小异,节奏如下:
      在这里插入图片描述这其实就是整个流程的缩影


    躬行方案

    当真正开始着手开发的时候,可谓是一路坎坷不平,几乎上面的每个开发阶段都踩过大大小小坑,有时候甚至哭笑不得

    安内

    俗语有言:“打铁还得自身硬”, 又“一屋不扫何以扫天下”,从内部中遇到的坑如下:

    1. 为了保障作为归档库的数据准确性,对我们已有的全量数据和DB进行一次比对发现存在丢失和不一致的现象

      原因:
      1. 有些归档表是一次性数据,无二次查询可能
      2. 历史1年前数据不一致为主,新数据以db查询为主,整体无感知。
      改变:
      1. 编写逐字段比对脚本,开启全量比对,还没开始同步,已经开始修复历史遗留bug【我和另一个parter商量要不跑路?】
      2. 更改_id为sn,在分表情况下存在的重复id导致数据覆盖进而丢失数据,因此必须要保证_id绝对唯一
      如下是部分demo

        public boolean selfEquals(XXXEsMapping that) {
          if (this == that) {
              return true;
          }
          if (that == null) {
              return false;
          }
          return objectsEquals("id", this.getId(), that.getId())
                  && objectsEquals("type", this.getType(), that.getType())
                  && objectsEquals("data", this.getData(), that.getData())
                  && objectsEquals("status", this.getStatus(), that.getStatus())
                  && objectsEquals("createTime", this.getCreateTime(), that.getCreateTime())
                  && objectsEquals("updateTime", this.getUpdateTime(), that.getUpdateTime())
                  ......
          ;
      }
      
      private boolean objectsEquals(String field, Object o1, Object o2) {
          boolean equals = Objects.equals( o1, o2);
          if (!equals) {
              log.warn("sn :{} 数据比对不一致字段: {} o1:{}  o2: {}", orderSn, field, o1, o2);
          }
          return equals;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
    2. 主键id增长顺序有变动,数据同步无法基于时间段去扫描,只能通过id进一步组合时间维度绑定每个批次

      原因:没有时间索引【可见设计之初,先见之明多么重要!】,好像这么处理没啥问题,但也是不得已给自己留的坑【搞起来连自己都埋了hhh】:
      小华有笔订单越看越不顺眼,删除了,好像就是delete_status=1[已删除]这么简单的事情,但我们有BI,必须得让BI感知到这个变化。此类场景,就会导致恢复到db的数据所使用id 还是历史的id,而增量同步任务再次基于id +时间段扫描时就会发生遗漏。
      解决: 恰好是归档结束后发现,重新添加索引,按时间一个纬度进行同步

    3. 初入茅庐,ES使用不当

      1.ES弱结构化,字段变更都是无责新增,有则覆盖;因此构造ES的VO对象时需要注意一比一复制mapping 字段,慎用继承
      2.DB 的 date 类型字段默认值 0000-00-00 ES date类型默认值 0001-00-00,这个容易造成业务误判
      3.对于不需要搜索的字段无需进行分词存储以减少倒排索引的构建【敬请期待:百炼成钢:ES生产使用手册-十八罗汉

    4. 并发场景

    攘外

    现象:基本就是各种事中沟通和事后落地没有对齐, 时间成本被无情扩大,对于已经给出结论但事后反悔不认或者忘记的只能由衷歌颂:听我说,谢谢有你,温暖了四级【mmp】。。。
    解决: 凡沟通必有记录和二次确认,是钉钉群宣贯


    卓有成效

    经过上述流程下来,历时2个Q,开发时间周期40天,其余时间基本会议协商、等待测试资源;
    整体效果:磁盘占用:60% ES占用 60%【无新的大表情况下,不再增长】,支撑此结论有以下脚本:

    任务描述
    数据同步每日增量归档DB数据到ES
    数据归档1年前数据每日增量归档 , 动态维持1年
    数据比对1年前数据每日比对
    离线恢复支持多维度数据恢复

    沉淀之石

    1. 沟通能力- 方法论

      1. 凡沟通必有结论,凡结论必须同步,凡会议必有纪要。 2. 埋头猛干不可取,风险预知要早知 3. 不要我觉得,要大家都觉得
    2. 分析能力- 知己知彼

      1. 纸上得来终觉浅,得知此事要躬行,没有业务理论支撑的技改都是刀尖舔血。2. 总揽全局,排除潜在业务隐患
    3. 心理承受能力- 百炼成钢

      遇事不慌,胆大心细,警惕Easy Answer

    4. 编码能力 - up

      并发编程脚手架【重构之美:当多线程批处理任务挑起大梁 - 万能脚手架】、异常感知、灰度策略设计、兜底方案设计


    道阻且长

    虽然看上去只是完成一项工作,但从事情本身带给我的价值远远超过完成任务,这些经验有的是跨部门有的甚至是跨行业的,感受良多:

    在这个过程中,面临很多困难和内外部的压力,从TL和其它同事那学习到了很多优秀经验和设计理念让我劈风斩棘,最终迎来胜利的曙光,这都离不开他们的帮助,在此感谢我的直属leader对我肯定和认可,他们的耐心指导和信任让我倍受鼓舞。

    非常感谢我那未过门的媳妇儿在背后默默的支持和没有时常陪伴她所做出的理解,乘彼之爱,铭记在心

    长路漫漫,继续求索

  • 相关阅读:
    驱动程序开发:SPI设备驱动
    Microservices communication
    详解如何利用PHP实现RPC
    如何在小红书上推广民宿?看旅游类如何玩转小红书
    基于二次近似(BLEAQ)的双层优化进化算法_matlab程序
    go语言初学03 连接mysql
    有关神经网络的训练算法,深度神经网络训练方法
    大流量、业务效率?从一个榜单开始
    java线程的状态与联系(一张图理解)
    Python开发技术—函数设计1
  • 原文地址:https://blog.csdn.net/lkg5211314/article/details/122658721