# 解决方案架构设计模式

# 多层架构模式

在多层架构需要采取松耦合设计原则,并考虑可伸缩性和弹性。在多层架构中,产品给你被划分为多个层,例如表示层、业务层、数据库层和服务层,这样每一层都可以独立的实现和伸缩。

多层架构为各层提供了灵活性,每一层都可以在不影响其他层的前提下添加新功能。在安全方面,可以保障各层的安全并将其与其他层隔离,因此即便某一层的安全受到损害,其他层也不受影响,这使应用程序故障排查和管理变得更可控,这种方式可以快速查明问题来源。

典型的三层架构如下:

  • web层:web层也称为表示层,是应用程序面向用户的部分,终端用户与web层进行交互以收集或提供信息。它提供用户界面,帮助用户与应用程序进行交互,收集信息后传递给应用层。
  • 应用层:应用层也称为逻辑层,它根据web层接收的信息进行处理业务逻辑,并将处理好的信息返回到web层呈现给用户。
  • 数据层:用户存储各种用户数据和应用程序数据。它包含了需要持久储存的所有数据,它不仅存储事务信息,还用于保存用户会话信息和应用程序的配置信息。

# 无状态和有状态架构模式

在设计复杂应用程序时,需要处理用户状态以维持活动流,其中用户可能正在进行一系列活动,例如添加购物车、下单、付款等,用户还可以通过各种渠道来访问应用程序,他们很可能使用多个不同的设备访问,这种情况下,系统需要在多个设备间保持用户活动并维持其状态,直到交易完成。因此架构设计和应用程序实现需要规划用户会话管理,以满足此需求。

为了保持用户状态并使应用程序无状态,需要将用户会话信息存储在持久化数据层,该状态可以在多个web服务器或微服务之间共享。

有状态的应用程序,状态信息由服务器处理,其通常不能很好的支持水平伸缩,应用程序的状态存储在本地服务器中,也无法替换。

我们的设计方法应该更多关注无状态方法共享会话状态,如下图所示:

为了使应用程序松耦合和可伸缩,所有的用户会话都持久化存储在Dynamo DB中,我们可以使用客户端cookie来保存会话ID,这种架构可以使用水平伸缩模式,而且不用担心用户状态信息丢失。无状态架构消除了创建和维护用户会话的开销,可以保持应用程序模块间的一致性,无状态应用可以减少服务端的内存使用,并消除了会话超时的问题。

# SOA模式

在SOA模式中,不同的应用程序组件通过网络通讯协议互相交互,每个服务都提供了端到端的功能。一般来说,SOA将单体应用程序中的一些处理逻辑划分为多个彼此独立的服务,使用SOA的目的是降低应用程序服务间的耦合。

实现SOA架构的方法有很多种,最常见的有简单对象访问协议(SOAP)的web服务架构和基于表示层状态转移(REST)的web服务架构。架构设计选择REST还是SOAP取决于组织需求,他们的区别如下:

下面是一个SOA架构图:

  • 当用户在浏览器中输入网站地址时,用户请求会触达DNS服务器以加载网站,Route 53 路由到web应用程序的托管服务器。
  • 用户浏览产品时,通过CDN缓存并向用户传递静态资源。
  • 产品目录中的内容(图片、视频)以及其他应用程序数据(日志文件)存储在Amazon S3中。
  • 用户可以通过多种设备浏览网站,例如,他们可能通过移动设备将商品添加到购物车,然后在PC上付款,为了处理用户会话,可以用DynamoDB来持久化存储会话。
  • 为了提高性能并减少延迟,可以用Amazon ElastiCache作为产品的缓存层,以减少对数据库读写操作。
  • 为了提供便捷搜索体验,可以用Amazon Cloud Search来构建可扩展的搜索功能。
  • 如果需要频繁部署多个层和组件,可以用AWS Elastic Beanstalk处理基础设施的自动配置、应用程序部署,通过应用自动伸缩功能来处理负载,并监控应用程序。

# 无服务器架构模式

在传统情况下,如果想开发一个应用程序,我们需要有一台服务器,并安装所需的操作系统和软件。在编码阶段,需要确保服务器启动并正常运行,在部署期间,需要添加更多的服务器满足用户需求,整个过程中,基础设施的管理和维护将消耗大量的精力。采用无服务器架构,团队可以专注于应用程序集功能特性开发,而不必担心底层基础设施的维护。无服务器意味着不需要服务器来运行我们的代码,这是一种低成本模型。

