屡试不爽的互联网架构三大马车 _服务

  • 数码
  • 2025年02月14日
  • 屡试不爽的互联网架构三大马车 _服务 作者:朱晔 https://www.cnblogs.com/lovecindywang/p/9617542.html 这里所说的三架马车是指微服务 、讯息伫列 和定时任务 。如下图所示,这里是一个三驾马车共同驱动的一个立体的互联网专案的架构。不管专案是大是小,这个架构模板的形态一旦定型了之后就不太会变,区别只是我们有更多的服务有更复杂的呼叫,更复杂的讯息流转

屡试不爽的互联网架构三大马车 _服务

屡试不爽的互联网架构三大马车 _服务

作者:朱晔

https://www.cnblogs.com/lovecindywang/p/9617542.html

这里所说的三架马车是指微服务讯息伫列定时任务。如下图所示,这里是一个三驾马车共同驱动的一个立体的互联网专案的架构。不管专案是大是小,这个架构模板的形态一旦定型了之后就不太会变,区别只是我们有更多的服务有更复杂的呼叫,更复杂的讯息流转,更多的Job,整个架构整体是可扩充套件的,而且不会变形,这个架构可以在很长的一段时间内无需有大的调整。

PS:下载完整版PDF,后台回复:马车

图上画了虚线框的都代表这个模组或专案是不包含太多业务逻辑的,纯粹是一层皮(会呼叫服务但是不会触碰数据库)。黑色线的箭头代表依赖关系,绿色和红色箭头分别是MQ的传送和订阅讯息流的方向。具体在后文都会进一步详细说明。

微服务

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

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

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

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

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

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

这里也要求我们做到几个方面的原则:

1. 服务的粒度划分需要把控好。我的习惯是先按照领域来分不会错,随着专案的进展慢慢进行更细粒度的拆分。比如对于互联网金融P2P业务,一开始可以分为:

2. 服务一定是立体的,不是在一个层次上的,如上图,我们的服务有三个层次:

希望在这里把这个事情说清楚了,怎么来划分服务怎么划分三个层次的服务是一个很有意思很有必要的事情,在服务划分之后最好有一个明确的档案来描述每一个服务的职责,这样我们在无需阅读API的情况下可以大概定位到业务所在的服务,整个复杂的系统就变得很直白了。

3.每一个服务对接的底层资料表是独立的没有交叉关联的,也就是资料结构是不直接对外的,需要使用其他服务的资料一定通过访问界面进行。好处也就是面向物件设计中封装的好处:

说白了就是我的资料我做主,我想怎么搞外面管不着,在重构或是做一些高层次技术架构(比如异地多活)的时候,没有底层资料被依赖,这太重要了。当然,坏处或是麻烦的地方就是跨服务的呼叫使得资料操作无法在一个数据库事务中完成,这并不是什么大问题,一是因为我们这种拆分方式并不会让粒度太细,大部分的业务逻辑是在一个业务服务里完成的,二是后面会提到跨服务的呼叫不管是通过MQ进行的还是直接呼叫进行的,都会有补偿来实现最终一致性。

4.考虑到跨机器跨程式呼叫服务稳定性方面的显著差异。在方法内部进行方法呼叫,我们需要考虑调用出现异常的情况,但是几乎不需要考虑超时的情况,几乎不需要考虑请求丢失的情况,几乎不需要考虑重复呼叫的情况,对于远端服务呼叫,这些点都需要去重点考虑,否则系统整体就是基本可用,测试环境不出问题,但是到了线上问题百出的状态。这就要求对于每一个服务的提供和呼叫多问几个上面的问题,细细考虑到因为网络问题方法没有执行多次执行或部分执行的情况:

如果你说,这么多服务,我在实现的时候很难考虑到这些点,我完全不去考虑分散式事务、幂等性、补偿(毫不夸张地说,有的时候我们花了20%的时间实现了业务逻辑,然后花80%的时间在实现这些可靠性方面的外围逻辑),行不行?也不是不可以,那么业务线上上跑的时候一定会是千疮百孔的,如果整个业务的处理对可靠性方面的要求不高或是业务不面向使用者不会受到投诉的话,这部分业务的是可以暂时不考虑这些点,但是诸如订单业务这种核心的不允许有不一致性的业务还是需要全面考虑这些点的。

5. 考虑到跨机器跨程式呼叫服务资料传输方面的显著差异。对于本地的方法呼叫,如果引数和返回值传的是物件,那么对于大部分的语言来说,传的是指标(或指标的拷贝),指标指向的是堆中分配的物件,物件在资料传输上的成本几乎忽略不计,也没有序列化和反序列化的开销。对于跨程式的服务呼叫,这个成本往往不能忽略不计。如果我们需要返回很多资料,往往界面的定义需要进行特殊的改造:

