软件架构的主要技术是将整个系统划分为较小的部分,并约定它们之间如何相互作用。每个较小的部分或单元,都应该有明确的功能和接口。
例如,在一个典型系统中,常见的是一个由以下几部分组成的提供Web服务的架构(如图1-1所示):
❍一个将所有数据存储于MySQL中的数据库。
❍一个Web Worker,用于解释执行基于PHP编写的动态HTML内容。
❍一个Apache Web服务器,用于处理所有Web请求,并返回所有静态文件,如CSS和图片,同时将动态请求转发给Web Worker。
图1-1 典型的Web服务架构
这种架构和技术栈自21世纪初以来一直非常流行,称为LAMP,这是由所涉及的几个开源项目名称组成的缩写:作为操作系统的(L)inux,以及(A)pache、(M)ySQL和(P)HP。现在,这些组合可被同类项目替代,比如用PostgreSQL代替MySQL,用Nginx代替Apache,但仍然使用LAMP的名称。在使用HTTP设计基于Web的客户端/服务器(Client/Server)系统时,LAMP架构可当作默认的起点,它为建立更复杂的系统提供了稳定可靠的基石。
正如你看到的,每个不同的组成单元在系统中都有其特定的功能,并以明确的方式定义了彼此之间如何交互。这就是所谓的 单一责任原则 (Single-Responsibility Principle)。当需要新的功能时,大多数情况下将只涉及系统中的单个组成单元。所有前端页面样式的变化都将由Web服务器处理,而动态内容的变化则由Web Worker处理。各个组成单元之间存在依赖关系,因为存储在数据库中的数据可能需要修改,以支持动态请求,但它们可以在访问请求处理过程中被及早发现。
我们将在第9章更详细地描述此架构。
每个组成单元都有不同的要求和特点:
❍数据库系统要做到可靠,因为它存储了所有的数据。备份和恢复等相关的维护工作非常重要。数据库系统本身不会频繁更新,因为数据库是非常稳定的。数据库中表的模式(schema)的更改需要重启Web Worker。
❍Web Worker要有可扩展性,并且不存储任何状态。也就是说,所有数据都来自数据库,或发送给数据库。Web Worker会经常更新,它可以在同一台机器或者多个不同机器上运行多个副本,以实现横向扩展(horizontal scaling,亦称水平扩展)。
❍Web服务器则需要为新的前端页面样式做一些调整,但这种情况不常发生。一旦完成了对Web服务器的正确配置,这个组成单元就将保持相当稳定的状态。每台机器只需要一个Web服务器单元,因为它能够在多个Web Worker之间实现负载均衡。
由此可见,各组成单元的任务负载状况是很不一样的,大多数新的工作任务都是交给Web Worker,而其他两个单元则比较稳定。数据库需要我们进行相关的维护,以确保它处于良好状态,因为它可以说是三个单元中最关键的一个。如果出现故障,其他两个单元可以迅速恢复,但数据库中的任何损坏都会导致许多问题。
系统中最关键且最有价值的单元几乎总是所存储的数据。
各单元所用的通信协议也不一样。Web Worker使用SQL语句与数据库进行对话。Web服务器则使用专用接口与Web Worker沟通,通常是FastCGI或类似的协议。Web服务器通过HTTP请求与外部客户端进行通信。Web服务器和数据库间不直接进行交互。
这三种协议各不相同。并非所有的系统都必须如此,不同的组件也可以共享同一协议。例如,有的系统中可能有多个RESTful接口,这在微服务架构中很常见。
看待不同单元的典型方法是将其视为独立运行的不同进程,但也并非总是如此。同一进程内的两个不同模块也可以遵循单一责任原则。
单一责任原则可以应用在不同的层面,用于定义功能或其他块之间的划分。因此,它可以在越来越小的范围内应用。这是一个“Turtles All the Way Down”问题(龟背上的世界,意指刨根问底、没完没了、无穷无尽,详见霍金《时间简史》中的故事)!但是,从软件系统架构的角度来看,较高层次的组成单元是最重要的,因为高层单元决定着架构。掌握好在细节方面该达到什么程度,显然是非常重要的。在设计系统架构的过程中,与其关注“太多细节”方面的问题,不如把关注重点放在“宏观”层面。
典型的例子是独立维护的程序库,它同时也可能是代码库中的某些模块。例如,你可能创建一个模块,用于完成各种外部HTTP调用,并处理各种复杂的保持连接、重试、错误处理等操作,也可能会创建一个模块,用于根据特定的参数生成不同格式的报告。
问题的关键在于,要创建一个独立的单元,需要清楚地定义其API,并且要很好地界定其职责。该模块应当可以被提取到不同的代码仓库中,并作为第三方单元来使用,这样才能被当作真正的独立单元。
创建一个只有内部功能职责划分的大型软件组件是一种众所周知的模式,称为单体架构。上面描述的LAMP架构就是一个例子,因为大部分的代码都被定义在Web Worker里面。事实上,项目最初通常采用的都是单体架构,因为一般来说在项目最开始的时候并没有长远的规划,当代码库规模很小时,把系统严格地分成多个组件并没有太大的优势。随着代码库和系统越来越复杂,单体架构系统内部单元的划分开始变得有意义,可能后来我们才逐渐意识到要将其分成多个组件。我们将在第9章进一步讨论单体架构相关的问题。
在同一组件内,通信通常是直接进行的,因为可以使用内部API。在绝大多数情况下,会基于相同的编程语言来完成通信。