Skip to content

介绍

随着移动互联网不断发展,计算机系统已从单机状态过渡到多机协作状态,计算机以集群的方式存在,按照分布式理论的指导构建出庞大复杂的应用服务。

分布式架构发展过程

众所周知,架构会随着业务场景而变化,而传统架构单一,无分层概念,模块之间耦合性过高,导致稳定性和扩展性较差,无法满足互联网高速迭代变化的需求。
同时,技术架构也会发生很大变化。不同业务系统侧重点不同,例如“百度”侧重于搜索。
当系统庞大后,后续会被拆分为多个子系统,部署在不同服务器上,各子系统相互协作,实现业务功能完整性。

以 Web 为例,搭建电商平台,最初传统结构是单体架构,如图1-1所示。


图1.1 电商整体业务耦合结构图

注意: 电商系统用户、商品、订单模块耦合在一块,无分层概念,部署在同一个服务器,为用户提供服务。

如图1-1所示,将应用和数据库部署在一起,一个简单的应用就搭建完成了。
随着后续业务的拓展,系统慢慢变得庞大臃肿,此时需要进行优化。
优化的方案如下。

1)增加单机的硬件设备,如处理器\内存\磁盘。
2)用物理机替换虚拟机。
3)增加服务器数量。
4)应用和数据库分离。

以上4种方案的优缺点如下。

1)增加服务器硬件设备资源,短时间内可以提高性能,但会留下隐患。
2)虚拟机的配置、性能不如物理机,但物理机费用高昂且不能治本。
3)增加服务器数量后需要做负载均衡才能充分利用资源,但会额外增加技术难度。
4)应用和数据库分离,分为 We b服务器和数据库服务器,这样不仅可以提高单机负载,也能提高 HA 高可用性。

这里采用方案4,分离应用和数据库,结构图如图1-2所示。


图1.2 电商应用和数据库分离结构图

注意:
当应用和数据库分别部署在不同服务器上时,应用所处服务器和数据库所处服务器要能正常ping通及访问,即应用可访问数据库获取数据且正常返回。

如图1-2所示,以上结构将应用和数据库分开单独部署,提高了单机负载以及服务器资源利用率,但若后续用户流量继续上涨,会存在安全风险(单机宕机后不能继续提供服务),需提供应用服务备份,结构图如图1-3所示。


图1.3 电商应用主/备结构图

注意:
外部流量都是到达主Web服务器,备Web服务器只在主Web服务器出现单点故障时使用,是维持业务正常访问的一种措施。

如图1-3所示,利用应用服务器预防单点故障,可以有效预防因服务器发生故障而不能及时访问的情况。
由于外部流量都是访问“主Web服务器”,单台服务器处理能力出现瓶颈时,可以把流量平摊到Web服务器集群的每台服务器中去,提高服务器资源整体利用率。

注意:
服务器集群是由多台服务器共同组成的一个虚拟体,对于调用方而言,它是一个大的虚拟集群,而代理服务器中存在多种策略,这里把流量平摊到Web服务器集群的每台服务器中时采用了“轮询”策略。
常用策略如下。

  • 随机:每个请求会随机访问Web服务器集群中的任意一台服务器。
  • IP-hash:每个请求按访问IP的hash结果分配,这样每个用户会访问Web服务器集群中固定的服务器。
  • 权重:可设置访问Web服务器集群中各服务器的比例占比,占比越大,处理请求量越多。

采用“轮询”策略将流量分发到主/备服务器,如图1-4所示。


图1.4 电商应用主/备负载均衡结构图

注意:
外部流量通过代理服务器Nginx负载均衡分发到主/备服务器上,代理服务器需要ping通且正常访问应用服务器,以防止因网络故障导致不能正常分发流量的情况出现。

如图1-4所示,该结构解决了应用层面服务器单点风险并提高了服务器负载利用率,但数据库层面存在单点风险和连接数瓶颈,故采用“主/从”方式搭配“哨兵”模式进行搭建,如图1-5所示。


