三层架构是如今大多数 Web 应用在底层仍然采用的形态:一部分负责提供用户界面,一部分负责运行业务逻辑,还有一部分负责存储数据。它很老、很无聊,却是大多数系统正确的起点,因为每一层都可以独立扩展、独立加固。
本文讲解这三层,展示它们如何借助子网和安全组映射到云网络中,并画出一个跨两个可用区的典型 AWS 部署。同样的结构适用于任何云平台。
三个层
| 层 | 职责 | 典型组件 | 扩展方式 |
|---|---|---|---|
| 表示层 | 交付界面并接收请求 | CDN、静态资源、负载均衡器、Web 服务器 | 边缘缓存、增加实例 |
| 应用层 | 业务逻辑和 API | 应用服务器、容器、函数 | 增加实例、自动扩缩容 |
| 数据层 | 存储和读取状态 | 关系型数据库、缓存、对象存储 | 更大的实例、副本、缓存 |
让它成为一种架构、而不只是三个方框的规则是:每一层只和相邻的层通信。浏览器永远不会直接访问数据库,表示层也永远不会执行 SQL。
AWS 在其 三层架构概述 中描述了这种模式,其中也包括用函数替代应用服务器的无服务器变体。
把各层映射到网络
当各层位于路由不同的子网中时,它们才成为真正的边界:
- 公有子网 只放必须能从互联网访问的东西:负载均衡器和 NAT 网关。
- 私有应用子网 放应用服务器。它们可以通过 NAT 出站访问互联网,用于更新和调用 API,但除了经过负载均衡器之外,外部没有任何途径能访问它们。
- 私有数据子网 放数据库和缓存,完全没有到互联网的路由。
接着用 安全组 只放行相邻层的流量:数据库的 5432 端口只接受来自应用层安全组的访问,而不是某个 IP 段。引用安全组而不是地址,能让规则在实例增减时依然正确。
跨可用区的高可用
让每一层至少运行在两个可用区中:
- 负载均衡器在设计上就是跨可用区的。
- 应用服务器运行在一个自动扩缩容组中,每个可用区都有实例。某个可用区出故障时,其他可用区接管流量,扩缩容组会补足容量。
- 数据库在另一个可用区配有同步的备用实例。使用 Amazon RDS Multi-AZ 时,切换到备用实例是自动的,端点名称也保持不变。
让应用层保持无状态,这样任何实例都能处理任何请求:会话放进共享缓存或数据库,上传文件放进对象存储,实例磁盘上不保存任何重要的东西。可以在工作时间随机终止一台应用服务器来检验;如果用户有所察觉,就说明还有状态藏在实例上。
分别扩展每一层
各层的扩展方式不同,这正是要把它们分开的原因:
- 表示层: CDN 吸收大部分静态流量,吸收多少由缓存头决定。
- 应用层: 增加实例。按请求数或 CPU 扩缩容,并让实例快速启动。
- 数据层: 最难的一层。先纵向扩展,读多写少时加只读副本,为热点键加缓存,把分片当作最后的手段。
一个请求穿过各层的过程
跟随一位正在加载订单历史的用户:
- DNS 返回 CDN 的地址。浏览器请求页面,HTML、脚本和图片来自 CDN 的缓存或其后的静态存储桶。
- 页面调用
GET /api/orders。CDN 把动态路径转发给负载均衡器,负载均衡器终止 TLS,并在任一可用区中选择一台健康的应用服务器。 - 应用服务器从缓存中读取会话,确认用户已登录,然后向主数据库查询订单。
- 响应经由负载均衡器和 CDN 返回;由于响应是私有的,CDN 不会缓存它。
每一跳只跨越一个层边界,每个边界都恰好只有一个允许的入口。这让整个设计易于理解,也易于加固。
成本与合理规格
数据层通常占账单的大头,事后也最难缩减,所以要根据实测而不是猜测来确定它的规格。应用层最容易调整:从小规格开始,按真实负载扩展,让自动扩缩容来应对峰值。NAT 网关和跨可用区流量是最容易让人意外的开销项;尽量把通信频繁的流量留在同一个可用区内,并为对象存储使用 VPC 端点,免得应用服务器每次上传都要付 NAT 费用。
架构图
图表内容
- Users
- DNS
- CDN
- Static assets bucket
- VPC across two availability zones
- Presentation tier: public subnets
- Application load balancer
- Application tier: private subnets
- App servers, zone A
- App servers, zone B
- Data tier: private subnets, no internet route
- Database primary, zone A
- Standby, zone B
- Session cache
- HTTPS
- resolve
- static files
- dynamic requests
- SQL
- sync
- sessions
这张图的读法如下:
- 顶部: 用户解析 DNS 并连接到 CDN,CDN 从存储桶提供静态文件,并转发动态请求。
- 表示层: 应用负载均衡器是公有子网中唯一的组件。
- 应用层: 可用区 A 和 B 的应用服务器位于私有子网中,只接收来自负载均衡器的流量。
- 数据层: 可用区 A 的主数据库同步复制到可用区 B 的备用实例;应用服务器还使用一个会话缓存。这一层没有到互联网的路由。
- Markdown 块 列出了安全组规则,真正落实分层的正是这些规则。
常见错误
- 把数据库放在公有子网里, 说是“临时的”,只为了从笔记本上快速查个数。应改用堡垒机或托管的会话工具。
- 层与层之间放行整个 CIDR 段的安全组, 而不是引用相邻层的安全组。
- 应用服务器上保存状态: 会话放在内存里,上传文件存在本地磁盘。自动扩缩容和故障切换时就会丢数据。
- 为了省钱让数据库只用一个可用区, 而应用层却分布在三个可用区。
- 跳过 CDN, 让每张图片都从应用服务器提供。
- 应用服务器没有健康检查路径, 导致负载均衡器继续把流量发给连不上数据库的实例。
三层不够用的时候
三层描述的是一个应用。当系统发展成多个服务时,每个服务往往仍保持自己的三层结构,架构图则要上升一个层次,展示服务之间如何通信。微服务架构图 介绍了这一步。网络细节请参阅 AWS VPC 图,多区域部署请参阅 AWS 多区域架构图。
在 Archboard 中构建
- 使用 Google 或 GitHub 登录,并新建一张图。
- 添加 C4 模型节点包中的一个 人员、网络节点包中的一个 DNS,以及云节点包中的一个 CDN 和一个 对象存储 节点。
- 添加网络节点包中的一个 VPC,并在其中放入三个基础节点包中的 框架 节点,每层一个。
- 在表示层添加一个 云负载均衡器,在应用层添加两个 计算实例 节点,在数据层添加两个 托管数据库 节点以及数据节点包中的 Redis。
- 让每一层只连接相邻的层,并添加一个基础节点包中的 Markdown 块,写明安全组规则。如果你的环境已经用 Terraform 描述,Terraform 转架构图工具 可以为你生成第一版草图。
这个示例共用 15 个图表节点,在 Free 套餐的限制之内。
常见问题
三层架构中的三层分别是什么? 表示层(用户界面和入口)、应用层(业务逻辑和 API)以及数据层(数据库、缓存和存储)。
三层架构过时了吗? 没有。它是 Web 应用的默认形态。微服务和无服务器系统仍然包含同样的三种职责,而且往往是每个服务各有一套。
三层(tier)架构和三层(layer)架构有什么区别? layer 是代码内部的逻辑划分;tier 则是物理划分,分布在彼此独立、之间隔着网络边界的机器或服务上。
数据库应该放在公有子网里吗? 不应该。把它放在没有互联网路由的私有子网中,只允许应用层的安全组访问。
如何让三层应用实现高可用? 让每一层至少运行在两个可用区中,保持应用服务器无状态,并使用在另一个可用区有自动备用实例的数据库。