想象你经营着一家连锁餐饮公司,马路上跑着很多辆“流动餐车”。每辆餐车(容器)都是按标准菜谱(镜像)现场做菜的,菜卖完了,餐车就要开回厂里彻底翻新——旧的直接报废,换一辆全新的车。麻烦的是:食材不能跟着报废的餐车一起扔掉。于是公司在中央厨房建了一排排“储物柜”(数据卷)。每辆餐车出摊前从储物柜里取食材,收摊后把没用完的食材放回储物柜。车换了,食材永远在。这就是 Docker 数据卷存在的意义——让数据独立于容器存活。今天我们就用这套“中央厨房 + 流动餐车”的故事,把数据卷的定义、生命周期、备份恢复、跨主机共享、安全与性能优化一次讲透。

岗位关联

对 AI 从业者而言,数据卷是管理训练数据、模型检查点和有状态服务的“仓储基本功”。数据管理能力将直接影响你在工程化岗位上的交付质量。

目录
  1. 可视化演示
  2. 数据卷基础概念
  3. 卷生命周期管理
  4. 数据卷备份恢复
  5. 跨主机数据同步
  6. 临时与加密存储
  7. 卷性能优化
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示数据卷类型对比、卷生命周期独立性与备份恢复数据流,切换模式理解容器持久化核心。

本节配有交互动效(进入视口后自动加载,约 3KB)。

数据卷类型对比 Volume由Docker管理,Bind Mount挂载宿主机路径,tmpfs存于内存 Volume > Bind > tmpfs Volume独立于容器生命周期,适合持久化数据存储

数据卷基础概念

中央厨房给每辆流动餐车都配了固定编号的储物柜。餐车出摊时从柜里取货,收摊时把剩货放回柜里。哪怕餐车报废换了新车,新餐车凭编号还是能打开同一个柜子。这里的“储物柜”就是 Docker 数据卷,而“餐车”就是容器。容器本身没有想象力——它由镜像创建,镜像只是一份只读配方,任何写入都会在容器销毁时随风而逝。要让数据留下来,就必须把它放进 Docker 托管的独立存储区域。

这个独立存储区域就是数据卷(Volume)。用 docker volume create my_data 创建命名卷后,启动容器时用 -v my_data:/app/data 挂载,容器里的 /app/data 目录就会映射到卷里。如果只写 -v /app/data 而不指定卷名,Docker 会自动创建一个“匿名卷”——相当于没贴标签的储物柜,也能存数据,但事后很难管理。与之相对的还有一种做法叫“绑定挂载”(bind mount),写法是 -v /宿主机路径:/容器路径,它直接把宿主机某个目录塞进容器,相当于把家里的冰箱搬到餐车上——路径写死,换台机器就失灵。

数据卷还有一个天然优势:多个容器可以同时挂载同一个卷,就像多辆餐车共用一个食材仓库,实现容器间数据共享。这也是有状态应用(数据库、AI 训练任务)钟爱数据卷的根本原因。

一句话回扣比喻:储物柜比餐车结实,餐车换了几轮,柜子里的食材还在等新餐车来取。

tvol≥tctr\displaystyle t_{\text{vol}} \geq t_{\text{ctr}}
这个不等式是说:数据卷的存活时间不短于任何使用它的容器——容器可以被随意销毁,卷中的数据却安然无恙。
  1. 符号 t\_vol表示数据卷从创建到被删除的总存活时间。"vol" 是 volume(卷)的缩写。
    tvol\displaystyle t_{\text{vol}}
  2. 符号 t\_ctr表示任意一个挂载了该卷的容器(container)的存活时间。
    tctr\displaystyle t_{\text{ctr}}
  3. 不等号 ≥说明卷比容器“活得久”。即使某个容器被 docker rm 删除,卷依然存在,数据依然可被下一个容器读取。

卷生命周期管理

储物柜不是买了就永远不管,中央厨房有完整的管理流程:新店开业申请柜子(创建卷)、定期盘点柜子编号(查看卷)、退租时清空并注销柜子(删除卷)。Docker 给数据卷的生命周期也设计了一整套命令。

创建卷用 docker volume create my_vol;查看所有卷用 docker volume ls;想看某个卷的挂载点、驱动等细节,用 docker volume inspect my_vol。容器启动时用 --mount source=my_vol,target=/app/data 把卷关联到容器,这种显式声明比 -v 更清晰,推荐在脚本和编排文件中使用。

