vlambda博客
学习文章列表

【可靠性设计 一】闲聊交互可靠性

  • 交互场景(怎样保证交互可靠)

    • 让我们先看看实际生活中两个人的发起对话如何保证

    • 让我们继续扩充这个模型的不同情况

    • 再来扩充下

    • 继续扩充场景

    • 还可以怎么扩展

  • 让我看下软件系统中的一些交互

    • 先看一个TCP的三次握手

    • 聊下分布式事务解决方案

      • 分布式事务解决方案大致可分为

      • 我们再讨论下以上模型的可靠性

      • 其他


交互场景(怎样保证交互可靠)

让我们先看看实际生活中两个人的发起对话如何保证


这三句话在我看来包含着对一个交互简单的可靠性维护机制,

  • 消息的响应

    • 在进行主要信息或任务的交代后,对这个消息的回应 通常是保证可靠性的一个机制

  • 重试机制(超时)

    • 这里隐藏了一个机制,就是重试机制,就是在对方没有回应时,你会重复问询

我们可以用数学来表示下这个交互的可靠性的提升

  • 假设每次消息成功接收的概率是 90% ,重试的次数是 3次 那么 成功率就变为1-0.1*0.1*0.1 = 0.999

  • 当然因为需要回馈信息,假设每次消息交互时间一致,我们的通话时间会延长到两倍以上,

  • 另外重试还会引发另一个问题 ,你必须知道下面这种情况,你是需要打印几张纸 


    【可靠性设计 一】闲聊交互可靠性

  • 上面就是我们通常会遇到“鱼与熊掌不可兼得”的问题,或者说一些策略都会带来另一些东西的牺牲

  • 一般我们需要寻找一个符合我们需求的平衡点,性能测试的数据可以作为我们确定这个平衡点的一些依据。

让我们继续扩充这个模型的不同情况


【可靠性设计 一】闲聊交互可靠性


  • 如上图,当一方存在前后数据不一致或丢失的情况下就会引发问题

  • 那之前有做好记录,或工作交接就很重要了 ,尤其在工作变换频繁的时候 【可靠性设计 一】闲聊交互可靠性

  • 再来,如果 一方经常生病,请病假咋办

    【可靠性设计 一】闲聊交互可靠性

再来扩充下

  • 比如 A 交代B 做一件事,但A很忙,不想一直等消息于是(这里省略发起过程)

    • 注意 这里 A必须记得这件事,及有没有完成

    • B必须 可以让 A查到这件事的完成状态

    • 通常这种情况是 A 不需要 B马上处理这个事情 

继续扩充场景

  • 来举个游戏的例子

    • 比如工会有很多任务需要 安排人去做,所以安排了任务栏,把所有任务都发布了上去

    • 工会里的英雄会根据自己的等级 拿走对应的任务完成

    • 我们这里把任务栏也独立出来做为一方,为啥(可能其他工会经常来踢馆,会损害任务栏(信息时代可能是领取处的服务器),所以纳入单独考虑因素)

    • 假设你如果把所有任务都存在任务栏里,如果一天被损害了,是不是很糟糕,

    • 还需保证你的任务都发布到了这个任务栏上

    • 我想大概任务发起方 都会有个任务备份

还可以怎么扩展

  • 比如一个任务 需要 多个步骤,每个步骤完成后才算完成,任一个失败 都需要取消之前做过的操作重头来过

  • 比如 一个任务 需要 多方 都完成后才算完成,

  • 再比如由于业务压力,各方都召集了很多小弟分别负责发布任务,和处理任务奖励事务

  • 再比如,一方为了确保对方一直都在,定时查岗,

  • 再比如,一方为了确保对方把交代的事完成,定时询问完成没

让我看下软件系统中的一些交互

先看一个TCP的三次握手

  • 这个和最初我们谈的第一种情况区别在于网络更像在一个公众场所说话,或是广播,需要辨识对方,先要知道对方的“频道”

  • TCP 为何是三次握手,这里网上资料很多,网上有很多例子

三次握手:
“喂,你听得到吗?”
“我听得到呀,你听得到我吗?”
“我能听到你,今天balabala……”

