尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

分布式文件系统与对象存储:把对象存储当文件系统用,一次 LIST 遍历把元数据节点打满

分布式文件系统与对象存储:把对象存储当文件系统用,一次 LIST 遍历把元数据节点打满 title: 分布式文件系统与对象存储把对象存储当文件系统用一次 LIST 遍历把元数据节点打满tags: [对象存储, 分布式文件系统, ceph, minio, 存储选型]categories: [后端, 分布式]我们曾经以为对象存储就是无限容量的网盘于是很自然地把文件系统那套习惯搬了过去用/2024/06/17/order_123.json当路径、用 LIST 接口遍历某天目录做对账、用复制再删除实现文件移动。直到对象数涨到 2 亿一次对账脚本的 LIST 请求把存储集群的元数据节点 CPU 打到 100%读写全链路抖动 20 分钟。这篇讲清楚对象存储和分布式文件系统HDFS/CephFS在你能怎么用它上的根本差异以及那次事故里我们改掉的 3 个文件系统式坏习惯。事故现场对账脚本把存储集群打挂背景是我们用自建 MinIO 集群兼容 S3 协议版本 RELEASE.2023-05 那代存订单快照 JSONkey 设计成/{date}/{orderId}.json。运营每天凌晨跑对账逻辑是列出昨天的所有对象逐个比对 DB。代码长这样// 错误示范用 LIST 当目录遍历前缀下对象越多越慢 ListObjectsV2Request req new ListObjectsV2Request() .withBucketName(order-snapshot) .withPrefix(2024/06/17/); // 一天约 600 万个对象 ListObjectsV2Result result; do { result s3.listObjectsV2(req); for (S3Object obj : result.getObjectSummaries()) { reconcile(obj.getKey()); // 逐个拉下来比对 } req.setContinuationToken(result.getNextContinuationToken()); } while (result.isTruncated());逐行看第 4 行withPrefix(2024/06/17/)前缀匹配一天 600 万个对象第 8 行listObjectsV2每次最多返回 1000 个isTruncated()为真就翻页。问题在于 S3 的 LIST 是最终一致 前缀无索引的——它本质是扫描元数据前缀越长、对象越多单次 LIST 越慢。我们那天 600 万对象的目录LIST 翻了 6000 页每页平均 80ms光拉列表就花了 8 分钟且每页请求都压在元数据节点上把它的 CPU 顶满连带正常读写PUT/GET一起变慢。对象存储的真相扁平 key 空间没有目录树很多人不知道S3/MinIO 的目录是用 key 里包含的/模拟出来的。存储引擎底层是一个扁平的 key-value 空间/2024/06/17/order_123.json和/a/b/c没有层级关系那个/只是 key 字符串的一部分。所以没有目录概念GET一个对象就是一次 KV 查LIST prefix是前缀扫描不是进目录。rename copy delete你想把文件从 A 移到 B在对象存储里是整文件复制再删原 key大文件代价极高。LIST 有频率与一致性限制S3 对 LIST 限流更狠且返回可能不是实时全量最终一致。对应的正确做法是对象元数据别靠 LIST 来管用 DB 或专用索引。对账时直接查自己库的记录对象存储只当最终落地点// 正确示范元数据走 DB对象存储只负责存/取不做遍历 Scheduled(cron 0 0 2 * * ?) public void reconcile() { // 对账从业务库拿昨天的 orderId 列表而不是 LIST 对象存储 ListString yesterdayOrders orderMapper.listByDate(LocalDate.now().minusDays(1)); for (String orderId : yesterdayOrders) { String key 2024/06/17/ orderId .json; // 只校验这个 key 是否真实存在一次 HEAD不是遍历 try { s3.getObjectMetadata(order-snapshot, key); } catch (AmazonS3Exception e) { if (e.getStatusCode() 404) alarm(丢失快照: key); } } }逐行看第 5 行从业务库orderMapper拿订单列表这是索引该待的地方第 9 行对每个 key 只发一次getObjectMetadataHEAD 请求轻量不拉内容第 11-13 行 404 才告警。整次对账从扫描 600 万对象变成按已知订单数发 HEAD对存储集群几乎零压力。第三个坑上传后立刻读拿到 404我们把用户头像上传到对象存储后立刻把 URL 返回给前端前端马上GET展示——偶发 404。根因是对象存储尤其自建 MinIO 前面挂了 Nginx/网关 CDN 边缘存在边缘缓存与域名解析的短暂不一致。虽然 AWS S3 现在对新对象保证读写一致但很多自建集群和带 CDN 的场景并不保证。// 正确示范上传后给前端读的 URL 加一段就绪校验必要时重试 HEAD public String publishAvatar(MultipartFile file, String userId) throws IOException { String key avatar/ userId .jpg; s3.putObject(user-assets, key, file.getInputStream(), new ObjectMetadata() {{ setContentType(image/jpeg); setContentLength(file.getSize()); }}); // 主动 HEAD 确认已可读最多重试 3 次间隔 200ms for (int i 0; i 3; i) { try { s3.getObjectMetadata(user-assets, key); break; // 读到了说明已生效 } catch (AmazonS3Exception e) { if (e.getStatusCode() 404) Thread.sleep(200); // 再等等 else throw e; } } return cdnBase key; // 确认就绪再返回给前端 }逐行看第 4 行putObject上传第 8 行起循环getObjectMetadata做 HEAD 校验第 12 行遇到 404 就 sleep 200ms 再试。只有确认能读到才把 CDN URL 返回前端。这个写后读校验把头像 404 率从千分之三降到零。注意如果前面挂了 CDN还要在上传后主动调一次 CDN 刷新refresh/purge否则边缘节点可能返回旧的 404 缓存。选型对比HDFS / CephFS / 对象存储维度HDFSCephFS文件接口对象存储S3 兼容接口模型专有 API POSIX 网关POSIX 文件系统REST API扁平 KV小文件友好度差NameNode 内存瓶颈中MDS 元数据压力优无目录树开销跨地域/无限扩容中 Federation 复杂中优天然水平扩展遍历/重命名有专门工具支持但慢弱LIST 慢、rename 贵适合场景离线大数据、批处理需要 POSIX 的通用存储图片/视频/快照/静态资源我们最终的划分订单快照、用户头像这类一次写、少改、海量的归对象存储需要频繁随机改、当文件系统挂载的归 CephFSTB 级离线分析归 HDFS。之前那次是把该放对象存储但当文件系统遍历的活干错了。一个反直觉的成本点请求费比存储费贵很多人算对象存储成本只看每 GB 多少钱忽略了请求次数计费。我们早期把订单快照按每笔订单一个对象存峰值 600 万笔/天就是 600 万次 PUT 对账时 600 万次 LIST/HEAD请求费反超了存储费。后来对 7 天前的冷快照做合并归档——把同一天的零散对象在离线任务里拼成少量大对象比如按 1MB 一个打包PUT 次数降到原来的 1/200请求费直接砍掉一个数量级。存储选型不能只比容量单价请求模式读写比、对象大小、是否遍历才是成本的决定项。复盘真实数字事故当天对账 LIST 涉及600 万个对象、6000 次翻页拉列表耗时8 分 12 秒期间元数据节点 CPU 峰值100%正常GET延迟从 30ms 涨到1.8 秒。改成DB 拿订单 HEAD 校验后对账对存储集群的请求量从 600 万次 LIST 降到约 12 万次 HEAD仅校验缺失元数据节点 CPU 峰值回到15%。头像 404 率从千分之三约每天 900 次降到0加的写后读校验平均只多花200-600ms。对象数当时已2.1 亿单桶后来按userId哈希打散到 16 个桶前缀避免单一前缀热点。我的取舍判断第一对象存储不是文件系统别用 LIST 当目录遍历、别用 rename 当移动。这是两个最常见的文件系统式误用每一个都是元数据节点的噩梦。元数据该放 DB 就放 DB对象存储只做它擅长的按 key 存/取。第二key 设计要打散前缀、避免时间序热点。我们最早用/日期/...做前缀导致每天的对象天然聚在同一个前缀下LIST 慢、底层分区也容易热点。改成hash(userId) % 16 / userId / ...后读写压力均匀分摊。第三对象存储的最终一致在带缓存的链路里会放大。如果你前面有 CDN 或网关写后立刻读一定要做 HEAD 校验 CDN 刷新别想当然认为PUT 完就能 GET。思考题如果你的系统现在用 LIST 接口做统计某目录下有多少文件对象数涨到千万级时这个操作的延迟和成本会怎么变你打算怎么改造欢迎评论区交流。存储选型的坑往往要等规模上来才暴露。下一篇写混沌工程讲一次给 Redis 注入超时、结果本地缓存没兜底把 DB 打挂的演练。
返回列表