1 服务拆分
单体系统在遇到用户增加和流量增加时,可以参考AKF扩展立方体对系统进行扩展。AKF扩展立方体,是一个叫AKF的公司的技术专家抽象总结的应用扩展的三个维度。理论上按照这三个扩展模式,可以将一个单体系统,进行无限扩展。AKF扩展立方如图所示。
X轴:水平复制,即在负载均衡服务器后增加多个web服务器。
Y轴:功能分解,将不同职能的模块分成不同的服务。从y轴这个方向扩展,才能将单体应用分解为一组不同的服务,例如订单管理中心、商品信息管理中心、库存管理中心等等。
Z轴:数据库的扩展,分库分表(分库是将关系紧密的表放在一台数据库服务器上,分表是因为一张表的数据太多,需要将一张表的数据通过hash放在不同的数据库服务器上)。
三个维度扩展简单对比:
维度 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
x轴扩展 | 成本低,实施简单 | 当单体应用过大时,服务响应变慢,水平扩展也无法提高响应速度 | 发展初期,业务复杂度低,需要增加系统容量 |
y轴扩展 | 解决单体应用代码复杂问题,修改一个小功能需要重新部署整个应用; 拆分后服务之间故障隔离,小团队聚焦于当前服务,有利于服务快速迭代 |
成本高,服务器成本和运维成本提高 | 业务复杂,代码耦合度高,数据量大,团队规模大 |
z轴扩展 | 解决数据库层面的并发压力 | 有一定成本,服务器成本呢和运维成本都有所提高 | 数据量指数级增长 |
1.1 AKF Cube
业界对于可扩展的系统架构设计有一个朴素的理念,就是: 通过加机器(水平扩展)就可以解决容量和可用性问题 。( 如果一台不行那就两台) 。
这一理念在“云计算”概念疯狂流行的今天,得到了广泛的认可!对于一个规模迅速增长的系统而言,容量和性能问题当然是首当其冲的。但是随着时间的向前,系统规模的增长,除了面对性能与容量的问题外,还需要面对功能与模块数量上的增长带来的系统复杂性问题以及业务的变化带来的提供差异化服务问题。而许多系统,在架构设计时并未充分考虑到这些问题,导致系统的重构成为常态,从而影响业务交付能力,还浪费人力财力!对此,《可扩展的艺术》一书提出了一 个更加系统的可扩展模型—— AKF 可扩展立方(Scalability Cube)。这个立方体中沿着三个坐标轴设置分别为:X、Y、Z。
下面看一下AKF的拆分实践:
1.2 AKF Cube 拆分应用
X轴:从单体系统或服务,水平克隆出许多系统,通过负载均衡平均分配请求。
Y轴 :面向服务分割,基于功能或者服务分割,例如电商网站可以将登录、搜索、下单等服务进行Y轴的拆分,每一组服务再进行X轴的扩展。
Z轴 :面向查找分割,基于用户、请求或者数据分割,例如可以将不同产品的SKU分到不同的搜索服务,可以将用户哈希到不同的服务等。
应用拆分小结:
要做好微服务的分层,梳理和抽取核心应用、公共应用,作为独立的服务下沉到核心和公共能力层,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。
对于服务的拆分,要使用迭代演进的方式,不能一次性完成所有的服务的拆分,需要确保团队可接受,粒度适中,同时需要优先考虑API的版本兼容性。不能够单纯以代码量来对服务拆分的成果进行评估。
1.3 AKF Cube 拆分数据库
X轴:从单库,水平克隆为多个库上读,一个库写,通过数据库的自我复制实现,要允许一定的读写时延。
Y轴 :根据不同的信息类型,分割为不同的数据库,即分库,例如产品库,用户库等。
Z轴 :按照一定算法,进行分片,例如将搜索按照MapReduce的原理进行分片,把SKU的数据按照不同的哈希值进行分片存储,每个分片再进行X轴冗余。
1.4 参考
2 微服务拆分
2.1 拆分先决条件
微服务与很多基础技术的发展紧密相关,这决定了要迈向微服务有一些必要前提。这其中主要有两个先决条件:一是研发环境和流程的转变,二是拆分前先做好解耦。
具体而言,要准备微服务相关的环境和流程,可以通过以下几个方面建立基本的条件:
- 快速申请硬件、软件资源(Paas平台即云平台)
- 包括自动化工具链(DevOps)
- 微服务框架
- 故障发现反馈机制。
至于研发流程的转变,是一个大工程,需要重新组建团队,以服务为核心,按照业务领域划分全功能团队,改变原有的研发流程、决策机制。例如,倡导敏捷文化、快速迭代,做更多的自动化测试,加强Code Review,给团队更多的自主决策权等。
关于解耦。在数学中,是指使含有多个变量的数学方程变成能够用单个变量表示的方程组,即变量不再同时影响一个方程的结果,从而简化计算。在软件世界里,解耦强调的是每个单元可以独立变化,尽量减少外界对系统内部的影响。解耦有几个关键词,包括状态外置、也就是无状态,去触发器、存储过程,通过接口隔离等。
总结一下,微服务作为云原生最重要的一项理念革新,这里我们只讲了一些微服务的基本概念、原则,后续还将分享更多实操经验,包括微服务API该如何设计,微服务框架如何选择等。
2.2 拆分需求
2.2.1 为什么需要拆分
以电商系统技术架构演进为例,电商系统从一个大系统工程向分布式架构演变过程,你就能很清楚的知道为什么要需要进行应用拆分。
1 人员的角度
维护一个代名工程Denali的百万级代码怪兽(虽然物理部署是分离的),从发布到上线,从人员的角度,百号人同时在一个工程上开发,一旦线上出问题,所有代码都需要回滚,从人员的角度,也基本忍受到了极致。
2 业务的角度
淘宝包含太多业务:用户、商品、交易、支付…等等,所有的代码早期都在denali一个工程里,代码已经严重影响到业务的效率,每个业务有各自的需求,需要给自己应用部署,各自开发需求。
3 从架构的角度
从数据库端oracle数据库集中式架构的瓶颈问题,连接池数量限制(oracle数据库大约提供5000个连接),数据库的CPU已经到达了极限90%。数据库端也需要考虑垂直拆分了。
4.急需走向一个大型的分布式时代,率先需要应用拆分
- 首先工程代码垂直拆分
把整个工程代码按照业务为单元进行垂直拆分。淘宝按照业务为单位拆分成了类似这样的系统:UM(UserManger)、SM(ShopManager)..等等几十个工程代码。
- 应用服务拆分
按照业务为单位,把所有调用相关的接口以业务为单元进行拆分。比如,一个店铺系统,需要访问一个用户的头像的接口,用户头像的接口是独立部署在用户中心(UIC)这边的集群服务器上的。随着拆分的进行,淘宝的业务接口中心就变成了:UIC(用户中心服务)、SIC(店铺中心服务)等等以业务为单元部署的集群。
最终就演变成下图,按照业务为单位拆分和部署服务,用户中心、商品中心等:
总之,系统拆分是单体程序向分布式系统演变的关键一步,也是很重要的一步,拆分的好坏直接关系到未来系统的扩展性、可维护性和可伸缩性等,拆分工作不难理解,但是如何正确拆分、有什么样的方法和原则能帮助我们拆分得到一个我们理想中的系统:高可用、可扩展、可维护、可伸缩的分布式系统。
以下主要再从拆分需求、拆分原则和拆分步骤谈起:
2.2.2 拆分需求
1 组织结构变化
从最初的一个团队逐渐成长并拆分为几个团队,团队按照业务线不同进行划分,为了减少各个业务系统和代码间的关联和耦合,几个团队不再可能共同向一个代码库中提交代码,必须对原有系统进行拆分,以减少团队间的干扰。
2 安全
这里所指的安全不是系统级别的安全,而是指代码或成果的安全,尤其是对于很多具有核心算法的系统,为了代码不被泄露,需要对相关系统进行模块化拆分,隔离核心功能,保护知识产权。
3 替换性
有些产品为了提供差异化的服务,需要产品具有可定制功能,根据用户的选择自由组合为一个完整的系统,比如一些模块,免费用户使用的功能与收费用户使用的功能肯定是不一样的,这就需要这些模块具有替换性,判断是免费用户还是收费用户使用不同的模块组装,这也需要对系统进行模块化拆分。
4 代码解耦提高交付速度
单体程序最大的问题在于系统错综复杂,牵一发而动全身,也许一个小的改动就造成很多功能没办法正常工作,极大的降低了软件的交付速度,因为每次改动都需要大量的回归测试确保每个模块都能正确工作,因为我们不清楚改动会影响到什么,所以需要做大量重复工作,增加了测试成本。这时候就需要对系统进行拆分,理清各个功能间的关系并解耦。
5 技术需求
- 单体程序由于技术栈固定,尤其的是比较庞大的系统,不能很方便的进行技术升级,或者说对引入新技术或框架等处于封闭状态;每种语言都有自己的特点,单体程序没有办法享受到其它语言带来的便利;对应到团队中,团队技术相对比较单一。
- 相比于基于业务的垂直拆分,基于技术的横向拆分也很重要,使用数据访问层可以很好的隐藏对数据库的直接访问、减少数据库连接数、增加数据使用效率等;横向拆分可以极大的提高各个层级模块的重用性。
6 业务需求
由于业务上的某些特殊要求,比如对某个功能或模块的高可用性、高性能、可伸缩性等的要求,虽然也可以将单体整体部署到分布式环境中实现高可用、高性能等,但是从系统维护的角度来考虑,每次改动都要重新部署所有节点,显然会增加很多潜在的风险和不确定定性因素,所以有时候不得不选择将那些有特殊要求的功能从系统中抽取出来,独立部署和扩展。
2.3 拆分原则和方法
2.3.1 拆分原则
1 自动化驱动
部署和运维的成本会随着服务的增多呈指数级增长,每个服务都需要部署、监控、日志分析等运维工作,成本会显著提升。因此,在服务划分之前,应该首先构建自动化的工具及环境。开发人员应该以自动化为驱动力,简化服务在创建、开发、测试、部署、运维上的重复性工作,通过工具实现更可靠的操作,避免微服务数量增多带来的开发、管理复杂度问题。
2 垂直划分优先
企业应该根据业务领域对服务进行垂直划分(DDD-Domain Drive Design,拆分的时候以业务功能划分服务边界),因为水平划分有可能导致下面这些问题,比如调用次数更多,导致性能大幅下降;实现一个功能要跨越更多服务,沟通成本增加等。
3 单一职责
一个服务尽可能只包含一类或一种功能,比如商品信息、订单信息。尽量消除对其他服务的强依赖,这样可以降低沟通成本,提升服务稳定性。服务通过标准的接口隔离,隐藏内部实现细节。这样可以让服务保持独立开发、测试、部署、运行,以服务为单位持续交付。
4 演进式拆分
服务数量快速增长带来架构复杂度急剧升高,开发、测试、运维等环节很难快速适应,会导致故障率大幅增加,可用性降低。非必要情况,应逐步划分、持续演进、避免服务数量的爆炸式增长。这等同于灰度发布的效果,先拿出几个不太重要的功能拆分出一个服务做试验,即便出现故障,影响范围也不会太大。
5 服务粒度适中
共同考虑到服务的单一职责和服务的维护成本。
6 避免环形依赖和双向依赖
将多个服务依赖的功能抽取到一个公共的服务中,或者将某些功能重新进行服务的划分,尽量的划分至最合适的服务中
7 考虑团队结构
2.3.2 拆分模式
有了基本原则,下面讲一些具体的划分模式。大的原则是基于业务复杂度选择服务的划分方法。
- 当业务复杂度足够高的时候,应该基于领域驱动划分服务(许多项目是一些微服务改造项目,业务已经比较复杂,代码量较大,建议选择领域驱动拆分);
- 当业务复杂度较低时,选择基于数据驱动划分服务。
领域驱动则是一种自上而下的架构设计方法,通过和领域专家建立统一的语言,不断交流,确定关键业务场景,逐步确定边界上下文。领域驱动更强调业务实现效果,认为自下而上的设计可能会导致技术人员不能更好地理解业务方向,进而偏离业务目标。通常,基于领域驱动划分服务的步骤如下:通过模型和领域专家建立统一语言,业务分析,寻找聚合,确定服务调用关系,业务流程验证和持续优化。
数据驱动是一种自下而上的架构设计方法,强调的是数据结构,也就是通过分析需求,确定整体数据结构,根据表之间的关系划分服务。统筹,基于数据驱动划分服务的步骤如下:需求分析,抽象数据结构,划分服务,确定调用关系和业务流程验证。
此外,还有一个很常见的服务拆分场景,那就是从已有单体架构中逐步划分服务。之所以说很常见,是因为很多场景下,并非从开始阶段就采用微服务架构,而是随着业务不断发展才逐步走向微服务。这种情况下,划分服务的步骤如下:前后端分离,提取公共基础服务(如单点登录)、不断从老系统中抽象出服务、垂直划分优先,适当水平切分。
2.4 拆分实战
1 设计工程模块骨架
module骨架的设计是基础,影响最终拆分结果,拆分成功的向导。按照技术,业务,部署打包,测试这几个维度来规划设计,下面是一个参考。
最终骨架模型层级:
root webapp //微服务根module,以下的所有子模块都可以打成jar包单独部署
micro-services
user-service
customer-service
order-service
other-service
api-gateway
biz //业务相关的module
entitys //所有实体类
biz-base //一些无法拆分的代码上有依赖的服务
biz-user //用户业务
biz-customer //客户业务
biz-order //订单业务
…
commons
async-framework //一部框架
utils //工具类
2 拆分技术commons
作为第一步,先对整个工程按业务和功能进行了maven多module的拆分。
首先是分离出技术上的commons,感觉这应该是最好拆分的了,把相关的类重构到一个包里,在分离出一个module即可。
3 拆分entity
很多在业务代码上都会共享entity类,通常没法也没法把entity类分门别类,最简单就是把所有的entity类放到一个module,谁需要谁依赖的原则。entity类也没有太多jar依赖和业务依赖,也不会形成污染源。
4 公共业务
同commons和entity方法,不在复述,也被各个业务依赖,这种业务大部分是过渡性的,在未来迭代演进时可以通过其他方式抽象集成。
5 拆分业务代码
拆分业务是最痛苦的事情了,这个要看原来的代码整洁度和互相依赖程度,一般采取2中方法:
新建业务module,加入基础module的pom依赖,再从源module复制和该业务module相关的代码(包括单元测试代码)过来,解决编译错误和单元测试错误,完成本业务拆分。
从源module复制一个新业务module,保持代码一致,先删除和本义务无关的代码(包括单元测试代码),再删除无关的pom依赖,解决编译错误和单元测试错误,完成本业务拆分。
选择哪种方法,可以根据代码整洁度和互相依赖程度,如果代码很整洁且相互依赖较弱,可以采取前者,否则就采取后者。
6 拆分微服务
有了以上的拆分基础,可以在合适的业务迭代将各个微服务module迁移到不同的代码仓库,完成进一步隔离管理。
总结:
微服务架构是互联网技术发展的必然结果,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。
- 明确拆分原则和拆分需求。
- 梳理出业务模块和之间的依赖关联关系。
- 按照业务为单位,拆分实体、以及应用工程单独部署。
- 按照业务为单位拆分应用服务,避免环形依赖和双向依赖。
- 抽离出公用的接口、实体,以及服务单独部署为公用服务。
2.4.2 微服务框架
3 其他参考
3.1 微服务的七大原则
1、接受自动化文化
微服务引入了很多复杂性,其中的关键部分是,我们不得不管理大量的服务。解决这个问题的一个关键方法是,拥抱自动化文化。前期花费一定的成本,构建支持微服务的工具是很有意义的,比如:自动化测试,调用一个统一的命令行,以相同的方式把系统部署到各个环境;使用环境定义来帮助你明确不同环境间的差异,同时保持使用统一的方式进行部署;创建自定义镜像来加快部署,创建全自动化不可变服务器。
PAAS & Devops工具链
2、围绕业务概念建模
经验表明,围绕业务的限界上下文定义的接口,比围绕技术概念定义的接口更加稳定。针对系统如何工作这个领域进行建模,不仅可以帮助我们形成更稳定的接口,也能确保我们能够更好地反映业务流程的变化。使用限界上下文来定义可能的领域边界。
DDD(Domain Drive Design,根据业务领域对服务进行垂直划分,拆分的时候以业务功能划分服务边界)
3、隐藏内部实现细节
为了使一个服务独立于其他服务,最大化独立演化的能力,隐藏实现细节至关重要。隐藏服务的数据库,以避免陷入数据库耦合;使用数据泵(data pump)或事件数据泵(event data pump),将跨多个服务的数据整合到一起,以实现报表功能。
4、让一切都去中心化
为了最大化微服务能带来的自治性,我们需要持续寻找机会,给拥有服务的团队委派决策和控制权。在这个旅程中,确保团队保持对服务的所有权是重要的一步,理想情况下,甚至可以让团队自己决定什么时候让那些更改上线。使用内部开源模式,确保人们可以更改其他团队拥有的服务,不过请注意,实现这种模式需要很多的工作量。
像企业服务总线或服务编配系统这样的方案,会导致业务逻辑的中心化和哑服务,应该避免使用它们。使用协同来代替编排或哑中间件,使用智能终端点(smart endpoint)确保相关的逻辑和数据,在服务限界内能保持服务的内聚性。
Pizza团队(一个微服务一个Pizza团队,pizza团队负责服务的需求、开发计划、编码、测试、构建、上线…)
5、可独立部署
我们应当始终努力确保微服务可以独立部署。我们也应该同时提供新旧两个版本,允许消费者慢慢迁移到新版本。当使用基于RPC的集成时,避免使用像Java RMI提供的那种使用生成的桩代码,紧密绑定客户端/服务器的技术。
通过采用单服务单主机模式,可以减少部署一个服务引发的副作用,比如影响另一个完全不相干的服务。请考虑使用蓝/绿部署或金丝雀部署技术,区分部署和发布,降低发布出错的风险。使用消费者驱动的契约测试,在破坏性的更改发生前捕获它们。
请记住,你可以更改单个服务,然后把它部署到生产环境,无需联动地部署其他任何服务,这应该是常态,而不是例外。你的消费者应该自己决定何时更新,你需要适应他们。
6、隔离失败
微服务架构比单块架构更具弹性,前提是我们了解系统的故障模式,并做出相应的计划。如果我们心中持有反脆弱的信条,预期在任何地方都会发生故障,这说明我们正走在正确的路上。请确保正确设置你的超时,了解何时及如何使用舱壁和断路器,来限制故障组件的连带影响。如果系统只有一部分行为不正常,要了解其对用户的影响。知道网络分区可能意味着什么,以及在特定情况下牺牲可用性或一致性是否是正确的决定。
Hystrix(熔断器)& Sentinel(限流)
7、高度可观察
我们不能依靠观察单一服务实例,或一台服务器的行为,来看系统是否运行正常。相反,我们需要从整体上看待正在发生的事情。通过注入合成事务到你的系统,模拟真实用户的行为,从而使用语义监控来查看系统是否运行正常。聚合你的日志和数据,这样当你遇到问题时,就可以深入的解析原因。而当需要重现令人讨厌的问题,或仅仅查看你的系统在生产环境是如何交互时,关联标识可以帮助你跟踪系统间的调用。
Spring Boot Admin(服务指标监控)& Skywalking(链路追踪)& Prometheus(服务器+服务指标监控)3.2 不一样的微服务拆分规范和原则
微服务的拆分规范和原则
4 参考文章
一起玩转微服务(8)——服务拆分原则
分布式架构系统拆分原则、需求、微服务拆分步骤
微服务,如何拆分服务是精髓|技术前沿
微服务拆分原则
微服务的拆分规范和原则
微服务拆分的原则、方法和误区