公有云在计算和数据存储领域提供了多种无服务器服务,使端到端的无服务应用程序开发更加容易。下图是一个采用了无服务器架构的应用程序:

  1. 客户通过HTTPS向网站发起请求,然后Amazon S3直接提供了Web页面
  2. 客户的调查问卷通过AJAX请求提交给Amazon API网关
  3. Amazon ApI网关将此项日志记录到Amazon CloudTrail
  4. Amazon API网关将AJAX请求转换为AWS Lambda函数的事件触发器,之后,Lambda函数将提取调查问卷数据并进行处理
  5. AWS Lambda函数将调查问卷结果发送到Amazon S3存储桶,并通过服务器端加密措施对其进行保护
  6. 调查问卷中不包含任何个人身份信息的元数据被写入并存储到DynamoDB表中,以便后续查询和分析

# 微服务架构模式

微服务是基于REST风格的web服务,可以独立伸缩,在保持其他组件不变的情况下,也轻松的扩展系统中的相关组件。微服务的优势是只需要维护较少的代码逻辑即可保持独立。

微服务的一个核心概念是限界上下文,它们组合在一起可以形成一个业务领域,业务领域设计整个业务流程,单个微服务定义了边界来封装其中所有的细节。以下是微服务架构的一些实践:

  • 创建单独的数据存储,使团队可以选择最适合其服务的数据库。
  • 使服务器保持无状态,从而提高伸缩性。
  • 进行独立构建,提高新功能发布的敏捷性。
  • 部署在容器中,意向图的标准和方式应对所有微服务。
  • 蓝绿部署,创建生产环境副本,新功能部署后将小部分流量路由到新环境,然后逐步增加新环境中的流量,知道整个用户群都能看到新功能。
  • 监控环境,通过适当的重路由、伸缩和受控降级来主动预防中断。

# 基于队列的架构模式

RESTful架构中,客户端服务会等待主机服务的响应,这就意味着HTTP请求可能会阻塞API,如果下游服务不可用,信息可能就会丢失。基于队列的架构通过在服务之间添加消息队列可以解决上述问题,由消息队列来为服务保留信息,信息保留在消息中,如果服务崩溃,这条消息也会在服务可用后立即得到处理。

基于队列的架构有如下常用属于:

  • 消息:包含消息头和消息体,消息头包含与消息有关的元数据,消息体则包含实际的内容。
  • 队列:保存着需要时可用的消息。
  • 生产者:产生消息并将其发布到队列的服务。
  • 消费者:消费和利用消息的服务。
  • 消息代理:在生产者和消费者之间帮助收集、路由和分发消息。

当有序流程需要在链接在一起的多个系统上执行时,可以采用队列链表模式。在各个系统和作业之间使用队列,可以消除单点故障,如下图所示:

这种架构的好处是:

  • 可以使用松耦合的异步处理,快速返回响应,而无须等待其他服务确认。
  • 可以使用SQS,通过松耦合的EC2实例构建系统。
  • 即使EC2实例发生故障,消息也会保留在队列服务中。

还有一种作业观察者模式,在这种模式下可以根据队列中待处理的消息数来创建自动伸缩组,作业观察者模式可以通过增加或减少用于作业处理的实例数来保持性能。

如上图,位于左侧的第一组EC2服务器运行批处理作业并将消息推送到队列中,右侧第二组EC2服务器使用处理这些消息,当消息达到一定的阈值,CloudWatch就会触发自动伸缩组,在消费者集群中添加额外的服务器以加快作业处理,当队列深度低于阈值时,自动伸缩组会删除多余的服务器。

观察者模式可以根据作业数量来计算规模,从而提高效率并节约成本,该处理过程有韧性,这意味着及时服务器发生故障,作业流程也不会停止,消费者可以根据其可用性从队列中拉取消息。

# 事件驱动架构模式

事件驱动架构可以帮助我们将一系列事件衔接在一起,已完成完整的功能流程。例如,在网站上购买商品时,我们希望在支付完成后自动生成发票,并立即收到电子邮件。在事件驱动架构中,我们常常会用到消息队列,事件驱动架构中,我们可以用到基于发布者/订阅者模型或事件流模型。

