高可扩展性(1)- 拆分

最后更新:2019-11-01

我们可以在单机系统中通过增加处理核心的方式,来增加系统的并行处理能力,但这个方式并不总奏效。因为当并行的任务数较多时,系统会因为争抢资源而达到性能上的拐点,系统处理能力不升反降。而对于由多台机器组成的集群系统来说也是如此。集群系统中,不同的系统分层上可能存在一些“瓶颈点”,这些瓶颈点制约着系统的横向扩展能力。

比方说,你系统的流量是每秒 1000 次请求,对数据库的请求量也是每秒 1000 次。如果流量增加 10 倍,虽然系统可以通过扩容正常服务,数据库却成了瓶颈。再比方说,单机网络带宽是 50Mbps,那么如果扩容到 30 台机器,前端负载均衡的带宽就超过了千兆带宽的限制,也会成为瓶颈点。那么,我们的系统中存在哪些服务会成为制约系统扩展的重要因素呢?

其实,无状态的服务和组件更易于扩展,而像 MySQL 这种存储服务是有状态的,就比较难以扩展。因为向存储集群中增加或者减少机器时,会涉及大量数据的迁移,而一般传统的关系型数据库都不支持。这就是为什么提升系统扩展性会很复杂的主要原因。

除此之外,我们需要站在整体架构的角度,而不仅仅是业务服务器的角度来考虑系统的扩展性 。所以说,数据库、缓存、依赖的第三方、负载均衡、交换机带宽等等都是系统扩展时需要考虑的因素。我们要知道系统并发到了某一个量级之后,哪一个因素会成为我们的瓶颈点,从而针对性地进行扩展。

1. 高可扩展性的基本思想

可扩展性架构的设计方法很多,但万变不离其宗,所有的可扩展性架构设计,背后的基本思想都可以总结为一个字:拆!拆,就是将原本大一统的系统拆分成多个规模小的部分,扩展时只修改其中一部分即可,无须整个系统到处都改,通过这种方式来减少改动范围,降低改动风险。

通过拆分可以将一个涉及面广、复杂度高的系统拆解为多个小模块,让各个团队单独负责并专项击破。拆分是架构设计大型复杂系统的第一步,对降低系统复杂性有着决定性的意义,它也是架构师的必备技能之一。

1.1. 为什么要做拆分

  • 应用间耦合严重。系统内各个应用之间不通,同样一个功能在各个应用中都有实现,后果就是改一处功能,需要同时改系统中的所有应用。这种情况多存在于历史较长的系统,因各种原因,系统内的各个应用都形成了自己的业务小闭环;

  • 业务扩展性差。数据模型从设计之初就只支持某一类的业务,来了新类型的业务后又得重新写代码实现,结果就是项目延期,大大影响业务的接入速度;

  • 代码老旧,难以维护。各种随意的if else、写死逻辑散落在应用的各个角落,处处是坑,开发维护起来战战兢兢;

  • 系统扩展性差。系统支撑现有业务已是颤颤巍巍,不论是应用还是DB都已经无法承受业务快速发展带来的压力;
  • 沟通困难。一个大型复杂项目可能需要上百名研发、产品、测试等不同职能的人员共建。如果没有系统拆分,所有人聚集在一起,开会讨论需求和技术方案将是一个很困难的事情,更不用说得出什么重要结论。
  • 代码冲突。上百名研发使用同一个代码仓库,每天可能要花费几个小时在解决代码冲突的问题上。

面对这样的情况,我们该如何解决呢?

首先,应该是几个业务专家、技术架构一起对整个系统进行全面的梳理和分析。然后便是对系统进行拆分,将大系统按一定的规则拆分成多个小模块。注意,这里我多说一句,“一定的规则”便是我们前面所讲的“按照不同维度”。

拆分后,上百人的产研测团队就可以认领工作了,各个团队安心处理自己模块内的工作(产品设计、代码开发等)。对于模块间需要交互的地方,每个团队出一个资深的产品和架构师进行沟通即可。沟通成本降低了,效率自然也就提升了。

通过上述案例,可以看出拆分在大型系统开发中的重要作用。一个好的拆分能够降低各个模块间的耦合性,极大地降低效率消耗,提升系统成功的可能性。

1.2. 何时拆分

当单体架构存在以下 3 类问题时,建议考虑进行拆分。

  • 系统常年处于多需求并行开发,导致代码维护成本太大

