保证金服务数据一致性问题-大数据解决方案

方案背景

观察近期保证金服务lark群聊天记录,保证金服务jraft频繁报错问题,特别是高吞吐量场景更加明显,并且出现异常时定位问题和解决问题都非常吃力,jraft成为整个保证金服务的可用性和性能瓶颈。

问题根源

集群环境中,采用jraft目的是在程序崩溃时能“自动”,“快速”,“准确”的恢复,让其他节点对外提供服务,外部系统无感知。但jraft集群是有缺陷的。以撮合服务撮合订单为例,具体表现至少如下几方面:

  • 监控报警:jraft缺乏监控和自动报警自动恢复,异常时需要人为重启,人为分析日志,效率低下。

  • 性能:jraft处理数据同步时需要获得半数以上节点认可,假若存在5个jraft节点并且委托单的tps为10万,在这过程中每秒钟需要处理40万次网络传输(同步到其余4个节点),并且按照半数以上原则至少20万次网络传输需要同步等待(同步到其余至少2个节点),否则无法处理下一秒钟的委托单,如果发生一些异常,整个系统还会阻塞重新选举,难于对付高吞吐场景。

  • 伸缩性:节点越多时需要网络资源越多,处理每个委托单性能越来越差,缺乏伸缩性。

  • 服务职责:整体架构上看主流程职责不单一,发送至其他jraft节点的非主流程的功能可能阻塞主流程,每个委托单处理需要耗费大量时间进行数据同步等待。

  • 使用成本:raft协议不易理解,编写的业务代码容易出错,难于针对性编写单元测试,无法把开发测试环境的数据备份到本地,很难保护错误现场,很难把错误现场迁移到其他地方回放。可以说从开发,运维,后续升级成本都非常大。

解决方案

本文探索一种解决方案,必须达到如下效果:

  • 监控方便,确保服务崩溃时能够自动快速准确的恢复

  • 确保单个交易对10万tps能轻松应对

  • 撮合服务节点增多不会影响每个委托单的处理能力,或者避免撮合服务的网络流量

  • 撮合服务jvm只处理核心业务,不需要等待非主流程功能执行完毕

  • 恢复时间足够短,如果自动恢复失败可以人工轻易恢复

  • 程序编写足够简单,易于理解,出问题时方便回放

基于以上特点,可以考虑釆用大数据方案,使用kafka,flink,redis进行实时同步和持久化,和多机热备份相结合

数据架构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HyuHVTDu-1656268331644)(3.drawio.png)]

  • kafka保存日志
  • redis充当镜像服务器,目的是备份jvm内存,
  • flink充当消费者的容器,管理消费者的存活
  • 撮合服务主节点将tradingResult推送到kafka中
  • 消费者订阅kafka中的日志恢复到redis内存
  • 正常情况下redis内存,撮合服务热备节点jvm内存和撮合服务主节点内存必定一致
  • 撮合服务主节点宕机时,自动切换程序会尝试把多机热备节点切换为主节点,
  • 极端情况热备也无能为力,需要人工处理
  • 使用人工方式,重启一个撮合服务节点作为主节点,从redis拉取镜像内存

整体流程

自动切换程序切换热备节点成功
自动切换程序切换热备节点失败
下单撤单事件触发撮合服务业务处理,此步骤已经存在
撮合服务应用把处理日志推送至kafka,此步骤已经存在
flink消费者组应用把kafka中日志写入redis,备份jvm内存
撮合服务应用出现异常
撮合服务重启时从redis加载内存镜像,恢复jvm内存

以交易委托账本orderBook为例(假设通过tradingResult消息能恢复orderBook)

原有逻辑

撮合服务 kafka 新委托单触发(此步骤已经存在) 更新本地orderBook(此步骤已经存在) 发送tradingResult消息到kafka(此步骤已经存在) loop [处理业务] 撮合服务 kafka
  • 交易等服务通过rpc调用,或者监听新委托单事件触发,进入撮合服务业务处理,处理完毕后推送tradingResult消息到kafka

全量备份流程