6. 这里还引申出方法粒度的问题,比如我们可以定义GetUserInfo通过传入不用的引数来返回不同的资料组合,也可以分别定义GetUserBasicInfo、GetUserVIPInfo、GetUserInvestData等等细粒度的界面,界面的粒度定义取决于使用者会怎么来使用资料,更趋向于一次使用单种型别资料还是复合型别的资料等等。

7. 然后我们需要考虑界面升级的问题,界面的改动最好是相容之前的界面,如果界面需要淘汰下线,需要先确保呼叫方改造到了新界面,确保呼叫方流量为0观察一段时间后方能从程式码下线老界面。一旦服务公开出去,要进行界面定义调整甚至下线往往就没有这么容易了,不是自己说了算了。所以对外API的设计需要慎重点。

8. 最后不得不说,在整个公司都搞起了微服务后,跨部门的一些服务呼叫在商定API的时候难免会有一些扯皮的现象发生,到底是我传给你呢还是你自己来拉,这个资料对我没用为什么要在我这里留一下呢?抛开非技术层面的事情不说,这些扯皮也是有一些技术手段来化解的:

你可能看到这里觉得很头晕,为什么微服务需要额外考虑这么多东西,实现的复杂度一下子上升了。我想说的是我们需要换一个角度来考虑这个事情:

1. 我们不需要在一开始的时候对所有逻辑都进行严密的考虑,先覆盖核心流程核心逻辑。因为跨服务成为了服务的提供方和使用方,相当于除了我自己,还有很多人会来关系我的服务能力,大家会提出各种问题,这对设计一个可靠的方法是有好处的。

2. 即使在不跨服务呼叫的时候我们把所有逻辑堆积在一起,也不意味着这些逻辑一定是事务性的,实现严密的,跨服务呼叫往往是一定程度放大了问题产生的可能性。

3. 我们还有服务框架呢,服务框架往往会在监控跟踪层次和运维系统结合在一起提供很多一体化的功能,这将封闭在内部的方法逻辑打散暴露出来,对于有一个完善的监控平台的微服务系统,在排查问题的时候你往往会感叹这是一个远端服务呼叫就好了。

4. 最大的红利还是之前说的,当我们以清晰的业务逻辑形成了一个立体化的服务体系之后,任何需求可以解剖为很少量的程式码修改和一些组合的服务呼叫,而且你知道我这么做是不会有任何问题的,因为底层的服务ABCDEFG都是经过历史考验的,这种爽快感体验过一次就会大呼过瘾。

但是,如果服务粒度划分的不合理,层次划分的不合理,底层资料来源有交叉,没考虑到网络呼叫失败,没考虑到资料量,界面定义不合理,版本升级过于鲁莽,整个系统会出各种各样的扩充套件问题效能问题和Bug,这是很头痛的,这也就需要我们有一个完善的服务框架来帮助我们定位各种不合理,在之后说到中介软件的文章中会再具体着重介绍服务治理这块。

讯息伫列

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

这里再说明一下Job的几个设计原则:

1. Job可以由各种排程框架来驱动,比如ElasticJob、Quartz等等,需要独立专案处理,不能和服务混在一起,部署启动多份往往会有问题。当然,自己实现一个任务排程框架也不是很麻烦的事情,在执行的时候来决定Job在哪台机器来跑,让整个丛集的资源使用更合理。说白了就是两种形态,一种是Job部署在那里由框架来触发,还有就是只是程式码在那里,由框架来起程式。

2. Job专案只是一层皮,最多有一些配置的整合,不应该有实际的业务逻辑,不会触碰数据库,大部分情况就是在呼叫具体服务的API界面。Job专案就负责配置和频次的控制。

3. 补偿类的Job注意补偿次数,避免整个任务被死信资料卡住的问题。

这每一个模组都可以打包成独立的包,所有的专案不一定都要在一个专案空间内,可以拆分为20个专案,服务的api+server+listener放在一个专案内,这样其实有利于CICD缺点就是修改程式码的时候需要开启N个专案。

之前开篇的时候说过,使用这套简单的架构既能够有很强的扩充套件余地,复杂程度上或者说工作量上不会比All-In-One的架构多多少,看到这里你可能觉得并不同意这个观点。其实这个还是要看团队的积累的,如果团队大家熟悉这套架构体系,玩转微服务多年的话,那么其实很多问题会在编码的过程中直接考虑进去,很多时候设计也可以认为是一个熟能生巧的活,做了多了自然知道什么东西应该放在哪里,怎么去分怎么去合,所以并不会有太多的额外时间成本。这三驾马车构成的这么一套简单实用的架构方案我认为可以适用于大多数的互联网专案,只是有些互联网专案会更偏重其中的某一方面弱化另一方面,希望本文对你有用。

猜你喜欢