图1.5 电商应用/数据库(主/备负载均衡)结构图

注意:
应用服务器和数据库之间要ping通且正常访问,主数据库需要同步数据至从数据库,主数据库和从数据库之间也要ping通且正常访问,主数据库用于“写”操作,从数据库用于“读”操作,应用服务器通过“虚拟IP”连接数据库,主从数据库通过“哨兵”控件监控。
当主服务器故障宕机时,从服务器会自动切换成主服务器并切换虚拟IP,应用无须改动,虚拟IP会映射成数据库真实的IP,提供读写功能。

到此,应用服务器和数据库都存在多节点,可以合理利用服务器资源,并避免单机风险。

分布式架构设计理念

分布式架构的核心理念按照一定维度(功能、业务、领域等)对系统进行拆分,通过合理的拆分结构,实现各业务模块解耦,
同时通过系统级容错设计,在廉价硬件基础设施上构建起高可用、可扩展的开放技术体系。

分布式系统的首要目标是提升系统的整体性能和吞吐量。
如果最终设计出来的分布式系统占用了 10 台机器才勉强达到单机系统的两倍性能,那么这个分布式系统还有存在的价值吗?
另外,即使采用了分布式架构,也仍然需要尽力提升单机上的程序性能,使得整体性能达到最高。
所以,我们仍然需要掌握高性能单机程序的设计和编程技巧,例如多线程并发编程、多进程高性能IPC通信、高性能的网络框架等。

另外,任何分布式系统都存在让人无法回避的风险和严重问题,即系统发生故障的概率大大增加:小到一台服务器的硬盘发生故障或宕机、一根网线坏掉,大到一台交换机甚至几十台服务器一起停机。
分布式系统下故障概率的增加,除了受到网络通信天生的不可靠性及物理上分布部署的影响,还受到X86服务器品质等的影响。

所以,分布式系统设计的两大关键目标是性能与容错性,而这两个目标的实现恰恰是很棘手的,而且相互羁绊!
举个例子,我们要设计一个分布式存储系统,出于对性能的考虑,在写文件时要先写一个副本到某台机器上并立即返回,然后异步发起多副本的复制过程,这种设计的性能最好,但存在“容错性”的风险,即在文件写完后,目标机器立即发生故障,导致文件丢失!
如果同时写多个副本,在每个副本都成功以后再返回,则又导致“性能”下降,因为该过程取决于最慢的那台机器的性能。

由于性能指标是绝对的,而容错性指标是相对的,而且实际上对于不同的数据与业务,我们要求的容错性可以存在很大的差异,比如允许意外丢失一些日志类的数据;
允许一些信息类的数据暂时不一致但最终达到一致;对交易类的数据要求有很高的可靠性。
所以我们会发现,很多分布式系统的设计都提供了多种容错性策略,以适应不同的业务场景,我们在学习和设计分布式系统的过程中也需要注意这一特性。

下面继续谈谈分布式系统设计中的两大思路:中心化和去中心化。

在分布式架构设计里,中心化始终是一个主流设计。
中心化的设计思想很简单,分布式集群中的节点器按照角色分工,大体上分为两种角色:Leader 和 Worker。
Leader 通常负责分发任务并监督 Worker,让 Worker 一直在执行任务;如果 Leader 发现某个 Worker 因意外状况不能正常执行任务,则将该 Worker 从 Worker 队列去除,并将其任务分给其他 Worker。
基于容器技术的微服务架构 Kubernetes 就恰好采用了这一设计思路。

在分布式中心化的设计思路中,还有一种设计思路与编程中敏捷开发的思路类似,即充分相信每个 Worker,Leader 只负责任务的生成而不再指派任务,由每个 Worker 自发领任务,从而避免让个别 Worker 执行的任务过多,并鼓励能者多劳。