消费者组 Redis 撮合服务 关闭消费者组 首次启动Redis时,数据为空 存在一个把本地内存镜像写入redis的方法,可随时触发 暂停撮合 清空redis 把orderBook和offset写入redis 写入成功 开启撮合 重启消费者组 消费者组 Redis 撮合服务
  • 初始化redis时,flink为暂停状态,

  • 初始化redis时,撮合引擎也为暂停状态,所以没有tradingResult推送kafka,可以知道kafka的最新offset

实时备份流程

消费者组 Redis kafka 消费者启动时,要确定从kafka的哪个位置同步到redis 获取redis中offset 返回offset 根据redis中的offset从kafka订阅tradingResult消息 订阅成功 批量拉取tradingResult消息 把所有tradingResult消息变成更新redis中orderBook命令集 记录这批数据最后一个offset 开启事务 更新redis中的orderBook和offset 事务执行成功 事务成功后,提交offset,否则重试 loop [批量处理tradingResult消息] 消费者组 Redis kafka
  • 新建一个kafka消费者组,消费者组的所有节点跟撮合服务不同jvm,消费者组第一次启动前要确定从kafka的哪个位置消费。然后订阅tradingResult,每隔10秒钟批量拉取tradingResult,解释所有tradingResult消息,变成更新orderBook的redis命令集

  • 向redis批量更新orderBook和更新offset,整个事务成功后再告诉kafka消费完毕

恢复流程

自动恢复

自动切换程序 热备节点 检测到主节点宕机 把流量切换到该备份节点,成为新的主节点 自动切换程序 热备节点
  • 主节点宕机,多机热备程序把热备节点当做新的主节点

人工恢复

消费者组 撮合服务 Redis 人工确认redis的最新offset和Kafka的offset一致 关闭消费者组 内部存在一个从redis同步orderBook的方法,撮合服务启动触发 暂停撮合 获取redis中的orderBook 返回orderBook 写入本地内存 开启撮合 重启消费者组 消费者组 撮合服务 Redis
  • 多机热备程序失效,人工启动一个新节点即可

方案优势

  • redis充当备份机器,其数据来自消费者组,不会占用撮合服务本身的资源

  • 撮合服务还是内存计算,宕机时保存了宕机前的内存,可以人工手动恢复

  • 重启撮合服务后加载orderBook数据是从局域网redis加载,每秒钟10万以上,能够快速恢复orderBook

  • 利用了原有kafka的tradingResult,避免了jraft写入和等待同步,处理委托单职责更加单一,速度更快

  • flink消费者组读取tradingResult和写入都是批量操作。写入目标为redis时,整体处理速度理论上要比上游推送tradingResult快的多,比订阅tradingResult的其他消费者(比如资产)更快,因为它们要持久化到关系型数据库,或者说速度瓶颈不太可能在flink消费者组

  • 撮合服务应用启动时比较redis中offset和kafka的offset是否一致,确保redis中的orderBook跟撮合服务宕机前一致,数据不丢失

  • kafka,redis,flink都有相对完善的监控体系,解决问题便捷

  • 此方案的撮合服务代码简单,基本上无状态,可以随时重启

  • 假若一种场景,测试人员发现一个问题,但根据日志文件看不出来,可以把redis数据同步到本地redis,启动本地程序进行debug

