对象与块存储提供不同的更新契约
块存储通过设备接口暴露可寻址的数据块,对象存储通过接口暴露带名字的对象。仅凭这两个分类,不能决定耐久性、一致性、共享能力或价格。应先列出应用真正需要执行的操作,再核对具体服务的契约。文件系统、数据库和兼容层可以增加行为,但不能让所有底层接口自动变得可互换;一个路径能够被打开,并不证明它符合应用的恢复要求。 比较实际支持的操作 块卷可以先格式化为文件系统,也可以由软件直接管理数据布局。块服务并不一定预装文件系统。应用可以进行小范围覆盖而不必替换整块卷,但共享访问仍然需要协调。把一个卷同时连接到多台机器,不会让普通单机文件系统自动变成集群文件系统。数据库能否恢复,还依赖块接口之上的写入顺序、同步和持久性约定,不能把能够写入等同于断电以后仍然正确。 对象通过桶或其他服务定义的命名空间寻址。普通 S3 通用桶写入会替换整个对象;读取却可以指定字节范围,所以“对象存储每次必须读完整个对象”并不正确。分段上传把多个部分组装成一个完成的对象,不等于通用文件系统的随机写契约。特殊产品还可能提供追加或其他更新能力,因此必须核对选中的桶类型与接口,不能把下面的小模型当成所有产品永远不变的规则。 对象存储通常适合媒体、导出结果和备份文件,让许多客户端按需读取。块存储通常适合数据库文件与虚拟机磁盘。但这只是常见工作负载,并不是互斥分类:数据库可以专门围绕对象接口设计,共享文件系统也可以建立在块设备之上。选择之前应测量请求大小、并发量、恢复过程,以及请求与数据传输的完整成本,不能只比较每月每单位容量的标价。 坏掉的样子 不要默认对象存储采用最终一致性。Amazon S3 对对象写入、删除及对应读取和列举提供强一致性。客户端手里保留旧下载结果,仍然可能看见旧内容,但这不能证明重新向服务读取也会得到旧值。分发缓存、跨区域复制和跨多个对象的业务更新又是不同边界,必须各自检查保证。把这些现象都叫作“对象存储有一致性窗口”,会让排查方向从一开始就偏离事实。 成功的 S3 PUT 不会暴露半个上传对象。请求失败或结果不明时,应确认实际状态,不能直接推断新版本被截断。单个对象的原子替换也无法防止丢失更新:两个客户端可能读取同一旧值,再分别覆盖它。条件请求可以在接受更新之前比较校验标识,但要遵循服务文档中的条件语义。ETag 并不总是内容校验和,更不是把多个键包进同一个事务的承诺。 生命周期删除与历史版本保留应通过真实恢复演练检验。打开版本控制不等于已经有独立备份,在程序里记一个版本数字,也不等于保存了所有旧载荷。大量小对象即使占用字节不多,请求开销仍可能主导成本。兼容挂载层能够接受数据库文件名,同样需要证明它满足锁、写入顺序与失败恢复语义,才能作为合适的数据库存放位置。 快速验证 保存为 storage_semantics.py,用 Python 3.9 或更新版本执行 python3 storage_semantics.py。这是使用虚构字节的独立内存模型,不访问服务,不测文件系统持久性,也不涉及网络延迟与计费。它只把明确写进类定义的规则变成可检查的操作,方便分辨几种常被混淆的保证。 """In-memory API model, not an implementation of a storage service.""" class BlockVolume: def __init__(self, data): self.data = bytearray(data) def write(self, offset, chunk): if offset < 0 or offset + len(chunk) > len(self.data): raise ValueError("write outside fixed volume") self.data[offset:offset + len(chunk)] = chunk class ObjectStore: def __init__(self): self.current = {} def put(self, key, data, expected=None): old = self.current.get(key) version = old[1] if old else 0 if expected is not None and expected != version: raise ValueError("version conflict") self.current[key] = (bytes(data), version + 1) return version + 1 def get(self, key): return self.current[key] block = BlockVolume(b"ABCDEF") block.write(2, b"XX") assert block.data == b"ABXXEF" try: block.write(5, b"too long") except ValueError: pass else: raise AssertionError("out-of-range write accepted") assert block.data == b"ABXXEF" store = ObjectStore() assert store.put("report", b"ABCDEF", expected=0) == 1 cached = store.get("report") assert store.put("report", b"XYZ", expected=1) == 2 assert store.get("report") == (b"XYZ", 2) assert cached == (b"ABCDEF", 1) try: store.put("report", b"stale overwrite", expected=cached[1]) except ValueError: pass else: raise AssertionError("stale conditional update accepted") assert store.get("report") == (b"XYZ", 2) assert store.get("report")[0][1:3] == b"YZ" print("PASS: bounded overwrite; complete replacement; cached copy differs; stale condition rejected") 块模型最初有六字节,覆盖中间两字节后,相邻内容保持不变。越界写入被拒绝,原数据没有因此变化。对象模型从第一版的六字节内容,整体替换成第二版的三字节内容;新的读取立即得到替换结果。另一个事先保存的元组仍然包含第一版,因为应用保留了自己的旧副本。这是本地缓存陈旧,不是服务读取出现最终一致性延迟。 ...