作者:腾讯云开发者
原文:深入微服务核心:从架构设计到规模化
01 微服务
什么是微服务?
微服务就是一些协同工作的小而自治的服务。我们在一个单体系统中,通常会采用一些抽象层或者模块来保证代码的内聚性,直白点就是,把因相同原因产生变化的一类东西聚合在一块儿。
什么时候需要考虑微服务?
当我们的代码库异常庞大,以致于可能会出现“牵一发动全身”的情况,那么根据业务的边界来确定服务的边界,服务专注于某个业务边界之内,那么应该就能很好的解决这个问题了。例如,一个大型的商城系统,通常会划分用户,订单,支付等微服务。
微服务需要满足自治性
微服务应该可以彼此间进行独立修改,某个微服务的部署不应该引起该服务的消费方的变动。
微服务的好处
技术异构性
假如一个庞大的业务,不同的模块所需要的存储不一样,适合的语言也不同,那么微服务可以很好的屏蔽技术细节,打破技术栈所带来的壁垒。而且当你需要引进某些新技术的时候,使用微服务也可以降低系统风险,即便出现问题也很好处理。
弹性
单体应用如果出现异常,整个服务可能都无法使用,但是假如拆分成微服务,并且进行多机部署,那么就能降低系统完全不可用的风险,同时也可以采用功能降级来保证核心功能不受影响。
扩展
单体应用中,如果某个模块出现性能问题,我们必须对整个服务进行扩展,但是拆分成微服务,则可以对某个服务进行扩展,把那些性能要求低的服务,部署到相对较差的硬件上,这样可以充分利用资源来提供系统的性能。
简化部署
单体应用如果需要发布,会影响整个系统,微服务架构中,只需要对某个服务独立发布部署,即时出现问题,也只会影响这一个服务,可以快速回滚。
与组织结构相匹配
微服务架构可以很好地实现小团队+小代码库这种模式,提高团队生产力。
可组合性
微服务架构中,已有的功能可以重用,甚至通过组合几个微服务,就可以为某个应用程序提供完整的后台支撑。
对可替代性的优化
单体应用中,某些模块可能已经淘汰了,但是删除他们可能会带来不可预估的问题,但是微服务架构中,某些小模块可以轻松地进行移除或者重写。
小结
业界也存在共享库、模块这些分解技术,但是可能会导致它成为系统的耦合点;同时我们也要记住,没有银弹,不可能存在一条通用的准则。使用微服务甚至要面对单体应用没有的部署、测试和监控等一些列复杂的工作,还要处理分布式事务,CAP 相关的问题。
02 演化式架构师
模糊的定位
架构师的一个重要职责是,保证团队有共同的技术愿景,能够保证项目的交付质量。但是我们这个行业很年轻,以至于有时候我们自己都不清楚自己要干什么,人们把程序员叫做XX开发工程师,但是和传统行业相比,比如建筑师,假如我们的工程也根据一些图表和文档进行设计,那么可能会忽略一些基础性的因素,实现起来将会非常困难。不同于建筑行业必须遵循自然规律,物理规则,软件需要足够灵活,能够根据用户的需求不断演化。
城市规划师
相对于“架构师”,“城市规划师”可能更加贴合这个角色,城市规划师的职责是优化城镇的布局,进行合适的分区,使其更易于现有的居民生活,同时也会考虑一切未来的因素,与我们架构师的职责如出一辙。
分区
城市规划师要对城镇进行合理分区,那么架构师这边的区域概念对应的是什么?就是服务边界。架构师要多关注服务间的交互,而忽略服务内的细节,如果每个服务对外暴露的协议五花八门,那么对于消费者来说是个噩梦。虽然说不用关心每个服务内部的具体实现,但是假如一个团队,10种技术栈,我相信会增加管理成本和招聘难度。另外,架构师最好和团队共同 Coding,才能指导并且了解你的团队。
原则性的方法
-
制定战略性目标-业务部门的愿景。
-
制定设计原则-最好不要超过10个。
-
实践-对原则的落地。
-
结合-原则要与实践相结合。
标准
-
监控-所有服务使用同样的方式上报健康状态和其他监控数据,最好标准化。
-
接口-对外暴露接口的类型最好固定那几种,HTTP/REST或者RPC。
-
架构安全性-所有服务都要能够应对下游的错误请求,避免系统逐渐脆弱。
代码治理
达成共识,大家才能做好事情。所以,提供范例和服务代码模板是比较有效的两种方式。有比较好的代码范例,供人们模仿,即使错了,也不会错得很离谱。模板也可以提高开发速度,提升服务质量。但是要注意不要落入DRY(Don't Repeat Yourself)的陷阱,因为过度依赖共享可能会导致系统过耦合。
技术债务
每个团队都可能由于排期等因素欠下技术债务,架构师需要提供一些温和的指导,让团队决定如何偿还,最好是列一个表格,定期回顾。
例外管理
原则有时候可能会被打破,但是不代表它不正确,可能它会演化成一个新的原则,架构师要有严谨但包容的心态,如果对开发人员的限制过多,可能微服务架构并不适合。
集中治理和领导
架构师需要了解新的技术,并且知道如何取舍,通常领导一个小组,整个小组都负责治理,并且能够得到高层的支持。架构师一个人的想法有时候可能会比较片面,通常小组比个人更聪明。
建设团队
帮助战友共同进步,使其能够独立负责某个服务,并且能够得到充分的锻炼。
小结
演化式架构师应该承担的责任:
-
愿景-确保团队有共同的技术员警,能够满足客户和组织的需求。
-
同理心-理解你所做的决定对客户和同事带来的影响。
-
合作-尽量多和同事进行沟通,从而更好的对愿景进行定义、修改和执行。
-
适应性-确保在你的客户和组织需要的时候调整技术愿景。
-
自治性-在标准化和团队自治之间寻找一个正确的平衡点。
-
治理-确保系统按照技术愿景的要求实现。
03 如何建模服务
什么样的服务是好服务
松耦合
修改一个服务而不需要修改另一个服务
高内聚
相关行为聚集在一起形成独立服务
限界上下文
每个限界上下文包含两个部份,一部分需要和外部通信,一部分则不需要。就像细胞膜一样,细胞之所以会存在是因为细胞膜定义了什么在细胞内,什么在细胞外,并且决定了什么东西可以通过细胞膜。在不同的限界上下文之间,存在共享模型,屏蔽了内部实现细节。
如果服务的边界和领域的限界上下文能保持一致,并且微服务可以很好地表示这些限界上下文,那么就走出了高内聚低耦合的微服务架构的第一步。当然,微服务不宜过早拆分,将大型单体拆成微服务比较容易,但是直接使用微服务架构会比较困难。
业务功能
不应该从共享数据的角度来考虑,而应该从这些上下文能够提供的功能出发,不应该成为基于CRUD的,贫血的服务。要思考这个上下文是做什么用的,然后考虑它需要什么样的数据。
逐步划分上下文
当考虑微服务的边界时,应该先考虑比较大的、粗粒度的,然后发现合适的缝隙之后,再进一步划分,就像劈柴一样,要先劈出来两条大缝隙,然后再逐步分解成比较细木片。
修改系统的目的是满足业务需求,基于业务的划分,在客户提需求的时候,就会更倾向于对一个服务的改动;按照技术接缝进行划分可能会导致洋葱式架构,服务被分成很多层,改动起来可能会很痛苦,所以一般来说这种方式不是首选方式。
04 集成
理想的集成技术
集成可能是微服务相关技术中最重要的一个,理想的集成技术要能够做到以下几点:
-
避免破坏性修改:在对一个微服务的响应中添加了一个字段,不应该影响消费方。
-
API要保证技术无关性:不应该选择那种对微服务的具体实现有限制的集成方式。
-
使服务易于消费方使用:如果消费方使用我们的微服务比登天还难,那么设计得再好也没用。
-
隐藏内部细节:微服务应该屏蔽内部实现细节,否则可能会提高和消费方耦合的风险。
共享数据库
这是业界常见的集成方式,它简单且快捷,但是他的问题很多。外部系统能看到内部细节,消费与特定的技术绑定,对逻辑修改可能要同时修改相关的系统,这样做的话,可以说和松耦合高内聚毫不相干了,它会给开发人员带来无穷无尽的痛苦。
同步与异步
服务之间到底是使用同步还是异步进行通信?同步=阻塞,调用方会阻塞自己直到整个操作完成,异步的话,调用方则不需要等待,甚至都不关心这个操作有没有成功。
同步可以知道事情执行到底成功与否,异步对于运行时间较长的任务比较有效。那么异步通信如何知道执行结果呢?可以采用注册回调的方式,服务端调用完成后,可以调用改回调来进行通知。
也可以基于事件进行协作,客户端发起的不是请求,而是一个事件,所有的订阅者可以收到这个事件,并决定是否需要处理。这种方式的耦合性很低。
编排与协同
使用编排的方式,我们会把某个服务当成中心大脑来指导并驱动整个流程。
使用协同的方式,仅会告知系统中的每个部分各自的职责,至于具体的实现,由他们自己决定。
使用编排的方式,可能会导致某个服务成为整个流程的中心枢纽,其他服务沦为贫血的、基于 CURD 的服务,但使用协同的方式,则需要额外对整个流程做跨服务的监控,以保证其正常运行。所以我们可以根据不同的场景,进行选择,或者混用。
RPC
远程过程调用,允许你调用一个方法,但事实上整个方法的结果是由某个远程服务产生的。RPC 帮程序员生成桩代码,从而能够快速使用,理论上,只调用方法,忽略细节确实是给程序员带来极大的便利。
但是要注意远程调用和本地调用其实是有区别的,因为网络并不可靠!
REST
表述性状态转移,它是一种风格,举个例子,我要对某个资源进行增删,我会使用 CreateCustomer和DeleteCustomer,但是使用 REST 风格就会变成:
POST : www.test.com/customer
DELETE : www.test.com/customer
是不是要简洁很多?
为避免把存储数据暴露给消费者,我们可以先设计外部接口,等稳定后,再实现微服务内部的数据持久化。
但是,使用 REST 就无法帮助你生成桩代码,而且性能上可能遇到问题,对于低延迟场景来说,使用 HTTP 可能不是一个很好的主意。
基于事件的异步协作
主要关注两件事:微服务发布事件机制和消费者接受事件机制。
我们可以使用像RabbitMQ这样的消息代理来解决,但是这么做系统虽然有较好的弹性和可伸缩性,但是也会增加开发流程的复复杂度,但是一旦做好了,它会成为实现一个松耦合系统的重要手段。
还有一个原则:中间件一定要足够简洁,不要掺杂业务逻辑。
事件驱动的系统虽然看起来有较好的弹性和可伸缩性,但是开发难度高,带来的一些一致性问题比较复杂,而且对程序员的要求也很高,要进行思维上的转换,这有时候真的很困难。当我们要追踪某个问题的时候,我们一定要要引入监控机制,并且对时间有关联 ID,方便我们追踪这种跨服务的请求。
DRY
在微服务架构中,如果一味追求 DRY 是很危险的,比如很多微服务使用了同一个共享库。
所以,建议微服务内不要违反 DRY,微服务间可以适当违背 DRY。
按引用访问
如果我们确定了一个领域实体,那么任何和该领域相关的改动,都应该由这个领域完成。
比如我有一个客户服务,任何对客户的改动,都应该调用客户服务来完成改动,即客户服务是客户信息唯一可靠的来源。
举一个例子,如果我要发一封邮件,邮件里面有用户的姓名,地址,订单信息等,但是请求邮件服务是异步的,但是在这个时间差中,用户信息可能发生改变,如何解决?我们可以仅把客户资源和订单资源的 URI,等真正要发送的时候,再去查询最新的信息即可。
版本管理
-
减小破坏性修改影响的最好办法是尽量不要这样修改。
-
要尽早发现破坏性修改。
-
使用语义化管理版本,见文知意。
-
新老接口可以共存,来保证灰度或者随时回退。
-
同时存在新老服务(建议不要这样做,成本很高)。
小结
无论如何都要避免数据库集成,在 RPC 和 REST 之间做取舍,协同优先于编排,避免破坏性修改。
05 分解单块系统
我们上面讨论了什么是好的服务,一些规范和建议,以及系统如何向好的方向进行演化,那么一个大型的单体应用应该如何拆分成微服务呢?
关键是接缝,这部分代码进行修改不会影响系统的其他部分,这些被识别出来的接缝就会成为服务的边界。
前面讨论过的限界上下文就是一个很好的接缝,它定义的就是组织内高内聚,低耦合的边界,很多代码会使用包或者命名空间来定义这个概念,例如:golang的package。
好,现在假如我们有一个巨大的单体服务,它是一个在线音乐系统。我们把它划分为以下几个上下文:
-
产品目录。
-
财务。
-
仓库。
-
推荐。
从哪个部分入手?
最好考虑一下那部分抽离出去的效益最大,而不是为了抽离而抽离。
我们考虑以下几个因素:
-
改变速度:某个功能抽离且可以自治,可以加快后期的开发速度
-
团队结构:根据团队结构来抽离,比如存在异地协作,可以拆分出去。
-
安全:某部分业务是做数据安全,抽离出去可以对所有服务做监控、数据保护等。
-
技术:推荐系统出了一种新的算法,独立部署后,能提高我们系统的速度。
在我们分析系统为什么会有各种杂乱的依赖的时候,通常来说数据库大概率是源头。
找到源头后,我们抽象出一个独立的存储层,把数据库相关操作聚类到一起。
几个例子
打破外键关系
我们要生成一份财务报表,8月份销售《青藏高原》2000份,获利10000元。就需要从产品表和账单表获取数据,甚至存在外键关联,所以最好是产品服务暴露API给财务服务,而不是直接访问数据库,这个API会成为微服务化的第一步。
有时候让系统的一部分变慢会带来更大的好处,而且这个慢事实上也能控制在一定范围。
打破的前提是知道系统期望的行为是什么,想清楚再做决定!
共享静态资源
把城市编号放到数据库,各个服务共享,那么就形成了数据库集中,这显然不是我们想要的。
应该怎么做?
-
每个服务一个库。
-
这部分资源放到各自的服务代码中。
-
这部分资源行程一个单独的服务(太极端了)。
建议采用第二种,但是以配置文件的形式,各个服务自己保存一份,并支持热更新。
共享数据
仓库服务和财务服务都需要用户记录,他们都会向客户记录写数据和读数据,形成了数据共享,那么我们可以把客户的概念抽象出来,形成服务,对外暴露API。
共享表
我们的产品目录服务和仓库服务都会用到产品的信息,他们共享一张表,这个时候,可以把这个表分离成两个表,产品目录项表和库存表。
实施分离
事实上,先分离数据库结构,暂时不对服务进行分离会更好一些,我们可以随时回退,不影响消费者,一旦稳定下来,在考虑对整个应用程序进行分离。
事务
事务的好处是,可以使一系列操作要么全部执行,要么全都不执行。
那么微服务架构中如何实现?
-
再试一次-通过日志或者放入队列,之后再次尝试。
-
终止整个操作-通过补偿事务来抵消之前的操作,如删除某行记录,失败怎么办?可以重试或者给个界面操作,也可以自动化解决。
-
分布式事务-借助其他工具,如事务管理器,常见的算法是两阶段提交(本文不做介绍)。
我们到底应该怎么做?
这些方案都会带来复杂性,而且分布式事务很容易出错,且不易扩展。我们是否可以讲各种操作放到本地事务,然后依赖最终一致性方案解决?我认为可以,而且不要仅仅从技术上去解决,因为可能它真的不好解决,我们可以从业务上解决,设立一个间态,例如这类问题订单统一归类为“处理中的异常工单”?然后通过其他方式进行管理和修复?
数据导出
当我们要导出数据或者生成报表,可以通过提供API,定时同步数据库,使用事件订阅等等,但是千万不要暴露数据库!
小结
我们要理解,服务一定会越来越大,但是我们需要在拆分太过昂贵这件事发生之前,意识到我们需要拆分,这非常关键。
06 康威定律和系统设计
康威定律说的是,任何组织在设计一套系统时,所交付的设计方案在结构上都与该组织的沟通结构保持一致。
Amazon 认为,小团队比大团队的工作更有效,于是产生了著名的“两个披萨团队”,即没有一个团队应该大到两个披萨不够吃。
07 规模化微服务
微服务形成一定规模后,系统变得异常复杂,我们应该怎么办?
首先故障一定会有,而且无处不在,我们要允许错误发生,控制在一定范围,当一个系统变得不是很可用的时候,要有安全降级方案,保证核心功能。
要从架构设计原则上,保证一个服务不会导致整个系统崩溃。
规模化微服务这种分布式架构下,我们要准备好应对系统故障的方案:
-
超时-跨进程调用要设置超时时间来保护下游。
-
断路器-当下游资源请求失败或超时达到一定数量,断路器打开后续不再请求下游。
-
舱壁-为每个下游服务设置一个连接池,形成舱壁,保证不会互相影响。
-
隔离-服务之间依赖越低越安全。
-
幂等- 允许重复调用,且每次调用造成的相同,不必担心多次调用会有不利的影响。
-
扩展:
-
强大的硬件可以提升系统性能。
-
拆分服务提高负载。
-
把服务进行分散部署。
-
做好负载均衡,避免单点故障。
-
基于worker的模式,按需分配数量。
-
系统架构完全无法应对当前负载,考虑重新设计。
-
-
扩展数据库:
-
使用副本数据库,并且可以在必要的时候切换为主数据库。
-
读取扩展:很多服务已读取为主,可以选择读副本。
-
写操作扩展:使用分片机制,如对某个关键字取hash,写入不同实例。
-
-
缓存。
-
自动伸缩:根据负载主动扩缩容。
-
CAP定理:一致性、可用性、分区容忍性不可能同时满足,我们最多只能保证其中的两个。
-
服务发现:告诉人们微服务在哪里。
08 总结
微服务的原则
围绕业务概念建模
通常围绕业务的限界上下文定义的接口要比围绕技术概念定义的接口更加稳定。
接受自动化文化
大量的微服务涌现,我们必须要借助自动化工具,开发维护我们的服务。
隐藏内部实现细节
为了实现服务自治,隐藏细节至关重要,服务还应该隐藏他的数据库,避免产生数据库集成。
让一切都去中心化
确保团队对服务的所有权,同时要做到内部开源,人们可以修改其他团队的代码。
可独立部署
服务可以独立部署,当你修改发布的时候,不需要通知其他团队进行改动。
隔离失败
防止系统遭受灾难级的级联故障,要了解舱壁和断路器,保证你的系统没有那么脆弱。
高度可观察
系统整体一个清晰的观测平台,通过关联标识,跟踪调用链路。
什么时候不应该使用微服务
当你不了解它的时候,因为这时候出错的概率会很大。
写到最后
微服务架构会给你带来很多选择,需要你做很多决策,虽然有时候的决策可能是错误的,但是我们应该保证错误的影响范围要足够的小,遵从“演化式架构”,不断地前进探索。我们不应该进行爆炸式的重写,应该随着时间的推移,温和地对我们的系统进行修改,持续地进行改变!