加载中...

本篇要回答的问题:服务端的业务状态和桌面有何本质不同?为什么会催生存储中间件,它在服务端为何地位如此重要?为什么说"存储即数据结构,存储中间件就是元数据结构"?

上一讲讲完服务端多出的第一类基础软件——负载均衡。本讲讲第二类——存储(DB/Storage)。切入点是"业务状态":从数据视角对比桌面与服务端,进而推导存储中间件的由来。

觉得抽象、抓不住"由来"时,可先跳到文末的 [[#为什么:把这篇倒过来推一遍]]——那节从一个四行的 Python 服务出发,把结论重新推了一遍。

服务端宏观体系架构(含存储)

业务状态:两者其实是同一个状态机

桌面与服务端都可看作状态机,差别只在驱动事件:

驱动事件 状态转移公式
桌面程序 用户交互事件 业务状态ᵢ₊₁ = F(用户交互事件ᵢ, 业务状态ᵢ)
服务端程序 网络 API 请求 业务状态ᵢ₊₁ = F(网络 API 请求ᵢ, 业务状态ᵢ)

桌面:用户交互事件驱动的状态转换
服务端:网络 API 请求驱动的状态转换

最大差别在于业务状态的表示

业务状态如何表示 持久化
桌面 内存中的数据结构(Model 层那棵以 Document 为根的 DOM 树) 多数无需持久化;需要时靠明确的用户操作(如 Ctrl+S)整体存盘
服务端 不能放内存——程序一挂数据就丢 每次 API 请求后都须可靠持久化

核心原则(比"大规模+24×7"更重要)坚决不能丢失用户的数据,即他认为已完成的业务状态。服务端对用户是黑盒,用户一旦收到 API 成功反馈,就认定这个成功是确认的。

存储中间件与容灾级别

服务端自己做持久化为什么难(远比桌面难):

难点 原因
持久化时间必须极短 多用户共享,持久化时若别的请求排队,轻则吞吐差、重则用户眼里"停止服务",须短到无感知
必须增量式 多租户状态量巨大(100 万用户 × 100K = 100G),不能像桌面"每次重新生成新文件"

让每个服务端开发者各自考虑持久化,代价过高 → 存储中间件应运而生。历史上第一个存储中间件是数据库(1974 年 IBM System R)。桌面很少用数据库,少数需增量持久化的场景才用(如微信本地聊天记录用嵌入式 SQLite)。

容灾级别随互联网普及逐步抬高:

阶段 诉求 方案
早期 高性能、稳定、经验证 单机数据库,低峰期离线备份即可
进阶① 防单机故障/数据丢失 主从结构(多机热备)
进阶② 规模可伸缩,突破单机容量上限 分布式数据库(早期靠手工分库分表)
进阶③ 防机房级灾难(网络中断、地震) 两地三中心、跨机房容灾灾备

存储即数据结构 → 存储中间件就是"元数据结构"

核心论断:存储即数据结构。存储中间件就是"元数据结构"

为什么在服务端实现一个数据结构如此之难:

论据 说明
基于外存而非内存 桌面数据结构基于内存、实现易;服务端每次状态改变都要持久化,核心数据结构都基于外存
稳定性/并发要求极高 服务端的伸缩能力完全取决于存储的伸缩能力——业务服务器多无状态、加机器容易;存储压力大却可能要重新划分和搬迁数据
品控极难 内存里实现一个 KV(Map/Dictionary)几十分钟搞定;但服务端 KV 极其复杂,写出来也不敢直接上,需海量测试,上线初期还常用"双写"(同时用成熟存储 + 新存储)兜底

正因如此,服务端所有业务涉及的数据结构都需抽象成存储中间件。"元数据结构"是作者自造词:数据结构的种类非常有限、最好理论可被证明,有了这些基本数据结构,所有业务需求都能高效实现。

今天常见的存储中间件(不完整、目前不可枚举):

存储中间件 英文
键值存储 KV-Storage
对象存储 Object Storage
数据库 Database
消息队列 MQ
倒排索引 SearchEngine

为什么:把这篇倒过来推一遍

原文是"结论式"的——先给状态机模型,再宣布存储中间件"应运而生"。这一节反过来:从一个能跑的四行 Python 服务出发,看它是怎么被一步步逼出中间件的。

起点:业务状态就是这个 dict

balance = {}                      # ← 这就是"业务状态"

def handle(req):                  # ← 这就是那个 F
    balance[req.user] += req.amount
    return "ok"

业务状态ᵢ₊₁ = F(请求ᵢ, 业务状态ᵢ) 说的就是这四行。桌面程序一模一样,只是 req 从"网络请求"换成"用户点了下鼠标"。

真正的差别只有一个:balance 活在内存里。桌面上无所谓——Word 崩了最多丢没存盘的几段,而且用户自己知道他没按 Ctrl+S。服务端不行:用户看到 "ok" 就认定事情办完了;进程一挂 dict 没了,用户却攥着一张"成功"的回执。

所以这篇的起点不是"服务端需要数据库",而是——服务端不允许那个 ok 是假的

自己扛持久化,会依次撞上四堵墙

第一版补丁,每次请求后存盘:

def handle(req):
    balance[req.user] += req.amount
    json.dump(balance, open("state.json", "w"))   # 存盘
    return "ok"

看着能用。然后会依次撞上这四堵墙:

# 撞上什么 被逼出什么
墙 1 100 万用户 × 100K = 100G,改一个人的余额重写了 100G。桌面能这么干(.docx 才几 MB,且用户按了 Ctrl+S 自己知道要等),服务端不能 增量式存储:只写变化的那一小块
墙 2 只写一小块,就得知道那块在文件的第几个字节;这张"用户→偏移量"的表本身也要存盘、也会随数据增长而分裂 索引 / B 树
墙 3 两个请求同时 json.dump 互相覆盖;写到一半进程被 kill,文件半新半旧直接损坏,连之前存好的数据一起赔进去 WAL(先把要做的事追加进日志,再改主文件,崩了照日志重放)与事务
墙 4 这台机器本身会坏:硬盘、机房断网、地震 主从热备 → 分库分表 → 两地三中心(即上一节那张容灾级别演进表)

走完这四步,手里那坨代码已经就是一个数据库了

这才是"由来":不是某天有人发明了数据库,而是每个写服务端的人都会被这四堵墙逼着走同一条路,走出来的东西还长得一模一样。既然如此,不如抽成一个独立进程,全世界共用一份、并被千万个生产环境验证过。1974 年的 IBM System R 就是第一个被这么抽出来的东西。

作用也是一句话:把"可靠 + 增量 + 高并发 + 不停服地保存状态"这件极难的事,从业务代码里整个搬走,让剩下的活儿变回写那个纯函数 F

"存储即数据结构"倒过来读就懂了

别顺着读,倒着读:存储中间件的清单,其实就是内存数据结构清单的服务端版本。

你想要的东西 内存里 服务端对应的中间件
dict d = {} KV 存储(Redis、etcd)
一大坨 bytes open(f, 'wb').write() 对象存储(S3、七牛)
一堆记录 + 按条件查 list of dataclass + 遍历 数据库(MySQL、PG)
队列 queue.Queue() 消息队列(Kafka、RabbitMQ)
倒排表 {词: [文档]} 二十行代码 搜索引擎(Elasticsearch)

左边一行代码,右边一个团队干几年。差的不是功能,是"落盘 + 并发 + 挂了不丢 + 能扩容"——也就是上面那四堵墙。

“元数据结构"于是可以这么理解:右边这一列不是无限长的,就像语言的内置类型也就那么几个。设计业务时脑子里该想的是"我这儿需要一个能持久化的 dict”,而不是"我要不要上 Redis"——先定数据结构,中间件只是它的服务端实现

为什么结论必然落在"扛不住都是存储扛不住"

再回到 F:业务服务器跑的是 F(请求, 状态),它自己不持有状态(状态全在存储里),所以是无状态的——压力大了,复制一台一模一样的挂到负载均衡后面即可。

存储不行,存储就是状态本身。加一台机器意味着把现有数据切开、搬一半过去,搬的过程中还要继续对外服务、还不能丢不能错。完全不同的难度量级。

整条链路上唯一那个"加机器解决不了"的环节就是存储,它必然是瓶颈。上一讲的负载均衡管无状态那半边怎么随便加机器,这一讲的存储中间件管有状态那半边——两讲拼起来才是完整的服务端骨架。

总结

本讲从"桌面与服务端都是状态机、差别只在业务状态的表示"切入,推出服务端必须可靠、增量、低延迟地持久化业务状态,从而催生了存储中间件,并随互联网演进抬高容灾级别(主从→分布式→两地三中心)。核心论断是"存储即数据结构,存储中间件就是元数据结构":服务端数据结构基于外存、品控极难,故所有业务数据结构都要抽象为经过千锤百炼的存储中间件。几乎所有服务端扛不住压力,根因都是存储没扛住。

先把"是什么"回答清楚

概念 一句话说明
业务状态 状态机的当前状态;桌面在内存,服务端必须可靠持久化
不丢数据原则 用户收到成功反馈即视为确认,服务端坚决不能丢这份业务状态
存储中间件 把业务状态持久化的能力抽象出来,避免每人各自造轮子
容灾级别演进 单机备份 → 主从热备 → 分布式可伸缩 → 两地三中心跨机房
存储即数据结构 存储本质是基于外存的数据结构
元数据结构 种类有限、最好可证明完备的一组基本存储数据结构
双写 新存储上线初期同时写成熟存储兜底,保品控

一句话速记

桌面与服务端都是状态机,唯一本质差别是业务状态怎么存——服务端因"绝不丢数据 + 多租户 + 低延迟"必须把持久化抽象成存储中间件(元数据结构);而服务端的伸缩与稳定,几乎完全由存储决定。

几条值得记住的判断

  • 不丢数据 > 高性能:用户认为已完成的状态,服务端必须可靠保住。
  • 伸缩能力取决于存储:业务服务器易扩,存储扩容要搬数据,是真正瓶颈。
  • 存储品控至关重要:自研存储再急也要海量测试 + 双写兜底。

思考题

作者说"存储中间件就是元数据结构",并坦言今天还没有系统理论能告诉我们"有了哪些数据结构就完备了"。结合你接触过的业务,你认为 KV、对象存储、数据库、MQ、倒排索引之外,还有哪些不可或缺的"元数据结构"?它们能否被更基本的几种组合出来?

公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关