中心化设计存在的最大问题是 Leader 的安全问题,如果 Leader 出了问题,则整个集群崩溃。
但我们难以同时安排两个 Leader 以避免单点问题。为了解决这个问题,大多数中心化系统都采用了主备两个 Leader 的设计方案,可以是热备或者冷备,也可以是自动切换或者手动切换,而且越来越多的新系统都具备了自动选举切换 Leader 的能力,以提升系统的可用性。
中心化设计还存在另外一个潜在的问题,即 Leader 的能力问题,如果系统设计和实现得不好,问题就会卡在 Leader 身上。

下面一起探讨去中心化设计。

在去中心化设计里通常不区分 Leader 和 Worker 这两种角色。
全球互联网就是一个典型的去中心化的分布式系统,联网的任意节点设备宕机,都只会影响很小范围的功能。
去中心化设计的核心是在整个分布式系统中不存在一个区别于其他节点的 Leader,因此不存在单点故障问题,但由于不存在 Leader,所以每个节点都需要与其他(所有)节点对话才能获取必要的集群信息,而分布式系统通信的不可靠性大大增加了上述功能的实现难度。

去中心化设计中最难解决的一个问题是“脑裂”问题,这种情况的发生概率很低,但影响很大。
脑裂指一个集群由于网络的故障,被分为至少两个彼此无法通信的单独集群,此时如果两个集群各自工作,则可能会产生严重的数据冲突和错误。
一般的设计思路是,当集群判断发生了脑裂问题时,规模较小的集群就“自杀”或者拒绝服务。

实际上,完全意义的真正去中心化的分布式系统并不多见。相反,在外部看来去中心化但工作机制采用了中心化设计思想的分布式系统不断出现。
在这种架构下,集群中的 Leader 是被动态选择出来的,而不是人为预先指定的,而且在集群发生故障的情况下,集群的成员会自发地举行“会议”选举新的 Leader 主持工作。
最典型的案例就是 ZooKeeper 及用 Go 实现的 Etcd。

分布式架构设计目标

设计目标可以明确方向,通过设计驱动和方向的把控,朝着既定方向前行并最终实现目标。
分布式架构中较为完整的架构体系设计包括以下几个方面。

系统拆分

系统拆分思路如下所示。

  • 以业务为导向,充分了解系统业务模型,按不同层面的业务模型可以将其划分为主模型、次模型。业务模型在一定比例上能够凸显出系统的业务领域及边界。
  • 业务依赖范围,由于业务存在重复依赖,从业务边界中按照业务功能去细分。
  • 把拆分结构图梳理出来,按照系统周边影响从小到大逐渐切换。
  • 拆分过程中尽量不要引入新的技术或者方案,如有需要,应讨论评估后再实施。

以购物平台为例,按照业务功能可以简单拆分为如下几个模块。

(1) 用户模块

  • 用户管理(新增用户、用户锁定、用户修改、用户删除、用户活跃状态)。
  • 用户分类(用户类别,定义多种用户体系,针对不同用户体系管理)。
  • 用户社交(用户之间关注、聊天)。
  • 用户行为收集、反馈、会员处理。
  • 用户统计。

(2) 商品模块

  • 商品管理(增加商品、修改商品信息、删除商品、查询商品信息、商品上下架、预/批处理等)。
  • 商品目录管理(商品的虚拟路径、商品之间关联)。
  • 商品类别(种类、自定义商品的各种信息)。
  • 商品社交(商品的评价、回复、点赞)。

(3) 订单模块

  • 订单管理(增删改查订单、拆分管理、订单处理)。
  • 支付管理(支付查看、人工支付处理)。
  • 结算管理(数据核对、费用结算)。

(4) 库存模块

  • 库存管理(数量动态调整、库存处理、数量提醒)。
  • 库存明细。
  • 处理非正常完成订单(退货、换货)。
  • 备货。

业务模块解耦

在拆分之前,模块和模块之间、系统和系统之间可能有非常强的依赖,所以我们在拆分过程中需要思考,哪些模块需要减少依赖。
依赖越少,独立性越强。

例如,用户强依赖商品,为了减少用户依赖而把商品从用户中剔除,显然并不合理。
所以可以减少用户依赖商品细粒度。

