分布式架构原理与实践读书笔记
IT 软件架构的更迭:从单体架构,到集群架构,到现在的分布式和微服务架构。
分布式架构具有分布性、自治性、并行性、全局性等特点。
为了应对请求的高并发和业务的复杂性,需要对应用服务进行合理拆分,将其从原来的大而集中变成小而分散;
要想让这些分散的服务共同完成计算任务,就需要解决它们之间的通信与协同问题;
和服务一样,负责存储的数据库也会有分散的情况,因此需要考虑分散存储;
如果说所有的服务、数据库都需要硬件资源作为支撑,那么对资源的管理和调度也是必不可少的;
此外,软件系统上线以后,还需要对关键指标进行监控。
高并发、高可用、可伸缩、可扩展、够安全一直都是架构设计所追求的目标。
架构的演进过程
1、应用与数据一体模式
2、应用与数据分离模式
随着业务的发展,用户数量和请求数量逐渐上升,服务器的性能便出现了问题。一个比较简单的解决方案是增加资源,将业务应用和数据分开存储。
3、缓存技术引入
随着信息化系统的发展和互联网使用人数的增多,业务量、用户量、数据量都在增长。同时我们还发现,用户对某些数据的请求量特别大,例如新闻、商品信息和热门消息。在之前的模式下,获取这些信息的方式是依靠数据库,因此会受到数据库 IO 性能的影响,久而久之,数据库便成为了整个系统的瓶颈。而且即使再增加服务器的数量,恐怕也很难解决这个问题,于是缓存技术就登场了。
缓存技术分为客户端浏览器缓存、应用服务器本地缓存和缓存服务器缓存。
● 客户端浏览器缓存:
如果将每次HTTP 请求都缓存下来,就可以极大地减小应用服务器的压力。
● 应用服务器本地缓存:
这种缓存使用的是进程内缓存,又叫托管堆缓存。以 Java 为例,这部分缓存存放在 JVM 的托管堆上面,会受到托管堆回收算法的影响。由于它运行在内存中,对数据的响应速度很快,因此通常用于存放热点数据。当进程内缓存没有命中时,会到缓存服务器中获取信息,如果还是没有命中,才会去数据库中获取。
● 缓存服务器缓存:
这种缓存相对于应用服务器本地缓存来说,就是进程外缓存,既可以和应用服务部署在同一服务器上,也可以部署在不同的服务器上。一般来说,为了方便管理和合理利用资源,会将其部署在专门的缓存服务器上。由于缓存会占用内存空间,因此这类服务器往往会配置比较大的内存。
加入缓存技术后,系统性能得到了提高。这是因为缓存位于内存中,而内存的读取速度要比磁盘快得多,能够很快响应用户请求。特别针对一些热点数据,优势尤为明显。同时,在可用性方面也有明显改善,即使数据服务器出现短时间的故障,在缓存服务器中保存的热点数据或者核心数据依然可以满足用户暂时的访问。
4、服务器集群:处理并发
可随着用户请求量的增加,另外一个问题又出现了,那就是并发。
把这两个字拆开了来看:并,可以理解为“一起并行”,有同时的意思;发,可以理解为“发出调用”,也就是发出请求的意思。
合起来,并发就是指多个用户同时请求应用服务器。
如果说原来的系统面对的只是大数据量,那么现在就需要面对多个用户同时请求。
服务器集群说白了,就是多台服务器扎堆的意思,用更多服务器来分担单台服务器的负载压力,提高性能和可用性。再说白一点,就是提高单位时间内服务处理请求的数量。原来是一台服务器处理多个用户的请求,现在是一堆服务器处理,就好像银行柜台一样,通过增加柜员的人数来服务更多的客户。
此时需要注意负载均衡器采用的均衡算法(例如轮询和加权轮询)要能保证用户请求均匀地分布到多台服务器上、属于同一个会话的所有请求在同一个服务器上处理,以及针对不同服务器资源的优劣能够动态调整流量。
5、数据库读写分离
加入缓存可以解决部分热点数据的读取问题,但缓存的容量毕竟有限,那些非热点的数据依然要从数据库中读取。数据库对于写入和读取操作的性能是不一样的。在写入数据时,会造成锁行或者锁表,此时如果有其他写入操作并发执行,就会出现排队现象。而读取操作不仅比写入操作更加快捷,并且可以通过索引、数据库缓存等方式实现。因此,推出了数据库读写分离的方案:
设置了主从数据库,主库(master)主要用来写入数据,然后通过同步binlog 的方式,将更新的数据同步到从库(slave)中。对于应用服务器而言,在写数据时只需要访问主库,在读数据时只用访问从库就好了。
体会到数据库读写分离带来的益处的同时,架构设计也需要考虑可靠性的问题。例如,如果主库挂掉,从库如何接替主库进行工作;之后主库恢复了,是成为从库还是继续担任主库,以及主从库如何同步数据。
6、反向代理和 CDN
随着互联网的逐渐普及,人们对网络安全和用户体验的要求也越来越高。
之前用户都是通过客户端直接访问应用服务器获取服务,这使得应用服务器暴露在互联网中,容易遭到攻击。
如果在应用服务器与互联网之间加上一个反向代理服务器,由此服务器来接收用户的请求,然后再将请求转发到内网的应用服务器,相当于充当外网与内网之间的缓冲,就可以解决之前的问题。
反向代理服务器只对请求进行转发,自身不会运行任何应用,因此当有人攻击它的时候,是不会影响到内网的应用服务器的,这在无形中保护了应用服务器,提高了安全性。同时,反向代理服务器也在互联网与内网之间起适配和网速转换的作用。
CDN,它的全称是Content Delivery Network,也就是内容分发网络。如果把互联网想象成一张大网,那么每台服务器或者每个客户端就是分布在这张大网中的节点。节点之间的距离有远有近,用户请求会从一个节点跳转到另外一个节点,最终跳转到应用服务器获取信息。跳转的次数越少,越能够快速地获取信息,因此可以在离客户端近的节点中存放信息。这样用户通过客户端,只需要跳转较少的次数就能够触达信息。
由于这部分信息更新频率不高,因此推荐存放一些静态数据,例如 JavaScript 文件、静态的HTML、图片文件等。
这样客户端就可以从离自己最近的网络节点获取资源,大大提升了用户体验和传输效率。
CDN 的加入明显加快了用户访问应用服务器的速度,同时减轻了应用服务器的压力,原来必须直接访问应用服务器的请求,现在不需要经过层层网络,只要找到最近的网络节点就可以获取资源。但从请求资源的角度来看,这种方式也有局限性,即它只对静态资源起作用,而且需要定时对 CDN 服务器进行资源更新。反向代理和 CDN 的加入解决了安全性、可用性和高性能的问题。
7、分布式数据库与分库分表
随着系统运行时间的增加,数据库中累积的数据越来越多,同时系统还会记录一些过程数据,例如操作数据和日志数据,这些数据也会加重数据库的负担。即便数据库设置了索引和缓存,但在进行海量数据查询时还是会表现得捉襟见肘。
如果说读写分离是对数据库资源从读写层面进行分配,那么分布式数据库就需要从业务和数据层面对数据库进行分配。
● 对于数据表来说,当表中包含的记录过多时,可将其分成多张表来存储。
● 对于数据库来说,每个数据库能够承受的最大连接数和连接池是有上限的。为了提高数据访问效率,会根据业务需求对数据库进行分割,让不同的业务访问不同的数据库。当然,也可以将相同业务的不同数据放到不同的数据库中存储。
**如果将数据库资源分别放到不同的数据库服务器中,就是分布式数据库设计。**由于数据存储在不同的表/库中,甚至在不同的服务器上面,因此在进行数据库操作的时候会增加代码的复杂度。此时可以加入数据库中间件来实现数据同步,从而消除不同存储载体间的差异。
数据库的分布式设计以及分表分库,会给系统带来性能的提升,同时也增大了数据库管理和访问的难度。原来只需访问一张表和一个库就可以获取数据,现在需要跨越多张表和多个库。
从软件编程的角度来看,有一些数据库中间件提供了最佳实践,例如 MyCat 和 Sharding JDBC。
此外,从数据库服务器管理的角度来看,需要监控服务器的可用性。
从数据治理的角度来看,需要考虑数据扩容和数据治理的问题。
8、业务拆分
通过对前面几个阶段的学习,我们知道系统提升依靠的基本都是以空间换取时间,使用更多的资源和空间处理更多的用户请求。随着业务的复杂度越来越高,以及高并发的来临,一些大厂开始对业务应用系统进行拆分,将应用分开部署。
如果说前面的服务器集群模式是将同一个应用复制到不同的服务器上,那么业务拆分就是将一个应用拆成多个部署到不同的服务器中。此外,还有的是对核心应用进行水平扩展,将其部署到多台服务器上。
应用虽然做了拆分,但应用之间仍旧有关联,存在相互之间的调用、通信和协调问题。
由此引入了队列、服务注册发现、消息中心等中间件,这些中间件可以协助系统管理分布到不同服务器、网络节点上的应用。
业务拆分以后会形成一个个应用服务,既有基于业务的服务,例如商品服务、订单服务,也有基础服务,例如消息推送和权限验证。这些应用服务连同数据库服务器分布在不同的容器、服务器、网络节点中,它们之间的通信、协调、管理和监控都是我们需要解决的问题。
9、分布式与微服务
微服务架构它对业务应用进行了更加精细化的切割,使之成为更小的业务模块,能够做到模块间的高内聚低耦合,每个模块都可以独立存在,并由独立的团队维护。
每个模块内部可以采取特有的技术,而不用关心其他模块的技术实现。
模块通过容器的部署运行,各模块之间通过接口和协议实现调用。可以将任何一个模块设为公开,以供其他模块调用,也可以热点模块进行水平扩展,增强系统的整体性能,这样当其中某一个模块出现问题时,就能由其他相同的模块代替其工作,增强了可用性。
大致总结下来,微服务拥有以下特点:业务精细化拆分、自治性、技术异构性、高性能、高可用。它像极了分布式架构,从概念上理解,二者都做了“拆”的动作,但在下面这几个方面存在区别:
微服务架构与分布式架构的区别
1、拆分目的不同
提出分布式设计是为了解决单体应用资源有限的问题,一台服务器无法支撑更多的用户访问,因此将一个应用拆解成不同的部分,然后分别部署到不同服务器上,从而分担高并发的压力。
微服务是对服务组件进行精细化,目的是更好地解耦,让服务之间通过组合实现高性能、高可用、可伸缩、可扩展。
2、拆分方式不同
分布式服务架构将系统按照业务和技术分类进行拆分,目的是让拆分后的服务负载原来单一服务的业务。
微服务则是在分布式的基础上进行更细的拆分,它将服务拆成更小的模块,不仅更专业化,分工也更为精细,并且每个小模块都能独立运行。
3、部署方式不同
分布式架构将服务拆分以后,通常会把拆分后的各部分部署到不同服务器上。
而微服务既可以将不同的服务模块部署到不同服务器上,也可以在一台服务器上部署多个微服务或者同一个微服务的多个备份,并且多使用容器的方式部署。
虽然分布式与微服务具有以上区别,但从实践的角度来看,它们都是基于分布式架构的思想构建的。
可以说微服务是分布式的进化版本,也是分布式的子集。
分布式架构的一个简单例子
此订单业务架构分为四层:
客户端、负载均衡器(可以称为接入层)、应用服务器(可以称为应用层)、数据服务器(可以称为存储层)
客户端:用户与系统之间的接口,用户在这里进行商品浏览、下单等。
接入层:负载均衡器可以通过用户 IP 将用户的请求路由到不同的服务器集群。另外还可以进行流量控制和身份验证等操作。
应用层:用于部署主要的应用服务,例如商品服务、订单服务、支付服务、库存服务和通知服务。
存储层:数据的读写,主、备数据库。
存储可以采用分布式存储,所谓分布式存储就是:电商系统中商品信息的数据量比较大,为了提高访问效率,通常会将数据分片存放,被拆分以后的商品表会分布到不同的数据库或者服务器中。
分布式架构的特征
分布性:
将分布两字分开来看,“分”指的是拆分,可以理解为服务的拆分、存储数据的拆分、硬件资源的拆分。
布”指的是部署,也指资源的部署。既有计算资源,也有存储资源。简单来说,
分布性就是拆开了部署。
自治性:
分布性导致了自治性。简单来说,自治性就是每个应用服务都有管理和支配自身任务和资源的能力。
并行性/并发性:
自治性导致每个应用服务都是一个独立的个体,拥有独立的技术和业务,占用独立的物理资源。这种独立能够减小服务之间的耦合度,增强架构的可伸缩性,为并行性打下基础。
全局性:
分布性使得服务和资源都是分开部署的,自治性说明单个服务拥有单独的业务和资源,多个服务通过并行的方式完成大型任务。
多个分布在不同网络节点的服务应用在共同完成一个任务时,需要有全局性的考虑。
说白了,就是分散的资源要想共同完成一件大事,需要沟通和协作,也就是拥有大局观。
分布式架构的问题
分布式架构需要解决的问题的顺序:
(1) 分布式是用分散的服务和资源代替几种服务和资源,所以先根据业务进行应用服务拆分。
(2) 由于服务分布在不同的服务器和网络节点上,所以要解决分布式调用的问题。
(3) 服务能够互相感知和调用以后,需要共同完成一些任务,这些任务或者共同进行,或者依次进行,因此需要解决分布式协同问题。
(4) 在协同工作时,会遇到大规模计算的情况,需要考虑使用多种分布式计算的算法来应对。
(5) 任何服务的成果都需要保存下来,这就要考虑存储问题。和服务一样,存储的分布式也可以提高存储的性能和可用性,因此需要考虑分布式存储的问题。
(6) 所有的服务与存储都可以看作资源,因此需要考虑分布式资源管理和调度。
(7) 设计分布式架构的目的是实现高性能和可用性。为了达到这个目的,一起来看看高性能与可用性的最佳实践,例如缓存的应用、请求限流、服务降级等。
(8) 最后,系统上线以后需要对性能指标进行有效的监控才能保证系统稳定运行,此时指标与监控就是我们需要关注的问题。
1、应用服务拆分
技术的实现来源于业务,那么对业务的分析就需要放在第一位。我们可以利用 DDD(Domain-Driven Design,领域驱动设计)的方法定义领域模型,确定业务和应用服务的边界,最终引导技术的实现。按照 DDD 方法设计出的应用服务符合“高内聚、低耦合”的标准。
DDD 并不是架构,而是一种架构设计的方法论,它通过边界划分将业务转化成领域模型,领域模型又形成应用服务的边界,协助架构落地。
DDD 是一种专注于复杂领域的设计思想,其围绕业务概念构建领域模型,并对复杂的业务进行分隔,再对分隔出来的业务与代码实践做映射。
主要包括:
● 领域驱动设计的模型结构:包括领域、领域分类、子域、领域事件、聚合、聚合根、实体和值对象的介绍。
● 分析业务需求形成应用服务:包括业务场景分析、抽象领域对象、划定限界上下文。
● 领域驱动设计分层架构:包括分层原则、每层内容和特征,以及分层实例。
A、基于 DDD 思想的业务拆分实践
完成整个应用服务的拆分需要三步,分别是分析、抽取和构建。
建立任何一个软件架构都是为了完成业务需求,而业务需求是用来完成商业目标的。这里以构建一个学生选课系统为例,讲解服务分析和抽取的整个过程。学生选课系统的业务背景如下。
● 学生可以通过系统选择选修课,并且提交选择选修课的申请,之后教务处负责审核。
● 教务处的老师收到选修课的申请以后,根据审批规则进行核对,最终产生审批结果:通过或者不通过。
● 获得上选修课资格的学生,去上课的时候需要签到,老师会检查签到情况,并在课程结束的时候生成签到明细。同时学生也可以查看自己的签到情况。
基本上可以总结为:学生申请选修课,教务处审批选修课,学生签到并且上课老师查询签到记录。
1、拆分思路
(1) 根据不同的业务场景创建业务流程,在每个业务流程的节点上标注参与者、命令和事件信息。
(2) 根据标注的参与者、命令和事件信息生成领域对象,包括实体、值对象、聚合、领域事件等。领域专家和技术团队通过通用语言,对相关的领域对象进行进一步划分,形成聚合并找到聚合根。
(3) 通过聚合划定限界上下文,这里需要依赖通用语言,因为同样一个事务在不同的限界上下文中所指的内容和含义可能有所不同。限界上下文就是服务的边界,根据它来创建服务或者应用。
2、拆分流程
通过上面对业务需求的描述,我们可以把业务需求分为三个场景,分别是申请选修课场景、审批选修课场景和选修课签到场景。接下来我们分别画出这三个场景对应的业务流程图,并且标注参与者、命令和事件信息。
3、抽取领域对象和生成聚合
通过分析业务,将需求分成了参与者、业务流程、命令和事件。然后将它们对应领域对象,生成了领域对象之间的关系。
抽取的目的是观察领域对象之间的关联和共性,最终对它们进行聚合和限界上下文划分
用不同的形状表示三个场景中的领域对象:
圆形表示实体,
长方形表示命令,
五边形表示事件。
注意,这里只粗略地划分领域对象,不做细分,因为目的是划分服务和聚合的边界。
通过抽取领域对象,可以看到:
1、选修课申请 这个实体在 申请选修课场景 和 审批选修课场景中都存在,且含义相同。
同样,审批规则实体 和 登录命令也都存在于申请选修课场景 和 审批选修课场景中。
2、签到明晰 实体单独存在于选修课签到场景,且学生和老师都存在于三个场景中则属于通用实体。
虽然选修课实体在三个场景中都存在,但申请选修课场景和审批选修课场景中的选修课描述的是课程本身,包括课程内容、学分;
而选修课签到场景中的选修课更多的是关心上下课的时间、上课的位置等信息。这正是上下文不一致导致的,同一事物在不同上下文中的含义出现了偏差。
聚合是逻辑上的边界,为限界上下文的划分提供依据。要想生成聚合,首先需要考虑聚合的逻辑独立性,即能否在聚合内部完成一个完整的业务逻辑。
对于前面提到的申请选修课场景、审批选修课场景、选修课签到场景,当然可以生成三个聚合。但是考虑到前两个场景都是在完成申请审批的业务流程,因此可以合并为一个聚合。
当然,如果业务合并在一起后显得比较复杂,也可以进行再次拆分。
同时,选修课签到场景可以自己生成一个聚合,其中学生、老师实体属于组织关系,比较通用,系统中的其他地方应该也会用到这样的概念,所以可以抽取出来作为单独的聚合。
4、划定限界上下文
生成的聚合划分限界上下文,也就是生成服务的边界。
如果说聚合是服务的逻辑边界,那么限界上下文就是服务的物理边界。
从完成业务的角度来看,选修课申请聚合和签到聚合分属不同的语义环境。
将三个聚合划分为选修课申请、签到、人员组织三个限界上下文。人员组织可以作为通用域,协助另外两个子域。
选修课申请作为单独的限界上下文,承载大部分实体、命令和事件,可以考虑将其称为核心域。
签到则可以作为支撑域,用来支撑核心域。
如上所示的三个限界上下文,可以由三个应用服务对应实现,分别是选修课申请服务、签到服务、人员组织服务。
这里也体现了我们想表达的分布式应用服务的拆分概念。
当然,这种划分不是唯一的选择,例如选修课申请本身就是一个聚合,可以将这个聚合继续拆分成申请和审批两个聚合。
随着业务的发展和变化,也可能衍生出新的限界上下文。这些需要不断利用领域驱动设计的思想去迭代。
关于限界上下文之间的通信,可以通过领域事件的方式进行。
B、领域驱动设计分层理论及其对应的项目代码结构
领域驱动设计分层能够帮助我们把领域对象转化为软件架构。
在分解复杂的软件系统时,分层是最常用的一种手段。
在领域驱动设计的思想中,分层代表软件框架,是整个分布式架构的“骨架”;
领域对象是业务在软件中的映射,好比“血肉”。
分层不仅让我们能够站在一个更高的位置看待软件设计,还给整个架构带来了高内聚、低耦合、可扩展、可复用等优势。
1、如何分层
架构分层看上去,就是按照功能对每层进行分割和堆叠。但在具体落地时还需要考虑清楚,每层的职责以及层与层之间的依赖关系。
架构有分成三层的,也有分成四层、五层的。
业务情况、技术背景,以及团队架构不同,分层也会有所不同。这里通过领域驱动设计的分层方式,给分布式架构提供分层思路。
从上往下分别是用户接口层、应用层、领域层和基础层。箭头表示层和层之间的依赖与被依赖关系。例如,箭头从用户接口层指向应用层,表示用户接口层依赖于应用层。从图中可以看到,基础层被其他所有层依赖,位于最核心的位置。
但这种分法和业务领导技术的理念是相冲突的,搭建分布式架构时是先理解业务,然后对业务进行拆解,最后将业务映射到软件架构。这么看来,领域层才是架构的核心,所以上图中的四层架构的依赖关系是有问题的。
于是出现了 DIP(Dependency Inversion Principle,依赖倒置原则),DIP 的思想指出:高层模块不应该依赖于底层模块,这两者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。因此,作为底层的基础层应该依赖于用户接口层、应用层和领域层提供的接口。高层是根据业务展开的,通过对业务抽象产生了接口,底层依赖这些接口为高层提供服务。
1、用户接口层
也称为表现层,包括用户界面、Web 服务和远程调用三部分。该层负责向用户显示信息和解释用户指令。该层的主要职责是与外部用户、系统交互,接受反馈,展示数据。
2、应用层
应用层比较简单,不包含业务逻辑,用来协调领域层的任务和工作。应用层负责组织整个应用流程,是面向用例设计的。
通常,应用服务是运行在应用层的,负责服务组合、服务编排和服务转发,组合业务执行顺序以及拼装结果。
并不能说应用层和业务完全无关,它以粗粒度的方式对业务做简单组合。
具体功能有信息安全认证、用户权限校验、事务控制、消息发送和消息订阅等。
3、领域层
领域层实现了应用服务的核心业务逻辑,并保证业务的正确性。这层体现了系统的业务能力,用来表达业务概念、业务状态和业务规则。
领域层包含领域驱动设计中的领域对象,例如聚合、聚合根、实体、值对象、领域服务。领域模型的业务逻辑由实体和领域服务实现。
领域服务描述了业务操作的过程,可以对领域对象进行转换,处理多个领域对象,产生一个结果。
领域服务和应用服务的区别是,它具有更多的业务相关性。
4、基础层
基础层为其他三层提供通用的技术和基础服务,包括数据持久化、工具、消息中间件、缓存等。
例如在基础层实现的数据库访问,就是面向领域层接口的。
领域层只是根据业务向基础层发出命令,告诉它需要提供的数据规格(数据规格包括用户名字、身份证、性别、年龄等信息),基础层负责获取对应的数据并交给领域层。具体如何获取数据、从什么地方获取数据,这些问题全部都是基础层需要考虑的,领域层是不关心的。
领域层都面向同一个抽象的接口,这个接口就是数据规格。当数据库的实现方式发生更换时,例如从 Oracle 数据库换成了 MySQL 数据库,只要基础层把获取数据的实现方式修改一下即可;领域层则还是遵循之前的数据规格,进行数据获取,不受任何影响。
2、分层结构图
从上往下看。
首先是用户接口层,包括用户界面、Web 服务以及信息通信功能。作为系统的入口,用户接口层下面是应用层,这一层主要包括应用服务,但不包含具体的业务,只是负责对领域层中的领域服务进行组合、编排和转发。
应用层下面是领域层,这一层包括聚合、实体、值对象等领域对象,负责完成系统的主要业务逻辑。领域服务负责对一个或者多个领域对象进行操作,从而完成需要跨越领域对象的业务逻辑。
用户接口层、应用层、领域层下方和右方的是基础层,这层就和它的名字一样,为其他三层提供基础服务,包括 API 网关、消息中间件、数据库、缓存、基础服务、通用工具等。除了提供基础服务,基础层还是针对通用技术的解耦。
3、服务内部的分层调用与服务间调用
将分层思想落地到分布式架构或者微服务架构,每个被拆分的应用或者服务都包含用户接口层、应用层、领域层。那么服务内部以及服务之间是如何完成调用的呢?可以看下图:
4、将分层映射到代码结构
代码结构是层次结构在代码实现维度的映射。好的层次设计有助于设计代码结构,好的代码结构设计更容易让人对整体软件架构有清晰的理解。
1、用户接口层的代码结构
展示层的 VO(ViewObject)传入到用户接口层后,先通过 Assembler 转换为 DTO,再由 Facade 往下传递。
● Assembler:起格式转换的作用。传入用户接口层的数据和用户接口层中的数据,格式有可能是不一样的。例如展示层提交了一个表单,我们称之为 VO(View Object,视图对象),这个 VO 传入用户接口层之后需要经过 Assembler 转换,形成用户接口层能够识别的 DTO 格式的数据。
● DTO(Data Transfer Object,数据传输对象):它是用户接口层数据传输的载体,不包含业务逻辑,由 Assembler 转换而得。DTO 可以将用户接口层与外界隔离。
● Facade:门面,是服务提供给外界系统的接口,也是调用领域服务的入口。Facade 提供较粗粒度的调用接口,通常不包含业务逻辑,只是将用户请求转交给应用服务进行处理。一般地,提供API 服务的 Controller 就是一个 Facade。
2、应用层代码结构
用户接口层传入的消息先转换成 Command,然后交给 Application Service 做处理。
Application Service 负责连接领域层,调用领域服务、聚合(根)等领域对象,对业务逻辑进行编排和组装。同时,Application Service 还协助领域层订阅和发布 Event。
● Command:命令,可以理解为用户所做的操作,例如下订单、支付等,是应用服务的传入参数。
● Application Service:应用服务,会调用和封装领域层的 Aggregate、Service、Entity、Repository、Factory。其主要实现组合和编排,本身不实现业务,只对业务进行组合。
● Event:事件,这里主要存放事件相关的代码,负责事件的订阅和发布。事件的发起和响应则放在领域层处理。如果用订报纸来举例,那么应用层的Event 负责的是订阅报纸和联系发布报纸,阅读订阅的报纸和发布报纸的具体工作则由领域层的Event 完成。
3、领域层代码结构
领域层的代码结构包括一个或者多个 Aggregate(聚合)。每个 Aggregate 又包括 Entity、Event、Repository、Service、Factory 等,这些领域模型共同完成核心业务逻辑。
应用层依赖于领域层中的Aggregate 和 Service。
Aggregate 中包含Entity 和值对象。
Service 会对领域对象进行组合,完成复杂的业务逻辑。
Aggregate 中的方法和 Service 中的动作都会产生 Event。
所有领域对象的持久化和查询都由 Repository 实现。
● Aggregate:聚合,聚合的根目录通常由一个实体的名字来表示,例如订单、商品。由于聚合定义了服务内部的逻辑边界,因此聚合中的实体、值对象、方法都围绕某一个逻辑功能展开,例如订单聚合包括订单项信息、下单方法、修改订单的方法和付款方法等,其主要目的是实现业务的高内聚。由于一个服务由多个聚合组成,因此服务的拆分和扩容都可以根据聚合重新编排。
比如当服务 1 中的聚合 C 成为业务瓶颈时,可以将其扩展到服务 3 中。又或者由于业务重组,聚合 A 可以从服务 1 迁移到服务 2 中。
● Entity:实体,包括业务字段和业务方法。跨实体的业务逻辑代码则可以放到 Service 中。
● Event:领域事件,包括与业务活动相关的逻辑代码,例如订单已创建、订单已付款。作为负责聚合间沟通的工具,Event 需要实现发送和监听事件的功能。建议将监听事件的代码单独存放在listener 目录中。
● Service:领域服务,包括需要由一个或者多个实体共同完成的服务,或者需要调用外部服务完成的功能,例如订单系统需要调用外部的支付服务来完成支付操作。如果 Service 的业务逻辑比较复杂,可以针对每个 Service 分别设计类,遇到需要调用外部系统的地方最好采用代理类来实现,以做到最大程度的解耦。
● Repository:仓库,其作用是持久化对象。针对数据的操作都放在这里,主要是读取和查询。一般来说,一个 Aggregate 对应一个 Repository。
4、基础层
基础层的代码结构主要包括工具、算法、缓存、网关以及一些基础通用类。这层的目录存放比较随意,根据具体情况具体决定。这里也不做具体的规定,仅给出一个例子以供参考。最上面是 infrastructure 目录,它下面存放着config 和 util 文件夹,分别存放与配置和工具相关的代码。
5、完整的分层结构及其代码目录
C、代码分层示例
先介绍业务背景,我们要实现一个创建订单的功能,其中每个订单都有多个订单项,每个订单项分别对应一个产品,产品有对应的价格;可以根据订单项和订单的价格计算订单总价,针对每个订单设置对应的送单地址。
其中 userinterface 文件夹下面就是用户接口层的内容,这里比较简单,是一个Web API 的 controller,负责对外提供访问接口,由于没有对象转换,所以 assembler 和 dto 文件夹是空的。
infrastructure 目录里面存放的是基础层的内容。由于需要定义聚合根,因此aggregate 目录中存放的是聚合的基础类。event 目录中存放的是事件相关的基础类。同样,exception 目录存放针对异常定义的基础类,jackson 目录存放针对序列化、反序列化的基础类,repository 目录存放数据仓库的基础类。
首先,请求由用户接口层传入,由于是创建订单操作,所以会把 CreateOrderCommand 命令作为参数传入 OrderController 类中。
接收到该命令以后,用户接口层会调用应用层中的OrderApplicationService,其中的 createOrder 方法会分别调用领域层的 OrderFactory 和OrderRepository。
OrderFactory 的 create 方法可以生成聚合根 Order,然后调用 Order 中的create 方法生成订单。
之后 Order 会调用raiseEvent 方法向其他服务发送OrderCreatedEvent,以通知其他服务订单已创建。
createOrder 会调用 OrderRepository 中的save 方法,传入参数是 Order,将 Order 保存到数据库中。
应用层中的服务只负责生成聚合根 Order,然后将其保存下来。
在领域层的聚合根 Order 中,是通过 create 方法创建订单的,在订单生成以后才通过 raiseCreatedEvent 发送消息。
D、领域驱动设计的一些补充
如果说领域与子域的概念是从业务角度出发告诉我们如何对业务定义边界,那么该如何划分这个边界,又如何将业务边界定义到技术上呢?
答案是限界上下文。这又是什么?我们来拆分一下这个词,“限”表示限制,“界”是边界的意思,“限界”就是限制边界;“上下文”是对话的语境,以一个产品为例,它在生产阶段是“原料和配件”,在销售阶段是“商品”,在物流阶段是“货物”。同样一个东西根据环境的不同被赋予了不同的意义,这个环境就是上下文。
2、分布式调用
服务与资源一旦分散开,要想调用就没有那么简单了。需要针对不同的用户请求,找到对应的服务模块,比如用户下订单就需要调用订单服务。当大量用户请求相同的服务,又存在多个服务的时候,需要根据资源分布将用户请求均匀分配到不同服务上去。就好像用户浏览商品时,有多个商品服务可供选择,那么由其中哪一个提供服务呢?
针对调用的问题,在不同架构层面有不同的处理方式:
在用户请求经过互联网进入应用服务器之前,需要通过负载均衡和反向代理;
在内网的应用服务器之间需要 API 网关调用;
服务与服务之间可以通过服务注册中心、消息队列、远程调用等方式互相调用。
因此可以将分布式调用总结为两部分,
第一部分是感知对方,包括负载均衡、API 网关、服务注册与发现、消息队列;
第二部分是信息传递,包括 RPC、RMI、NIO 通信。
3、分布式协同
分布式协同顾名思义就是大家共同完成一件事,而且是一件大事。
在完成这件大事的过程中,难免会遇到很多问题。
例如,同时响应多个请求的库存服务会对同一商品的库存进行“扣减”,为了保证商品库存这类临界资源的访问独占性,引入了分布式锁的概念,让多个“扣减”请求能够串行执行。
又例如,在用户进行“下单”操作时,需要将“记录订单”(订单服务)和“扣减库存”(库存服务)放在事务中处理,要么两个操作都完成,要么都不完成。
再例如,对商品表做了读写分离之后,产生了主从数据库,当主库发生故障时,会通过分布式选举的方式选举出新的主库,以替代原来主库的工作。我们将这些问题归纳为以下几点。
● 分布式系统的特性与互斥问题:集中互斥算法、基于许可的互斥算法、令牌环互斥算法。
● 分布式锁:分布式锁的由来和定义、缓存实现分布式锁、ZooKeeper 实现分布式锁、分段加锁。
● 分布式事务:介绍分布式事务的原理和解决方案。包括 CAP、BASE、ACID 等的原理;DTP 模型;2PC、TCC 方案。
● 分布式选举:介绍分布式选举的几种算法,包括Bully 算法、Raft 算法、ZAB 算法。
● 分布式系统的实践: ZooKeeper
4、分布式计算
针对海量数据的计算,分布式架构通常采用水平扩展的方式来应对挑战。在不同的计算场景下计算方式会有所不同,计算模式分为两种:
针对批量静态数据计算的 MapReduce 模式,
以及针对动态数据流进行计算的 Stream 模式。
5、分布式存储
简单理解,存储就是数据的持久化。从参与者的角度来看,数据生产者生产出数据,然后将其存储到媒介上,数据使用者通过数据索引的方式消费数据。
从数据类型上来看,数据又分为结构化数据、半结构化数据、非结构化数据。在分布式架构中,会对数据按照规则分片,对于主从数据库还需要完成数据同步操作。如果要建立一个好的数据存储方案,需要关注数据均匀性、数据稳定性、节点异构性以及故障隔离几个方面。
● 数据存储面临的问题和解决思路:RAID 磁盘阵列。
● 分布式存储的要素和数据类型分类。
● 分布式关系数据库:分表分库、主从复制、数据扩容。
● 分布式缓存:缓存分片算法、Redis 集群方案、缓存节点之间的通信、请求分布式缓存的路由、缓存节点的扩展和收缩、缓存故障的发现和恢复。
6、分布式资源管理与调度
如果把每个用户请求都看成系统需要完成的任务,那么分布式架构要做的就是对任务与资源进行匹配。
● 分布式调度的由来与过程。
● 资源划分和调度策略。
● 分布式调度架构。
● 中心化调度的特点是由一个网络节点参与资源的管理和调度。
● 两级调度在单体调度的基础上将资源的管理和调度从一层分成了两层,分别是资源管理层和任务分配层。
● 共享状态调度,通过共享集群状态、共享资源状态和共享任务状态完成调度工作。
● 资源调度的实践:如使用 Kubernetes 的架构及其各组件的运行原理。
7、高性能与可用性
高性能和可用性本身就是分布式架构要达成的目的。分布式架构拆分和分而治之的思想也是围绕着这个目的展开的。这部分主要从缓存、可用性两个方面展开。
在分布式架构的每个层面和角度,都可以利用缓存技术提高系统性能。由于技术使用比较分散,
对于可用性来说,为了保证系统的正常运行会通过限流、降级、熔断等手段进行干涉。
● 缓存的应用:HTTP 缓存、CDN 缓存、负载均衡缓存、进程内缓存、分布式缓存。
● 可用性的策略:请求限流、服务降级、服务熔断。
8、指标与监控
判断一个架构是好是坏时,有两个参考标准,即性能指标和可用性指标,分布式架构也是如此。
性能指标又分为吞吐量、响应时间和完成时间。
由于系统的分布性,服务会分布到不同的服务器和网络节点,因此监控程序需要在不同的服务器和网络节点上对服务进行监控。在分布式监控中会提到监控系统的分类、分层以及 Zabbix、Prometheus、ELK 的最佳实践。
● 性能指标:延迟、流量、错误、饱和度。
● 分布式监控系统:创建监控系统的步骤、监控系统的分类、监控系统的分层。
● 流行监控系统的最佳实践:包含 Zabbix、Prometheus。
架构设计总结
架构设计思维、模型
代码重构
务量的增加一般会导致系统架构的代码量增加,代码暴露的问题也随之增多。原来为了抢着上线在代码中留下的坑,终于要自己填了。为了提高代码质量,让业务走得更远,设计师加大了代码审核和重构的力度。
● 何时重构是一个有趣的问题。通常,在我们开始编码的时候,就应该对代码架构和组件模型进行设计。但是由于种种原因,基本上采取的都是牛仔式编程,即想到哪里就写到哪里,之后踩了坑才明白应该时刻对代码进行重构。针对这一点,可以有以下几个方面的参考:
● 事不过三原则:
在你第 1 次写代码实现某业务功能的时候,没有做设 计,姑且就这么写了;第 2 次遇到相似的功能,发现这 个功能之前好像用过,于是又写一遍;第 3 次又遇到了 同样的功能,这时就要告诉自己需要重构了。这个原则 有大量的应用场景,特别是开发应用时,遇到一些通用 的业务组件或者系统组件抽取的时候。
● 添加功能时重构:
当你往旧模块里添加新功能的时候,发现这个新功能原来 可以由几个原有功能组合完成,但是那几个原有功能的通 用性不太好,于是重构原有功能,让其具有更强的复用性
● 修复 bug 时重构:
程序员在修复完 bug 之后,往往会有非常深的满足感。如 果在修复以后能分析一下 bug 出现的原因,检查一下其他 地方是否也存在相同的 bug,是否能够通过通用组件抽取的 方式彻底解决 bug,那么代码重构就显得非常有意义了
● 审核代码时重构:
这在极限编程和结对编程中比较多见。一个程序员写代 码,另一个程序员审核代码,二人教学相长,共同进步。 不同的人拥有不同的背景、思路以及理解深度,因此协作 编写同一段代码会使代码显得更加立体,此时的重构是高 效的
性能测试与压力测试
如果说性能测试的结果是系统的基准线,那么压力测试的结果就是系统的上限或者高压线。基准线到高压线之间就是系统可以伸缩的范围,我们通过这两条线密切关注系统的负载情况。