如果一个系统变得非常庞大,它的需求及并行的需求就会非常多。每个并行需求都会有一个开发分支,当开发需求完成陆续上线,那么在代码合并时就会产生冲突,比如你写的一个功能块被别人改了、新增了一个类似的公共类等,此时就可以考虑进行系统拆分。

  • 在开发一个需求时,花费几天时间阅读代码,而开发只要一半甚至更少的时间

在一个过于庞大的系统里,如果你长期处于熟悉代码的时间和实际编写的时间配比不对等的情况,此时就应考虑进行系统拆分。因为信息过载了,你无法完全掌握此系统。如果不拆分,每次重新熟悉代码并进行修改,可能会导致 Bug 频发,线上质量低下。

经过拆分后,你只需负责你熟悉的小模块,其他部分依赖第三方接口即可,让更专业的人保证它的质量。

  • 在一个需求上线前,需要召集上百人对齐上线风险和上线步骤

对于上面的第二个问题,你可能会说,我找不同的人负责系统里的不同部分不就好了。这虽然可以解决熟悉的问题,但耦合度是解决不了的。一个需求由十个人开发一个系统和三波人开发三个系统,这期间的沟通成本、耦合度等是无法比拟的。想象一下,一屋子开会的人你一言我一语和确定好边界后,三波人单独开会,一定是后者更高效。

1.3. 如何拆分

按照不同的思路来拆分软件系统,就会得到不同的架构。常见的拆分思路有如下三种。

  • 面向流程拆分:将整个业务流程拆分为几个阶段,每个阶段作为一部分。

  • 面向服务拆分:将系统提供的服务拆分,每个服务作为一部分。

  • 面向功能拆分:将系统提供的功能拆分,每个功能作为一部分。

理解这三种思路的关键就在于如何理解“流程”“服务”“功能”三者的联系和区别。从范围上来看,从大到小依次为:流程 > 服务 > 功能,单纯从概念解释可能难以理解。

下面是不同拆分方式应对扩展时的优势。

  • 面向流程拆分:扩展时大部分情况只需要修改某一层,少部分情况可能修改关联的两层,不会出现所有层都同时要修改。例如我们将存储层从 MySQL 扩展为同时支持 MySQL 和 Oracle,那么只需要扩展存储层和数据层即可,展示层和业务层无须变动。

  • 面向服务拆分:对某个服务扩展,或者要增加新的服务时,只需要扩展相关服务即可,无须修改所有的服务。

  • 面向功能拆分:对某个功能扩展,或者要增加新的功能时,只需要扩展相关功能即可,无须修改所有的服务。

不同的拆分方式,将得到不同的系统架构,典型的可扩展系统架构有:

  • 面向流程拆分:分层架构

  • 面向服务拆分:SOA、微服务

  • 面向功能拆分:微内核架构

2. 分层架构

分层架构是很常见的架构模式,它也叫 N 层架构,通常情况下,N 至少是 2 层。例如,C/S 架构、B/S 架构。常见的是 3 层架构(例如,MVC、MVP 架构)、4 层架构,5 层架构的比较少见,一般是比较复杂的系统才会达到或者超过 5 层,比如操作系统内核架构。按照分层架构进行设计时,根据不同的划分维度和对象,可以得到多种不同的分层架构。

典型的 J2EE 系统架构就是逻辑分层架构,架构图如下:

无论采取何种分层维度,分层架构设计最核心的一点就是需要保证各层之间的差异足够清晰,边界足够明显,让人看到架构图后就能看懂整个架构,这也是分层不能分太多层的原因。否则如果两个层的差异不明显,就会出现一个人认为某个功能应该放在 A 层,而另一个人却认为同样的功能应该放在 B 层,这样会导致分层混乱。如果这样的架构进入实际开发落地,则 A 层和 B 层就会乱成一锅粥,也就失去了分层的意义。

分层架构之所以能够较好地支撑系统扩展,本质在于隔离关注点(separation of concerns),即每个层中的组件只会处理本层的逻辑。比如说,展示层只需要处理展示逻辑,业务层中只需要处理业务逻辑,这样我们在扩展某层时,其他层是不受影响的,通过这种方式可以支撑系统在某层上快速扩展。例如,Linux 内核如果要增加一个新的文件系统,则只需要修改文件存储层即可,其他内核层无须变动。

当然,并不是简单地分层就一定能够实现隔离关注点从而支撑快速扩展,分层时要保证层与层之间的依赖是稳定的,才能真正支撑快速扩展。