删除卷要格外谨慎。docker volume rm my_vol 会永久删除卷里的所有数据;docker volume prune 会把所有未被任何容器引用的“孤儿卷”一次性清空——这就像仓库管理员清理所有长时间无人认领的旧柜子。另外注意:docker rm container 默认不会删除卷,只有加上 -v 参数才会连带删除容器关联的匿名卷。所以容器删了,卷可能还在硬盘上占着空间,需要定期体检清理。

回扣比喻:租柜子要有台账,退柜要办手续。卷的生命周期管理就是这份“柜台账”。

Nref=∑i=1nCi\displaystyle N_{\text{ref}} = \sum_{i=1}^{n} C_i
这个公式计算一个数据卷被多少个容器“引用”。C_i 表示第 i 个容器是否挂载了该卷(挂载为 1,否则为 0)。当引用数归零,卷就成了可清理的孤儿。
  1. 符号 C\_i第 i 个容器的挂载状态,取值只能是 1 或 0。
    Ci∈{0,1}\displaystyle C_i \in \{0, 1\}
  2. 求和符号 Σ把 n 个容器的状态累加起来。
    ∑i=1n\displaystyle \sum_{i=1}^{n}
  3. 结果 N\_ref当前仍在使用该卷的容器总数。当 N_ref = 0 时,说明没有餐车在取用这个柜子,管理员可以安全清理。

数据卷备份恢复

冰箱里的食材怕坏,中央厨房每周都会把各储物柜里的食材打包送进冷库冷冻存档。Docker 数据卷也一样需要定期备份。最经典的做法是:启动一个临时容器,同时挂载目标卷和宿主机备份目录,在容器里用 tar 把卷内容打包。

备份命令长这样:

恢复就是反向操作:把备份文件解压回卷里。

--rm 表示容器执行完立即自毁,不会留下一堆“备份工具”垃圾容器。实际生产环境中,数据库这类有状态服务不应在运行时直接打包数据文件,而应优先使用数据库自带的一致性备份工具(如 pg_dump);文件型数据(AI 模型权重、样本集)则用 tar 备份完全足够。备份完成后最好对文件计算哈希,确认备份包完好无损。

回扣比喻:冷库取出来的食材要检查有没有解冻变质,备份校验就是这份“体检报告”。

hash(Dbackup)=hash(Dsource)⇒backup OK\displaystyle \text{hash}(D_{\text{backup}}) = \text{hash}(D_{\text{source}}) \Rightarrow \text{backup OK}
这条公式表达“校验通过才算备份成功”:如果备份数据的哈希值等于原始数据的哈希值,就证明备份包和源数据逐字节一致。
  1. 函数 hash()一种指纹算法(如 SHA-256),给任意数据算出一个固定长度的唯一指纹。
    hash(⋅)\displaystyle \text{hash}(\cdot)
  2. D\_backup 与 D\_source分别是备份文件与原始数据。任何一位不同,指纹都会天差地别。
    Dbackup,Dsource\displaystyle D_{\text{backup}}, D_{\text{source}}
  3. 等号与蕴含符指纹相同推导出备份完好,这是数据校验的基本逻辑。
bash4 行
docker run --rm \  -v my_vol:/data \  -v $(pwd):/backup \  ubuntu tar czf /backup/backup.tar.gz -C /data .
bash4 行
docker run --rm \  -v my_vol:/data \  -v $(pwd):/backup \  ubuntu tar xzf /backup/backup.tar.gz -C /data

跨主机数据同步

连锁餐饮集团在全国多个城市开了分店,每辆餐车不能只依赖本地储物柜,还得能从总部大仓库远程调货。Docker 卷默认只存在本机磁盘,跨主机共享就需要网络存储,最常见的是 NFS(网络文件系统)。

创建一个 NFS 卷只需一条命令,把卷的后端指向远程 NFS 服务器:

使用时,所有主机都挂载同一个 shared_vol,相当于多辆餐车同时从同一座总仓库取货。NFS 的优点是配置简单、天然支持多主机数据一致;缺点是性能受网络影响很大,读写每次都要经过网线,延迟比本地磁盘高出一个量级。在 AI 多机训练场景中,训练数据集放在 NFS 卷上共享很常见,但模型检查点这类高频写入数据建议写本地卷,避免拖慢训练速度。

