云时代架构阅读笔记之四

原文连接:

 https://mp.weixin.qq.com/s?__biz=MzAwNTQ4MTQ4NQ==&mid=2453562461&idx=1&sn=cfc2bd0b4182710e8ffa608af71140b6&chksm=8cd1313fbba6b829556bea4a2af21cb329231b63f3737857ac91fed05c12ac8174a47d6a7cfd&scene=27&ascene=0&devicetype=android-28&version=2700043c&nettype=cmnet&abtest_cookie=BQABAAoACwASABMAFQAGACOXHgBWmR4Ay5keANyZHgD5mR4AC5oeAAAA&lang=zh_CN&pass_ticket=KADs%2Bb6XXZVFsw67HoLMXVbukuRbgjhDozp7ORtOl0nVsGV%2FMmHkN1fAhHMC6rpm&wx_header=1 
 
互联网架构三马车是指:微服务、消息队列和定时任务

微服务

微服务并不是一个很新的概念,在10年前的时候我就开始实践这个架构风格,在四个公司的项目中全面实现了微服务,越来越坚信这是非常适合互联网项目的一个架构风格。不是说我们的服务一定要跨物理机器进行远程调用,而是我们通过进行有意的设计让我们的业务在一开始的时候就按照领域进行分割,这能让我们对业务有更充分的理解,能让我们在之后的迭代中轻易在不同的业务模块上进行耕耘,能让我们的项目开发越来越轻松,轻松来源于几个方面:

1. 如果我们能进行微服务化,那么我们一定事先经过比较完善的产品需求讨论和领域划分,每一个服务精心设计自己领域内的表结构,这是一个很重要的设计过程,也决定了整个技术架构和产品架构是匹配的,对于All-In-One的架构往往会省略这一过程,需求到哪里代码写到哪里。

2. 我们对服务的划分和职责的定位如果是清晰的,对于新的需求,我们就能知道需要在哪里改怎么样的代码,没有复制粘贴的存在少了很多坑。

3. 我们大多数的业务逻辑已经开发完毕,直接重用即可,我们的新业务只是现有逻辑的聚合。在PRD评审后,开发得到的结论是只需要组合分别调用ABC三个服务的XYZ方法,然后在C服务中修改一下Z方法增加一个分支逻辑,就可以构建起新的逻辑,这种爽快的感觉难以想象。

4. 在性能存在明显瓶颈的时候,我们可以针对性地对某些服务增加更多机器进行扩容,而且因为服务的划分,我们更清楚系统的瓶颈所在,从10000行代码定位到一行性能存在问题的代码是比较困难的,但是如果这10000行代码已经是由10个服务构成的,那么先定位到某个服务存在性能问题然后再针对这个服务进行分析一下子降低了定位问题的复杂度。

5. 如果业务有比较大的变动需要下线,那么我们可以肯定的是底层的公共服务是不会淘汰的,下线对应业务的聚合业务服务停掉流量入口,然后下线相关涉及到的基础服务进行部分接口即可。如果拥有完善的服务治理平台,整个操作甚至无需改动代码。

消息队列

消息队列MQ的使用有下面几个好处,或者说我们往往处于这些目的来考虑引入MQ:

1. 异步处理:类似于订单这样的流程一般可以定义出一个核心流程,这个流程用于处理核心订单的状态机,需要尽快同步落库完成,然后围绕订单会衍生出一系列和用户相关的库存相关的后续的业务处理,这些处理完全不需要卡在用户点击提交订单的那刹那进行处理。下单只是一个确认合法受理订单的过程,后续的很多事情都可以慢慢在几十个模块中进行流转,这个流转过程哪怕是消耗5分钟,用户也无需感受到。

2. 流量洪峰:互联网项目的一个特点是有的时候会做一些toC的促销,免不了有一些流量洪峰,如果我们引入了消息队列在模块之间作为缓冲,那么backend的服务可以以自己既有的舒服的频次来被动消耗数据,不会被强压的流量击垮。当然,做好监控是必不可少的,下面再细说一下监控。