分层结构的另外一个特点就是层层传递,也就是说一旦分层确定,整个业务流程是按照层进行依次传递的,不能在层之间进行跳跃。最简单的 C/S 结构,用户必须先使用 C 层,然后 C 层再传递到 S 层,用户是不能直接访问 S 层的。传统的 J2EE 4 层架构,收到请求后,必须按照下面的方式传递请求:

分层结构的这种约束,好处在于强制将分层依赖限定为两两依赖,降低了整体系统复杂度。例如,Business Layer 被 Presentation Layer 依赖,自己只依赖 Persistence Layer。但分层结构的代价就是冗余,也就是说,不管这个业务有多么简单,每层都必须要参与处理,甚至可能每层都写了一个简单的包装函数。

3. SOA架构

SOA 的全称是 Service Oriented Architecture,即“面向服务的架构”

SOA 出现 的背景是企业内部的 IT 系统重复建设且效率低下,主要体现在:

  • 企业各部门有独立的 IT 系统,比如人力资源系统、财务系统、销售系统,这些系统可能都涉及人员管理,各 IT 系统都需要重复开发人员管理的功能。例如,某个员工离职后,需要分别到上述三个系统中删除员工的权限。
  • 各个独立的 IT 系统可能采购于不同的供应商,实现技术不同,企业自己也不太可能基于这些系统进行重构。
  • 随着业务的发展,复杂度越来越高,更多的流程和业务需要多个 IT 系统合作完成。由于各个独立的 IT 系统没有标准的实现方式(例如,人力资源系统用 Java 开发,对外提供 RPC;而财务系统用 C# 开发,对外提供 SOAP 协议),每次开发新的流程和业务,都需要协调大量的 IT 系统,同时定制开发,效率很低。

为了应对传统 IT 系统存在的问题,SOA 提出了 3 个关键概念。

  • 服务

所有业务功能都是一项服务,服务就意味着要对外提供开放的能力,当其他系统需要使用这项功能时,无须定制化开发。

服务可大可小,可简单也可复杂。例如,人力资源管理可以是一项服务,包括人员基本信息管理、请假管理、组织结构管理等功能;而人员基本信息管理也可以作为一项独立的服务,组织结构管理也可以作为一项独立的服务。到底是划分为粗粒度的服务,还是划分为细粒度的服务,需要根据企业的实际情况进行判断。

  • ESB

ESB 的全称是 Enterprise Service Bus,中文翻译为“企业服务总线”。从名字就可以看出,ESB 参考了计算机总线的概念。计算机中的总线将各个不同的设备连接在一起,ESB 将企业中各个不同的服务连接在一起。因为各个独立的服务是异构的,如果没有统一的标准,则各个异构系统对外提供的接口是各式各样的。SOA 使用 ESB 来屏蔽异构系统对外提供各种不同的接口方式,以此来达到服务间高效的互联互通。

  • 松耦合

松耦合的目的是减少各个服务间的依赖和互相影响。因为采用 SOA 架构后,各个服务是相互独立运行的,甚至都不清楚某个服务到底有多少对其他服务的依赖。如果做不到松耦合,某个服务一升级,依赖它的其他服务全部故障,这样肯定是无法满足业务需求的。

典型的 SOA 架构样例如下:

SOA 解决了传统 IT 系统重复建设和扩展效率低的问题,但其本身也引入了更多的复杂性。SOA 最广为人诟病的就是 ESB,ESB 需要实现与各种系统间的协议转换、数据转换、透明的动态路由等功能。

ESB 虽然功能强大,但现实中的协议有很多种,如 JMS、WS、HTTP、RPC 等,数据格式也有很多种,如 XML、JSON、二进制、HTML 等。ESB 要完成这么多协议和数据格式的互相转换,工作量和复杂度都很大,而且这种转换是需要耗费大量计算性能的,当 ESB 承载的消息太多时,ESB 本身会成为整个系统的性能瓶颈。

4. 微服务架构

微服务与SOA的区别

  • 服务粒度

服务粒度整体上来说,SOA 的服务粒度要粗一些,而微服务的服务粒度要细一些。例如,对一个大型企业来说,“员工管理系统”就是一个 SOA 架构中的服务;而如果采用微服务架构,则“员工管理系统”会被拆分为更多的服务,比如“员工信息管理”“员工考勤管理”“员工假期管理”和“员工福利管理”等更多服务。

  • 服务粒度

SOA 采用了 ESB 作为服务间通信的关键组件,负责服务定义、服务路由、消息转换、消息传递,总体上是重量级的实现。微服务推荐使用统一的协议和格式,例如,RESTful 协议、RPC 协议,无须 ESB 这样的重量级实现。

  • 服务交付

SOA 对服务的交付并没有特殊要求,因为 SOA 更多考虑的是兼容已有的系统;微服务的架构理念要求“快速交付”,相应地要求采取自动化测试、持续集成、自动化部署等敏捷开发相关的最佳实践。如果没有这些基础能力支撑,微服务规模一旦变大(例如,超过 20 个微服务),整体就难以达到快速交付的要求,这也是很多企业在实行微服务时踩过的一个明显的坑,就是系统拆分为微服务后,部署的成本呈指数上升。

  • 应用场景

SOA 更加适合于庞大、复杂、异构的企业级系统,这也是 SOA 诞生的背景。这类系统的典型特征就是很多系统已经发展多年,采用不同的企业级技术,有的是内部开发的,有的是外部购买的,无法完全推倒重来或者进行大规模的优化和重构。因为成本和影响太大,只能采用兼容的方式进行处理,而承担兼容任务的就是 ESB。

微服务更加适合于快速、轻量级、基于 Web 的互联网系统,这类系统业务变化快,需要快速尝试、快速交付;同时基本都是基于 Web,虽然开发技术可能差异很大(例如,Java、C++、.NET 等),但对外接口基本都是提供 HTTP RESTful 风格的接口,无须考虑在接口层进行类似 SOA 的 ESB 那样的处理。

因此,我们可以看到,SOA 和微服务本质上是两种不同的架构设计理念,只是在“服务”这个点上有交集而已。

5. 微内核架构

微内核架构(Microkernel Architecture),也被称为插件化架构(Plug-in Architecture),是一种面向功能进行拆分的可扩展性架构,通常用于实现基于产品(原文为 product-based,指存在多个版本、需要下载安装才能使用,与 web-based 相对应)的应用。例如 Eclipse 这类 IDE 软件、UNIX 这类操作系统、淘宝 App 这类客户端软件等,也有一些企业将自己的业务系统设计成微内核的架构,例如保险公司的保险核算逻辑系统,不同的保险品种可以将逻辑封装成插件。

5.1. 基本架构

微内核架构包含两类组件:核心系统(core system)和插件模块(plug-in modules)。核心系统负责和具体业务功能无关的通用功能,例如模块加载、模块间通信等;插件模块负责实现具体的业务逻辑。微内核的基本架构示意图如下:

上面这张图中核心系统 Core System 功能比较稳定,不会因为业务功能扩展而不断修改,插件模块可以根据业务功能的需要不断地扩展。微内核的架构本质就是将变化部分封装在插件里面,从而达到快速灵活扩展的目的,而又不影响整体系统的稳定。

5.2. 设计关键点

微内核的核心系统设计的关键技术有:插件管理、插件连接和插件通信。

  • 插件管理

核心系统需要知道当前有哪些插件可用,如何加载这些插件,什么时候加载插件。常见的实现方法是插件注册表机制。核心系统提供插件注册表(可以是配置文件,也可以是代码,还可以是数据库),插件注册表含有每个插件模块的信息,包括它的名字、位置、加载时机(启动就加载,还是按需加载)等。

  • 插件连接

插件连接指插件如何连接到核心系统。通常来说,核心系统必须制定插件和核心系统的连接规范,然后插件按照规范实现,核心系统按照规范加载即可。常见的连接机制有 OSGi(Eclipse 使用)、消息模式、依赖注入(Spring 使用),甚至使用分布式的协议都是可以的,比如 RPC 或者 HTTP Web 的方式。

  • 插件通信

件通信指插件间的通信。虽然设计的时候插件间是完全解耦的,但实际业务运行过程中,必然会出现某个业务流程需要多个插件协作,这就要求两个插件间进行通信。由于插件之间没有直接联系,通信必须通过核心系统,因此核心系统需要提供插件通信机制。这种情况和计算机类似,计算机的 CPU、硬盘、内存、网卡是独立设计的配件,但计算机运行过程中,CPU 和内存、内存和硬盘肯定是有通信的,计算机通过主板上的总线提供了这些组件之间的通信功能。微内核的核心系统也必须提供类似的通信机制,各个插件之间才能进行正常的通信。

像OSGI,规则引擎这些都是微内核架构,下面这篇文章描述了电商里面的微内核架构实现

https://mp.weixin.qq.com/s/aPbhtIWsBTJGLoCq4ukEPQ

6. 参考资料

《从0开始学架构》

Edgar

Edgar
一个略懂Java的小菜比