更专业的跨主机方案还有 Ceph、GlusterFS 等分布式存储,它们通过多副本和一致性协议保证数据可靠,但部署复杂度也直线上升。小团队从 NFS 起步,是性价比最高的选择。

回扣比喻:从总仓库调货要算运输时间,网络卷省了仓储空间,却付出了“物流成本”。

Ttotal=Tnet+Tdisk\displaystyle T_{\text{total}} = T_{\text{net}} + T_{\text{disk}}
跨主机读写一次的耗时 = 网络传输耗时 + 存储设备耗时。本地卷的 T_net 为零,这也是为什么网络卷永远比本地卷慢一拍。
  1. T\_net数据在网络中往返传输的时间,跨主机场景下通常占大头。
    Tnet\displaystyle T_{\text{net}}
  2. T\_disk存储设备本身完成读写的时间,取决于磁盘是 SSD 还是 HDD。
    Tdisk\displaystyle T_{\text{disk}}
  3. 加法关系总延迟不是取最大值而是简单相加。想提速就要同时压减两个分量。
bash6 行
docker volume create \  --driver local \  --opt type=nfs \  --opt o=addr=192.168.1.10,rw \  --opt device=:/data/shared \  shared_vol

临时与加密存储

餐车厨房里有两样东西很特殊:一是“一次性砧板”,用完直接扔,根本不会带回仓库;二是“带密码锁的保险柜”,专门放贵重的调味料配方。对应到 Docker 中,前者是 tmpfs 挂载,后者是加密卷。

tmpfs 挂载把数据直接放在内存里,不写磁盘。启动容器时加上 --mount type=tmpfs,destination=/tmp,tmpfs-size=100m,容器内存里的 /tmp 目录读写飞快,但容器一停,数据立刻消失。这种“易失性”反而适合保存临时文件、进程间缓存等敏感中间数据——不落盘,天然不留痕。

真正的加密卷则要保护“长期数据”。Docker 本身未内置卷加密,常用做法有两种:一是使用支持加密的卷驱动或存储插件(如基于 LUKS/dm-crypt 的加密文件系统作为卷后端);二是将文件加密后再放入卷,由应用层管理密钥。密钥解密能力与密钥位数直接相关,现代加密普遍使用 256 位密钥,暴力破解在计算上是不可能的——真正的安全短板往往不是算法,而是密钥被写在代码里或配置文件里,相当于把保险柜钥匙贴在柜门上。

回扣比喻:砧板用完即弃不沾指纹,保险柜要防的是钥匙保管人,而不是锁本身的强度。

Nkey=2256\displaystyle N_{\text{key}} = 2^{256}
256 位密钥共有 2 的 256 次方种可能的取值。这个数字比宇宙中的原子总数还大,逐一遍历完全不可行。
  1. 底数 2每个二进制位只有 0 和 1 两种状态。
    2\displaystyle 2
  2. 指数 256密钥长度为 256 位。
    2256\displaystyle 2^{256}
  3. N\_key密钥空间的规模,代表了暴力破解所需尝试的上界。

卷性能优化

同样是生鲜运输,用普通面包车和用冷链车效果天差地别。数据卷的性能优化,就是给不同数据选择合适的“运输车”。

首先是存储驱动。Docker 默认的 overlay2 驱动兼顾镜像分层与读写性能,是绝大多数场景的默认首选,避免使用性能较差的 vfs(无层复用,每次写都完整复制)。其次是卷的类型选择:本地卷永远比网络卷快,因为省去了网络往返;使用绑定挂载直接绕过 Docker 存储层,能获得接近裸盘的 I/O 性能,适合数据库这类高 I/O 应用。

对于 NFS 网络卷,可以通过挂载参数调优,比如增大 rsize/wsize(单次读写块大小)来减少交互次数,或者使用数据缓存选项降低读延迟。性能监控方面,docker system df 可以查看卷占用的磁盘空间,docker stats 能观察容器的内存与 CPU,再配合宿主机上的 iostat、fio 等工具,就能定位瓶颈是在存储设备、网络还是 Docker 层。

回扣比喻:冷链车拉货快,但路况(网络)太烂一样跑不快;选对车、选好路,才谈得上高效持久化。