维护升级

  • 怎么确保redis的数据和kafka是一致的?

    • 提交到redis时,把offset和更新orderbook命令集同时提交,成功后对kafka进行commit
  • flink消费者组宕机了怎么办?

    • 状态都保存在redis,flink消费者组为无状态,可以随时重启
  • 可以不使用flink吗?

    • 当然可以,flink只是充当消费者组的容器,我们可以直接建立消费者组,做好必要监控即可。
  • redis宕机后,持久化的数据不见了,但撮合服务还活着,怎么办?

    • 此问题跟第一次启动redis要怎么初始化redis是同一个问题,也就是执行全量同步操作
    • 关闭flink消费者组
    • 重新建立redis集群,数据为空
    • 触发撮合服务的把内存镜像写入redis的方法:暂停撮合,同步至redis,重启撮合
    • 重启flink消费者组
  • 升级维护时,比如增加了或删除了字段或修改了字段名,怎么办?

    • 对于需要全内存运行的数据结构,一般都是稳定的,不会经常修改
    • 如果缺失修改了,则需要针对性设计转换逻辑
    • 新增字段名时,此时redis并没有对应的数据,产品要设置一个默认数据或者干脆为null,然后扫描redis,增加每个订单的数据
    • 删除字段名时,此时redis虽然存在对应的数据,但撮合服务应用不需要,扫描redis,把redis中的多余字段删除即可
    • 修改字段名时,扫描redis,增加新的字段,值跟原来的一样,然后删除旧的字段
    • jraft如何处理?
  • 当内存只有几百M时,撮合服务启动加载可能很快,但当内存有几G时,怎么办?

    • 整个交易系统除了撮合服务比较特殊,需要全内存执行,其他都可以做到横向增加机器

    • 撮合服务还可以按照每个交易对拆分,orderBook需要保证状态的只有买盘卖盘两个队列

    • 每个队列元素的核心字段只有委托单id,价格,委托时间,委托数量,未成交数量

    • 只传输这5个关键字段即可,加上字段名也不到100字节,传输过程甚至可以把字段名也省去

    • 1K流量可以传输10个订单,1M流量可以传输1万个订单,1G流量可以传输1000万个订单

    • 从现实来看,单个交易对1000万未成交订单几乎不可能,因此每次启动都会几十秒内完成

  • 假定数据实在太大,比如100G内存,这样的话哪怕一秒钟传输100M,撮合服务应用也要几十分钟才能启动,怎么办?

    • 这种情况下,可以对方案进行增强

    • flink消费者组每次启动时,创建一个有序列表(如果已存在,则先删除或清空),用于保存flink消费者组本次启动后解释的所有groovy命令

    • flink消费者组每次提交redis命令集和offset时,把带数据的groovy命令也提交到redis的一个有序列表

    • 撮合服务应用内部编写一个接口,执行groovy脚本,目的为了更新内存中的orderbook

    • 每天隔一段时间,比如8个小时,暂停flink消费者组(此时撮合服务应用,kafka集群,redis都还正常运行)

    • 启动新的撮合服务节点,会从redis加载全量数据,100G数据比较大,可能要1个钟头才能启动完后,启动完后新撮合服务节点jvm内存跟redis的内存一样,但新撮合服务节点和redis都落后kafka和主节点(因为撮合服务主节点和kafka都在持续运行)。

    • 确保新的撮合服务jvm成功后,启动flink消费者组,此时redis的有序列表刚刚被清空,开始保存flink消费者组启动以来的groovy

    • 撮合服务应用宕机时,如果多机热备节点无法使用,读入redis中的groovy命令集文件,并调用grovvy命令的方法执行

    • 因为这些groovy脚本都是近期8个小时内产生的,数据量不大很快就能执行完,然后把这个新的撮合服务节点当做主节点

  • 除了kafka,撮合服务不想引入redis依赖怎么办?

    • 引入redis是方便把内存镜像写入redis和从redis读内存镜像,调用方法的时候开启redis连接,调用完关闭连接即可,不会浪费内存
    • 如果实在不想引入redis依赖或跟redis网络不通,则要把内存镜像同步至redis和从redis恢复为内存镜像两部分通过中间文件处理
    • 内存镜像同步至redis,可以把内存镜像替换成写本地文件,文件内容为redis命令集,把文件拷贝到redis所在机器执行
    • 从redis恢复内存镜像,又有两种方式
    • 对于1G以下数据量的,可采用全量方式,直接编写另外一个java程序读取redis全量数据生成groovy命令集文件,拷贝到本地,调用执行groovy命令的方法
    • 对于1G以上数据量的,可采用8小时全量+增量的方式,由于新的撮合服务节点已经有8小时以前的数据,只需编写另外一个java程序读取redis的有序列表生成groovy命令集文件,拷贝到新的撮合服务节点,调用执行groovy命令的方法
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值