3. 模块解耦:随着项目复杂度的上升,我们会有各种来源于项目内部和外部的事件(用户注册登陆、投资、提现事件等),这些重要事件可能不断有各种各样的模块(营销模块、活动模块)需要关心,核心业务系统去调用这些外部体系的模块,让整个系统在内部纠缠在一起显然是不合适的,这个时候通过MQ进行解耦,让各种各样的事件在系统中进行松耦合流转,模块之间各司其职也相互没有感知,这是比较适合的做法。

4. 消息群发:有一些消息是会有多个接收者的,接收者的数量还是动态的(类似指责链的性质也是可能的),在这个时候如果上下游进行一对多的耦合就会更麻烦,对于这种情况就更适用使用MQ进行解耦了。上游只管发消息说现在发生了什么事情,下游不管有多少人关心这个消息,上游都是没有感知的。

这些需求互联网项目中基本都存在,所以消息队列的使用是非常重要的一个架构手段。在使用上有几个注意点:

1. 我更倾向于独立一个专门的listener项目(而不是合并在server中)来专门做消息的监听,然后这个模块其实没有过多的逻辑,只是在收到了具体的消息之后调用对应的service中的API进行消息处理。listener是可以启动多份做一个负载均衡的(取决于具体使用的MQ产品),但是因为这里几乎没有什么压力,不是100%必须。注意,不是所有的service都是需要有一个配到的listener项目的,大多数公共基础服务因为本身很独立不需要感知到外部的其它业务事件,所以往往是没有listener的,基础业务服务也有一些是类似的原因不需要有listener。

2. 对于重要的MQ消息,应当配以相应的补偿线作为备份,在MQ集群一切正常作为补漏,在MQ集群瘫痪的时候作为后背。我在日千万订单的项目中使用过RabbitMQ,虽然QPS在几百上千,远远低于RabbitMQ压测下来能抗住的数万QPS,但是整体上有那么十万分之一的丢消息概率(我也用过阿里的RocketMQ,但是因为单量较小目前没有观察到有类似的问题),这些丢掉的消息马上会由补偿线进行处理了。在极端的情况下,RabbitMQ发生了整个集群宕机,A服务发出的消息无法抵达B服务了,这个时候补偿Job开始工作,定期从A服务批量拉取消息提供给B服务,虽然消息处理是一批一批的,但是至少确保了消息可以正常处理。做好这套后备是非常重要的,因为我们无法确保中间件的可用性在100%。

3. 补偿的实现是不带任何业务逻辑的,我们再梳理一下补偿这个事情。如果A服务是消息的提供者,B-listener是消息监听器,听到消息后会调用B-server中具体的方法handleXXMessage(XXMessage message)来执行业务逻辑,在MQ停止工作的时候,有一个Job(可配置补偿时间以及每次拉取的量)来定期调用A服务提供的专有方法getXXMessages(LocalDateTime from, LocalDateTime to, int batchSize)来拉取消息,然后还是(可以并发)调用B-server的那个handleXXMessage来处理消息。这个补偿的Job可以重用的可配置的,无需每次为每一个消息都手写一套,唯一需要多做的事情是A服务需要提供一个拉取消息的接口。那你可能会说,我A服务这里还需要维护一套基于数据库的消息队列吗,这个不是自己搞一套基于被动拉的消息队列了吗?其实这里的消息往往只是一个转化工作,A一定在数据库中有落地过去一段时间发生过变动的数据,只要把这些数据转化为Message对象提供出去即可。B-server的handleXXMessage由于是幂等的,所以无所谓消息是否重复处理,这里只是在应急情况下进行无脑的过去一段时间的数据的依次处理。

4. 所有消息的处理端最好对相同的消息处理实现幂等,即使有一些MQ产品支持消息处理且只处理一次,靠自己做好幂等能让事情变得更简单。

5. 有一些场景下有延迟消息或延迟消息队列的需求,诸如RabbitMQ、RocketMQ都有不同的实现方式。