IOPSeff=min⁡(IOPSdisk,1Trtt)\displaystyle IOPS_{\text{eff}} = \min(IOPS_{\text{disk}}, \frac{1}{T_{\text{rtt}}})
有效读写能力取决于两个瓶颈中最弱的一方:存储设备本身的 IOPS 上限,以及网络往返时间的倒数。本地卷因为没有网络项,瓶颈往往更高。
  1. IOPS\_disk存储设备每秒能完成的读写次数,SSD 通常远高于 HDD。
    IOPSdisk\displaystyle IOPS_{\text{disk}}
  2. 1 / T\_rtt网络往返时间越长,每秒能发起的请求就越少。
    1Trtt\displaystyle \frac{1}{T_{\text{rtt}}}
  3. min() 取小函数木桶效应——短板决定总容量,任何一项拖后腿都会拉低整体性能。

AI 应用场景

AI 训练数据统一管理

阶段:数据准备与模型训练

解决的问题:AI 训练依赖海量数据集和模型检查点(checkpoint)。传统做法把数据直接放容器里,容器一删数据就没了;多机训练时每台机器各复制一份,版本混乱、同步困难。使用数据卷后,训练集挂载为只读卷,检查点写入命名卷,训练容器随任务销毁,数据和模型成果却完整保留。

具体案例:某 CV 团队用 GPU 集群跑图像分类训练,每个训练任务是一个临时容器,数据卷挂载 20TB 的训练样本,单机训练结束后检查点写入命名卷。下次调参只需启动新容器并复用同一卷,从断点继续训练,省去大量重复下载和数据搬运时间。

数据库容器化持久化

阶段:有状态服务部署与运维

解决的问题:数据库容器升级或迁移时,容器重建会导致数据丢失。将数据库数据目录挂载到数据卷,容器随意重建,数据文件始终安全。

具体案例:AI 平台的服务端用 PostgreSQL 存用户与任务元数据,PostgreSQL 容器将 /var/lib/postgresql/data 挂载到命名卷。每次升级数据库镜像,只需启动新容器并挂载同一卷,同时用临时容器和 pg_dump 定期备份到对象存储,实现数据库无损升级。

Python 演示

python51 行
#!/usr/bin/env python3"""Docker 数据卷备份工具: 用临时容器把所有命名卷打包为 tar.gz"""import osimport subprocessimport time BACKUP_DIR = "/tmp/volume_backups"  def run_cmd(cmd):    """执行 shell 命令并返回 CompletedProcess"""    return subprocess.run(cmd, shell=True, capture_output=True, text=True)  def list_volumes():    """返回所有 Docker 数据卷名称列表"""    out = run_cmd("docker volume ls -q")    return out.stdout.strip().split()  def backup_volume(vol):    """把单个数据卷备份为 tar.gz 文件"""    os.makedirs(BACKUP_DIR, exist_ok=True)    stamp = time.strftime("%Y%m%d_%H%M%S")    backup_path = os.path.join(BACKUP_DIR, f"{vol}_{stamp}.tar.gz")    # 用临时容器挂载卷和备份目录, 再执行 tar 打包    cmd = (        "docker run --rm "        f"-v {vol}:/data "        f"-v {BACKUP_DIR}:/backup "        f"ubuntu tar czf /backup/{os.path.basename(backup_path)} -C /data ."    )    result = run_cmd(cmd)    if result.returncode == 0:        print(f"[OK] {vol} -> {backup_path}")    else:        print(f"[FAIL] {vol}: {result.stderr.strip()}")  def main():    vols = list_volumes()    if not vols:        print("没有找到任何数据卷")        return    print(f"发现 {len(vols)} 个数据卷, 开始备份...")    for v in vols:        backup_volume(v)  if __name__ == "__main__":    main()
目的:一键遍历宿主机上所有 Docker 数据卷,模拟“把储物柜食材打包送冷库”的流程,为每个卷生成带时间戳的 tar.gz 备份文件。
  1. subprocess.run(cmd, shell=True, capture_output=True, text=True)在 Python 里执行 Docker 命令,并捕获控制台输出,方便判断成败。
  2. out.stdout.strip().split()把 docker volume ls -q 输出的每行一个卷名拆成数组。
  3. os.makedirs(BACKUP_DIR, exist_ok=True)确保备份目录存在,不存在就自动创建。
  4. time.strftime("%Y%m%d_%H%M%S")给备份文件名加上精确到秒的时间戳,避免重名覆盖。
  5. docker run --rm启动一次性容器,任务结束立即自动删除,不给宿主机留垃圾。
  6. -v {vol}:/data把待备份卷挂载到容器内的 /data 目录。
  7. tar czf /backup/xxx.tar.gz -C /data .打包时先切到 /data 再打包,避免恢复时多套一层目录。

常见误区

误区“反正都是把目录放进容器里,`docker run -v /host:/container` 和 `docker run -v my_vol:/container` 没本质区别,用哪个都一样。”

正解两者机制完全不同。数据卷由 Docker 管理,存储在 Docker 专属目录(/var/lib/docker/volumes),不依赖宿主机具体路径,可移植性好;绑定挂载直接映射宿主机任意目录,与宿主机文件系统强耦合,换台机器路径不一样就失效。简单说,数据卷是“中央厨房的储物柜”,绑定挂载是“把你家冰箱搬上车”。

为何常见:-v 命令的两种写法外观太像,只差一个“卷名放在路径前”,小白很容易忽略这个决定性差异。

误区“容器都没了,里面的数据自然跟着没了,所以不用单独清理卷。”

正解docker rm 默认不删除任何卷。删除卷需要显式执行 docker volume rm 或 docker volume prune。卷可能变成“孤儿”继续占用磁盘,日积月累会把磁盘塞满。

为何常见:很多人把容器里的文件系统与卷混为一谈,以为“容器即数据”,忽略了卷是独立对象。

误区“tmpfs 也是一种挂载方式,数据写进去应该能长期保存吧?”

正解tmpfs 把数据放在内存,容器停止或崩溃后数据立即消失,绝不能用于持久化。它适合临时缓存、密钥等敏感中间数据——“用完即弃”本身就是它的设计目标。

为何常见:看到 --mount type=tmpfs 与 type=volume 写法相似,误以为功能也相似。

误区“反正容器读写都是通过卷,NFS 卷速度应该跟本地卷差不多。”

正解NFS 每次读写都要经过网络往返,延迟通常比本地磁盘高一个数量级,带宽也成为瓶颈。高频小文件读写用 NFS 会非常吃力,AI 训练中检查点写入应谨慎放置。

为何常见:在测试环境数据量小时感受不到差距,数据量上来后网络开销才会暴露。

误区“卷不就是个目录吗?直接 `cp -r` 把卷目录拷贝走就完成备份了。”

正解直接复制存在两个问题:一是正在运行的数据库可能写出中间状态,复制结果不一致;二是盲目操作 Docker 内部目录容易破坏权限结构。正确姿势是用临时容器挂载卷,配合 tar 或数据库自带的 pg_dump 等工具,确保一致性备份。

为何常见:数据卷在宿主机上确实表现为一个目录,让人误以为像普通文件夹一样随便拷贝即可。

面试题

Q1. 下面哪个命令可以创建一个命名数据卷?

单选题 · easy

  • A.docker volume create 新建卷← 正确答案
  • B.docker image build 构建镜像
  • C.docker network create 建网络
  • D.docker container run 运行容器

答案:A。docker volume create 是专门创建数据卷的命令;其余选项分别用于构建镜像、创建网络和运行容器。就像申请储物柜要去仓库管理处,而不是去厨房。

Q2. 关于数据卷与绑定挂载的区别,正确的是?

单选题 · easy

  • A.数据卷不依赖宿主机目录结构← 正确答案
  • B.绑定挂载由 Docker 管理更安全
  • C.数据卷与绑定挂载没有区别
  • D.命名卷必须手动删除才能清理

答案:A。数据卷存储在 Docker 自己管理的目录中,不依赖宿主机具体路径,可移植性好;绑定挂载直接映射宿主机目录,与宿主路径强耦合。选项 D 错误,因为匿名卷可以通过 docker rm -v 或 docker volume prune 清理。

Q3. 查看当前所有 Docker 数据卷的命令是?

单选题 · easy

  • A.docker volume ls 查看列表← 正确答案
  • B.docker ps 查看运行容器
  • C.docker images 查看本地镜像
  • D.docker logs 查看容器日志

答案:A。docker volume ls 专门列出所有数据卷;docker ps 看容器,docker images 看镜像,docker logs 看日志。四个命令各管一摊,记住"volume"对应卷即可。

Q4. 删除容器且保留其数据卷,应使用哪条命令?

单选题 · mid

  • A.docker rm 容器ID← 正确答案
  • B.docker rm -v 容器ID
  • C.docker volume rm 容器ID
  • D.docker system prune -a

答案:A。docker rm 默认只删容器不删卷;加上 -v 后会连带删除匿名卷;docker volume rm 后面应接卷名而非容器 ID;docker system prune -a 主要清理未使用的镜像和网络。想保留数据,就用最普通的 docker rm。

Q5. 使用临时容器备份数据卷时,关键参数是?

单选题 · mid

  • A.使用 --rm 自动清理临时容器← 正确答案
  • B.必须使用数据库专用备份工具
  • C.备份容器必须与业务容器同启
  • D.备份过程必须要使用 --link 参数

答案:A。--rm 让临时容器执行完自动删除,不留垃圾;备份文件型数据用 tar 足够,数据库才需要专用工具;备份容器不需要与业务容器同时运行;--link 是早已过时的容器互联方式。临时容器“用完即走”正是它的价值所在。

Q6. 下列哪些属于 Docker 数据持久化的有效方案?

多选题 · mid

  • A.使用命名数据卷存储← 正确答案
  • B.使用绑定挂载同步← 正确答案
  • C.用 tmpfs 挂载目录
  • D.写入容器内部文件

答案:AB。命名卷和绑定挂载都能让数据在容器销毁后保留;tmpfs 存内存里,容器停止数据即清空;写入容器内部文件随容器一起消亡。只有“储物柜”和“绑定的冰箱”能长期保存食材,砧板和餐车把手都不行。

Q7. 关于 tmpfs 挂载,下列说法正确的是?

单选题 · mid

  • A.数据存于内存且退出即清空← 正确答案
  • B.数据会落盘并可以永久保留
  • C.非常适合长期保存数据库数据
  • D.每个 tmpfs 挂载必须指定容量

答案:A。tmpfs 把数据放在内存,容器停止数据立即消失;正因如此它不能落盘、不适合长期保存数据库数据;tmpfs-size 是可选参数,不指定则使用容器内存限额。它就像一次性砧板,用完即弃是特性而非缺陷。

Q8. 关于 NFS 卷,下列说法错误的是?

单选题 · hard

  • A.NFS 卷支持跨主机共享数据
  • B.NFS 卷性能受网络环境限制
  • C.NFS 读写延迟与本地卷一致← 正确答案
  • D.使用 NFS 需要安装客户端

答案:C。NFS 卷的网络存储特性带来两个必然结果:支持跨主机共享、性能受网络限制。每次读写都有网络往返,延迟必然高于本地卷。宿主机需要安装 NFS 客户端(如 nfs-common)才能挂载。说它与本地卷延迟一致,是对“网络存储”的误解。

Q9. 关于数据卷性能优化,正确的做法有?

多选题 · hard

  • A.本地 SSD 存储卷数据← 正确答案
  • B.低延迟后端存网络卷← 正确答案
  • C.全部数据放入 tmpfs
  • D.overlay2 驱动优化镜像← 正确答案

答案:ABD。本地 SSD 能显著提升卷 I/O;网络卷应选择低延迟存储后端以降低传输损耗;overlay2 是 Docker 默认的高效存储驱动。选项 C 不可取:tmpfs 易失,把全部数据放内存等于把所有鸡蛋放在一只“会消失的篮子”里。

Q10. 关于数据卷备份恢复策略,正确的说法有?

多选题 · extreme

  • A.数据库优先用自身备份工具← 正确答案
  • B.直接复制卷目录始终安全
  • C.备份文件存宿主机独立目录← 正确答案
  • D.恢复前先建卷再导数据← 正确答案

答案:ACD。数据库需用 pg_dump 等一致性备份工具,直接 cp 目录可能复制到中间状态;备份文件放在宿主机独立目录才能与容器解耦;恢复时先创建目标卷再导入数据,保证卷结构干净。B 错在忽略了一致性问题——直接把正在写的账本复印一份,页码可能是乱的。