在发布者/订阅者模型中,发布事件时,系统会想所有订阅者发送通知,每个订阅者都可以根据其数据处理需求进行必要的操作。如下如所示:

在事件流模型中,消费者可以读取来自生产者的连续事件流,例如,我们基于使用事件流来捕获点击流日志的连续流,还可以在检测到异常时发送告警。

Kinesis是一个用户摄取、处理和储存连续流数据的服务。用户在web和移动应用程序上点击产生了点击事件流,这些点击流通过API网关发往数据分析应用进行实时分析。在数据分析应用中,Kinesis Data Analytics可以计算特定时间段的转化率,例如,最近五分钟内完成购买的人数,实时数据汇总完成后,Amazon Kinesis Data Analytics会将结果发送到Kinesis Data Firehose,后者将所有数据文件存储在Amazon S3存储中,根据需要进行进一步处理。

Lambda函数从事件流中读取并检测数据是否存在异常。当检测到转化率异常时,该AWS Lambda函数就会通过电子邮件发送通知。在该架构下,事件流是持续发生的,我们可以利用此架构分离生产者和消费者,保持架构的可扩展性。

# 基于缓存的架构模式

缓存是为了让后续的请求更快,并降低网络吞吐量,而将数据或文件临时性的存储在请求者与持久化之间中间位置的过程,缓存可以提高应用程序的运行速度并降低成本,为了提高应用程序的性能,可以将缓存应用于架构的各个层。

通常,服务器的RAM和内存缓存引擎用于支持应用程序的缓存,但是如果服务器崩溃,缓存将不会保存数据。在分布式的环境中,有一个独立于应用程序生命周期的专用缓存层,在水平伸缩时,基于实现最佳性能。架构各层缓存机制如下:

  • 客户端缓存。存适用于移动端和桌面端等用户设备。它将先前访问的Web内容缓存下来,以便更快地响应后续的请求。每个浏览器都有自己的缓存机制。HTTP缓存通过将内容缓存在本地浏览器来加速应用程序。HTTP头cache-control为客户端请求和服务器响应定义了浏览器的缓存策略。这些策略定义了内容应该缓存在哪里以及缓存多长时间,后者也被称为生存时间(Time To Live, TTL)。cookie是另一种用于在客户端机器上存储信息以加速浏览器响应的方法。
  • DNS缓存。当用广通过互联网访问网站地址时,公共域名系统(Domain Name System, DNS)服务器将会查找其IP地址。缓存DNS的解析信息将减少网站的加载时间。在第一个请求完成之后,DNS可以缓存到本地服务器或浏览器,对该网站的后续请求都将更快。
  • web缓存。大多数的请求都涉及检索web内容,例如图像、视顿和HTML页面。将这些资源缓存在用户位置附近可以加快页面的加载速度。这也消除了磁盘读取和服务器加载的时间。CDN提供了一个边缘位置网络,可以用来缓存像高分辨率的图像和视频之类的静态内容,对于频繁读取的应用程序(例如游戏、博客、电子商务产品目录页面等)来说非常有用。用户会话中包含了很多关于用户偏好及其状态的信息。将用户会话存储在自己的键值对存储中可以提供非常好的用户体验,并且可以将用户会话进行缓存,以加速用户响应。
  • 应用程序缓存。在应用层,可以将复杂重复请求的结果缓存起来,以避免重复的业务逻辑计算和数据库命中。缓存提高了应用程序的性能,减少了数据库和基础设施的负载。
  • 数据库缓存。应用程序的性能在很大程度上取决于数据库的速度和吞吐量。数据库缓存可以显著提高其吞吐量并降低数据检索的延迟。可以在任何类型的关系型或非关系型数据库前面应用数据库缓存。

# 断路器模式

分布式系统通常会调用下游服务,可能会因为调用失败或挂起而导致没有响应,我们经常能看到一些代码对失败调用进行多次重试,当代码重试几次后,终端用户需要等待更长时间才能得到错误响应,重试功能会消耗更多线程,甚至导致级联故障。

断路器模式的目的是了解下游依赖项的运行状况,当它检测到依赖项的健康状态不正常时,就会通过其实现逻辑驳回请求,知道检测到依赖项恢复正常,通过使用持久层监控过去一段时间内的成功和失败请求数,可以实现断路器模式。观测到的异常请求百分比超过定义的阈值,断路器都将被标记为开启,这种情况下,所有请求都会抛出异常,不会继续集成依赖系统。