6. MQ消息一般而言有两种,一种是(最好)只能被一个消费者进行消费并且只消费一次的,另一种是所有订阅者都可以来处理,不限制人数。不用的MQ中间件对于这两种形式都有不同的实现,有的时候使用消息类型来做,有的使用不同的交换机来做,有的是使用group的划分来做(不同的group可以重复消息相同的消息)。一般来说都是支持这两种实现的。在使用具体产品的时候务必研究相关的文档,做好实验确保这两种消息是以正确的方式在处理,以免发生妖怪问题。

7. 需要做好消息监控,最最重要的是监控消息是否有堆积,有的话需要及时增强下游处理能力(加机器,加线程),当然做的更好点可以以热点拓扑图绘制所有消息的流向流速一眼就可以看到目前哪些消息有压力。你可能会想既然消息都在MQ体系中不会丢失,消息有堆积处理慢一点其实也没什么问题。是的,消息可以有适当的堆积,但是不能大量堆积,如果MQ系统出现存储问题,大量堆积的消息有丢失也是比较麻烦的,而且有一些业务系统对于消息的处理是看时间的,过晚到达的消息是会认为业务违例进行忽略的。

8. 图上画了两个MQ集群,一套对内一套对外。原因是对内的MQ集群我们在权限上控制可以相对弱点,对外的集群必须明确每一个Topic,而且Topic需要由固定的人来维护不能在集群上随意增删Topic造成混乱。对内对外的消息实现硬隔离对于性能也有好处,建议在生产环境把对内对外的MQ集群进行隔离划分。

 

定时任务

定时任务的需求有那么几类:

1. 如之前所说,跨服务调用,MQ通知难免会有不可达的问题,我们需要有一定的机制进行补偿。

2. 有一些业务是基于任务表进行驱动的,有关任务表的设计下面会详细说明。

3. 有一些业务是定时定期来进行处理的,根本不需要实时进行处理(比如通知用户红包即将过期,和银行进行日终对账,给用户出账单等)。和2的区别在于,这里的任务的执行时间和频次是五花八门的,2的话一般而言是固定频次的。

详细说明一下任务驱动是怎么一回事。其实在数据库中做一些任务表,以这些表驱动作为整个数据处理的核心体系,这套被动的运作方式是最最可靠的,比MQ驱动或服务驱动两种形态可靠多,天生必然是可负载均衡的+幂等处理+补偿到底的,任务表可以设计下面的字段:

  • 自增ID

  • 任务类型:表明具体的任务类型,当然也可以不同的任务类型直接做多个任务表。

  • 外部订单号:和外部业务逻辑的唯一单号关联起来。

  • 执行状态:未处理(等待处理),处理中(防止被其它Job抢占),成功(最终成功了),失败(暂时失败,会继续进行重试),人工介入(永远不会再变了,一定需要人工处理,需要报警通知)

  • 重试次数:处理过太多次还是失败的可以归类为死信,由专门的死信队列任务单独再进行若干次的重试不行的话就报警人工干预

  • 处理历史:每一次的处理结果,Json的List保存在这里供参考

  • 上次处理时间:最近一次执行时间

  • 上次处理结果:最近一次执行结果

  • 创建时间:数据库维护

  • 最后修改时间:数据库维护

除了这些字段之外,还可能会加一些业务自己的字段,比如订单状态,用户ID等等信息作为冗余。任务表可以进行归档减少数据量,任务表扮演了消息队列的性质,我们需要有监控可以对数据积压,出入队不平衡处理不过来,死信数据发生等等情况进行报警。如果我们的流程处理是任务ABCD顺序来处理的话,每一个任务因为有自己的检查间隔,这套体系可能会浪费一点时间,没有通过MQ实时串联这么高效,但是我们要考虑到的是,任务的处理往往是批量数据获取+并行执行的,和MQ基于单条数据的处理是不一样的,总体上来说吞吐上不会有太多的差异,差的只是单条数据的执行时间,考虑到任务表驱动执行的被动稳定性,对于有的业务来说,这不失为一种选择。

原文地址:https://www.cnblogs.com/tianzeyangblog/p/11054867.html