美文网首页
微服务与异常处理(上)

微服务与异常处理(上)

作者: xuyaozuo | 来源:发表于2019-06-29 15:44 被阅读0次

    背景


    微服务的价值

    微服务架构的价值在于扩展(scale),主要有一下三个方面

    • 通过无状态的水平扩展可以分担流量
    • 通过添加不同类型的业务服务可以轻易实现业务功能的扩展
    • 通过对数据集的分区处理可以实现数据集的扩展

    但是作为分布式系统的一种依然引入了分布式天生的问题。
    分布式系统中局部错误是不可避免的,长远来看所有的系统都可能发生错误。
    微服务的系统的一直运行在一个可能局部失败的状态下。

    本文目标


    对于分布式容错系统中的一些常见问题给出泛用的应对思路

    如何应对微服务架构中的错误


    缓存和数据降级

    缓存不但可以减少服务间通讯的次数,提高性能。当下游服务发生异常的情况下,可以返回缓存的服务作为数据降级。
    但是必须考虑缓存击穿和缓存更新的策略的问题。

    使用缓存需要考虑雪崩

    如果缓存暂时不可用(比如一个热key还没有从数据库写入缓存),所有的请求会压到数据库,如果未提前做容量预估,可能会把数据库压垮(在缓存恢复之前,数据库可能一直都起不来),导致系统整体不可服务。解决方案:
    1 缓存的高可用/缓存分区,减小单实例缓存压力。
    2 多个请求相同的key请求时,合并请求,保证只有同时只有一个请求会穿透到数据库。
    3 缓存击穿的数量过大之后限流,保护数据库/下游服务压力过大。

    使用缓存如何考虑保证数据库和缓存的一致性

    旁路缓存:
    读取时:首先访问缓存 --> 缓存不存在 --> 访问数据库 --> 更新缓存 --> 返回数据
    修改时:对于存在缓存过期机制的情况下,修改数据库服务 --> 将缓存删除。

    旁路缓存的优点是在于可以在只有在读取的时候会更新缓存。不需要担心读取和修改的先后关系(读取中修改缓存的先后关系可以通过防止缓存穿透的方案解决),数据库中数据的正确性要求高于缓存。当数据库更新成功,缓存重置失败的时候,容许缓存和数据库暂时的不一致,通过最终一致的方式保证一致。

    当对于缓存和数据库一致要求很高的时候,可以使用分布式锁或者两步提交的方案。但是此方案消耗太高,缓存往往是用于解决一致性问题的,如此操作很有可能得不偿失。

    微服务系统与事务

    对于需要保证数据一致性的业务场景,通常使用两步提交的方案。利用数据库的事务特性,操作多条记录完成后再提交,否则多条操作一起失败回滚至先前状态。

    但是在微服务的分布式环境下,往往一个服务会有自己独立的数据库作为存储。本地的两步提交方案无法适维护整个系统的一致性,大大提高了事务处理的复杂度。

    分布式事务解决方案 - saga

    如何在分布式系统中保证事务的一致性问题。


    image

    如图所示当一个请求会涉及到多个服务并需要服务间保持一致性的情况下就涉及到分布式事务。

    saga 模式是将一整个分布式的事务分解成一个序列的事务,这些分解后的各个事务分别由独立的服务负责更新各自存储中的数据。第一个事务处理由外部请求触发,下一个事务处理依据上一个事务处理的结果触发。形成逻辑上执行的一个序列。

    image

    继续细分下去由两种主要方式来实现saga模式。

    • 事件驱动
      没有统一的中控服务,当上一个服务的事务提交的时候发送一个消息(事件)出来,这个消息会被传递给下一个服务,下一个服务监听这个事件并作出相应的处理,处理完之后也会抛出一个事件给再下一个服务直到整个分布式事务的结束。


      image

      但是事件驱动的方式存在一个缺点是,当随着版本迭代,不同的子事务被添加进系统。会发现各个微服务需要处理事务种类越来越多,事件发送的路径越来越复杂,维护成本极具升高。

    • 命令模式(乐团模式)
      命令模式定义了一个单独的服务,类似交响乐团的指挥,分别告诉各个子服务分布式事务当前阶段应该做什么。而各个服务不需要关心上下游的服务依赖。当然付出的代价是需要多维护一个“指挥”的服务,并且这个“指挥”的服务将比不同服务承担更多的复杂度。

    image

    消息队列与消息处理

    消息类中间件被广泛运用于微服务架构中,起到业务结偶,消息分区,削峰平谷等作用。
    但是如何可口的处理消息是一个需要深入思考的问题。

    消息队列与一致性

    在以消息为基础的异步系统中,强一致的分布式事务成本过高,往往一致性目标是“最终一致性”。

    最终一致性指的是两个系统的状态保持一致,要么都成功,要么都失败。当然有个时间限制,理论上越快越好,但实际上在各种异常的情况下,可能会有一定延迟达到最终一致状态,但最后两个系统的状态是一样的。
    以一个银行的转账过程来理解最终一致性,转账的需求很简单,如果A系统扣钱成功,则B系统加钱一定成功。反之则一起回滚,像什么都没发生一样。
    然而,这个过程中存在很多可能的意外:
    1 A扣钱成功,调用B加钱接口失败。
    2 A扣钱成功,调用B加钱接口虽然成功,但获取最终结果时网络异常引起超时。
    3 A扣钱成功,B加钱失败,A想回滚扣的钱,但A机器down机。

    上文的saga模式就是一种使用最终一致性实现分布式事务的方案。使用消息队列作为消息通讯的中间件可以有效减少,业务服务放在异步/最终一致的问题中处理的难度。消息队列可以暂存一部分消息(kafka)。对于consumer的投递失败可以做反复重新投递。消息队列可以实现广播消息而不需要上游服务维护监听列表。

    可靠发送

    为了满足分布式系统中的最终一致性,常常需要接受以下条件:
    1 同一个消息可能会被投递多次
    2 消息接受的顺序不一定和消息发送的顺序相同
    3 消息的接收可能会有延时。

    方案:
    当服务发送一个消息前,先将消息或者消息的等价信息落地。然后再发送消息,当发送失败或者无法知道消息投递成功的情况下,以一个超时时间不停轮询所有待发送消息。最终保证消息能够发送成功。

    这种做法类似于消息队列可靠投递的方案:

    producer往broker发送消息之前,需要做一次落地。
    请求到server后,server确保数据落地后再告诉客户端发送成功。
    支持广播的消息队列需要对每个待发送的endpoint,持久化一个发送状态,直到所有endpoint状态都OK才可删除消息。

    这种方式隐形地对于服务的自治提供了一种可能性。使用消息队列关联的服务不需要依赖于下游服务的健康状态,最终消息会在下游服务健康的时候被送达,只需要保证当前自己服务消息的传递是可靠的。

    可靠消费

    当消息服务器把消息传递给消费者后(可推可拉),消费者需要能够明确的告知服务器是否处理了当前消息,(回ack 或者 nack 消息)。即使逻辑上当前服务能够处理当前消息,但是由于服务状态,服务载荷等问题,consumer无法在收到消息的开始知晓消息的处理状况,所以ack消息的回复往往是在对应消息的处理完成之后。这种方式决定了消息可能在被处理,或者处理完之后再次消费,所以cosumer必须要保证幂等消费消息的能力。

    当broker把消息投递给消费者后,消费者可以立即响应我收到了这个消息。但收到了这个消息只是第一步,我能不能处理这个消息却不一定。或许因为消费能力的问题,系统的负荷已经不能处理这个消息;或者是刚才状态机里面提到的消息不是我想要接收的消息,主动要求重发。

    把消息的送达和消息的处理分开,这样才真正的实现了消息队列的本质-解耦。所以,允许消费者主动进行消费确认是必要的。当然,对于没有特殊逻辑的消息,默认Auto Ack也是可以的,但一定要允许消费方主动ack。

    消费的顺序

    分布式系统中保证消费消息的顺序和发送消费的顺序一致往往是很困难的,或者需要付出更严苛的条件。

    • 消费发送和消费接受都是需要单点单线程的。
      逻辑上消息的接受和消息的发送都是排他的,不然难以同步/接受方的顺序,(在一个排队执行的场景下,并行操作意义不大)。

    • 消息可能丢失:
      当一个消息反复投递或者处理失败时,为了保证接下来的消息能够继续消费,只能丢弃当前的消息。不然整个消息队列会进入一个死锁的状态。
      综上所述 消息生产/消费系统的一般的设计思路是在保证消息可达的情况下尽量少的投递/消费次数

    消息的幂等处理

    应对接收到重复消息的处理方法:

    • MessageId方法
      每个消息生成一个独立的messageId,这个messageId可以用于作为存储/中间件的主键,可以快速判断消息是否已被接收过。但是付出的代价是需要存储大量的消息,并且需要考虑当前处理过程中消息存储和业务数据存储的一致性。

    • 版本号方案:

    对于同一类的消息,上下游保证一个版本号,下游处理完记录版本号,只有当新消息的版本号大于当前接收的最新版本号才接收这条消息,付出的代价就是对于乱序的消息,小版本号的消息如果后至,则会被抛弃。

    • 状态机方案(复杂)
      每个消息带上一个自增的版本号,将所有接收到的消息存储下来,每次接收到消息之后使用存储的日志重新构建消息的最终状态。其中使用状态机:消息输入状态机,基于上一个状态产生一个新的状态,以此循环往复,最终获取最终的状态。

    • 状态机方案(简单)
      对于一个逻辑、流程较为简单的业务,设计的时候可以保证在一个状态下所能够接收的消息类型没有交集。即可以保证在当前状态下不会接收上一个(上上个)状态下可接收的消息。

    减少消息的反复投递

    消息队列需要考虑减少反复投递带来的系统开销,特别是当流量突发的情况下,反复重试会造成消息队列堵塞,服务过载等情况,进而引发雪崩效应。

    消息投递重试的问题,如果多次重试后依然投递失败,应当修正消息的继续投递。

    • 停止继续投递,根据业务特性,必要的时候可以进入业务回滚
    • 降低继续投递的频率,避免因为重试消耗过多资源。
    • 重试投递的消息进入另一个备份的慢消息处理队列,避免因为重试消息使当前的消息队列堆积。

    缺失的篇章

    消息队列堆积的处理方案

    相关文章

      网友评论

          本文标题:微服务与异常处理(上)

          本文链接:https://www.haomeiwen.com/subject/vrexfctx.html