本节我们来谈一谈分布式架构的设计思想
架构和微服务架构
SOA 全称(Service Oriented Architecture),中文意思为“面向服务的架构”,他是一种设计方法,其中包含多个服务,服务之间通过相互依赖最终提供一系列的功能。一个服务通常以独立的形式存在与操作系统进程中。
各个服务之间通过网络调用跟 SOA 相提并论的还有一个 ESB(企业服务总线),简单来说 ESB 就是一根管道,用来连接各个服务节点。为了集成不同系统,不同协议的服务,ESB 做了消息的转化解释和路由工作,让不同的服务互联互通;
1.系统集成:站在系统的角度,解决企业系统间的通信问题,把原先散乱、无规划的系统间的网状结构,梳理成规整、可治理的系统间星形结构,这一步往往需要引入一些产品,比如 ESB、以及技术规范、服务管理规范;这一步解决的核心问题是【有序】
2.系统的服务化:站在功能的角度,把业务逻辑抽象成可复用、可组装的服务,通过服务的编排实现业务的快速再生,目的:把原先固有的业务功能转变为通用的业务服务,实现业务逻辑的快速复用;这一步解决的核心问题是【复用】
3.业务的服务化:站在企业的角度,把企业职能抽象成可复用、可组装的服务;把原先职能化的企业架构转变为服务化的企业架构,进一步提升企业的对外服务能力;“前面两步都是从技术层面来解决系统调用、系统功能复用的问题”。第三步,则是以业务驱动把一个业务单元封装成一项服务。这一步解决的核心问题是【高效】
微服务架构其实和 SOA 架构类似,微服务是在 SOA 上做的升华,微服务架构强调的一个重点是“业务需要彻底的组件化和服务化”,原有的单个业务系统会拆分为多个可以独立开发、设计、运行的小应用。这些小应用之间通过服务完成交互和集成。
组件表示一个可以独立更换和升级的单元,就像 PC 中的CPU、内存、显卡、硬盘一样,独立且可以更换升级而不影响其他单元。如果我们把 PC 作为组件以服务的方式构建,那么这台 PC 只需要维护主板和一些必要的外部设备。CPU、内存、硬盘都是以组件方式提供服务,PC 需要调用 CPU 做计算处理,只需要知道 CPU 这个组件的地址即可。
微服务的特征
1.通过服务实现组件化
2.按业务能力来划分服务和开发团队
3.去中心化
4.基础设施自动化(devops、自动化部署)
1.微服务不再强调传统 SOA 架构里面比较重的 ESB 企业服务总线,同时 SOA 的思想进入到单个业务系统内部实现真正的组件化
2.Docker 容器技术的出现,为微服务提供了更便利的条件,比如更小的部署单元,每个服务可以通过类似 Node 或者 Spring Boot 等技术跑在自己的进程中。
3.还有一个点大家应该可以分析出来,SOA 注重的是系统集成方面,而微服务关注的是完全分离
我拿来项目实战专题的系统架构图先讲一下:
我们传统的代码模型就是基于Controller、Service、Dao层来实现解耦,但是通常都是Controller作为空实现,数据透传后,所有的业务逻辑都在Service里去写,这样Service层就会显得很重。所以,本小节提出领域概念。实现高内聚,低耦合的效果!
领域驱动设计(DDD,Domain-Driven Design),软件开发不是一蹴而就的事情,我们不可能在不了解产品(或行业领域)的前提下进行软件开发,在开发前,通常需要进行大量的业务知识梳理,然后才到软件设计的层面,最后才是开发。而在业务知识梳理的过程中,我们必然会形成某个领域知识,根据领域知识来一步步驱动软件设计,就是领域驱动设计的基本概念
业务初期,功能大都非常简单,普通的 CRUD 就能满足,此时系统是清晰的。随着产品不断迭代和演化,业务逻辑变得越来越复杂,我们的系统也越来越冗杂。各个模块之间彼此关联,甚至到后期连作者都很难说清模块的具体功能意图是啥。导致在修改一个功能时,要追溯到这个功能需要的修改点就需要很长时间,更别提修改带来的不可预知的影响面。 比如说:
订单服务接口中提供了查询、创建订单相关的接口,也提供了订单评价、支付的接口。同时订单表是个大表,包含了非常多字段。在我们维护代码时,将会导致牵一发而动全身,很可能只是想改下评价相关的功能,却影响到了创建订单的核心路径。虽然我们可以通过测试来保证功能完备性,但当我们在订单领域有大量需求同时并行开发时,改动重叠、恶性循环、疲于奔命修改各种问题。
绝大部分公司都是这样一个状态,然后一般的解决方案是不断的重构系统,让系统的设计随着业务成长也进行不断的演进。通过重构出一些独立的类来存放某些通用的逻辑解决混乱问题,但是我们很难给它一个业务上的含义,只能以技术纬度进行描述,这个带来的问题就是其他人接手这块代码的时候不知道这个的含义或者可以通过修改这块通用逻辑来达到某些需求
实领域模型本身就不是一个陌生的单词,说直白点,在早期领域模型就是数据库设计. 我们做传统项目的流程或者说包括现在我们做项目的流程,都是首先讨论需求,然后是数据库建模, 在需求逐步确定的过程不断的去更新数据库的设计。接着我们在项目开发阶段,发现有些关系没有建、有些字段少了、有些表结构设计不合理,又在不断的去调整设计。最后上线。在传统项目中,数据库是整个项目的根本,数据模型出来以后后续的开发都是围绕着数据展开;然后形成如下的一个架构
1.service 很重,所有逻辑处理基本都放在 service 层。
2.POJO()作为 service 层的非常重要的一个实体,会因为不同场景的需求做不同的变化和组合,就会早成 POJO 的几种不同模型(失血、贫血、充血),用来形容领域模型太胖或者太瘦
随着业务变得复杂以后,包括数据结构的变化,那么各个模块就需要进行修改,原本清晰的系统经过不断的演化变得复杂、冗余、耦合度高。后果就非常严重
我们试想一下如果一个软件产品不依赖数据库存储设备,那我们怎么去设计这个软件呢?如果没有了数据存储,那么我们的领域模型就得基于程序本身来设计。那这个就是 DDD 需要去考虑的问题
以抽奖设计为例
抽奖活动代码见本文后记
BD理解起来有点抽象, 这个有点像设计模式,感觉很有用,但是不知道怎么应用到自己写的代码里面,或者生搬硬套最后看起来又很别扭,那么接下来以一个简单的转盘抽奖案例来分析一下 DDD 的应用
针对功能层面划分边界
这个系统可以划分为运营管理平台和用户使用层,运营平台对于抽奖的配置比较复杂但是操作频率会比较低。而用户对抽奖活动页面的使用是高频率的但是对于配置规则来说是误感知的,根据这样的特点,我们把抽奖平台划分针对 C 端抽奖和 M 端抽奖两个子域
在确认了 M 端领域和 C 端的限界上下文后,我们再对各自上下文内部进行限界上下文的划分,接下来以 C 端用户为例来划分界限上下文
确认基本需求
首先我们要来了解该产品的基本需求
1.抽奖资格(什么情况下会有抽奖机会、抽奖次数、抽奖的活动起始时间)
2.抽奖的奖品(实物、优惠券、理财金、购物卡…)
3.奖品自身的配置,概率、库存、某些奖品在有限的概率下还只能被限制抽到多少次等
4.风控对接, 防止恶意薅羊毛
针对产品功能划分边界
抽奖上下文是整个领域的核心,负责处理用户抽奖的核心业务。
1.对于活动的限制,我们定义了活动资格的通用语言,将活动开始/
结束时间,活动可参与次数等限制条件都收拢到活动资格子域中。
2.由于 C 端存在一些刷单行为,我们根据产品需求定义了风控上下文,用于对活动进行风控
3.由于抽奖和发放奖品其实可以认为是两个领域,一个负责根据概率去抽奖、另一个负责将选中的奖品发放出去。所以对于这一块也独立出来一个领域
细化上下文
通过上下文划分以后,我们还需要进一步梳理上下文之间的关系,梳理的好处在于:
1.任务更好拆分(一个开发人员可以全身心投入到相关子
域的上下文中),
2.方便沟通,明确自身上下文和其他上下文之间的依赖关
系,可以实现更好的对接
然后是基于上下文的更进一步细化建模,在 DDD 中存在一
些名字定义
实体
当一个对象由其标识(而不是属性)区分时,这种对象称
为实体(Entity)。
值对象
当一个对象用于对事物进行描述而没有唯一标识时,它被
称作值对象
聚合根
聚合根属于实体对象,它是领域对象中一个高度内聚的核心对象。(聚合根具有全局的唯一标识,而实体只有在聚合内部有唯一的本地标识,值对象没有唯一标识,不存在这个值对象或那个值对象的说法)
领域服务
一些重要的领域行为或操作,可以归类为领域服务。它实现了全部业务逻辑并且通过各种校验手段保证业务的正确性。
资源库
资源封装了基础设施来提供查询和持久化聚合操作。这样能够让我们始终关注在模型层面,把对象的存储和访问都委托给资源库来完成。他不是数据库的封装,而是领域层与基础设施之间的桥梁。DDD 关心的是领域内的模型,而不是数据库的操作。
代码设计
详见文末
在实际开发中,我们一般会采用模块来表示一个领域的界限上下文,比如
com.test.michael.bussiness.lottery.;//抽奖上下文
com.test.michael.bussiness.riskcontrol.;// 风控上下文
com.test.michael.bussiness.prize.;//奖品上下文
com.test.michael.bussiness.qualification.;// 活动资格上下文
com.test.michael.bussiness.stock.;//库存上下文*
对于模块内的组织结构,一般情况下我们是按照领域对象、
领域服务、领域资源库、防腐层等组织方式定义的。
com.test.michael.bussiness.lottery.domain.valobj.;//领域对象-值对象
com.test.michael.bussiness.lottery.domain.entity.;//领域对象-实体
com.test.michael.bussiness.lottery.domain.aggregate.;//领域对象-聚合根
com.test.michael.bussiness.lottery.service.;//领域服务
com.test.michael.bussiness.lottery.repo.;//领域资源库*
领域驱动的好处
用 DDD 可以很好的解决领域模型到设计模型的同步、演进最后映射到实际的代码逻辑。总的来说,DDD 有几个好处
1.DDD 能够让我们知道如何抽象出限界上下文上下文以及如何去分而治之
分而治之:把复杂的大规模软件拆分成若干个子模块,每一个模块都能独立运行和解决相关问题。并且分割后各个部分可以组装成为一个整体。
抽象:使用抽象能够精简问题空间,而且问题越小越容易理解,比如说我们要对接支付,我们抽象的纬度应该是支付,而不是具体的微信支付还是支付宝支付
2.DDD 的限界上下文可以完美匹配微服务的要求
在系统复杂之后,我们都需要用分治来拆解问题。一般有两种方式,技术维度和业务维度。技术维度是类似 MVC 这样,业务维度则是指按业务领域来划分系统。
微服务架构更强调从业务维度去做分治来应对系统复杂度,而 DDD 也是同样的着重业务视角
领域驱动设计其实我们可以简单认为是一种指导思想,是一种软件开发方法,通过 DDD 可以将系统解构更加合理,最终满足高内聚低耦合的本质。在我的观点来看,有点类似数据库的三范式,我们开始在学的时候并不太理解,当有足够的设计经验以后慢慢发现三范式带来的好处。同时我们也并不一定需要严格按照这三范式去进行实践,有些情况下是可以灵活调整。
说 CAP、BASE 理论之前,先要了解下分布式一致性的这个问题
实际上,对于不同业务的产品,我们对数据一致性的要求是不一样的,比如 12306,他要求的是数据的严格一致性,不能说把票卖给用户以后发现没有座位了;比如银行转账,你们通过银行转账的时候,一般会收到一个提示:转账申请将会在 24 小时内到账;实际上这个场景满足的是最终钱只要汇出去了即可,同时以及如果钱没汇出去要保证资金不丢失就行;所以说,用户在使用不同的产品的时候对数据一致性的要求是不一样的
在分布式系统中要解决的一个重要问题就是数据的复制。在我们的日常开发经验中,相信很多开发人员都遇到过这样的问题:在做数据库读写分离的场景中,假设客户端 C1 将系统中的一个值 K 由 V1 更新为 V2,但客户端 C2 无法立即读取到 K 的最新值,需要在一段时间之后才能 读取到。这很正常,因为数据库复制之间存在延时。
所谓的分布式一致性问题,是指在分布式环境中引入数据复制机制之后,不同数据节点之间 可能出现的,并无法依靠计算机应用程序自身解决的数据不一致的情况。简单讲,数据一致性就是指在对一个副本数据进行更新的时候,必须确保也能够更新其他的 副本,否则不同副本之间的数据将不一致。
那么如何去解决这个问题?按照正常的思路,我们可能会想,既然是因为网络延迟导致的问题,那么我们可以把同步动作进行阻塞,用户 2 在查询的时候必须要等到数据同步完成以后再来做。但是这个方案带来的问题是性能会收到非常大的影响。如果同步的数据比较多或者比较频繁,
那么因为阻塞操作可能将导致整个新系统不可用的情况;总结: 所以我们没有办法找到一种能够满足数据一致性、又不影响系统运行的性能的方案,所以这个地方就诞生了一个一致性的级别:
1.强一致性:这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
2.弱一致性:这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不久承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
3.最终一致性:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较用的多的模型
一个经典的分布式系统理论。CAP 理论告诉我们:一个分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个基本需求,最多只能同时满足其中两项。CAP 理论
在互联网界有着广泛的知名度,也被称为“帽子理论”,它是由 Eric Brewer 教授在 2000 年举行的 ACM 研讨会提出的一个著名猜想:
一致性(Consistency)、可用性(Availability)、分区容错(Partition-tolerance)三者无法在分布式系统中同时被满足,并且最多只能满足两个!
一致性:所有节点上的数据时刻保持同步
可用性:每个请求都能接收一个响应,无论响应成功或失败
分区容错:系统应该持续提供服务,即时系统内部(某个节点分区)有消息丢失。比如交换机失败、网址网络被分成几个子网,形成脑裂;服务器发生网络延迟或死机,导致某些 server 与集群中的其他机器失去联系
分区是导致分布式系统可靠性问题的固有特性,从本质上来看,CAP 理论的准确描述不应该是从 3 个特性中选取两个,所以我们只能被迫适应,根本没有选择权;
总结一下:CAP 并不是一个普适性原理和指导思想,它仅适用于原子读写的 NoSql 场景中,并不适用于数据库系统。
从前面的分析中知道:在分布式(数据库分片或分库存在的多个实例上)系统下,CAP 理论并不适合数据库事务(因为更新一些错误的数据而导致的失败,无论使用什么样的高可用方案都是徒劳,因为数据发生了无法修正的错误)。此外 XA 事务虽然保证了数据库在分布式系统下的 ACID (原子性、一致性、隔离性、持久性)特性,但也带来了一些性能方面的代价,对于并发和响应时间要求比较高的电商平台来说,是很难接受的。
eBay 尝试了另外一条完全不同的路,放宽了数据库事务的 ACID 要求,提出了一套名为 BASE 的新准则。BASE 全称是 Basically available,soft-state,Eventually Consistent.
系统基本可用、软状态、数据最终一致性。相对于 CAP 来说,它大大降低了我们对系统的要求。
Basically available(基本可用),在分布式系统出现不可预知的故障时,允许瞬时部分可用性
1.比如我们在淘宝上搜索商品,正常情况下是在 0.5s 内返回查询结果,但是由于后端的系统故障导致查询响应时间变成了 2s
2.再比如数据库采用分片模式,100W 个用户数据分在 5 个数据库实例上,如果破坏了一个实例,那么可用性还有 80%,也就是 80%的用户都可以登录,系统仍然可用
3.电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现
soft-state(软状态). 表示系统中的数据存在中间状态,并且这个中间状态的存在不会影响系统的整体可用性,也就是表示系统允许在不同节点的数据副本之间进行数据同步过程中存在延时;比如订单状态,有一个待支付、支付中、支付成功、支付失败, 那么支付中就是一个中间状态,这个中间状态在支付成功以后,在支付表中的状态同步给订单状态之前,中间会存在一个时间内的不一致。
Eventually consistent(数据的最终一致性),表示的是所有数据副本在一段时间的同步后最终都能达到一个一直的状态,因此最终一致性的本质是要保证数据最终达到一直,而不需要实时保证系统数据的强一致
BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性
a)负载均衡技术(failover/选址/硬件负载/
软件负载 / 去中心化的软件负载( gossip(redis-cluster)))
b)热备(linux HA)
c)多机房(同城灾备、异地灾备)
a)故障监控(系统监控(cpu、内存)/链路监控/日志监控) 自动预警
b)应用的容错设计、(服务降级、限流)自我保护能力
c)数据量(数据分片、读写分离)
垂直伸缩 提升硬件能力
水平伸缩 增加服务器
CDN 是 Content Delivery Network 的缩写,表示的是内容分发网络。CDN 的作用是把用户需要的内容分发到离用户最近的地方,这样可以是用户能够快熟获取所需要的内容。 CDN 其实就是一种网络缓存技术,能够把一些相对稳定的资源放到距离最终用户较近的地方,一方面可以节省整个
广域网的贷款消耗,另外一方面可以提升用户的访问速度,改进用户体验。我们一般会把静态的文件(图片、脚本、静态页面)放到 CDN 中
1.当用户点击网站页面上的内容 URL,经过本地 DNS 系统解析,DNS系统会最终将域名的解析权交给 CNAME 指向的 CDN 专用 DNS 服务器
2.CDN 的 DNS 服务器将 CDN 的全局负载均衡设备 IP 地址返回用户
3.用户向 CDN 的全局负载均衡设备发起内容 URL 访问请求
4.CDN 全局负载均衡设备根据用户 IP 地址,以及用户请求的内容 URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求。
5.区域负载均衡设备会为用户选择一台合适的缓存服务器提供服务,选择的依据包括:根据用户 IP 地址,判断哪一台服务器距用户最近;根据用户所请求的 URL 中携带的内容名称,判断哪一台服务器上有用户所需内容;查询各个服务器当前的负载情况,判断哪一台服务器尚有服务能力。基于以上这些条件的综合分析之后,区域负载均衡设备会向全局负载均衡设备返回一台缓存服务器的 IP 地址
6.局负载均衡设备把服务器的 IP 地址返回给用户
用户向缓存服务器发起请求,缓存服务器响应用户请求,将用户所需内容传送到用户终端。如果这台缓存服务器上并没有用户想要的内容,而区域均衡设备依然将它分配给了用户,那么这台服务器就要向它的上一级缓存服务器请求内容,直至追溯到网站的源服务器将内容拉到本地。
最适合的是那些不会经常变化的内容,比如图片,JS 文件, CSS 文件,图片文件包括程序模板中的,CSS 文件中用到的背景图片,还有就是作为网站内容组成部分的那些图片,都可以;
发布的时候一般会采用灰度发布,也就是会对新应用进行分批发布,逐步扩大新应用在整个及群众的比例直到最后全部完成。灰度发布是针对新引用在用户体验方面完全无感知。
灰度发布系统的作用在于,可以根据自己的配置,来将用户的流量导到新上线的系统上,来快速验证新的功能修改,而一旦出问题,也可以马上的恢复,简单的说,就是一套 A/BTest 系统.
本节抽奖活动项目代码:
抽奖活动 github项目地址