本文主要介绍了Docker所使用到的几种存储驱动。
Tamer of Unicorns and Tinkerer Extraordinaire¹
脾气暴躁的法国DevOps人员 喜爱Shell scripts Go Away Or I Will Replace You Wiz Le Very Small Shell Script
有一些容器技术的使用经验 (负责 dotCloud PaaS 的构建和运维工作)
打算使用Markdown来制作ppt(这的确是个好主意)
Docker速览
简要介绍 copy-on-write
Docker 存储驱动的发展历史
AUFS, BTRFS, Device Mapper, Overlayfs, VFS
结论
一个由 Docker Engine 和 Docker Hub 组成的平台
Docker Engine指的是容器的运行时环境
Docker是开源的 由Go语言所开发 http://www.slideshare.net/jpetazzo/docker-and-go-why-did-we-decide-to-write-docker-in-go
它是一个守护进程, 被REST API控制
还是不清楚,它到底是什么!? 这周五 参与在线的 “Docker 101” 会议: http://www.meetup.com/Docker-Online-Meetup/events/219867087/
This will help!
jpetazzo@tarrasque:~$ docker run -ti python bash
root@75d4bf28c8a5:/# pip install IPython
Downloading/unpacking IPython
Downloading ipython-2.3.1-py3-none-any.whl (2.8MB): 2.8MB downloaded
Installing collected packages: IPython
Successfully installed IPython
Cleaning up...
root@75d4bf28c8a5:/# ipython
Python 3.4.2 (default, Jan 22 2015, 07:33:45)
Type "copyright", "credits" or "license" for more information.
IPython 2.3.1 -- An enhanced Interactive Python.
? -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help -> Python's own help system.
object? -> Details about 'object', use 'object??' for extra details.
In [1]:
我们创建了一个 容器 (~相当于一个轻量级的虚拟机), 它拥有:
python
镜像)我们通过一个 bash
进程来启动
(no init
, no systemd
, no problem)
我们通过pip安装了IPython, 并且将它运行起来
python
镜像安装过程在 容器 中完成, 而并非是在 镜像 中完成:
我们并没有修改 python
镜像本身
我们并没有影响其他容器的运行 (当前使用的镜像或者其他的镜像)
我们使用的是 copy-on-write 机制 (Docker 帮助我们进行处理)
我们并没有对’python’镜像进行完整地拷贝,我们仅仅是跟踪容器相对于镜像所发生的变化
这个过程节省了大量的硬盘空间 (1 个容器 = 小于 1 MB 的存储空间)
节省了大量的时间 (1 个容器 = 小于 0.1s 的启动时间)
注意: 我并非是一个历史学家.
下面这些零散信息介绍的并不全面.
fork()
(linux中的进程创建函数)
快速地创建一个新的进程
… 即使是这个进程使用了许多 GBs 的 RAM
在类似于 e.g. Redis SAVE
的功能中被频繁地使用,
为了获得一致的镜像(consistent snapshots)
mmap()
(将文件映射到指定内存空间) 使用 MAP_PRIVATE
参数
使用MAP_PRIVATE参数之后 内存段变为私有 改变仅对本进程可见Changes are visible only to current process
私有映射进行得很快 即使对大文件也是这样Private maps are fast, even on huge files
粒度: 1 次一个页面 (通常大小为 4 KB)
它是如何工作的?
多亏了 MMU! (Memory Management Unit)
每次对内存的访问都需要通过MMU
MMU可以把对于内存的访问请求 (虚拟的位置¹ + 操作²) 转化为:
实际的物理地址
或者会返回一个页错误 (page fault)
当页错误发生的时候, MMU 就会通知 OS.
之后会发生什么?
要求访问不存在的内存空间 Access to non-existent memory area = SIGSEGV
(即 “段错误 Segmentation fault” 或是 “请继续学习指针的使用”)
访问已换出的内存空间 = 从硬盘中导入 (即 “我的程序怎么比以前满了1000倍”)
尝试向代码区写入内容 = seg fault (有时会发生)
尝试向拷贝区(copy area)写入内容 = 去重操作(deduplication operation) 之后如果什么也没有发生就恢复到初始化操作(initial operation)
在非执行区域也可以捕获尝试执行的请求 (比如利用栈来避免某些漏洞(stack, to protect against some exploits))
最初的应用(个人看法)可能是 镜像服务
(即是为更新频繁地数据库建立一致的备份 确保在开始备份到备份结束没有发生其他的操作)
在外接地存储设备上也可以使用(个人看法)Initially available on external storage (NAS, SAN)
(因为这个部分确实很复杂)
–
基于Copy-on-write存储服务构建系统镜像Put system image on copy-on-write storage
为每一台虚拟机创建一个copy-on-write实例
如果系统镜像中包含了许多有用地软件 使用虚拟机的时候就不需要再安装额外的东西了
每一个额外生成地虚拟机仅仅需要硬盘空间来存储数据就行!
(下面地排列并没有按照特定的顺序;列出的内容也并非详尽)
LVM (Logical Volume Manager) on Linux
ZFS on Solaris, then FreeBSD, Linux …
BTRFS on Linux
AUFS, UnionMount, overlayfs …
Virtual disks in VM hypervisors
如果没有 copy-on-write…
一个容器永远无法启动起来
容器会占据很大的存储空间
如果你的笔记本电脑上没有…
Junjiro R. Okajima (以及其他的AUFS贡献者)
Chris Mason (以及其他的BTRFS贡献者)
Jeff Bonwick, Matt Ahrens (以及其他的ZFS贡献者)
Miklos Szeredi (以及其他的overlay文件系统的贡献者)
Linux device mapper, thinp target, 等等服务的众多贡献者
… 以及该领域的先驱者们 站在他们的肩上 我们才能看得更远
Docker公司的前身是dotCloud (PaaS层产品, 类似 Heroku, Cloud Foundry, OpenShift…)
dotCloud 从2008年开始使用AUFS技术 (那时 vserver, then OpenVZ 都开始使用AUFS, 之后是LXC)
对于高密度的PaaS 应用 这是一个不错的选择 (后面我们会有具体介绍!)
并没有被包括在Linux的主线内核中
使用补丁程序曾经是一件激动人心地事情
… 特别是与 GRSEC 相结合
… 并且加上其他定制的功能比如 setns()
(将线程与namespace技术再结合)
特别是dotCloud
Debian 以及 Ubuntu 在他们默认地内核中 使用了AUFS 对于Live CD 以及类似的使用情况:
Docker 的第一个版本就是针对Ubuntu设计的 (以及 Debian)
Red Hat用户要求在他们最受欢迎的发行版中添加对Docker的支持
Red Hat Inc. 也想让这一切发生
… 他们于是为Docker贡献代码 添加了对 Device Mapper driver的支持
… 之后是 BTRFS driver
… 接着是 overlayfs driver
Alexander Larsson
Vincent Batts
+ 当然还有全部地贡献者和维护者
(上面两位贡献者在最初BTRFS、Device Mapper、以及overlay驱动的开发、支持和维护过程中扮演了极为重要的角色,再次感谢!)
按照特定的顺序将多个分支结合在一起
每一个分支都是一个标准的的目录
通常会包括:
至少一个只读分支 (在最低层)
恰好一个读写分支 (再最顶层)
(也可能有其它的组合方式!)
通过 O_RDONLY
- 只读的方式来进行访问:
在每一个分支中进行查找 ,从最顶层的分支开始
打开找到的第一个文件
通过 O_WRONLY
或 O_RDWR
- 可写入的方式进行访问:
首先在顶层分支中进行查找 如果在顶层分支中找到,就打开文件
如果没有找到, 就在其他分支中进行查找; 如果在其他分支中找到文件,就把它拷贝到读写分支中(顶层) 之后打开拷贝过去的文件
如果所打开的文件本身比较大 则向上拷贝的操作可能要多花一些时间
#### docker run ubuntu rm /etc/shadow
#### ls -la /var/lib/docker/aufs/diff/$(docker ps --no-trunc -lq)/etc
total 8
drwxr-xr-x 2 root root 4096 Jan 27 15:36 .
drwxr-xr-x 5 root root 4096 Jan 27 15:36 ..
-r--r--r-- 2 root root 0 Jan 27 15:36 .wh.shadow
容器中AUFS的挂载点是
/var/lib/docker/aufs/mnt/$CONTAINER_ID/
只有在容器运行地时候 文件系统才会被挂载
AUFS的分支(只读分支和读写分支)的位置在
/var/lib/docker/aufs/diff/$CONTAINER_OR_IMAGE_ID/
所有写入的内容都存在 /var/lib/docker
目录下
dockerhost# df -h /var/lib/docker
Filesystem Size Used Avail Use% Mounted on
/dev/xvdb 15G 4.8G 9.5G 34% /mnt
查看 AUFS 挂载的相关细节:
在 /proc/mounts
文件夹下 查看 内部ID
查找/sys/fs/aufs/si_.../br*
目录
可以把每一个分支 (除去顶层的两个分支) 理解成一个镜像
dockerhost# grep c7af /proc/mounts
none /mnt/.../c7af...a63d aufs rw,relatime,si=2344a8ac4c6c6e55 0 0
dockerhost# grep . /sys/fs/aufs/si_2344a8ac4c6c6e55/br[0-9]*
/sys/fs/aufs/si_2344a8ac4c6c6e55/br0:/mnt/c7af...a63d=rw
/sys/fs/aufs/si_2344a8ac4c6c6e55/br1:/mnt/c7af...a63d-init=ro+wh
/sys/fs/aufs/si_2344a8ac4c6c6e55/br2:/mnt/b39b...a462=ro+wh
/sys/fs/aufs/si_2344a8ac4c6c6e55/br3:/mnt/615c...520e=ro+wh
/sys/fs/aufs/si_2344a8ac4c6c6e55/br4:/mnt/8373...cea2=ro+wh
/sys/fs/aufs/si_2344a8ac4c6c6e55/br5:/mnt/53f8...076f=ro+wh
/sys/fs/aufs/si_2344a8ac4c6c6e55/br6:/mnt/5111...c158=ro+wh
dockerhost# docker inspect --format {{.Image}} c7af
b39b81afc8cae27d6fc7ea89584bad5e0ba792127597d02425eaee9f3aaaa462
dockerhost# docker history -q b39b
b39b81afc8ca
615c102e2290
837339b91538
53f858aaaf03
511136ea3c5a
AUFS mount()
速度很快 因此创建容器的过程也很快
对内存进行读/写操作的速度与原先区别不大
但是最初的 open()
操作 在写大文件的时候 比较费时
在以下方面仍有问题:日志文件(log files),数据库(databases) …
并没有许多需要可以调优的地方(Not much to tune)
使用技巧: 当我们构建dotCloud的时候,我们最后把所有重要的数据都放在存储卷上 (putting all important data on volumes)
当多次启动一个容器的时候,数据只被从硬盘中导入了一次,并且只需要在内存中缓存一次(cached only once in memory)
(but dentries
will be duplicated)
Device Mapper 是一个复杂的子系统; 它可以完成以下工作:
磁盘阵列(RAID)
设备编码(encrypted devices)
镜像 (即使用 copy-on-write 机制)
以及其它地一些零碎地功能
在Docker的环境下, “Device Mapper” 指的是 “the Device Mapper system + its thin provisioning 存储” (有些时候标记为 “thinp”)
Copy-on-write 机制发生在存储块级别 (而不是文件级别)
每一个容器额每个镜像都有它们自己的块设备
在任何给定地时间,都可能对以下内容进行快照:
已经存在的容器 (创建一个静态的镜像(frozen image))
已经存在的镜像 (从镜像中创建一个文件)
如果块设备一直没有被写入:
就认为对应的空间没有内容(it’s assumed to be all zeros)
不会在硬盘上被分配空间 (所谓的 “thin” provisioning)
容器挂载点的目录是在
/var/lib/docker/devicemapper/mnt/$CONTAINER_ID/
只有在容器运行的时候 才会被挂载
数据存在两个文件中,一个是"data"文件 一个是"metadata” 文件 (这个稍后会进行具体介绍)
因为我们实际的工作在block的层面上进行,所以对于镜像和容器之间的差别,我们并不全部可见
docker info
命令会告诉你当前资源池的状态
(已用空间/可用空间)
使用 dmsetup ls
列出全部可用设备
设备名称以"docker-MAJ:MIN-INO"为前缀
MAJ, MIN, and INO 这几个简称来源于存储Docker数据的主块设备(block major) 从块设备(block minor) 以及索引结点号(inode number) (为了避免运行多个Docker实例的时候发生冲突 即在Docker中运行Docker)
通过 dmsetup info
, dmsetup status
命令可以查看更多的信息
镜像有一个内部的数值形式的ID
/var/lib/docker/devicemapper/metadata/$CONTAINER_OR_IMAGE_ID
是一个小的JSON文件 用于跟踪记录镜像的ID以及它的大小
需要两个存储区: 一个用于存储数据(data), 另一个用于存储元信息(metadata)
“data” 也可以理解成 “pool”; 它是一个存储块构成的巨大的资源池 (Docker使用尽可能小的存储块,64KB)
“元信息(metadata)“包含了虚拟地址偏移(在镜像中)到实际物理偏移 (在资源池中)的映射
每一次一个新的存储块(或者一个copy-on-write块被写入) 一个存储块就从资源池中被分配出来
当资源池中没有新地存储块时,尝试进行写入的操作就会停止,直到资源池中资源的数量增加(或者写操作被终止)
默认情况下 Docker将数据和元信息都存储在一个由稀疏文件(sparse file)做支撑的loop device上
从可用性的角度来看 这一点比较方便 (基本上不需要进行配置)
从性能的角度来看 可能比较糟糕
帮自己一个忙:如果你想使用 Device Mapper 就把数据(以及元信息)存在实际的设备上(real devices)!
终止Docker进程
修改参数
删除 /var/lib/docker
(这一点很重要!)
重启Docker进程
docker -d --storage-opt dm.datadev=/dev/sdb1 --storage-opt dm.metadatadev=/dev/sdc1
让每一个容器都有它自己的块存储设备
所以你也可以调整 (通过--storage-opt
参数):
文件系统的类别
文件系统的大小
discard
(这个后面有更多介绍)
警告: 当你1000次启动容器的时候, 文件会从硬盘中被导入1000次!
https://www.kernel.org/doc/Documentation/device-mapper/thin-provisioning.txt
https://github.com/docker/docker/tree/master/daemon/graphdriver/devmapper
在文件系统的级别上完成全部的"copy-on-write"的工作
创建¹ 一个 “subvolume” (设想 mkdir
操作有极大的权限)
对任何的 subvolume 在任何时候生成镜像¹
BTRFS 从文件系统的级别而非是存储块设备的级别 将镜像和资源管理池的特性结合在一起
/var/lib/docker
必须要是一个BTRFS文件系统
对于一个容器或者一个镜像 BTRFS 的挂载点位于
/var/lib/docker/btrfs/subvolumes/$CONTAINER_OR_IMAGE_ID/
即使容器没有在运行BTRFS也会被使用
数据并没有直接被写入而是先是被写入到日志(it goes to the journal first) (在某些情况下¹, 这可能会影响性能)
BTRFS 通过把存储设备分成不同的数据块(chunks)来发挥作用
一个数据块包含着元标签或者元信息(meta or metadata)
你可以用完全部的数据块 (会得到 No space left on device
的消息)
即便如此通过 df
命令还是会显示出有可用空间
(因为存储块并没有占满所有空间(because the chunks are not full))
快速修复:
#### btrfs filesys balance start -dusage=1 /var/lib/docker
没有太多可以优化的地方
注意 btrfs filesys show
命令的输出!
表明文件系统正在正常运行:
#### btrfs filesys show
Label: none uuid: 80b37641-4f4a-4694-968b-39b85c67b934
Total devices 1 FS bytes used 4.20GiB
devid 1 size 15.25GiB used 6.04GiB path /dev/xvdc
下面这种情况是文件块全部占满的情况(没有空闲的文件块) 即使上面没有太多的数据信息:
#### btrfs filesys show
Label: none uuid: de060d4c-99b6-4da0-90fa-fb47166db38b
Total devices 1 FS bytes used 2.51GiB
devid 1 size 87.50GiB used 87.50GiB path /dev/xvdc
为何将fs标记为灰色?
它曾经被称为 overlayfs
当并入到 3.18 版本之后, 名称就变为了 overlay
这个文件系统与AUFS很类似,只有很少的地方有差别:
只有两个分支only two branches (被称为文件层(“layers”))
但是分支只能进行自我覆盖
你需要内核版本为 3.18
在Ubuntu¹上:
locate the most recent directory, e.g. v3.18.4-vidi
download the linux-image-..._amd64.deb
file
dpkg -i
that file, reboot, enjoy
镜像以及容器在以下目录下被具体化
/var/lib/docker/overlay/$ID_OF_CONTAINER_OR_IMAGE
镜像只有一个’root’子目录 (包含了root FS)
容器含有:
lower-id
→ 文件包含镜像的ID
merged/
→ 容器的挂载点(需要在运行的时候)
upper/
→ 容器的读写层
work/
→ 用于原子拷贝操作的临时的空间
目前阶段没有什么需要调优的地方
性能方面应该与AUFS比较类似:
向上拷贝速度较慢
对内存资源的利用较好
具体实现细节: 同样的文件在不同镜像之间通过硬链接的方式连在一起 (这样可以避免进行复杂的覆盖( avoids doing composed overlays))
没有 copy on write 机制 Docker每次都要进行全部的拷贝!
并没有依赖于这些及为花哨的内核机
当将Docker移植到一个新的平台上的时候 这是一个不错的选择 (think FreeBSD, Solaris…)
空间利用率低 速度慢
在产品安装的时候可能比较有用
(如果你不想/不能 使用 存储卷,并且不想/不能使用任何 copy-on-write机制)
如果你做的是PaaS或使用其他的密集环境(high-density environment):
AUFS (要求内核提供对应的支持)
overlayfs (在其他的情况下)
如果你把一个大的可写的文件放在CoW文件系统:
discard
and TRIM
TRIM
发送给SSD硬盘的内容 告诉SSD硬盘Command sent to a SSD disk, to tell it: “这个存储块已经不在被使用了”
这个功能很有用 因为对于SSD来说 erase的代价非常高 (速度很慢)
允许SSD 来提前预先擦除cells (并不是即时的 而是在 写操作之前)
这也对支持 copy-on-write 机制的存储有意义 (如果/当 所有的镜像都作为一个trimmed block 那么它就可以被释放)
discard
文件系统选择的含义:
“can I has TRIM
on this pls”
Can be enabled/disabled at any time
文件系统也可以使用fstrim
通过手工地方式被修剪(be trimmed)
(即使对于已经挂载了的文件系统)
discard
的困惑discard
在 Device Mapper + loopback devices 上工作
… 但是在 loopback devices 上速度特别慢 (在容器或者镜像删除之后 loopback文件需要被"re-sparsified” 这是一个特别慢的操作)
你可以根据自己的偏好将其打开或者关闭
To get those slides, follow me on twitter: @jpetazzo Yes, this is a particularly evil scheme to increase my follower count
Also WE ARE HIRING!
infrastructure (servers, metal, and stuff)
QA (get paid to break things!)
Python (Docker Hub and more)
Go (Docker Engine and more)
Send your resume to jobs@docker.com Do it do it do it NOW NOW!
原文地址:http://static.dockerone.com/ppt/filedriver.html
*注:原文是一个用Markdown写的PPT,非常有趣,建议可以看看。
15 Jun 2016 #docker