两次握手:
“喂,你听得到吗?”
“我听得到呀”“喂喂,你听得到吗?”
“草,我听得到呀!!!!”
“你TM能不能听到我讲话啊!!喂!”
“……”

四次握手:
“喂,你听得到吗?”
“我听得到呀,你听得到我吗?”
“我能听到你,你能听到我吗?”
“……不想跟傻逼说话”


TCP 传递信息可以理解为美国与中国用货船来传货物,
但因为一艘轮船放不下,货物要分开一只只轮船来发货。
所以需要一个序列号来识别该货物是第几个,以便到达后将其拼接回原来的货物。
因为同一条航道(也就是 tcp连接)上,可能会有多批货物发送(复用 tcp 连接)。
发货时,双方需要通知对方这个序列号是从哪里开始(init seq)的,这样才能辨识过来的是不是一个对的货物,以及能拼接成完整的货物。
货物运输拼接(tcp)最重要的是可靠性,如果没有用三次握手来确认双方都可以获得对方的 序列号(seq)的话,
就无法知道当前航班(连接)中,对的货物序号是怎么样的了。

  • 上述第一个是在确认对方的信息,对话双方认识打招呼后只会把对方的话拼起来理解.

  • 下面那个例子关键也在确认对方,当一个新连接建立时,初始序列号( initial sequence number ISN)生成器会生成一个新的32位的 ISN。TCP的每一个包和这个ISN有关联, ISN是唯一的,和这个ISN相关的包就可以被组装起来作为一个完整的消息

聊下分布式事务解决方案

分布式事务解决方案大致可分为

  • 刚性事务

    • 全局事务(标准的分布式事务)

  • 柔性事务

    • 可靠消息最终一致(异步确认型,补偿)

    • TCC (两阶段型、补偿型)

    • 最大努力通知(非可靠消息 、 定期校对)

我们这里讨论 柔性事务, 柔性事务符合 BASE 理论, 相关理论基础可以百度下

  • 可靠消息最终一致(异步确认型)

    • 理解

      • 把自己要做的事情分为很多件

      • 你每完成一个步骤 ,找个第三方先存起来(可以是可靠消息服务,也可以是自己建表存起来)

      • 等所有这件事的步骤都完成了,再把这个完成的事情告诉 相关服务

      • 如果失败了一个步骤,可以取消这个事情,恢复到这个步骤之前的状态,不再告诉下游相关服务,这个就是补偿

      • 可以加一个机制,就是根据你记录事情的状态,定期查询事情的进展,这里肯定会有一种很久都查不到的情况


  • TCC (两阶段型、补偿型)

    • 理解    

      • 一个完成的任务也是分为很多,这次是需要多方去完成

      • TCC 是Try Confirm Cancel, 与异步确认型区别在于,这里任务是由多方去完成,所以需要多方去确认完成,才算完成,任一方cancel也会cancel

      • 补偿指的就是cancel


  • 最大努力通知型

    • 这个比较好理解,就是你发任务给另一个人后,定时问询任务是不是已经完成了

    • 网络层TCP 发送得到ACK 表示发送到了,而业务层不仅关心发送到了,还关心业务是否完成了

    • 理解

我们再讨论下以上模型的可靠性

可靠性需要很多机制去结合才能达到很好的可靠性,在使用工具框架时要了解验证框架实现的机制 比如 你这样实现

  • A 服务在开始一个任务后,给消息服务器发送一条HalfMsg,表示还“待确认”,消息服务器先存着这条消息

  • 任务 数据入库 后 再 发送一条 结束的消息,让消息服务发送“已发送”的消息 到 下游的服务进行消费

这里 试想下 如果哪天消息服务器down掉了,A一直在执行任务,如果发送这个HalfMsg不影响任务的执行。,很多任务执行后是不是都没有发送消息

可靠性可以从以下几个方面考虑

