基于空间的架构(Space-Based Architecture)
本篇是 五种经典架构模式 中的基于空间的架构。评分和选型见 横向对比。
基于空间是五种里最不常见、也最“重”的一种。2015 版报告也称它为“云架构模式”(cloud architecture pattern)。
核心思想与它要解决的问题
先看它针对的痛。大多数 Web 应用的结构是:
用户量上来时,Web 服务器可以加,应用服务器也可以加,但所有请求最终都压到同一个数据库上。数据库是最难水平扩展的一层,这就形成了一个“三角形”瓶颈:越往下越窄。用户量突然暴涨时(秒杀、抢票),扩前两层只会把压力更快地传到数据库,最终整体崩溃。
基于空间的架构的做法是:把中心数据库从同步请求路径上拿掉。
- 每个处理单元把应用需要的数据放在自己的内存里(内存数据网格)。
- 处理单元之间互相复制数据,保持一致。
- 写数据库这件事改为异步进行,用户请求完全不用等数据库。
“空间”这个名字来自元组空间(tuple space):多个并行进程通过一块共享内存交换数据的思想。这里的“共享内存”就是分布在各处理单元之间、相互复制的内存数据。
组件与拓扑
两类主要组件:处理单元(processing unit) 和 虚拟化中间件(virtualized middleware)。文章的 Figure 5 画的正是这两块:上面是一排处理单元,每个内部有应用模块、内存数据网格和一个本地小存储;下面是虚拟化中间件,包括消息网格、数据网格、处理网格和部署管理器。
处理单元
处理单元包含:
- 应用模块:业务逻辑。可以是整个应用,也可以只是应用的一部分(按功能分成几类处理单元)。
- 内存数据网格:这个处理单元需要的数据,放在内存里。
- 数据复制引擎:把本单元的数据变化同步给其他单元。2015 版报告里还提到一个可选的异步持久化存储,用于故障恢复。
文章说的“处理单元可以按需求自动扩缩,数据在单元之间复制处理,不需要持久化到中心数据库”,就是在描述这一部分。
虚拟化中间件
虚拟化中间件负责处理单元之间的“家务事”,由四部分组成:
| 组件 | 职责 |
|---|---|
| 消息网格(messaging grid) | 管理输入请求和会话状态。请求进来后,决定交给哪个处理单元(可以是轮询,也可以更复杂) |
| 数据网格(data grid) | 管理处理单元之间的数据复制。Fundamentals 指出它往往其实就分布在各处理单元的复制引擎里,而不是一个独立组件 |
| 处理网格(processing grid) | 可选。一个请求需要多个不同类型的处理单元协作时,由它编排(类似中介者) |
| 部署管理器(deployment manager) | 根据负载动态启动和关闭处理单元,用户多了就加,少了就减 |
关键概念
内存中的复制数据
每个处理单元里都有一份数据。一个单元修改了数据,复制引擎把变化异步地推给其他单元。读取几乎都是本地内存读取,所以非常快。
Fundamentals 区分了两种缓存模型:
- 复制缓存(replicated cache):每个单元都有完整的一份。读最快,容错最好;但数据量要能放进每个单元的内存,更新频率也不能太高。
- 分布式缓存(distributed cache):数据分片存放在一个外部缓存集群里,单元去远程读。一致性更好、能存更多数据;但多了网络调用,而且缓存集群本身要高可用。
还有一种折中叫近缓存(near cache):本地放热点数据,其余去分布式缓存读。Fundamentals 对近缓存并不推荐,因为各处理单元的本地热点不同,行为难以预测。
数据泵、数据写入器、数据读取器
数据库并没有消失,只是被挪出了请求路径。Fundamentals 引入三个名词描述数据怎么最终落库,以及怎么从库里回来:
- 数据泵(data pump):处理单元更新了内存数据后,把这次变化异步地发出去(通常是消息队列),送往数据库方向。数据泵保证投递,一般按业务领域或处理单元类型分开。
- 数据写入器(data writer):从数据泵里接收消息,把变化写进数据库。写入器可以按领域划分,也可以一个写入器服务所有类型。
- 数据读取器(data reader):从数据库读数据,送回处理单元。它不在日常请求路径上,只在三种情况下工作:所有处理单元同时崩溃需要重建、重新部署所有处理单元、需要读取不在缓存里的归档数据。这条反向的路径也叫反向数据泵。
为什么它能去掉数据库瓶颈
关键是两点:
- 读写都在内存:用户请求只和本地内存数据网格打交道,不等数据库。
- 数据库写入异步化、削峰:峰值时刻的写入先进数据泵排队,数据写入器按数据库能承受的速度慢慢写。数据库从“同步瓶颈”变成“最终落盘的地方”。
于是伸缩只取决于能起多少个处理单元,而不是数据库能扛多少连接。
数据冲突与一致性风险
代价也正来自“每个单元一份数据、异步复制”:
- 数据冲突(data collision):单元 A 和单元 B 几乎同时修改同一条数据,各自先改了本地内存,再把变化复制给对方——结果互相覆盖。复制延迟越长、单元越多、同一数据更新越频繁,冲突越多。
- Fundamentals 给了一个估算冲突率的公式,大意是:冲突数与处理单元实例数、同一数据的更新频率的平方、复制延迟成正比,与缓存中数据条数成反比。(公式形式我凭记忆写出,原书的确切符号和形式请以原书为准。)
- 最终一致:数据库总是落后于内存,报表和外部系统读数据库时看到的是“过一会儿之前”的数据。
- 数据丢失风险:处理单元在数据泵发出消息前崩溃,那次变化可能丢失;需要持久化的消息队列和确认机制。
- 缓存容量:复制缓存要求数据能放进每个单元的内存,不适合海量数据。
对付冲突的常见手段:按业务键把请求固定路由到同一个处理单元(让同一张票总是由同一个单元处理)、缩短复制延迟、对极少数关键数据改用分布式缓存或分布式锁。
一次请求怎么流动
演唱会抢票的一次购票请求:
取舍:适合与不适合
适合:
- 并发量极高且波动剧烈:抢票、秒杀、拍卖、在线投票。
- 对响应时间要求很高,数据库撑不住同步访问。
- 数据量能放进内存,或者“热”数据能放进内存。
不适合:
- 大规模关系型数据、复杂查询和报表为主的系统。
- 需要强一致事务(金融账务等),冲突的代价无法接受。
- 团队和预算有限:内存数据网格产品(如 Hazelcast、Apache Ignite、GigaSpaces、Oracle Coherence)有许可和运维成本,测试高负载场景也很贵。
书中评分
| 维度 | 评价 | 理由(概括) |
|---|---|---|
| 整体敏捷性 | 高 | 处理单元可以动态启动和关闭,应对环境变化快 |
| 易部署性 | 高 | 虽然不是彻底解耦的分布式架构,但它是动态的,借助云平台可以方便地把单元推到服务器上 |
| 可测试性 | 低 | 很难在测试环境里模拟出足以考验伸缩能力的高负载,成本高 |
| 性能 | 高 | 内存数据访问加缓存机制 |
| 可扩展性(伸缩) | 高 | 几乎不依赖中心数据库,去掉了伸缩的主要瓶颈 |
| 易开发性 | 低 | 缓存与内存数据网格产品复杂,开发时要时刻考虑不影响伸缩和一致性 |
Fundamentals 里基于空间的架构在弹性(elasticity)、伸缩、性能上是满分,在简单性、可测试性上很低,成本也偏高。我对星级的具体数字没有把握,这里只说倾向。
真实风格的例子
一个演唱会票务平台:平时访问量很小,开票那一刻瞬间几十万人涌入。
- 开票前,部署管理器预先拉起一批处理单元,数据读取器把座位库存加载进内存。
- 开票时,消息网格把同一场次的请求固定路由到少数处理单元,减少冲突;所有购票都在内存里完成。
- 订单通过数据泵排队写入数据库,数据库只需按自己的速度消化。
- 结束后,部署管理器缩容到最少几个单元。
Fundamentals 还举了在线拍卖的例子:出价需要即时响应,而且出价数量和时间难以预测。
和插件化的关系
基于空间的架构解决的是伸缩与性能,和插件化几乎是两个维度: