Go 存储基础 | 来给文件打个洞

聊聊背景

文件还能打洞?

支持稀疏文件语义的文件系统就可以。

支持稀疏语义的文件系统有什么基本特征?

  • 实现 fallocate 接口,能够满足文件空间预分配和打洞;
  • 实现 fiemap 的功能,返回文件的具体物理块分配信息;

打洞是什么意思?

英文是“punch hole”,就是在保证文件其他属性不变(比如,文件大小,inode 编号,权限等等)的条件下,主动释放一段文件所占的物理空间

关于承诺的语义?

文件系统:punch hole 成功,文件系统可能释放,也可能没释放这部分空间,此结果不对用户承诺

程序猿:反而是程序猿要遵守承诺,一旦 puhch hole 成功,用户将不能对这部分数据做任何假设,要当它已经没了,无论它是不是真的没了

创建实分配的文件

为了打洞,我们需要先创建一个实际占用 4M 的文件,用 dd 命令如下:

root@ubuntu:~/temp# dd if=/dev/urandom of=./test.txt.4M bs=1M count=4

可以用 du 命令看一下实际的物理空间:

root@ubuntu:~/temp# du -sh ./test.txt.4M
4.0M	./test.txt.4M

确实是 4M,再用 stat 命令看一下:

root@ubuntu:~/temp# stat ./test.txt.4M
  File: './test.txt.4M'
  Size: 4194304   	Blocks: 8192       IO Block: 4096   regular file
Device: fc00h/64512d	Inode: 1335860     Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)

文件 Size 4194304 字节,物理占用 Blocks 数是 8192,这里每个 Block 单位是 512 字节,所以物理占用也是 4194304 字节,刚好 4M。

文件打个洞

原材料准备好了,Go 程序怎么给文件打个洞呢 ?

关键在于 fallocate 系统调用。

这是一个跟平台强相关的系统调用,非系统兼容的,下面以 Linux 为例。

由于这个非系统兼容的,类似于这类调用,一般都是用 syscall 这个标准库,直接下发系统调用。程序示例如下:

// +build linux
package main

import (
    "log"
    "os"
    "syscall"
)

//  mode 0 change to size                  0x0
//  FALLOC_FL_KEEP_SIZE                  = 0x1
//  FALLOC_FL_PUNCH_HOLE                 = 0x2
func punchHoleLinux(file *os.File, offset int64, size int64) error {
    return syscall.Fallocate(int(file.Fd()), 0x1|0x2, offset, size)
}

func main() {
    f, err := os.OpenFile("./test.txt.4M", os.O_RDWR|os.O_CREATE, 0666)
    if err != nil {
        log.Fatalf("open failed. err(%v)\n", err)
    }

    // discard 0-2M 的空间,预期实际物理占用能减少 2M
    err = punchHoleLinux(f, 0, 2*1024*1024)
    if err != nil {
        log.Fatalf("punch hole failed")
    }

    log.Printf("punch hole success.\n")
}

关键几个事项:

  1. 文件头部要加上 // +build linux
  2. 调用的是 syscall.Fallocate 接口;

好了,编译一下吧:

go build -gcflags "-N -l" ./punchhole.go

把编译出的二进制 punchhole 和 test.txt.4M 这两个放在一个目录下,实验一下效果:

root@ubuntu:~/temp# ./punchhole 
2021/09/08 22:22:21 punch hole success.

du 看下文件结果:

root@ubuntu:~/temp# du -sh ./test.txt.4M
2.0M	./test.txt.4M

嗷,确实变成了 2M,stat 再看一下:

root@ubuntu:~/temp# stat ./test.txt.4M
  File: './test.txt.4M'
  Size: 4194304   	Blocks: 4096       IO Block: 4096   regular file
Device: fc00h/64512d	Inode: 1335860     Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2021-07-26 15:39:06.672000000 +0800

文件大小还是 4M,实际物理空间变成了 2M( 4096 * 512 ),inode 编号、权限都没变。

完美,一个空洞文件就诞生了。

文件解析:

  • 这个文件 [ 0,2M ] 的位置是空洞,不占物理空间,读出来会是 0 数据;
  • [ 2M,4M ] 的数据还是原来从 /dev/urandom 设备读出来的数据,占用实际物理空间;

思考题

抛出几个关键的思考问题,大家可以自行验证。

如果 punchhole 传参是非 4k 对齐的,会怎么样?

划重点:由于文件系统内部都是按照 4k 的单位管理空间的,所以非 4k 对齐的空间是释放不掉的。 punch hole 一定要注意按照 4k 对齐。

特别还要注意一点,虽然非 4k 对齐释放不掉,但是 fallocate 调用也不会报错,这点很重要。最开始就提过,文件系统没给你承诺过啥时候释放啥。

大家可以手动验证下。

文件 test.txt.4M 的 [ 0,2M ] 被打洞之后,这个区域会是什么数据?

**全 0 数据,这个是稀疏文件系统给你的语义。**这个上面也提到过了。

奇伢教你快速用 hexdump 命令看一下:

root@ubuntu:~/temp# hexdump ./test.txt.4M|more
0000000 0000 0000 0000 0000 0000 0000 0000 0000
*
0200000 80e3 2c11 f8d8 256b 23b5 a191 fb80 eb5e
0200010 f454 e3e2 cb8b 664a a893 6f5a 2df0 99dd
0200020 9d30 4f19 144f b4f1 f2cd 7312 c16c 719f
0200030 2ef7 3195 48a1 b2c0 03f1 a08a aff3 a022
.................
.................

看到了吗?

0x0000000 - 0x0200000 这个区域都是 0 数据。这是 16 进制表示,换算成 10 进制,就是 [ 0 ,2M ] 的区域。

大家也可以用程序去 read 验证下。

总结

总结几个关键点:

  1. 文件打洞用的是系统调用 fallocate ;
  2. 文件打洞的时候要注意 4k 对齐,不然非对齐部分释放不掉,并且不会报错
  3. 文件系统没承诺什么,所以当没 4k 对齐的时候,虽然没释放空间,也不会报错;
  4. 程序猿要遵守承诺,一旦声明了某段空间要释放,以后不能对此空间内容做假设;
  5. 文件打洞的位置,不占物理空间,后续读是返回 0 数据

后记

关于稀疏文件原理,大家可以再看一下 cp 的秘密


坚持思考,方向比努力更重要。关注公众号:奇伢云存储,获取更多干货。 关注我公众号, 获取更多干货