从设计上看

  • 如何保证上游服务对消息的 100% 可靠投递?
    首先,上游服务需要发送一条消息给可靠消息服务。这条消息说白了,你可以认为是对下游服务一个接口的调用,里面包含了对应的一些请求参数。
    然后,可靠消息服务就得把这条消息存储到自己的数据库里去,状态为“待确认” 。接着,上游服务就可以执行自己本地的数据库操作,根据自己的执行结果,再次调用可靠消息服务的接口。
    如果本地数据库操作执行成功了,那么就找可靠消息服务确认那条消息。如果本地数据库操作失败了,那么就找可靠消息服务删除那条消息。
    此时如果是确认消息,那么可靠消息服务就把数据库里的消息状态更新为“已发送”,同时将消息发送给 MQ。
    这里有一个很关键的点,就是更新数据库里的消息状态和投递消息到MQ。这俩操作,你得放在一个方法里,而且得开启本地事务。
    啥意思呢?如果数据库里更新消息的状态失败了,那么就抛异常退出了,就别投递到 MQ;如果投递 MQ 失败报错了,那么就要抛异常让本地数据库事务回滚。这俩操作必须得一起成功,或者一起失败。如果上游服务是通知删除消息,那么可靠消息服务就得删除这条消息。以上就保证了当投递出现问题时,状态一定是 “待确认”

    我们们后台再加个定时运行的线程,不停的检查各个消息的状态。如果是待确认状态,就回调上游服务提供的一个接口,如果是成功的就标记为已发送,并投递消息到mq,如果是没执行成功,删除掉消息服务器中的消息即可。

  • 如何保证下游服务对消息的 100% 可靠接收?下游服务就一直等着从 MQ 消费消息好了,如果消费到了消息,那么就操作自己本地数据库。如果操作成功了,就反过来通知可靠消息服务,说自己处理成功了,然后可靠消息服务就会把消息的状态设置为“已完成”。如果消费出了问题,那么消息状态一直是“已发送”,始终没有变成“已完成”, 在可靠消息服务里开发一个后台线程,不断的检查消息状态。
    此时可靠消息服务就可以再次尝试重新投递消息到 MQ,让下游服务来再次处理。
    只要下游服务的接口逻辑实现幂等性,保证多次处理一个消息,不会插入重复数据即可。

从找问题的思路看

  • 就是基于设计,看宕机发生在哪里 那个时间,系统的应对情况

    • 哪里指的是哪个节点,比如系统 有 服务 A ,消息服务B ,服务C ,那就是每个服务都发生宕机所会发生的情况的

    • 哪个时间指的是 在哪个子任务处理时发生,比如 一个任务在本地需要 写数据库 发消息,如果在写数据库的时候宕机

其他

  • 本地持久化

    • 这个好比日常工作中的你重要的事情都需要存档,因为你的脑子或人比文档记录不“持久化”些

    • 不能发生持久化的消息状态变更了而消息没发送出去这种事

  • 重试与幂等性

    • 因为每次投递或任务执行时不是100%,重试次数 和超时时间,关系着响应时间与成功率的平衡

    • 这样你可能会收到两条一样的消息,你要知道 怎么处理,因为你要区分每条消息,所以不同的消息必须要有各自唯一的id

    • 超时的消息看场景可以做告警或丢弃处理

  • 轮询

    • 也是算是重试,是事后的一个处理,需要任务信息有持久化,状态可查。向人反复催债,总得有借据在手里

  • 集群与选举制度

    • 运维角度考虑可靠性

  • 再谈利弊

    • 以上所述的柔性事务的方案,在性能上会有差距,在时延要求高的场合还需谨慎使用,确保符合要求

    • 很多策略都是时间 空间 或其他的对换,比如消息的有序消费, 读写分离等 都会引发其他问题

参考

  • https://github.com/clsaa/Distributed-Transaction-Noteshttps://www.cnblogs.com/jajian/p/10014145.html

  • https://juejin.im/post/5b5a0bf9f265da0f6523913b

  • https://juejin.im/post/5aa3c7736fb9a028bb189bca

  • https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html

  • https://segmentfault.com/a/1190000018057083

  • https://www.kubernetes.org.cn/5580.html

  • http://myfjdthink.com/2019/04/26/分布式事务的-n-种实现/

  • https://www.infoq.cn/article/8bu33kuSyJ6P-wAAoELT

  • https://www.jianshu.com/p/73beee3c70e9

  • http://www.justdojava.com/2019/04/17/java-distributed-transaction/

  • https://yq.aliyun.com/articles/582282