用户模块和商品模块独立出来后,两者之间如何交互调用呢?
我们可以把两者之间通用性较强的业务独立到共通的服务中,通过调用共通服务减少耦合性。

这样做的优点是:

1)减少模块和模块之间的大量耦合交互;
2)后期修改维护成本少;
3)简单透明。

通过以上优化,如两者之间还存在少量的依赖,考虑是否进一步拆分。
如需拆分,可以通过一定维度(规划计划、系统后续完整性、后续维护工作量等)拆分,
也可以通过接口的方式解耦,例如用户、商品之间提供一个共用接口,用户调用接口,商品实现接口。
这种方式比较烦琐。

系统容错

容错是为了避免系统架构和业务层面发生故障而引起其所在系统的不稳定。
优秀的容错设计能让系统的反馈对故障不敏感,甚至是自适应的。容错设计包括以下两个层面。

(1) 架构设计层面

  • 重试:因网络传输、超时、业务短暂异常等系统问题而提供的一种补偿机制,能更大程度弥补因异常导致的丢失。
    实现方式为利用消息中间件自动重试。若无自动重试,可自行实现重试推送。
  • 服务降级:可分为自动降级和手动降级。当系统压力上升、出现各种延迟卡顿时,系统会自动检测影响面范围。若影响面积小,可自动降级,反之,需要人工去分析是否需要降级以及替换。
    实现方式有两种。第一种是全局合法请求拦截,根据系统的运行状态去判断是否需要手动/自动关闭。另一种是检测调用系统方是否正常,利用超时机制,规定某时间范围内超过多少次则自动替换。
  • 熔断和限流:熔断和限流是为了应对性能过载的情形。高压力期间如全部业务都失败,可控制拒绝大部分请求失败,尝试利用小部分流量去重试。
    当发现小部分流量某范围内成功频率很高时,可适当开放流量入口,依次递增,直到最终流量全部正常访问。应尽量使用熔断器实现,如无可自行实现。

(2) 业务功能层面

  • 幂等:系统可能会出现多次同样的请求,如重试的请求、网络超时的请求等,因此需要针对这些场景设置一些优化手段,防止业务执行出错、数据异常等。
    大部分请求具有时效性,幂等需保证在一定时间内,重复的请求不再消费。
    实现方式如下:为分布式的缓存或者数据库设置唯一主键,对比请求中的唯一标识,如不存在则处理请求业务,否则不处理,直接返回之前处理的结果。
  • 异步处理:由于请求的生命周期漫长,会经历多个环节,且请求完成时间较长,导致大量线程处于等待状态,久而久之会形成堵塞、假死。
    如采用异步处理方式,系统在收到请求后会立即处理并返回结果,无须等待,可以减少请求耗时,也可以充分利用重试来保证收到消息后立即处理。
    实现方式为利用消息特性提供异步处理,如可自主实现阻塞队列。
  • 事务补偿机制:业务程序中需要支持这种补偿,针对重要场景提供多种处理方案,特定情况下其中一种方案通过即可。业务程序中可设置开关,通过开关切换实现事务补偿。

高可用

通过设计和监控可以提高系统正常提供服务的可靠性,那么,如何才能保障系统的高可用?
单点系统面临严重高可用问题,所以在设计过程中要尽量避免系统的单点出现,保证系统处于多机状态,俗称冗余。
冗余指重复配置系统某些部件,当系统发生故障不可用时,冗余配置的部件介入并承担故障部件的工作,减少系统的故障时间。
分布式架构中,互联网调用过程如下。

1)客户端层:首页、App。
2)代理服务层、请求加速层:减少请求访问次数,达到加速。
3)应用服务层:系统业务服务。
4)数据缓存层:业务数据内存存储。
5)数据库层:业务数据数据库存储。

注意:
系统的高可用:可以对不同访问层进行特定优化,如常用集群化和自动故障转移。

分布式架构应用场景

随着现代化网络通信飞速发展,传统硬件结构以及软件架构已无法满足业务对性能、扩展的高要求,因此,分布式架构成为广泛讨论并应用的解决方案。