断路器中的决策逻辑使用了状态机来跟踪和共享健康及不健康的请求计数,我们可以使用如Redis或Memcached这种低延迟持久化存储来维护服务状态。

# 隔板模式

在大型系统中,系统会被分区解耦服务间的依赖关系,其理念是一个故障不应该导致整个系统崩溃。在隔板模式中,最好将应用程序高度依赖的元素隔离成多个服务池,这样及时其中一个发生故障,其他池仍然可以为上游提供服务。

引入隔板模式时需要考虑如下要点

  • 应用程序不应该因为一个服务故障而停机。
  • 一个分区的性能问题不应该影响整个应用程序。
  • 要选择合适的颗粒度,不要将服务池设计的太小,要确保其能够应对应用程序负载。
  • 监控每个服务分区的性能并遵守SLA,确保所有活动部件能够协同工作。

# 浮动IP模式

单体应用对部署的服务器有很多依赖,应用程序的配置和代码中有些硬编码的参数是基于服务器DNS名称和IP地址的,当原来服务器出现问题,需要启动新服务器时,硬编码的IP配置将会带来挑战。

为了应对这种情况,我们需要创建一个新服务器,并保留原来服务器IP地址和DNS名称,实现此类功能,可以将网络接口从故障服务器转移到新服务器,转移网络接口意味着新服务器承担了旧服务器的身份。

# 使用容器部署应用程序

不同的应用程序需要不同的硬件和软件部署环境,解决方案需要可以在任何地方运行任何内容,并且保持一致性、轻量级且可执行。Docker创建了一种容器,其中包含了运行软件应用程序所需的所有文件和内容,例如文件系统结构、守护程序、库和应用程序依赖项等等,容器将软件与其周边的开发环境隔离开,有助于减少在同一基础设施上运行不同软件的冲突。

虚拟机是操作系统级别的隔离,容器是内容级别的隔离,这种隔离运行多个应用程序同时运行在单主机的操作系统上,每个应用程序都仍然有其自己的文件系统、存储、RAM、库,以及他们自己的系统视图。

使用容器可以将多个应用程序部署在单个虚拟机中,每个应用程序都有自己的运行时环境,因此可以在相同数量的服务器上同时运行很多独立的应用程序。容器具有以下优点:

  • 可移植的应用程序运行时环境。容器提供了与平台无关的功能,只需要构建一次应用程序,就可以部署到任何地方,无论其底层操作系统是什么。
  • 更快的开发和部署周期。应用程序修改后,可以在任何地方快速启动。
  • 能够将依赖项和应用程序打包在同一个工件中
  • 可以同时运行应用程序的不同版本。具有不同依赖项的应用程序也可以在单个服务器中同时运行。
  • 自动化支持友好。容器的管理和部署可以通过脚本完成,可以节约成本和降低人为错误。
  • 提高资源利用率。统一微服务容器的多个副本可以跨服务器部署。

在开发模式下构建容器、推进到测试,然后发布到生产环境,容器化部署会非常实用。有时我们也需要处理一些短期的工作流程,需要搭建临时环境,这些环境可能是队列系统或持续集成任务,它们并不总能有效地利用服务器资源,这时我们可能就需要Kubernetes之类的容器编排服务,Kubernetes可以自动化的置备容器,并提供安全性、网络和可伸缩性的支持。

图中单个EC2虚拟机中部署了多个容器,有ECS(弹性容器服务)进行管理,并辅助代理通信服务和集群管理,负载均衡器将所有用户请求在容器之间进行分配。AWS还提供了EKS(Elastic Kubernetes Service)服务进行容器管理。

# 解决方案架构中的反模式

在我们的实际工作中,由于时间紧迫或资源不足,团队可能会偏离最佳实践,需要持续关注的反模式如下:

  • 伸缩是被动的,需要手动完成。
  • 缺少自动化。
  • 服务器长期使用硬编码IP地址或配置信息。
  • 架构的所有层都紧密耦合并依赖于服务器。
  • 应用程序与服务器绑定,并且服务器之间直接通信,用户身份验证和会话信息存储在本地服务器中。
  • 将一种类型的数据库应用于各种需求。
  • 仅使用单个数据库实例为应用程序提供服务。
  • 直接从服务器提供静态内容,没有进行任何缓存。
  • 在没有安全策略的情况下开放服务器的访问权限。