ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

对象存储核心设计思想:从扁平命名空间到纠删码

对象存储核心设计思想:从扁平命名空间到纠删码 对象存储这个概念很多人第一次接触时会被“对象”两个字绕晕觉得它是某种高深的新技术。但真正在存储行业待久了你会发现对象存储的设计思想本质上回答的是一个非常朴素的问题当数据量从TB级涨到PB级、EB级文件数量从百万级涨到几十亿、几百亿的时候我们怎么用最低的成本、最可靠的方式把这些数据管起来、存得住、随时能取出来用。我最早是从传统文件存储转到对象存储研发的刚接触时很不适应总觉得没有目录结构、没有层级关系、连“打开文件”的操作都没有这玩意儿能算存储产品吗直到我理解了它背后的几套核心设计思想才发现这套体系其实非常自洽——它不是为了解决“个人电脑里放文件”这种小问题而是从诞生那一刻起就在为“无限扩展的海量非结构化数据”做设计。这篇文章我不打算抄文档式的概念定义而是想从一个存储从业者的角度把对象存储的底层设计思想掰开揉碎讲清楚包括它的核心模型、关键取舍、分布式架构背后的考量以及我这些年踩过的坑。无论你是刚入行的后端工程师、要做技术选型的架构师还是只是对云存储好奇的开发者这篇文章应该都能帮你建立一套完整的认知框架。1. 先把核心模型搞懂桶、对象、键三件套要理解对象存储的设计思想第一关就是彻底搞清楚它的基本模型。跟文件系统那种“根目录-子目录-文件”的树形结构不同对象存储只有三层概念桶Bucket、对象Object、键Key。整个系统所有的复杂设计都是围绕这三个概念展开的。1.1 对象是什么数据和元数据打包成一坨对象是对象存储里最基本的存储单元。一个对象里面装了什么东西除了你真正要存的数据内容Data之外它还自带了一套元数据Metadata比如这个对象的创建时间、大小、内容类型甚至是你自定义的一些业务属性标签。关键点在于在对象存储里数据和元数据是“打成一个包”存放的脱离开这个包数据本身没有独立意义。这一点和传统文件系统有本质区别——在文件系统里文件的内容inode里指向的磁盘块和文件的属性inode里的权限、时间戳是分开管理的系统可以单独修改属性而不动内容。而在对象存储里一个对象就是一个完整的、不可拆分的整体它天然具备自描述能力。这个设计的直接好处是对象存储非常适合存放“写一次、读很多次”的数据。视频、图片、日志归档、备份文件这些数据一旦写入内容基本上不会改动把数据和元数据打包存储读取时一次就能拿到全部信息减少了多次寻址的开销。我经常跟同事开玩笑说对象就像一个密封的快递盒盒子上贴着标签你不需要打开盒子就能知道里面装的是什么要寄走或者入库保管整盒一起处理就行。1.2 桶是什么扁平命名空间里的唯一容器桶是对象存储的命名空间容器有点类似文件系统里的“顶层目录”但只是一个非常弱的“目录”因为它没有层级结构桶下面不能嵌套子桶。在同一个桶里所有对象都处于同一个平面层级上。为什么用“桶”来比喻而不是用“文件夹”因为桶的设计理念源自于一种叫“扁平命名空间”Flat Namespace的思路。在传统文件系统里你创建一个目录目录之间的关系是树形的查找一个文件需要逐层遍历路径。而在对象存储里桶下面的每一个对象都通过一个全局唯一的Key来标记理论上你可以直接通过这个Key定位到对象不需要任何中间层级的参与。这里要特别注意的是桶的命名是全局唯一的尤其在使用云厂商的对象存储服务时桶名会在整个平台的全局命名空间中唯一。这就好比快递公司里每一个包裹都有唯一的运单号不管这个快递是在北京的仓库还是上海的仓库你只要报出运单号就能查到对应的包裹。这种全局唯一性设计后面会衍生出很多分布式寻址的方案是整个系统能够水平扩展的基石。1.3 键和寻址一次HTTP请求直接命中数据对象存储对外暴露的接口最典型的就是基于HTTP的RESTful API。你要上传一个对象就是往http://{endpoint}/{bucket}/{key}发一个PUT请求你要下载就是往同样的地址发一个GET请求你要列举桶里的对象就是往http://{endpoint}/{bucket}?list-type2发一个GET请求。这种设计看起来简单背后其实藏着一个非常核心的寻址思想把存储操作转化成HTTP路由问题。在这个模型里{endpoint}指向的是对象存储网关或者入口服务而{bucket}/{key}拼起来的字符串会被网关当作一个路由键用来定位这个对象数据到底落在集群里的哪些节点上。为了让这个定位过程足够快对象存储的寻址通常遵循“一次计算、直接命中”的原则。网关拿到Key之后先查一下元数据服务或者通过本地缓存就能知道这个对象存储在哪个数据节点上然后把请求转发过去。整个过程最多发生两次网络跳转一次从客户端到网关一次从网关到数据节点。对比一下传统文件系统打开一个深层路径的文件可能需要多次磁盘寻道和目录项解析你就知道这个设计在超大规模场景下的优势有多明显了。2. 核心设计思想之一扁平化命名空间把扩展问题转成路由问题我在给团队分享对象存储架构时最喜欢问一个问题如果你要设计一个能支撑几百亿个文件、分布在几万台机器上的存储系统你会怎么组织这些文件的目录结构大部分人的第一反应还是传统的树形目录方式。但只要你稍微算一下账就会发现树形结构在分布式场景下几乎走不通。根目录下可能有几百万个子目录每个子目录里又有几百万个文件目录本身的元数据管理、并发锁控制、路径解析开销都会变成系统扩展的瓶颈。文件系统擅长的是“组织有序的小规模数据”而不是“摊平状的超大规模数据”。2.1 为什么树形结构在分布式场景下会崩根本原因在于树形目录是有状态的而且状态之间存在强依赖关系。你要往某个深层目录里创建文件必须先确保整条路径上的所有目录都存在且有权访问这就意味着一次写入操作需要协调多个层级的元数据节点。当请求量大起来以后目录逐级查找的延迟会放大层级依赖也会让水平扩展变得极其困难——你总不能轻易把一个目录树拆成几万份分给不同的机器因为层级关系本身就锁死了数据的归属。文件系统里还有一种常见操作是“列目录”比如ls /data/logs你希望看到这个目录下所有的文件。在单机文件系统里这个操作靠目录项的线性扫描就能完成。但在分布式场景下如果数据是分散存储的要列出一个“目录”下的所有文件就必须跨集群汇总这个代价几乎没人能接受。2.2 一致性哈希与分片数据放哪由哈希决定对象存储的解法是从我一开始就不建目录树整个命名空间直接就是扁平的。对象存储里所谓的“目录”本质上只是Key的前缀比如logs/2025/01/access.log这里面的/并不是真正的目录层级只是对象Key字符串里的一部分而已。那么问题来了不分层级怎么决定一个对象应该存在哪台机器上答案是哈希和分片。常见的做法是把对象Key做哈希运算得到一个哈希值再根据集群的节点数和分片数把这个哈希值映射到具体的存储节点上。如果引入了虚拟节点还可以让映射更均匀也能在节点上下线时减少数据迁移量。打个比方把一个集群想象成一个大衣柜柜子有几十个抽屉你存东西的时候不需要考虑“该放进哪个抽屉”只需要根据物品标签算一个数这个数决定它进哪个抽屉。抽屉满了就加抽屉物品总量大了就加柜子整个映射过程完全自动化。对象存储的核心扩展能力就是从这个“哈希决定位置”的思路里长出来的。2.3 设计取舍放弃“文件夹”体验换来无限扩展当然扁平化命名空间不是没有代价的。最大的代价就是它放弃了人类习惯的“文件夹”式组织方式用户不能像操作本地磁盘那样拖拽文件、右键重命名、直接预览目录大小。正因为如此很多对象存储产品都会在用户体验层做一层“伪装”在控制台上显示“文件夹”支持按前缀搜索甚至提供listObjects接口来模拟目录浏览。但你要记住这些都是表面功夫底层依然是一个扁平命名空间加上Key前缀匹配。这个取舍值不值从工程角度看实在是太值了。因为目录树的每一次数据定位都需要层级查找而扁平命名空间的每一次定位都是常数级复杂度——不管你存了10个对象还是100亿个对象定位一个对象的时间几乎是不变的。这种特性叫做“水平扩展能力”它让对象存储成为了真正意义上可以无限增长的系统而这恰恰是做海量数据存储时最核心的诉求。3. 核心设计思想之二元数据与数据分离两套系统独立生长对象存储的第二大设计核心是把存储系统拆成了“控制面”和“数据面”两个层面。控制面负责管理元数据也就是对象清单、Bucket信息、ACL权限、版本状态等等数据面负责存储真正的数据内容也就是那些大块的文件块、视频帧、日志文本。这个分离设计的思想跟“路由器和交换机分离”“数据库和缓存分离”是一个套路不同特征的负载用不同形态的组件去承载各自独立扩展互不拖累。3.1 为什么要拆元数据请求和数据请求的特征完全不同先来看看两种请求的特征。元数据请求的特征是请求量极大、单条数据极小、对一致性和实时性要求极高。你每次访问对象第一步都要校验这个对象是否存在、权限是否够、有没有版本控制这些操作都是元数据请求。一个热点对象可能一分钟被访问几百万次元数据服务的QPS就会成为系统吞吐的瓶颈。数据请求的特征则是单个请求体量大、并发带宽高、对延迟有一定容忍度。上传一段视频可能要传几百兆甚至几个GB下载的时候希望吞吐越快越好但真实的业务对这个请求的处理时间没有那种“微秒级”的苛刻要求。如果不把这两者分开让同一个组件既处理高QPS的元数据查询又承担高带宽的数据传输最后的结果往往是既快不起来也扛不住大流量。我见过不少自研存储的团队最早把元数据和数据放在一起管理后来规模上去了单节点的CPU和内存全部被元数据请求打满数据写入反而被拖垮。3.2 元数据服务的架构形态从单点到分布式再到分片早期的对象存储系统元数据服务往往是一个单点的主从架构主节点负责读写从节点负责备份。这种架构在小规模场景下没有问题能保证强一致也容易实现事务操作但撑到几亿对象之后单机的内存和磁盘就会成为硬瓶颈。现代对象存储的元数据服务普遍会用分布式KV存储或分布式关系数据库来承载。拿比较通用的解法来说元数据以“分片”的方式打散到多台机器上每个分片分管一部分Bucket和Key区间分片内部再通过Raft等共识算法做多副本复制保证高可用。这里有一个我自己踩过的坑最早我们的元数据是存在一张MySQL表里的对象一多表数据量过亿之后索引失效、锁竞争、主从同步延迟全部涌出来每天都有告警。后来我们改用了HBase作为元数据存储把Bucket和Key作为行键查询模式完全匹配对象存储的访问模式问题才彻底缓解。元数据分片设计里的核心权衡是保证查询路径足够短同时让分片可以在节点间动态迁移以实现负载均衡和扩缩容。3.3 数据节点的设计要点写追加、读随机、批量淘汰数据面这边对象存储的数据节点设计也有讲究。因为对象的内容一旦写入基本不修改所以数据节点通常采用“追加写”的存储格式。每一次写入数据节点把对象内容追加到数据文件比如Segments里然后重建索引。这种做法非常适合大文件顺序写入能充分利用磁盘和底层文件系统的顺序IO性能。读取侧则正好相反是典型的随机读。一个对象被存储在一个或多个Segment的某个偏移位置读取时需要先通过索引找到偏移再发起随机读。为了做随机读加速数据节点通常会在内存里维护对象索引的缓存或者引入SSD缓存层来减少磁盘寻道。另外数据节点还需要处理压缩和加密。压缩能节省存储成本加密能保障数据安全。很多系统会在写入时先做压缩、再切片、再叠加校验和写入磁盘。读的时候逆向操作校验通过后才返回给客户端。这个设计的工程细节很多但根本思想只有一条在数据面上一切设计都以读写效率和存储密度为优先元数据那些复杂的语义完全不掺和。这种解耦让数据节点可以做得非常纯粹也特别容易水平扩容。4. 核心设计思想之三用最终一致性换取可用性和扩展性分布式系统里有一个著名的CAP理论——在网络分区发生时你必须在一致性和可用性之间二选一。对象存储作为典型的分布式存储系统它的设计取向非常明确大多数主流对象存储选择“可用性优先”接受最终一致性而不是每个操作都提供强一致保证。这一点很多刚从关系型数据库转过来的开发者在理解上会有障碍。你在MySQL里做一个事务要么成功、要么失败绝对不会有中间状态。但在对象存储里对某些操作的“一致性”定义要宽松许多。4.1 强一致与最终一致业务上到底缺什么先看一个典型的场景用对象存储存用户上传的头像。用户上传成功后马上通过URL去访问这时如果一致性做得不好用户可能会看到一个“404 Not Found”体验就会很差。所以现在主流的对象存储产品在“新建对象”这个操作上基本都能提供读写一致性——也就是上传完成后立刻读取是可以读取到最新数据的。但真正采用最终一致性的是另一些场景比如“列对象列表”。某用户在桶里上传了一个新对象立刻去列举桶内所有对象新的对象可能出现在列表中也可能要等几秒才出现。因为列举操作涉及的范围很大系统为了性能会做缓存和异步索引更新。如果业务对列表实时性要求极高就得靠应用层自己去兼容。更典型的是分布在不同数据中心的多个Region之间的同步。比如你在北京Region上传了一个对象想要在纽约Region也能读到这中间通常需要异步复制延迟可能是秒级甚至分钟级的。这种跨地域的一致性是标准的最终一致模型——你确定的是一定时间后数据能达成一致但不承诺精确的时间点。老话说“宁要最终一致不要永远一致”在工程上强一致往往意味着写操作必须达成跨节点的共识这个共识过程耗时且脆弱而最终一致可以用异步批量复制数据先写入本地、再同步到远端写操作的主路径延迟大大降低系统的整体可用性也随之提升。4.2 纠删码代替多副本把存储成本打下来一半以上聊完一致性得再说一个对象存储里极具代表性的成本设计——纠删码Erasure Coding。这块算是我认为对象存储和传统分布式文件系统最本质的区别之一。传统分布式存储为了保证数据可靠最笨的办法是副本机制一个数据存3份占3倍空间可靠性确实高但存储成本也是实打实的3倍。假设你要存10PB的数据副本方式实际占用30PB物理空间这个成本对于云厂商、大数据公司来说几乎是不可承受的。而对象存储通常采用纠删码机制。一个对象的数据被切分成k个数据块计算得到m个校验块然后把km个块分散存在不同的机器上。只要丢失的块数不超过m就能通过算法完整恢复原始数据。最常见的配置是 k4, m2 或者 k10, m4。前者每份数据实际占用 1.5 倍空间后者才 1.4 倍相比3副本省下的存储成本超过一半。代价是计算开销。每写入一次数据都要做一次编码运算每次读取损坏数据并修复都要做解码运算。所以对象存储在写入路径上多多少少会消耗一些CPU这也是为什么对象存储网关节点普遍要配强一些的CPU。不过现在的硬件算力越来越强开源库也提供了高效的SIMD实现这个CPU成本在存储成本面前已经可以忽略不计。一句话总结对象存储用CPU资源换磁盘空间在超大规模场景下这台账非常划算。4.3 版本控制、生命周期和不透明细节对象存储里还有一堆看起来简单、实际很考验设计思想的特性比如版本控制、生命周期管理、对象锁、事件通知等等。这些功能从表面上看是“附加能力”但底层设计都绕不开刚才说的那几个核心思想。版本控制解决的是数据误删除和覆盖修改的问题。开启版本控制后每次对同一个Key的操作都会生成一个新的版本ID旧的版本不会被删除而是变成历史版本。这个能力得益于对象存储“不可变性”的设计——对象一旦写入内容不可变要修改本质上是写入一个新版本。这和SQL数据库的Update完全不同更接近Git的提交概念。生命周期管理则利用了扁平命名空间和元数据分离的优势。你可以定义规则logs/前缀下的对象保留30天30天后自动转成低频存储90天后删除。这个规则由后台任务在元数据层面扫描匹配然后触发存储层级切换或删除操作。整个过程不需要人工干预对大企业的合规审计和成本控制非常有价值。5. 对象存储的适用边界哪些场景该选哪些场景别去碰聊完设计思想很多读者可能已经跃跃欲试觉得自己明白了想把所有存储都搬到对象存储上来。我劝你先冷静一下因为对象存储虽然优秀但并不是万能的。它有自己的强项也有明显不合适的场景。选型前搞清楚边界比什么技术都重要。5.1 强烈推荐的场景海量非结构化数据、高吞吐读、备份归档对象存储最适合的场景基本上是这几个方向。第一类是海量非结构化数据。图片、音视频、PDF、压缩包、模型文件、基因测序数据……凡是“内容大、数量多、很少改动”的数据都是对象存储的天然主场。我参与过的项目中有一个是存储一家视频公司每日生成的追踪录像每天新增几百TB几十亿个子文件。这种量级传统文件系统基本不可能撑得住但对象存储的扁平命名空间加水平扩展可以轻松应对。第二类是高吞吐读取场景。CDN的回源、数据湖分析比如直接对Parquet/ORC文件做查询、模型训练前的数据读取这类场景对“大批量数据连续读取”的吞吐要求远高于随机小IO。对象存储的追加写、大块读的设计恰好符合这种需求。第三类是备份、归档和容灾。对象存储自带的生命周期管理、跨地域复制、版本控制功能让它成为备份系统的理想存储层。很多企业已经把数据库备份、虚拟机镜像、业务日志全部迁到对象存储上因为低成本、高可靠还能自动化过期清理。5.2 不推荐的场景小文件密集随机写、强一致事务、POSIX语义对应地这几类场景请谨慎使用对象存储。首先是大量小文件的高频随机写。每写入一个小对象都要经过一次HTTP请求、元数据操作、数据编码、元数据更新这么一套流程。对象大小只有几KB但系统的各项开销却跟写入一个1GB的大对象没有本质区别。所以如果你需要尽在文字中让人别一眼看出来对吗已经在目的地了我们只是偶然路过的旅者没有人会在城镇入口核对全部包裹我们是安全的。
返回列表