分布式架构适用于如下情景:

1)对数据密集/实时要求比较高的项目或系统;
2)对服务器高可用运用指数较高的系统;
3)大型业务复杂/统计类系统。

分布式架构设计难点

网络因素

由于服务和数据分别部署在不同的机器上,它们之间的交互通信会存在如下问题。

1)网络延迟。
延迟是指在传输介质中传输所用的时间,如部署在同一个机房,网络I/O传输相对较快,但是很多公司为了增加系统的可用性,有多套机房(线上、线下),此时会面临跨机房、跨网络传输等情况。
尤其是跨IDC,其网络I/O会存在不确定性,出现延迟、超时等情况,虽然可以通过换网卡解决宽带瓶颈,但不能从根本上解决物理延迟。
由于这些现象会给整个设计带来整体性的难点,我们在做分布式架构设计的同时需要考虑这些要素,并且提供相关高效解决方案,从而规避此问题。

  • 由于分布式系统调用会出现失败、超时等情况,方案设计时需考虑以上场景,提供重试功能,保证请求的完整性。
  • 传输内容体过大、业务链条太长也会导致网络I/O传输阻塞,此时我们可以精简传输内容,优化业务链条,如通过同步转异步、数据压缩等方式避免阻塞。

2)网络故障。
若出现网络故障问题,可以先了解数据是以什么协议方式在网络中传输导致丢包、错乱,然后采用比较稳定的TCP网络协议进行传输。

服务可用性

为了保证服务器正常运行,可对服务器进行监控,如探针、心跳检测等,而这些仅仅是针对服务器的运行数据和日志分析。
为了提高服务器服务的可用性,可进一步实施服务器负载均衡、主从切换、故障转移等。

探针监控是定时去请求访问服务器,需要通过请求回应来收集服务器状态。
定时需设置在合理范围值内,太短会给服务器带来压力,太长会导致不能及时收集报错信息而错过最佳时机。
基于以上情况,可以采用服务器集群化的方式,根据系统场景,设置合理探针请求频率,当发现异常时及时剔除替换。

【示例】电商系统:

电商系统分为(用户、商品、订单)模块,当用户模块中用户数量逐渐增多时,由于用户模块依赖商品、订单模块,潜移默化会给它们带来高额压力,
所以需要监控并跟踪模块之间调用链路的状况,以及时发现并优化调用过程产生的问题,提供系统模块调用直观图,如图1-6所示。


图1.6 电商多模块调用直观结构图

图1-6体现了整个系统的调用链条,包括服务、数据、交互方式、请求频率以及错误率统计,其中,实线条代表运行正常,虚线条表示存在调用缓慢、异常等相关问题。
通过上图可以看到应用内部调用服务存在缓慢处理的情况,同时通过数值可以直观看到服务较慢的数量,这些数值可以协助反馈目前系统的运行状态。
通过反馈的问题点可以提前预知系统服务的瓶颈,从而优化处理。

系统运行健康变化趋势可以直观体现系统的吞吐量,如图1-7所示。


图1.7 电商多模块调用线性趋势图

从图1-7中可以看出,系统稳定运行占比高达84%,较慢的请求占用15%,这里的较慢指请求过程中由于其他原因导致的运行缓慢,如网络异常、超时等。
重点需关注很慢、停滞的请求,这部分请求可反馈出系统应用层面的问题,需要进行特定优化。

数据一致性

由于数据架构需要提供多节点部署,不同节点之间通信存在数据差异,在很多场景下往往会产生脏数据、异常数据,让业务不能正常运转。
数据一致性指关联数据之间的逻辑关系是否正确和完整。
那么在分布式情况下如何让不同模块之间的数据保证完整性、一致性?

可以从系统构建层面考虑,采用分布式事务处理,牺牲一部分性能去保证数据一致性。

【示例】电商系统:

