- 云原生
- 后端
- 前端
- 运维
- 可观测性
- 开发工具
【免费下载链接】octant
Highly extensible platform for developers to better understand the complexity of Kubernetes clusters.
本篇文章以 octant 仓库中随 vendored 依赖引入的github.com/vbatts/tar-split/tar/asm包的官方文档(vendor/github.com/vbatts/tar-split/tar/asm/README.md)为核心,讲解如何在 Go 生态中实现对 tar 归档的字节级精确拆解(disassembly)与重组(assembly)。读完本文,你将理解 asm 包与tar/storage的分工关系、覆盖路径(clobbering records)带来的精确性问题及两种应对策略,并通过仓库内源码与实际调用场景掌握NewInputTarStream与NewOutputTarStream的完整工作链路。
asm 包是什么:流式组装/拆解 API 的定位
tar/asm是 tar-split 项目中的一个 Go 包,其包级文档(vendor/github.com/vbatts/tar-split/tar/asm/doc.go)给出清晰定义:
Package asm provides the API for streaming assembly and disassembly of tar archives.
它本身不负责 tar 格式的解析,而是借助同一个仓库下的github.com/vbatts/tar-split/tar/storage完成两类工作:
- Packing/Unpacking(打包/解包):把 tar 流的元数据条目(
Entry)序列化到存储,或从存储按序读回; - Getting/Putting(取/存):实现 tar 中每个文件载荷的实际读写。
换言之,asm是"组装/拆解引擎",storage是"元数据与载荷的存储抽象",二者通过接口协作,这也是 README 第一段就指明该库由tar/storage协助的原因。从源码结构看,asm目录只包含三个文件:disassemble.go(拆解入口)、assemble.go(组装入口)与 doc.go(包文档),职责非常聚焦。
核心概念:FileType 与 SegmentType 两条元数据流
拆解的本质,是把一个 tar 归档流"切开"成两类有序片段,并逐一写入storage.Packer。storage包在 entry.go 中定义了两种条目类型:
| 类型 | 含义 | Payload 内容 |
|---|---|---|
FileType | 归档中某个文件的载荷 | 该文件载荷的CRC64 校验和(8 字节),而非文件内容本身 |
SegmentType | 归档流中的原始字节段 | 原始头部字节、块填充(padding)等,序列化时按 base64 编码 |
对应的Entry结构体字段如下(JSON 序列化友好):
type Entry struct { Type Type `json:"type"` Name string `json:"name,omitempty"` NameRaw []byte `json:"name_raw,omitempty"` Size int64 `json:"size,omitempty"` Payload []byte `json:"payload"` // SegmentType 存原始字节;FileType 存 crc64 校验和 Position int `json:"position"` }两个细节值得注意:
- 文件名可能不是合法 UTF-8。tar 格式允许任意字节的文件名,因此
SetName/SetNameBytes会先做utf8.ValidString检查,不合法时把原始字节存入NameRaw,而GetName/GetNameBytes则保证无论存于哪个字段都能取回名字(对应 tar-split issue #17 的修复)。 Position用于顺序还原。Entries切片实现了sort.Interface(Less按Position比较),这为后续把元数据记录按原始顺序回放提供了基础。
拆解:NewInputTarStream 的字节级"录音"
disassemble.go 中NewInputTarStream的签名与职责:
func NewInputTarStream(r io.Reader, p storage.Packer, fp storage.FilePutter) (io.Reader, error)它包装一段 tar 归档输入流,返回同一段流的读取器;在中间做"中间人(MITM)":把归档的头部字节段与文件元数据打包进Packer,把文件载荷写入FilePutter。实现上有四个关键技术决策:
io.TeeReader+io.Pipe组合:TeeReader 把读到的数据同时写给 PipeWriter,实现"用户读一份、我们录一份"。源码注释特别解释:由于 tar 归档末尾常有多余填充,必须由库自己读完这些填充(即使使用方自己的archive/tar不关心),所以返回的是 PipeReader 而不是直接返回原 Reader。RawAccounting追踪原始字节:内部使用 fork 过的archive/tar,tr.RawAccounting = true后,每次tr.Next()之间通过tr.RawBytes()取回"这一段归档流到底读了哪些原始字节",把这些头部字节作为SegmentType条目写入Packer。- 文件载荷按需落盘:当
hdr.Size > 0时调用fp.Put(hdr.Name, tr),得到(size, crc64校验和, err),随后以FileType条目记录;如果调用方传入nil的FilePutter,库内部会自动替换为storage.NewDiscardFilePutter()——只算校验和、不保存内容。 - 末尾填充的分块读取防攻击:除 tar 规范要求的 1024 个 null 字节外,归档后可能还有更多填充。源码以
paddingChunkSize = 1024 * 1024的固定块循环读取并逐块登记为SegmentType,注释明确指出这是为了避免恶意构造的 tar 文件诱导程序一次性读入数 GB 数据到内存。
拆解完成后,你手上就有两份产物:一份可持久化的元数据记录流(Segment 头部字节 + File 的 CRC64 校验和),一份按文件名存放的文件载荷存储。
组装:NewOutputTarStream 的字节级"回放"
assemble.go 提供两个入口:
func NewOutputTarStream(fg storage.FileGetter, up storage.Unpacker) io.ReadCloser func WriteOutputTarStream(fg storage.FileGetter, up storage.Unpacker, w io.Writer) errorWriteOutputTarStream是核心:循环调用up.Next()读取元数据记录,按entry.Type分流:
SegmentType:把entry.Payload的原始字节原样写入输出流——这正是"精确还原"的关键,tar 的头部、填充字节都靠这些片段逐字节还原;FileType:若entry.Size == 0则跳过(硬链接场景下没有独立载荷);否则调用fg.Get(entry.GetName())读取该文件的载荷流,一边写入输出、一边计算 CRC64,最后与元数据中记录的校验和比对,不一致即返回错误file integrity checksum failed for %q。
几个工程细节:
- 校验和计算使用
crc64与storage.CRCTable(getter.go 中定义为crc64.MakeTable(crc64.ISO));README 与源码注释均强调 CRC64只用于文件完整性校验,不具备密码学强度。 - 复制缓冲取自
sync.Pool(默认 32 KiB),减少高频复制时的内存分配;copyWithBuffer是标准库io.Copy实现的移植版本。 NewOutputTarStream内部用io.Pipe+ goroutine 把WriteOutputTarStream包成可流式读取的io.ReadCloser,写入出错时通过CloseWithError传递错误。
README 的核心关切:覆盖记录(clobbering records)与精确性问题
README 用大量篇幅讨论了一个容易忽略的事实:tar 归档格式允许同一路径出现多条记录,而"最后一个记录生效"——即使先前记录携带的是完全不同的载荷内容。这直接破坏了"用相对路径载荷重组精确归档"的假设:
In this way, when assembling an archive from relative paths, if the archive has multiple entries for the same path, then all payloads read in from a relative path would be identical.
也就是说,如果拆解时把每个文件名直接落到磁盘的单一相对路径上,遇到覆盖记录时旧载荷会被新载荷顶掉;等到重组时,所有同名条目读到的都是最后一份内容,再也无法还原出原始归档。
storage包对这种"破坏性"场景也做了防守:NewJSONPacker与NewJSONUnpacker内部维护seenNames集合,一旦发现同一个filepath.Clean后的文件名出现第二次,立即返回ErrDuplicatePath("duplicates of file paths not supported"),见 packer.go。
两种应对策略:旁视 CAS 存储 vs 拒绝追加记录
README 给出了两条技术路线:
方案一:内容可寻址存储(CAS)旁视目录
思路是:拆解阶段如果发现覆盖记录,先把"被覆盖掉的旧载荷"保存起来,这样覆盖记录的载荷照常提取,而重组精确归档所需的旧载荷也没有丢失。README 中给出的示意命名:
clobbered/path/to/file.[0-N]即把同路径的每一份历史载荷按序号保存在一个旁视目录(look-aside directory)或存储中,并以内容可寻址(checksum 映射)方式索引——这也是 README 开头"Concerns"一节强调"完全安全的组装/拆解需要一个 CAS 目录,映射到storage.Entity中storage.FileType的校验和"的原因:校验和本身就是内容寻址的键,同一路径不同历史版本的载荷可以据此区分。
方案二:干脆不支持覆盖路径的归档
README 明确给出替代思路:"We could justnotsupport tar streams that have clobbering file paths."理由如下:
- 追加同路径记录(appending records)在 tar 使用中并不常见,多数实现默认不会产生;
- 在安全性上也无明显损失:如果归档真的包含覆盖记录,说明它本来就没有被签名/校验和验证过,重组出来的归档本就不该被信任。
因此该方案允许把"对追加文件的完整支持"作为FUTURE FEATURE(未来特性)推迟实现,同时保持当前行为的简单与可预测。
从仓库源码看,当前storage的 JSON Packer/Unpacker 恰好就是"方案二"的实现形态——直接以ErrDuplicatePath拒绝重复路径,而非引入 CAS 旁视目录。
真实使用场景:containers/storage 中的组装与拆解
asm并非纸上谈兵的库,octant 的 vendor 目录中github.com/containers/storage就同时用到了拆解与组装两个入口:
- vendor/github.com/containers/storage/layers.go:在层归档落盘时调用
asm.NewOutputTarStream(fgetter, metadata),配合FileGetter与解包出的元数据重建 tar 流; - vendor/github.com/containers/storage/layers.go:拆解层时用
asm.NewInputTarStream(io.TeeReader(uncompressed, uncompressedWriter), metadata, storage.NewDiscardFilePutter()),并显式传入NewDiscardFilePutter()——即"只记账、不存载荷"的高效路径,对应 README 与 disassemble.go 中描述的可选nil FilePutter场景。
这印证了该库的典型用法:在需要"先扫描归档元数据、后按需重组归档"的容器镜像层管理中,拆解阶段可以只收集元数据与校验和,载荷存储交给调用方决定策略(丢弃、落盘或入内存缓冲)。
结语:精度与取舍
tar/asm的设计哲学可以概括为"用一次拆解换取任意次精确重组":SegmentType保字节、FileType保校验,二者配合Packer/Unpacker的有序回放,理论上可以把归档还原到字节级一致。而 README 所揭示的覆盖记录问题,则提醒使用者:tar 格式的"宽容"(同路径多记录)与"精确重组"之间存在根本张力,要么引入 CAS 旁视存储保留历史载荷,要么明确拒绝此类归档并把支持推迟为未来特性。对于在 Go 中处理容器层、镜像或任意需要保真重组的 tar 工具链开发者,理解这两条取舍路径,是正确使用asm包的前提。
- 云原生
- 后端
- 前端
- 运维
- 可观测性
- 开发工具
【免费下载链接】octant
Highly extensible platform for developers to better understand the complexity of Kubernetes clusters.
相关推荐
tar-split 原理与实战:精确拆解与重组 Tar 归档,让 Docker 镜像层校验和可复现
tar split 原理与实战:精确拆解与重组 Tar 归档,让 Docker 镜像层校验和可复现 导读 tar split 是一个 Go 语言实现的 Tar
操作系统云原生容器运行时vcluster 依赖解析:tar-split/asm 的流式 tar 拆解(Disassembly)与精确重组(Assembly)原理与实践
vcluster 依赖解析:tar split/asm 的流式 tar 拆解(Disassembly)与精确重组(Assembly)原理与实践 本文围绕 vcl
云原生集群管理虚拟化多集群tar-split 的 tar/asm 流式拆装:RancherOS 中实现 Docker 镜像层精确重建的 Tar 组装/拆解库
tar split 的 tar/asm 流式拆装:RancherOS 中实现 Docker 镜像层精确重建的 Tar 组装/拆解库 本指南围绕当前仓库 vend
操作系统云原生容器运行时
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考