在购物平台看中一款商品,然后加入购物车,下单成功后生成订单,支付成功后扣除账户余额,然后通知仓库发货,生成物流轨迹。
如果商品库存、订单、支付、仓库等应用模块独立部署,各模块之间通过远程调用,则正常流程是所有应用模块调用都正常返回。
若出现异常,需要考虑以下场景。

1)商品库存数量已扣除,订单正常下单成功,由于网络异常等原因,提示用户“下单失败”,用户刷新页面后,显示一个“未支付订单”;
2)订单正常下单成功,用户支付成功后,由于网络异常等原因,提示用户“支付失败”,用于刷新页面后,显示“已支付成功”;
3)用户支付成功后,通知仓库发货,由于网络异常等原因,仓库系统未发货,无轨迹;
4)用户支付成功后,通知仓库发货,仓库可能没有货品,但商品数量已扣减,用户余额已扣除等。

系统拆分成多个应用模块后,往往会存在数据不一致性等问题,不同模块之间通过远程调用存在多种不确定因素,如调用过程中顺序不同,网络、宽带、超时等一系列问题,增加了系统复杂性。

假如把商品库存、订单、支付、仓库多个应用模块并行调用,也就是同时调用这些模块进行业务处理,由于同时调用不同模块存在延迟、网络异常等情况,需要设定合理的超时机制。
并行执行过程中任意模块执行失败或者超时时,模块执行状态差异需要通过消息发布/订阅等模式,通知以上全部模块进行失败处理,相关模块收到消息后,进行幂等处理,进而执行失败处理。

注意:
保证消息发布/订阅的可用性、可靠性,让业务模块都能正常接收消息。
任意模块执行失败或者超时等情况出现时,由于涉及模块众多,业务复杂,应通知相关模块进行失败处理。

分布式架构解决痛点

分布式架构主要用于解决如下问题。

系统宕机

系统业务量逐渐增多,导致系统压力增大,通过监控和各方面指标发现系统频繁报警,需要通过优化让系统变得稳定、负载降低。
最直接的方式是增加系统容量,调整系统参数,但是硬件扩展并非解决问题的最优方式,会存在以下弊端:

  • 硬件设备费用高额;
  • 后续会带来更大的维护代价。

进一步优化过程需要垂直或者水平拆分业务系统,按照一定维度拆分成多个模块,降低耦合性,通过合理的设计方案,从端到端、点到点优化,让系统变得健壮,为后续复杂业务提供模块化管理和运营。

分布式的架构体系具有良好的横向扩展性,通过横向扩展机器能够快速高效提高系统的并发量和吞吐量,为复杂的业务系统提供良好支撑。
而分布式架构体系调用过程较长,从外界流量入口分发、代理服务、网络传输、容器、应用服务、数据存储,存在很高的优化空间,通过合理的设计方案能让系统承载更多更高的指标,从而稳定运行。

系统瘫痪

很多外部因素也会导致系统瘫痪,如机房停电、线路关闭、网络堵塞等,因此需要一套完整的分布式架构方案(高可用、监控、故障转移等)来支撑。

系统在构建时期需要考虑这些外在因素,然后构思设计相应的处理方案并落地实施,在测试环境中演练外在因素导致系统瘫痪的场景,不断探索、改进、完善,这样,当外部因素真的出现时,系统可以从容面对,从侧面凸显出系统的健壮。

分布式架构体系中针对以上场景有众多解决方案,从设计之初就已经考虑到这些因素,确保系统是可用的、可靠的,而多机房部署就能从根源上解决由机房停电引起的事故。

系统故障

当系统发生故障时,因系统构建庞大,维修排查故障时间过久会影响用户使用。
分布式架构讲究系统拆分模块化,使用更轻量级的模块、可用的部署策略,可从一定程度上规避故障风险,如出现故障,通过有效的故障转移方式能让系统在短时间之内正常服务。

系统臃肿

系统庞大、内核聚集多,臃肿不堪。
迭代维护运营成本高,风险过大。
分布式架构将系统拆分成模块化,模块细化后可读性、维护性会变得简单明了,针对细化后的模块可更专注开发和优化。